diff --git a/.alexrc b/.alexrc
new file mode 100644
index 00000000000..168d412c177
--- /dev/null
+++ b/.alexrc
@@ -0,0 +1,16 @@
+{
+ "allow": [
+ "attack",
+ "attacks",
+ "bigger",
+ "color",
+ "colors",
+ "failure",
+ "hook",
+ "hooks",
+ "host-hostess",
+ "invalid",
+ "remain",
+ "special"
+ ]
+}
diff --git a/.doctor-rst.yaml b/.doctor-rst.yaml
new file mode 100644
index 00000000000..a0d74a57714
--- /dev/null
+++ b/.doctor-rst.yaml
@@ -0,0 +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_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: ~
+ 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: ~
+
+ # master
+ versionadded_directive_major_version:
+ major_version: 7
+
+ versionadded_directive_min_version:
+ min_version: '7.0'
+
+ deprecated_directive_major_version:
+ major_version: 7
+
+ deprecated_directive_min_version:
+ 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:
+ - '/``.yml``/'
+ - '/(.*)\.orm\.yml/' # currently DoctrineBundle only supports .yml
+ lines:
+ - 'in config files, so the old ``app/config/config_dev.yml`` goes to'
+ - '#. The most important config file is ``app/config/services.yml``, which now is'
+ - 'The bin/console Command'
+ - '.. _`LDAP injection`: http://projects.webappsec.org/w/page/13246947/LDAP%20Injection'
+ - '.. 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";'
+ - '.. versionadded:: 0.2' # MercureBundle
+ - '.. 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/.editorconfig b/.editorconfig
index 6f5286431d4..f9366facfb0 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,8 +1,8 @@
root = true
-[*.{rst,rst.inc}]
+[*]
indent_style = space
-indent_size = 2
+indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 00000000000..9eb5d91783b
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,30 @@
+# GithubActions workflows
+/.github/workflows* @OskarStark
+
+# Console
+/console* @chalasr
+/components/console* @chalasr
+
+# Form
+/forms.rst @xabbuh @HeahDude
+/components/form* @xabbuh @HeahDude
+/reference/forms* @xabbuh @HeahDude
+
+# PropertyInfo
+/components/property_info* @dunglas
+
+# Security
+/security* @chalasr
+/components/security* @chalasr
+
+# Validator
+/validation/* @xabbuh @HeahDude
+/components/validator* @xabbuh @HeahDude
+/reference/constraints* @xabbuh @HeahDude
+
+# Workflow
+/workflow* @lyrixx
+/components/workflow* @lyrixx
+
+# Yaml
+/components/yaml* @xabbuh
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 bc7d6a94182..f32043e4523 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,9 +1,9 @@
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 00000000000..fa36efbcbcf
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,145 @@
+name: CI
+
+on:
+ push:
+ branches-ignore:
+ - 'github-comments'
+ pull_request:
+ branches-ignore:
+ - 'github-comments'
+
+permissions:
+ contents: read
+
+jobs:
+ symfony-docs-builder-build:
+ name: Build (symfony-tools/docs-builder)
+
+ runs-on: ubuntu-latest
+
+ continue-on-error: true
+
+ steps:
+ - name: "Checkout"
+ uses: actions/checkout@v4
+
+ - name: "Set-up PHP"
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: 8.4
+ coverage: none
+
+ - name: Get composer cache directory
+ id: composercache
+ working-directory: _build
+ run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
+
+ - name: Cache dependencies
+ uses: actions/cache@v3
+ with:
+ path: ${{ steps.composercache.outputs.dir }}
+ key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
+ restore-keys: ${{ runner.os }}-composer-
+
+ - name: "Install dependencies"
+ working-directory: _build
+ run: composer install --prefer-dist --no-progress
+
+ - name: "Build the docs"
+ working-directory: _build
+ run: php build.php --disable-cache
+
+ doctor-rst:
+ name: Lint (DOCtor-RST)
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: "Checkout"
+ uses: actions/checkout@v4
+
+ - name: "Create cache dir"
+ run: mkdir .cache
+
+ - name: "Extract base branch name"
+ run: echo "branch=$(echo ${GITHUB_BASE_REF:=${GITHUB_REF##*/}})" >> $GITHUB_OUTPUT
+ id: extract_base_branch
+
+ - name: "Cache DOCtor-RST"
+ 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:1.68.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 a5eb433eea3..b69047f69a1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,2 @@
-/_build/doctrees
-/_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/.travis.yml b/.travis.yml
deleted file mode 100644
index cccb01eef30..00000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-language: python
-
-python: 2.7
-
-sudo: false
-cache:
- directories: [$HOME/.cache/pip]
-
-install: pip install -r _build/.requirements.txt
-
-script: make -C _build SPHINXOPTS=-nW html
-
-branches:
- except:
- - github-comments
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
new file mode 100644
index 00000000000..547ac103984
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,340 @@
+LICENSE
+=======
+
+**Creative Commons Attribution-ShareAlike 3.0 Unported**
+https://creativecommons.org/licenses/by-sa/3.0/
+
+-----
+
+THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS
+PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR
+OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS
+LICENSE OR COPYRIGHT LAW IS PROHIBITED.
+
+BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE
+BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED
+TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN
+CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS.
+
+1. Definitions
+--------------
+
+a. **"Adaptation"** means a work based upon the Work, or upon the Work and other
+pre-existing works, such as a translation, adaptation, derivative work,
+arrangement of music or other alterations of a literary or artistic work, or
+phonogram or performance and includes cinematographic adaptations or any other
+form in which the Work may be recast, transformed, or adapted including in any
+form recognizably derived from the original, except that a work that constitutes
+a Collection will not be considered an Adaptation for the purpose of this
+License. For the avoidance of doubt, where the Work is a musical work,
+performance or phonogram, the synchronization of the Work in timed-relation with
+a moving image ("synching") will be considered an Adaptation for the purpose of
+this License.
+
+b. **"Collection"** means a collection of literary or artistic works, such as
+encyclopedias and anthologies, or performances, phonograms or broadcasts, or
+other works or subject matter other than works listed in Section 1(f) below,
+which, by reason of the selection and arrangement of their contents, constitute
+intellectual creations, in which the Work is included in its entirety in
+unmodified form along with one or more other contributions, each constituting
+separate and independent works in themselves, which together are assembled into
+a collective whole. A work that constitutes a Collection will not be considered
+an Adaptation (as defined below) for the purposes of this License.
+
+c. **"Creative Commons Compatible License"** means a license that is listed at
+https://creativecommons.org/compatiblelicenses that has been approved by
+Creative Commons as being essentially equivalent to this License, including, at
+a minimum, because that license: (i) contains terms that have the same purpose,
+meaning and effect as the License Elements of this License; and, (ii) explicitly
+permits the relicensing of adaptations of works made available under that
+license under this License or a Creative Commons jurisdiction license with the
+same License Elements as this License.
+
+d. **"Distribute"** means to make available to the public the original and
+copies of the Work or Adaptation, as appropriate, through sale or other transfer
+of ownership.
+
+e. **"License Elements"** means the following high-level license attributes as
+selected by Licensor and indicated in the title of this License: Attribution,
+ShareAlike.
+
+f. **"Licensor"** means the individual, individuals, entity or entities that
+offer(s) the Work under the terms of this License.
+
+g. **"Original Author""** means, in the case of a literary or artistic work, the
+individual, individuals, entity or entities who created the Work or if no
+individual or entity can be identified, the publisher; and in addition (i) in
+the case of a performance the actors, singers, musicians, dancers, and other
+persons who act, sing, deliver, declaim, play in, interpret or otherwise perform
+literary or artistic works or expressions of folklore; (ii) in the case of a
+phonogram the producer being the person or legal entity who first fixes the
+sounds of a performance or other sounds; and, (iii) in the case of broadcasts,
+the organization that transmits the broadcast.
+
+h. **"Work"** means the literary and/or artistic work offered under the terms of
+this License including without limitation any production in the literary,
+scientific and artistic domain, whatever may be the mode or form of its
+expression including digital form, such as a book, pamphlet and other writing; a
+lecture, address, sermon or other work of the same nature; a dramatic or
+dramatico-musical work; a choreographic work or entertainment in dumb show; a
+musical composition with or without words; a cinematographic work to which are
+assimilated works expressed by a process analogous to cinematography; a work of
+drawing, painting, architecture, sculpture, engraving or lithography; a
+photographic work to which are assimilated works expressed by a process
+analogous to photography; a work of applied art; an illustration, map, plan,
+sketch or three-dimensional work relative to geography, topography, architecture
+or science; a performance; a broadcast; a phonogram; a compilation of data to
+the extent it is protected as a copyrightable work; or a work performed by a
+variety or circus performer to the extent it is not otherwise considered a
+literary or artistic work.
+
+i. **"You"** means an individual or entity exercising rights under this License
+who has not previously violated the terms of this License with respect to the
+Work, or who has received express permission from the Licensor to exercise
+rights under this License despite a previous violation.
+
+j. **"Publicly Perform"** means to perform public recitations of the Work and to
+communicate to the public those public recitations, by any means or process,
+including by wire or wireless means or public digital performances; to make
+available to the public Works in such a way that members of the public may
+access these Works from a place and at a place individually chosen by them; to
+perform the Work to the public by any means or process and the communication to
+the public of the performances of the Work, including by public digital
+performance; to broadcast and rebroadcast the Work by any means including signs,
+sounds or images.
+
+k. **"Reproduce"** means to make copies of the Work by any means including
+without limitation by sound or visual recordings and the right of fixation and
+reproducing fixations of the Work, including storage of a protected performance
+or phonogram in digital form or other electronic medium.
+
+2. Fair Dealing Rights
+----------------------
+
+Nothing in this License is intended to reduce, limit, or restrict any uses free
+from copyright or rights arising from limitations or exceptions that are
+provided for in connection with the copyright protection under copyright law or
+other applicable laws.
+
+3. License Grant
+----------------
+
+Subject to the terms and conditions of this License, Licensor hereby grants You
+a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the
+applicable copyright) license to exercise the rights in the Work as stated
+below:
+
+a. to Reproduce the Work, to incorporate the Work into one or more Collections,
+and to Reproduce the Work as incorporated in the Collections;
+
+b. to create and Reproduce Adaptations provided that any such Adaptation,
+including any translation in any medium, takes reasonable steps to clearly
+label, demarcate or otherwise identify that changes were made to the original
+Work. For example, a translation could be marked "The original work was
+translated from English to Spanish," or a modification could indicate "The
+original work has been modified.";
+
+c. to Distribute and Publicly Perform the Work including as incorporated in
+Collections; and,
+
+d. to Distribute and Publicly Perform Adaptations.
+
+e. For the avoidance of doubt:
+
+ 1. **Non-waivable Compulsory License Schemes.** In those jurisdictions in
+ which the right to collect royalties through any statutory or compulsory
+ licensing scheme cannot be waived, the Licensor reserves the exclusive
+ right to collect such royalties for any exercise by You of the rights
+ granted under this License;
+
+ 2. **Waivable Compulsory License Schemes.** In those jurisdictions in which
+ the right to collect royalties through any statutory or compulsory
+ licensing scheme can be waived, the Licensor waives the exclusive right to
+ collect such royalties for any exercise by You of the rights granted under
+ this License; and,
+
+ 3. **Voluntary License Schemes.** The Licensor waives the right to collect
+ royalties, whether individually or, in the event that the Licensor is a
+ member of a collecting society that administers voluntary licensing
+ schemes, via that society, from any exercise by You of the rights granted
+ under this License.
+
+The above rights may be exercised in all media and formats whether now known or
+hereafter devised. The above rights include the right to make such modifications
+as are technically necessary to exercise the rights in other media and formats.
+Subject to Section 8(f), all rights not expressly granted by Licensor are hereby
+reserved.
+
+4. Restrictions
+---------------
+
+The license granted in Section 3 above is expressly made subject to and limited
+by the following restrictions:
+
+a. You may Distribute or Publicly Perform the Work only under the terms of this
+License. You must include a copy of, or the Uniform Resource Identifier (URI)
+for, this License with every copy of the Work You Distribute or Publicly
+Perform. You may not offer or impose any terms on the Work that restrict the
+terms of this License or the ability of the recipient of the Work to exercise
+the rights granted to that recipient under the terms of the License. You may not
+sublicense the Work. You must keep intact all notices that refer to this License
+and to the disclaimer of warranties with every copy of the Work You Distribute
+or Publicly Perform. When You Distribute or Publicly Perform the Work, You may
+not impose any effective technological measures on the Work that restrict the
+ability of a recipient of the Work from You to exercise the rights granted to
+that recipient under the terms of the License. This Section 4(a) applies to the
+Work as incorporated in a Collection, but this does not require the Collection
+apart from the Work itself to be made subject to the terms of this License. If
+You create a Collection, upon notice from any Licensor You must, to the extent
+practicable, remove from the Collection any credit as required by Section 4(c),
+as requested. If You create an Adaptation, upon notice from any Licensor You
+must, to the extent practicable, remove from the Adaptation any credit as
+required by Section 4(c), as requested.
+
+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
+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),
+(ii) or (iii) (the "Applicable License"), you must comply with the terms of the
+Applicable License generally and the following provisions: (I) You must include
+a copy of, or the URI for, the Applicable License with every copy of each
+Adaptation You Distribute or Publicly Perform; (II) You may not offer or impose
+any terms on the Adaptation that restrict the terms of the Applicable License or
+the ability of the recipient of the Adaptation to exercise the rights granted to
+that recipient under the terms of the Applicable License; (III) You must keep
+intact all notices that refer to the Applicable License and to the disclaimer of
+warranties with every copy of the Work as included in the Adaptation You
+Distribute or Publicly Perform; (IV) when You Distribute or Publicly Perform the
+Adaptation, You may not impose any effective technological measures on the
+Adaptation that restrict the ability of a recipient of the Adaptation from You
+to exercise the rights granted to that recipient under the terms of the
+Applicable License. This Section 4(b) applies to the Adaptation as incorporated
+in a Collection, but this does not require the Collection apart from the
+Adaptation itself to be made subject to the terms of the Applicable License.
+
+c. If You Distribute, or Publicly Perform the Work or any Adaptations or
+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,
+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
+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
+"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
+credit will appear, if a credit for all contributing authors of the Adaptation
+or Collection appears, then as part of these credits and in a manner at least as
+prominent as the credits for the other contributing authors. For the avoidance
+of doubt, You may only use the credit required by this Section for the purpose
+of attribution in the manner set out above and, by exercising Your rights under
+this License, You may not implicitly or explicitly assert or imply any
+connection with, sponsorship or endorsement by the Original Author, Licensor
+and/or Attribution Parties, as appropriate, of You or Your use of the Work,
+without the separate, express prior written permission of the Original Author,
+Licensor and/or Attribution Parties.
+
+d. Except as otherwise agreed in writing by the Licensor or as may be otherwise
+permitted by applicable law, if You Reproduce, Distribute or Publicly Perform
+the Work either by itself or as part of any Adaptations or Collections, You must
+not distort, mutilate, modify or take other derogatory action in relation to the
+Work which would be prejudicial to the Original Author's honor or reputation.
+Licensor agrees that in those jurisdictions (e.g. Japan), in which any exercise
+of the right granted in Section 3(b) of this License (the right to make
+Adaptations) would be deemed to be a distortion, mutilation, modification or
+other derogatory action prejudicial to the Original Author's honor and
+reputation, the Licensor will waive or not assert, as appropriate, this Section,
+to the fullest extent permitted by the applicable national law, to enable You to
+reasonably exercise Your right under Section 3(b) of this License (right to make
+Adaptations) but not otherwise.
+
+5. Representations, Warranties and Disclaimer
+---------------------------------------------
+
+UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS
+THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING
+THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT
+LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR
+PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY,
+OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME
+JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH
+EXCLUSION MAY NOT APPLY TO YOU.
+
+6. Limitation on Liability
+--------------------------
+
+EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE
+LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL,
+PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE
+WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+7. Termination
+--------------
+
+a. This License and the rights granted hereunder will terminate automatically
+upon any breach by You of the terms of this License. Individuals or entities who
+have received Adaptations or Collections from You under this License, however,
+will not have their licenses terminated provided such individuals or entities
+remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8
+will survive any termination of this License.
+
+b. Subject to the above terms and conditions, the license granted here is
+perpetual (for the duration of the applicable copyright in the Work).
+Notwithstanding the above, Licensor reserves the right to release the Work under
+different license terms or to stop distributing the Work at any time; provided,
+however that any such election will not serve to withdraw this License (or any
+other license that has been, or is required to be, granted under the terms of
+this License), and this License will continue in full force and effect unless
+terminated as stated above.
+
+8. Miscellaneous
+----------------
+
+a. Each time You Distribute or Publicly Perform the Work or a Collection, the
+Licensor offers to the recipient a license to the Work on the same terms and
+conditions as the license granted to You under this License.
+
+b. Each time You Distribute or Publicly Perform an Adaptation, Licensor offers
+to the recipient a license to the original Work on the same terms and conditions
+as the license granted to You under this License.
+
+c. If any provision of this License is invalid or unenforceable under applicable
+law, it shall not affect the validity or enforceability of the remainder of the
+terms of this License, and without further action by the parties to this
+agreement, such provision shall be reformed to the minimum extent necessary to
+make such provision valid and enforceable.
+
+d. No term or provision of this License shall be deemed waived and no breach
+consented to unless such waiver or consent shall be in writing and signed by the
+party to be charged with such waiver or consent.
+
+e. This License constitutes the entire agreement between the parties with
+respect to the Work licensed here. There are no understandings, agreements or
+representations with respect to the Work not specified here. Licensor shall not
+be bound by any additional provisions that may appear in any communication from
+You. This License may not be modified without the mutual written agreement of
+the Licensor and You.
+
+f. The rights granted under, and the subject matter referenced, in this License
+were drafted utilizing the terminology of the Berne Convention for the
+Protection of Literary and Artistic Works (as amended on September 28, 1979),
+the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the WIPO
+Performances and Phonograms Treaty of 1996 and the Universal Copyright
+Convention (as revised on July 24, 1971). These rights and subject matter take
+effect in the relevant jurisdiction in which the License terms are sought to be
+enforced according to the corresponding provisions of the implementation of
+those treaty provisions in the applicable national law. If the standard suite of
+rights granted under applicable copyright law includes additional rights not
+granted under this License, such additional rights are deemed to be included in
+the License; this License is not intended to restrict the license of any rights
+under applicable law.
diff --git a/README.markdown b/README.markdown
deleted file mode 100644
index 218c3354128..00000000000
--- a/README.markdown
+++ /dev/null
@@ -1,35 +0,0 @@
-Symfony Documentation
-=====================
-
-This documentation is rendered online at https://symfony.com/doc/current/
-
-Contributing
-------------
-
->**Note**
->Unless you're documenting a feature that was introduced *after* Symfony 3.4
->(e.g. in Symfony 4.2), all pull requests must be based off of the **3.4** branch,
->**not** the master or older branches.
-
-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)
-
-SymfonyCloud
-------------
-
-Pull requests are automatically built by [SymfonyCloud](https://symfony.com/cloud).
-
-Docker
-------
-
-You can build the doc locally with these commands:
-
-```bash
-# build the image...
-$ docker build . -t symfony-docs
-
-# ...and serve it locally on http//:127.0.0.1:8080
-# (if it's already in use, change the '8080' port by any other port)
-$ docker run --rm -p 8080:80 symfony-docs
-```
diff --git a/README.md b/README.md
new file mode 100644
index 00000000000..5c063058c02
--- /dev/null
+++ b/README.md
@@ -0,0 +1,56 @@
+
+
+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 430bce090b0..00000000000
--- a/_build/.requirements.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-alabaster==0.7.10
-Babel==2.4.0
-docutils==0.13.1
-imagesize==0.7.1
-Jinja2==2.9.6
-MarkupSafe==1.0
-Pygments==2.2.0
-pytz==2017.2
-requests==2.20.0
-six==1.10.0
-snowballstemmer==1.2.1
-Sphinx==1.3.6
-git+https://github.com/fabpot/sphinx-php.git@7312eccce9465640752e51373a480da700e02345#egg_name=sphinx-php
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 ' where 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/_themes/_exts/symfonycom/__init__.py b/_build/_themes/_exts/symfonycom/__init__.py
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/_build/_themes/_exts/symfonycom/sphinx/__init__.py b/_build/_themes/_exts/symfonycom/sphinx/__init__.py
deleted file mode 100644
index 426d79826d7..00000000000
--- a/_build/_themes/_exts/symfonycom/sphinx/__init__.py
+++ /dev/null
@@ -1,163 +0,0 @@
-from sphinx.highlighting import lexers, PygmentsBridge
-from pygments.style import Style
-from pygments.formatters import HtmlFormatter
-from pygments.token import Keyword, Name, Comment, String, Error, \
- Number, Operator, Generic, Whitespace, Punctuation, Other, Literal
-
-from sphinx.writers.html import HTMLTranslator
-from docutils import nodes
-from sphinx.locale import admonitionlabels, lazy_gettext
-
-customadmonitionlabels = admonitionlabels
-l_ = lazy_gettext
-customadmonitionlabels['best-practice'] = l_('Best Practice')
-
-def _getType(path):
- return path[:path.find('/')]
-
-def _isIndex(path):
- return 'index' in path
-
-class SensioHTMLTranslator(HTMLTranslator):
- def __init__(self, builder, *args, **kwds):
- HTMLTranslator.__init__(self, builder, *args, **kwds)
- builder.templates.environment.filters['get_type'] = _getType
- builder.templates.environment.tests['index'] = _isIndex
- self.highlightlinenothreshold = 0
-
- def visit_literal(self, node):
- self.body.append(self.starttag(node, 'code', '', CLASS='docutils literal notranslate'))
-
- def depart_literal(self, node):
- self.body.append('')
-
- def visit_admonition(self, node, name=''):
- self.body.append(self.starttag(node, 'div', CLASS=('admonition-wrapper')))
- self.body.append('
')
- if name and name != 'seealso':
- node.insert(0, nodes.title(name, customadmonitionlabels[name]))
- self.set_first_last(node)
-
- def depart_admonition(self, node=None):
- self.body.append('
-
- {% trans %}Free document hosting provided by Read the Docs.{% endtrans %}
-
-
-
-{% endif %}
-
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
+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: The application that builds Symfony Docs does not support Windows. You can try using a Linux distribution via WSL (Windows Subsystem for Linux).');
+
+ 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('', '', $htmlContents);
+ $htmlContents = str_replace('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 b707b25a477..00000000000
--- a/_build/conf.py
+++ /dev/null
@@ -1,287 +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('_themes/_exts'))
-
-# adding PhpLexer
-from sphinx.highlighting import lexers
-from pygments.lexers.compiled import CLexer
-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.0'
-
-# 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',
- 'sensio.sphinx.refinclude', 'sensio.sphinx.configurationblock', 'sensio.sphinx.phpcode', 'sensio.sphinx.bestpractice', 'sensio.sphinx.codeblock',
- 'symfonycom.sphinx'
-]
-
-# 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 = 'sphinx'
-
-# A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
-
-# -- Settings for symfony doc extension ---------------------------------------------------
-
-# enable highlighting for PHP code not between ```` by default
-lexers['markdown'] = TextLexer()
-lexers['php'] = PhpLexer(startinline=True)
-lexers['php-annotations'] = PhpLexer(startinline=True)
-lexers['php-standalone'] = PhpLexer(startinline=True)
-lexers['php-symfony'] = PhpLexer(startinline=True)
-lexers['rst'] = RstLexer()
-lexers['varnish3'] = CLexer()
-lexers['varnish4'] = CLexer()
-lexers['terminal'] = TerminalLexer()
-
-config_block = {
- 'apache': 'Apache',
- 'markdown': 'Markdown',
- 'nginx': 'Nginx',
- 'rst': 'reStructuredText',
- 'terminal': 'Terminal',
- 'varnish3': 'Varnish 3',
- 'varnish4': 'Varnish 4'
-}
-
-# use PHP as the primary domain
-primary_domain = 'php'
-
-# set url for API links
-api_url = 'http://api.symfony.com/master/%s'
-
-
-# -- 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"
-html_theme_path = ["_themes", ]
-
-# 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 = {}
-
-# 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
-# " v 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 = None
-
-# 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']
-
-# 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 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
new file mode 100644
index 00000000000..9758b4e7397
--- /dev/null
+++ b/_build/maintainer_guide.rst
@@ -0,0 +1,378 @@
+Symfony Docs Maintainer Guide
+=============================
+
+The `symfony/symfony-docs`_ repository stores the Symfony project documentation
+and is managed by the `Symfony Docs team`_. This article explains in detail some
+of those management tasks, so it's only useful for maintainers and not regular
+readers or Symfony developers.
+
+Reviewing Pull Requests
+-----------------------
+
+All the recommendations of the `Symfony's respectful review comments`_ apply,
+but there are extra things to keep in mind for maintainers:
+
+* Always be nice in all interactions with all contributors.
+* Be extra-patient with new contributors (GitHub shows a special badge for them).
+* Don't assume that contributors know what you think is obvious (e.g. lots of
+ them don't know what to "squash commits" means).
+* Don't use acronyms like IMO, IIRC, etc. or complex English words (most
+ contributors are not native in English and it's intimidating for them).
+* Never engage in a heated discussion. Lock it right away using GitHub.
+* Never discuss non-tech issues. Some PRs are related to our Diversity initiative
+ and some people always try to drag you into politics. Never engage in that and
+ lock the issue/PR as off-topic on GitHub.
+
+Fixing Minor Issues Yourself
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+It's common for new contributors to make lots of minor mistakes in the syntax
+of the RST format used in the docs. It's also common for non English speakers to
+make minor typos.
+
+Even if your intention is good, if you add lots of comments when reviewing a
+first contribution, that person will probably not contribute again. It's better
+to fix the minor errors and typos yourself while merging. If that person
+contributes again, it's OK to mention some of the minor issues to educate them.
+
+.. code-block:: terminal
+
+ $ gh merge 11059
+
+ 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 "6.2" refs/notes/github-comments
+
+ # Now, open your editor and make the needed changes ...
+
+ $ git commit -a
+ # 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 "6.2" refs/notes/github-comments
+
+Merging Pull Requests
+---------------------
+
+Technical Requirements
+~~~~~~~~~~~~~~~~~~~~~~
+
+* `Git`_ installed and properly configured.
+* ``gh`` tool fully installed according to its installation instructions
+ (GitHub token configured, Git remote configured, etc.)
+ This is a proprietary CLI tool which only Symfony team members have access to.
+* Some previous Git experience, specially merging pull requests.
+
+First Setup
+~~~~~~~~~~~
+
+First, fork the using the GitHub web
+interface. Then:
+
+.. code-block:: terminal
+
+ # Clone your fork
+ $ git clone https://github.com//symfony-docs.git
+
+ $ cd symfony-docs/
+
+ # Add the original repo as 'upstream' remote
+ $ git remote add upstream https://github.com/symfony/symfony-docs
+
+ # Add the original repo as 'gh' remote (needed for the 'gh' tool)
+ $ git remote add gh https://github.com/symfony/symfony-docs
+
+ # Configure 'gh' in Git as the remote used by the 'gh' tool
+ $ git config gh.remote gh
+
+Merging Process
+~~~~~~~~~~~~~~~
+
+At first, it's common to make mistakes and merge things badly. Don't worry. This
+has happened to all of us and we've always been able to recover from any mistake.
+
+Step 1: Select the right branch to merge
+........................................
+
+PRs must be merged in the oldest maintained branch where they are applicable:
+
+* Here you can find the currently maintained branches: https://symfony.com/roadmap.
+* Typos and old undocumented features are merged into the oldest maintained branch.
+* New features are merged into the branch where they were introduced. This
+ usually means ``master``. And don't forget to check that new feature includes
+ the ``versionadded`` directive.
+
+It's very common for contributors (specially newcomers) to select the wrong
+branch for their PRs, so we must always check if the change should go to the
+proposed branch or not.
+
+If the branch is wrong, there's no need to ask the contributor to rebase. The
+``gh`` tool can do that for us.
+
+Step 2: Merge the pull request
+..............................
+
+Never use GitHub's web interface (or desktop clients) to merge PRs or to solve
+merge conflicts. Always use the ``gh`` tool for anything related to merges.
+
+We require two approval votes from team members before merging a PR, except if
+it's a typo, a small change or clearly an error.
+
+If a PR contains lots of commits, there's no need to ask the contributor to
+squash them. The ``gh`` tool does that automatically. The only exceptions are
+when commits are made by more than one person and when there's a merge commit.
+``gh`` can't squash commits in those cases, so it's better to ask to the
+original contributor.
+
+.. code-block:: terminal
+
+ $ cd symfony-docs/
+
+ # make sure that your local branch is updated
+ $ git checkout 4.4
+ $ git fetch upstream
+ $ git merge upstream/4.4
+
+ # merge any PR passing its GitHub number as argument
+ $ gh merge 11159
+
+ # the gh tool will ask you some questions...
+
+ # push your changes (you can merge several PRs and push once at the end)
+ $ git push origin
+ $ git push upstream
+
+It's common to have to change the branch where a PR is merged. Instead of asking
+the contributors to rebase their PRs, the "gh" tool can change the branch with
+the ``-s`` option:
+
+.. code-block:: terminal
+
+ # e.g. this PR was sent against 'master', but it's merged in '4.4'
+ $ gh merge 11160 -s 4.4
+
+Sometimes, when changing the branch, you may face rebase issues, but they are
+usually simple to fix:
+
+.. code-block:: terminal
+
+ $ gh merge 11160 -s 4.4
+
+ ...
+
+ Unable to rebase the patch for pull/11183
+ The command "'git' 'rebase' '--onto' '4.4' '5.0' 'pull/11160'" failed.
+ Exit Code: 128(Invalid exit argument)
+
+ [...]
+ Auto-merging reference/forms/types/entity.rst
+ CONFLICT (content): Merge conflict in reference/forms/types/entity.rst
+ Patch failed at 0001 Update entity.rst
+ The copy of the patch that failed is found in: .git/rebase-apply/patch
+
+ # Now, fix all the conflicts using your editor
+
+ # Add the modified files and continue the rebase
+ $ git add reference/forms/types/entity.rst ...
+ $ git rebase --continue
+
+ # Lastly, re-run the exact same original command that resulted in a conflict
+ # There's no need to change the branch or do anything else.
+ $ gh merge 11160 -s 4.4
+
+ The previous run had some conflicts. Do you want to resume the merge? (Y/n)
+
+Later in this article you can find a troubleshooting section for the errors that
+you will usually face while merging.
+
+Step 3: Merge it into the other branches
+........................................
+
+If a PR has not been merged in ``master``, you must merge it up into all the
+maintained branches until ``master``. Imagine that you are merging a PR against
+``4.4`` and the maintained branches are ``4.4``, ``5.0`` and ``master``:
+
+.. code-block:: terminal
+
+ $ git fetch upstream
+
+ $ git checkout 4.4
+ $ git merge upstream/4.4
+
+ $ gh merge 11159
+ $ git push origin
+ $ git push upstream
+
+ $ git checkout 5.0
+ $ git merge upstream/5.0
+ $ git merge --log 4.4
+ # here you can face several errors explained later
+ $ git push origin
+ $ git push upstream
+
+ $ git checkout master
+ $ git merge upstream/master
+ $ git merge --log 5.0
+ $ git push origin
+ $ git push upstream
+
+.. tip::
+
+ If you followed the full ``gh`` installation instructions you can remove the
+ ``--log`` option in the above commands.
+
+.. tip::
+
+ When the support of a Symfony branch ends, it's recommended to delete your
+ local branch to avoid merging in it unawarely:
+
+ .. code-block:: terminal
+
+ # if Symfony 3.3 goes out of maintenance today, delete your local branch
+ $ git branch -D 3.3
+
+Troubleshooting
+~~~~~~~~~~~~~~~
+
+Wrong merge of your local branch
+................................
+
+When updating your local branches before merging:
+
+.. code-block:: terminal
+
+ $ git fetch upstream
+ $ git checkout 4.4
+ $ git merge upstream/4.4
+
+It's possible that you merge a wrong upstream branch unawarely. It's usually
+easy to spot because you'll see lots of conflicts:
+
+.. code-block:: terminal
+
+ # DON'T DO THIS! It's a wrong branch merge
+ $ git checkout 4.4
+ $ git merge upstream/5.0
+
+As long as you don't push this wrong merge, there's no problem. Delete your
+local branch and check it out again:
+
+.. code-block:: terminal
+
+ $ git checkout master
+ $ git branch -D 4.4
+ $ git checkout 4.4 upstream/4.4
+
+If you did push the wrong branch merge, ask for help in the documentation
+mergers chat and we'll help solve the problem.
+
+Solving merge conflicts
+.......................
+
+When merging things to upper branches, most of the times you'll see conflicts:
+
+.. code-block:: terminal
+
+ $ git checkout 5.0
+ $ git merge upstream/5.0
+ $ git merge --log 4.4
+
+ Auto-merging security/entity_provider.rst
+ Auto-merging logging/monolog_console.rst
+ Auto-merging form/dynamic_form_modification.rst
+ Auto-merging components/phpunit_bridge.rst
+ CONFLICT (content): Merge conflict in components/phpunit_bridge.rst
+ Automatic merge failed; fix conflicts and then commit the result.
+
+Solve the conflicts with your editor (look for occurrences of ``<<<<``, which is
+the marker used by Git for conflicts) and then do this:
+
+.. code-block:: terminal
+
+ # add all the conflicting files that you fixed
+ $ git add components/phpunit_bridge.rst
+ $ git commit -a
+ $ git push origin
+ $ git push upstream
+
+.. tip::
+
+ When there are lots of conflicts, look for ``<<<<<`` with your editor in all
+ docs before committing the changes. It's common to forget about some of them.
+ If you prefer, you can run this too: ``git grep --cached "<<<<<"``.
+
+Merging deleted files
+.....................
+
+A common cause of conflict when merging PRs into upper branches are files which
+were modified by the PR but no longer exist in newer branches:
+
+.. code-block:: terminal
+
+ $ git checkout 5.0
+ $ git merge upstream/5.0
+ $ git merge --log 4.4
+
+ Auto-merging translation/debug.rst
+ CONFLICT (modify/delete): service_container/scopes.rst deleted in HEAD and
+ modified in 4.4. Version 4.4 of service_container/scopes.rst left in tree.
+ Auto-merging service_container.rst
+
+If the contents of the deleted file were moved to a different file in newer
+branches, redo the changes in the new file. Then, delete the file that Git left
+in the tree as follows:
+
+.. code-block:: terminal
+
+ # delete all the conflicting files that no longer exist in this branch
+ $ git rm service_container/scopes.rst
+ $ git commit -a
+ $ 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 -m 1
+ $ git checkout 5.x
+ $ git revert -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
+.. _`Git`: https://git-scm.com/
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 ^` where ^ 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 66e8aaab63a..ee14c191025 100644
--- a/_build/redirection_map
+++ b/_build/redirection_map
@@ -13,7 +13,7 @@
/cookbook/email /email
/cookbook/gmail /cookbook/email/gmail
/cookbook/console /components/console
-/cookbook/tools/autoloader /components/class_loader
+/cookbook/tools/autoloader https://github.com/symfony/class-loader
/cookbook/tools/finder /components/finder
/cookbook/service_container/parentservices /service_container/parent_services
/cookbook/service_container/factories /service_container/factories
@@ -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
@@ -295,15 +295,15 @@
/components/asset/introduction /components/asset
/components/browser_kit/index /components/browser_kit
/components/browser_kit/introduction /components/browser_kit
-/components/class_loader/introduction /components/class_loader
-/components/class_loader/index /components/class_loader
-/components/class_loader/cache_class_loader /components/class_loader
-/components/class_loader/class_loader /components/class_loader
-/components/class_loader/class_map_generator /components/class_loader
-/components/class_loader/debug_class_loader /components/class_loader
-/components/class_loader/map_class_loader /components/class_loader
-/components/class_loader/map_class_loader /components/class_loader
-/components/class_loader/psr4_class_loader /components/class_loader
+/components/class_loader/introduction https://github.com/symfony/class-loader
+/components/class_loader/index https://github.com/symfony/class-loader
+/components/class_loader/cache_class_loader https://github.com/symfony/class-loader
+/components/class_loader/class_loader https://github.com/symfony/class-loader
+/components/class_loader/class_map_generator https://github.com/symfony/class-loader
+/components/class_loader/debug_class_loader https://github.com/symfony/class-loader
+/components/class_loader/map_class_loader https://github.com/symfony/class-loader
+/components/class_loader/map_class_loader https://github.com/symfony/class-loader
+/components/class_loader/psr4_class_loader https://github.com/symfony/class-loader
/components/config/introduction /components/config
/components/config/index /components/config
/components/console/helpers/tablehelper /components/console/helpers/table
@@ -345,15 +345,15 @@
/components/http_kernel/index /components/http_kernel
/components/property_access/introduction /components/property_access
/components/property_access/index /components/property_access
-/components/routing/index /components/routing
-/components/routing/introduction /components/routing
+/components/routing/index https://github.com/symfony/routing
+/components/routing/introduction https://github.com/symfony/routing
/components/routing/hostname_pattern /routing/hostname_pattern
/components/security/introduction /components/security
/components/security/index /components/security
-/components/templating/introduction /components/templating
-/components/templating/index /components/templating
-/components/templating/helpers/assetshelper /components/templating/assetshelper
-/components/templating/helpers/slotshelper /components/templating/slotshelper
+/components/templating/introduction https://github.com/symfony/templating
+/components/templating/index https://github.com/symfony/templating
+/components/templating/helpers/assetshelper https://github.com/symfony/templating
+/components/templating/helpers/slotshelper https://github.com/symfony/templating
/components/translation/introduction /components/translation
/components/translation/index /components/translation
/components/var_dumper/introduction /components/var_dumper
@@ -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
@@ -399,7 +402,7 @@
/security/acl_advanced /security/acl
/security/password_encoding /security
/weblink /web_link
-/components/weblink /components/web_link
+/components/weblink https://github.com/symfony/web-link
/frontend/encore/installation-no-flex /frontend/encore/installation
/http_cache/form_csrf_caching /security/csrf
/console/logging /console
@@ -411,3 +414,163 @@
/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
+/workflow/state-machines /workflow/workflow-and-state-machine
+/workflow/introduction /workflow/workflow-and-state-machine
+/workflow/usage /workflow
+/introduction/from_flat_php_to_symfony2 /introduction/from_flat_php_to_symfony
+/configuration/environment_variables /configuration/env_var_processors
+/configuration/configuration_organization /configuration
+/configuration/environments /configuration
+/configuration/configuration_organization /configuration
+/email/dev_environment /mailer
+/email/spool /mailer
+/email/testing /mailer
+/contributing/community/other /contributing/community
+/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
+/service_container/parameters /configuration
+/routing/generate_url_javascript /routing
+/routing/slash_in_parameter /routing
+/routing/scheme /routing
+/routing/optional_placeholders /routing
+/routing/conditions /routing
+/routing/requirements /routing
+/routing/redirect_trailing_slash /routing
+/routing/debug /routing
+/routing/service_container_parameters /routing
+/routing/redirect_in_config /routing
+/routing/external_resources /routing
+/routing/hostname_pattern /routing
+/routing/extra_information /routing
+/console/request_context /routing
+/form/action_method /forms
+/reference/requirements /setup
+/bundles/inheritance /bundles/override
+/templating /templates
+/templating/escaping /templates#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#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
+/best_practices/index /best_practices
+/best_practices/introduction /best_practices
+/best_practices/creating-the-project /best_practices
+/best_practices/configuration /best_practices
+/best_practices/business-logic /best_practices
+/best_practices/controllers /best_practices
+/best_practices/templates /best_practices
+/best_practices/forms /best_practices
+/best_practices/i18n /best_practices
+/best_practices/security /best_practices
+/best_practices/web-assets /best_practices
+/best_practices/tests /best_practices
+/components/debug https://github.com/symfony/debug
+/components/translation https://github.com/symfony/translation
+/components/translation/usage /translation
+/components/translation/custom_formats https://github.com/symfony/translation
+/components/translation/custom_message_formatter https://github.com/symfony/translation
+/components/notifier https://github.com/symfony/notifier
+/components/routing https://github.com/symfony/routing
+/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
+/components/polyfill_ctype https://github.com/symfony/polyfill-ctype
+/components/polyfill_iconv https://github.com/symfony/polyfill-iconv
+/components/polyfill_intl_grapheme https://github.com/symfony/polyfill_intl-grapheme
+/components/polyfill_intl_icu https://github.com/symfony/polyfill_intl-icu
+/components/polyfill_intl_idn https://github.com/symfony/polyfill_intl-idn
+/components/polyfill_intl_normalizer https://github.com/symfony/polyfill_intl-normalizer
+/components/polyfill_mbstring https://github.com/symfony/polyfill-mbstring
+/components/polyfill_php54 https://github.com/symfony/polyfill-php54
+/components/polyfill_php55 https://github.com/symfony/polyfill-php55
+/components/polyfill_php56 https://github.com/symfony/polyfill-php56
+/components/polyfill_php70 https://github.com/symfony/polyfill-php70
+/components/polyfill_php71 https://github.com/symfony/polyfill-php71
+/components/polyfill_php72 https://github.com/symfony/polyfill-php72
+/components/polyfill_php73 https://github.com/symfony/polyfill-php73
+/components/polyfill_uuid https://github.com/symfony/polyfill-uuid
+/components/web_link https://github.com/symfony/web-link
+/components/templating https://github.com/symfony/templating
+/components/error_handler https://github.com/symfony/error-handler
+/components/class_loader https://github.com/symfony/class-loader
+/frontend/encore/versus-assetic /frontend
+/components/http_client /http_client
+/components/mailer /mailer
+/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
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 @@
-
+
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 @@
-
diff --git a/_images/components/string/bytes-points-graphemes.png b/_images/components/string/bytes-points-graphemes.png
new file mode 100644
index 00000000000..18d971cecf7
Binary files /dev/null and b/_images/components/string/bytes-points-graphemes.png differ
diff --git a/_images/components/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/pull_request.png b/_images/components/workflow/pull_request.png
index 1aa8886728c..692a95345ae 100644
Binary files a/_images/components/workflow/pull_request.png and b/_images/components/workflow/pull_request.png differ
diff --git a/_images/components/workflow/pull_request_puml_styled.png b/_images/components/workflow/pull_request_puml_styled.png
new file mode 100644
index 00000000000..cda9233d731
Binary files /dev/null and b/_images/components/workflow/pull_request_puml_styled.png differ
diff --git a/_images/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/code/stack-trace.gif b/_images/contributing/code/stack-trace.gif
new file mode 100644
index 00000000000..97a2043448d
Binary files /dev/null and b/_images/contributing/code/stack-trace.gif 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/errors-in-prod-environment.png b/_images/controller/error_pages/errors-in-prod-environment.png
index 79fe5341b47..808d0d70028 100644
Binary files a/_images/controller/error_pages/errors-in-prod-environment.png and b/_images/controller/error_pages/errors-in-prod-environment.png differ
diff --git a/_images/controller/error_pages/exceptions-in-dev-environment.png b/_images/controller/error_pages/exceptions-in-dev-environment.png
index 5e7da2cf6a1..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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
diff --git a/_images/form/form-custom-type-postal-address-fragment-names.svg b/_images/form/form-custom-type-postal-address-fragment-names.svg
new file mode 100644
index 00000000000..db9463b8327
--- /dev/null
+++ b/_images/form/form-custom-type-postal-address-fragment-names.svg
@@ -0,0 +1 @@
+
diff --git a/_images/form/form-custom-type-postal-address.svg b/_images/form/form-custom-type-postal-address.svg
new file mode 100644
index 00000000000..42ffce4067f
--- /dev/null
+++ b/_images/form/form-custom-type-postal-address.svg
@@ -0,0 +1 @@
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 58edf13f3f3..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 @@
+
+
diff --git a/_images/http/xkcd-request.png b/_images/http/xkcd-request.png
deleted file mode 100644
index 86e767db9b5..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 @@
+
+
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 @@
+
+
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 @@
+
+
diff --git a/_images/mercure/panel.png b/_images/mercure/panel.png
new file mode 100644
index 00000000000..22b214f5ff2
Binary files /dev/null and b/_images/mercure/panel.png differ
diff --git a/_images/mercure/schema.png b/_images/mercure/schema.png
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 8c8c4d508d1..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 @@
+
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 @@
+
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 @@
+
diff --git a/_images/release-process.jpg b/_images/release-process.jpg
deleted file mode 100644
index 9868404b07f..00000000000
Binary files a/_images/release-process.jpg and /dev/null differ
diff --git a/_images/security/anonymous_wdt.png b/_images/security/anonymous_wdt.png
index 8dbf1cd8298..80736afce39 100644
Binary files a/_images/security/anonymous_wdt.png and b/_images/security/anonymous_wdt.png differ
diff --git a/_images/security/login_link_email.png b/_images/security/login_link_email.png
new file mode 100644
index 00000000000..8331b878f68
Binary files /dev/null and b/_images/security/login_link_email.png differ
diff --git a/_images/security/profiler-badges.png b/_images/security/profiler-badges.png
new file mode 100644
index 00000000000..a19f8539581
Binary files /dev/null and b/_images/security/profiler-badges.png differ
diff --git a/_images/security/security_events.svg b/_images/security/security_events.svg
new file mode 100644
index 00000000000..f1b93923da6
--- /dev/null
+++ b/_images/security/security_events.svg
@@ -0,0 +1,338 @@
+
+
diff --git a/_images/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 @@
+
+
diff --git a/_images/sources/README.md b/_images/sources/README.md
new file mode 100644
index 00000000000..84810a9783d
--- /dev/null
+++ b/_images/sources/README.md
@@ -0,0 +1,102 @@
+How to Create Symfony Images
+============================
+
+Creating Diagrams
+-----------------
+
+* Use [Dia][1] as the diagramming application;
+* Use [PT Sans Narrow][2] as the only font in all diagrams (if possible, use
+ only the "normal" weight for all contents);
+* Use 36pt as the base font size;
+* Use 0.10 cm width for lines and shape borders;
+* Use the following color palette:
+ * Text, lines and shape borders: black (#000000)
+ * Shape backgrounds:
+ * Grays: dark (#4d4d4d), medium (#b3b3b3), light (#f2f2f2)
+ * Blue: #b2d4eb
+ * Red: #ecbec0
+ * Green: #b2dec7
+ * Orange: #fddfbb
+
+In case of doubt, check the existing diagrams or ask to the
+[Symfony Documentation Team][3].
+
+### Saving and Exporting the Diagram
+
+* Save the original diagram in `*.dia` format in `_images/sources/`;
+* Export the diagram to SVG format and save it in `_images/`.
+
+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
+
+
+```
+
+### 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
+
+* On some macOS systems, Dia cannot be executed as a regular application and
+ you must run the following console command instead:
+ `export DISPLAY=:0 && /Applications/Dia.app/Contents/Resources/bin/dia`
+
+Creating Console Screenshots
+----------------------------
+
+* Use [Asciinema][4] to record the console session locally:
+
+ ```
+ $ asciinema rec -c bash recording.cast
+ ```
+* Use `$ ` as the prompt in recordings. E.g. if you're using Bash, add the
+ following lines to your ``.bashrc``:
+
+ ```
+ if [ "$ASCIINEMA_REC" = "1" ]; then
+ PS1="\e[37m$ \e[0m"
+ fi
+ ```
+* Save the generated asciicast in `_images/sources/`.
+
+### Rendering the Recording
+
+Rendering the recording can be a difficult task. The [documentation team][3]
+is always ready to help you with this task (e.g. you can open a PR with
+only the asciicast file).
+
+* Use [agg][5] to generated a GIF file from the recording;
+* Install the [JetBrains Mono][6] font;
+* Use the ``_images/sources/ascii-render.sh`` file to call agg:
+
+ ```
+ AGG_PATH=/path/to/agg ./_images/sources/ascii-render.sh recording.cast --cols 45 --rows 20
+ ```
+
+ This utility configures a predefined theme;
+* Always configure `--cols`` (width) and ``--rows`` (height), try to use as
+ low as possible numbers. Do not exceed 70 columns;
+* Save the generated GIF file in `_images/`.
+
+[1]: http://dia-installer.de/
+[2]: https://fonts.google.com/specimen/PT+Sans+Narrow
+[3]: https://symfony.com/doc/current/contributing/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
new file mode 100644
index 00000000000..ca12fcdeadc
Binary files /dev/null and b/_images/sources/form/form-custom-type-postal-address-fragment-names.dia differ
diff --git a/_images/sources/form/form-custom-type-postal-address.dia b/_images/sources/form/form-custom-type-postal-address.dia
new file mode 100644
index 00000000000..1b7c6226315
Binary files /dev/null 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 ed43b8f51d8..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
-
-
-
-
-
-
-
- sendmail
-
-
-
-
- .. 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
new file mode 100644
index 00000000000..6211d042f0b
--- /dev/null
+++ b/best_practices.rst
@@ -0,0 +1,456 @@
+The Symfony Framework Best Practices
+====================================
+
+This article describes the **best practices for developing web applications with
+Symfony** that fit the philosophy envisioned by the original Symfony creators.
+
+If you don't agree with some of these recommendations, they might be a good
+**starting point** that you can then **extend and fit to your specific needs**.
+You can even ignore them completely and continue using your own best practices
+and methodologies. Symfony is flexible enough to adapt to your needs.
+
+This article assumes that you already have experience developing Symfony
+applications. If you don't, read first the :doc:`Getting Started `
+section of the documentation.
+
+.. tip::
+
+ Symfony provides a sample application called `Symfony Demo`_ that follows
+ all these best practices, so you can experience them in practice.
+
+Creating the Project
+--------------------
+
+Use the Symfony Binary to Create Symfony Applications
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The Symfony binary is an executable command created in your machine when you
+`download Symfony`_. It provides multiple utilities, including the simplest way
+to create new Symfony applications:
+
+.. code-block:: terminal
+
+ $ symfony new my_project_directory
+
+Under the hood, this Symfony binary command executes the needed `Composer`_
+command to :ref:`create a new Symfony application `
+based on the current stable version.
+
+Use the Default Directory Structure
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Unless your project follows a development practice that imposes a certain
+directory structure, follow the default Symfony directory structure. It's flat,
+self-explanatory and not coupled to Symfony:
+
+.. code-block:: text
+
+ your_project/
+ ├─ assets/
+ ├─ bin/
+ │ └─ console
+ ├─ config/
+ │ ├─ packages/
+ │ ├─ routes/
+ │ └─ services.yaml
+ ├─ migrations/
+ ├─ public/
+ │ ├─ build/
+ │ └─ index.php
+ ├─ src/
+ │ ├─ Kernel.php
+ │ ├─ Command/
+ │ ├─ Controller/
+ │ ├─ DataFixtures/
+ │ ├─ Entity/
+ │ ├─ EventSubscriber/
+ │ ├─ Form/
+ │ ├─ Repository/
+ │ ├─ Security/
+ │ └─ Twig/
+ ├─ templates/
+ ├─ tests/
+ ├─ translations/
+ ├─ var/
+ │ ├─ cache/
+ │ └─ log/
+ └─ vendor/
+
+Configuration
+-------------
+
+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
+application behavior.
+
+:ref:`Use env vars in your project ` to define these options
+and create multiple ``.env`` files to :ref:`configure env vars per environment `.
+
+.. _use-secret-for-sensitive-information:
+
+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 `.
+
+Use Parameters for Application Configuration
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+These are the options used to modify the application behavior, such as the sender
+of email notifications, or the enabled `feature toggles`_. Their value doesn't
+change per machine, so don't define them as environment variables.
+
+Define these options as :ref:`parameters ` in the
+``config/services.yaml`` file. You can override these options per
+:ref:`environment ` in the ``config/services_dev.yaml``
+and ``config/services_prod.yaml`` files.
+
+Unless the application configuration is reused multiple times and needs
+rigid validation, do *not* use the :doc:`Config component `
+to define the options.
+
+Use Short and Prefixed Parameter Names
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Consider using ``app.`` as the prefix of your :ref:`parameters `
+to avoid collisions with Symfony and third-party bundles/libraries parameters.
+Then, use just one or two words to describe the purpose of the parameter:
+
+.. code-block:: yaml
+
+ # config/services.yaml
+ parameters:
+ # don't do this: 'dir' is too generic, and it doesn't convey any meaning
+ app.dir: '...'
+ # do this: short but easy to understand names
+ app.contents_dir: '...'
+ # it's OK to use dots, underscores, dashes or nothing, but always
+ # be consistent and use the same format for all the parameters
+ app.dir.contents: '...'
+ app.contents-dir: '...'
+
+Use Constants to Define Options that Rarely Change
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Configuration options like the number of items to display in some listing rarely
+change. Instead of defining them as :ref:`configuration parameters `,
+define them as PHP constants in the related classes. Example::
+
+ // src/Entity/Post.php
+ namespace App\Entity;
+
+ class Post
+ {
+ public const NUMBER_OF_ITEMS = 10;
+
+ // ...
+ }
+
+The main advantage of constants is that you can use them everywhere, including
+Twig templates and Doctrine entities, whereas parameters are only available
+from places with access to the :doc:`service container `.
+
+The only notable disadvantage of using constants for this kind of configuration
+values is that it's complicated to redefine their values in your tests.
+
+Business Logic
+--------------
+
+.. _best-practice-no-application-bundles:
+
+Don't Create any Bundle to Organize your Application Logic
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When Symfony 2.0 was released, applications used :doc:`bundles ` to
+divide their code into logical features: UserBundle, ProductBundle,
+InvoiceBundle, etc. However, a bundle is meant to be something that can be
+reused as a stand-alone piece of software.
+
+If you need to reuse some feature in your projects, create a bundle for it (in a
+private repository, 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
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+:doc:`Service autowiring ` is a feature that
+reads the type-hints on your constructor (or other methods) and automatically
+passes the correct services to each method, making it unnecessary to configure
+services explicitly and simplifying the application maintenance.
+
+Use it in combination with :ref:`service autoconfiguration `
+to also add :doc:`service tags ` to the services
+needing them, such as Twig extensions, event subscribers, etc.
+
+Services Should be Private Whenever Possible
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+:ref:`Make services private ` to prevent you from accessing
+those services via ``$container->get()``. Instead, you will need to use proper
+dependency injection.
+
+Use the YAML Format to Configure your own Services
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you use the :ref:`default services.yaml configuration `,
+most services will be configured automatically. However, in some edge cases
+you'll need to configure services (or parts of them) manually.
+
+YAML is the format recommended configuring services because it's friendly to
+newcomers and concise, but Symfony also supports XML and PHP configuration.
+
+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 PHP
+attributes because they are by far the most convenient and agile way of setting
+up and looking for mapping information.
+
+Controllers
+-----------
+
+Make your Controller Extend the ``AbstractController`` Base Controller
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Symfony provides a :ref:`base controller `
+which includes shortcuts for the most common needs such as rendering templates
+or checking security permissions.
+
+Extending your controllers from this base controller couples your application
+to Symfony. Coupling is generally wrong, but it may be OK in this case because
+controllers shouldn't contain any business logic. Controllers should contain
+nothing more than a few lines of *glue-code*, so you are not coupling the
+important parts of your application.
+
+.. _best-practice-controller-annotations:
+.. _best-practice-controller-attributes:
+
+Use Attributes to Configure Routing, Caching, and Security
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+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 get access to the most
+common services (e.g ``twig``, ``router``, ``doctrine``, etc.), directly from the
+container via ``$this->container->get()``.
+Instead, you must use dependency injection to fetch services by
+:ref:`type-hinting action method arguments ` or
+constructor arguments.
+
+Use Entity Value Resolvers If They Are Convenient
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you're using :doc:`Doctrine `, then you can *optionally* use
+the :ref:`EntityValueResolver ` 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 EntityValueResolver, it's better to make the Doctrine query
+inside the controller (e.g. by calling to a :doc:`Doctrine repository method `).
+
+Templates
+---------
+
+Use Snake Case for Template Names and Variables
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Use 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``).
+
+Prefix Template Fragments with an Underscore
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Template fragments, also called *"partial templates"*, allow to
+:ref:`reuse template contents `. Prefix their names
+with an underscore to better differentiate them from complete templates (e.g.
+``_user_metadata.html.twig`` or ``_caution_message.html.twig``).
+
+Forms
+-----
+
+Define your Forms as PHP Classes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Creating :ref:`forms in classes ` allows reusing
+them in different parts of the application. Besides, not creating forms in
+controllers simplifies the code and maintenance of the controllers.
+
+Add Form Buttons in Templates
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Form classes should be agnostic to where they will be used. For example, the
+button of a form used to both create and edit items should change from "Add new"
+to "Save changes" depending on where it's used.
+
+Instead of adding buttons in form classes or the controllers, it's recommended
+to add buttons in the templates. This also improves the separation of concerns
+because the button styling (CSS class and other attributes) is defined in the
+template instead of in a PHP class.
+
+However, if you create a :doc:`form with multiple submit buttons `
+you should define them in the controller instead of the template. Otherwise, you
+won't be able to check which button was clicked when handling the form in the controller.
+
+Define Validation Constraints on the Underlying Object
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Attaching :doc:`validation constraints ` to form fields
+instead of to the mapped object prevents the validation from being reused in
+other forms or other places where the object is used.
+
+.. _best-practice-handle-form:
+
+Use a Single Action to Render and Process the Form
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+:ref:`Rendering forms ` and :ref:`processing forms `
+are two of the main tasks when handling forms. Both are too similar (most of the
+time, almost identical), so it's much simpler to let a single controller action
+handle both.
+
+.. _best-practice-internationalization:
+
+Internationalization
+--------------------
+
+Use the XLIFF Format for Your Translation Files
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Of all the translation formats supported by Symfony (PHP, Qt, ``.po``, ``.mo``,
+JSON, CSV, INI, etc.), ``XLIFF`` and ``gettext`` have the best support in the tools used
+by professional translators. And since it's based on XML, you can validate ``XLIFF``
+file contents as you write them.
+
+Symfony also supports notes in XLIFF files, making them more user-friendly for
+translators. At the end, good translations are all about context, and these
+XLIFF notes allow you to define that context.
+
+Use Keys for Translations Instead of Content Strings
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Using keys simplifies the management of the translation files because you can
+change the original contents in templates, controllers, and services without
+having to update all the translation files.
+
+Keys should always describe their *purpose* and *not* their location. For
+example, if a form has a field with the label "Username", then a nice key
+would be ``label.username``, *not* ``edit_form.label.username``.
+
+Security
+--------
+
+Define a Single Firewall
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Unless you have two legitimately different authentication systems and users
+(e.g. form login for the main site and a token system for your API only), it's
+recommended to have only one firewall to keep things simple.
+
+Use the ``auto`` Password Hasher
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The :ref:`auto password hasher ` automatically
+selects the best possible encoder/hasher depending on your PHP installation.
+Currently, the default auto hasher is ``bcrypt``.
+
+Use Voters to Implement Fine-grained Security Restrictions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If your security logic is complex, you should create custom
+:doc:`security voters ` instead of defining long expressions
+inside the ``#[Security]`` attribute.
+
+Web Assets
+----------
+
+.. _use-webpack-encore-to-process-web-assets:
+
+Use AssetMapper to Manage Web Assets
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Web assets are the CSS, JavaScript, and image files that make the frontend of
+your site look and work great. :doc:`AssetMapper ` lets
+you write modern JavaScript and CSS without the complexity of using a bundler
+such as `Webpack`_ (directly or via :doc:`Webpack Encore `).
+
+Tests
+-----
+
+Smoke Test your URLs
+~~~~~~~~~~~~~~~~~~~~
+
+In software engineering, `smoke testing`_ consists of *"preliminary testing to
+reveal simple failures severe enough to reject a prospective software release"*.
+Using `PHPUnit data providers`_ you can define a functional test that
+checks that all application URLs load successfully::
+
+ // tests/ApplicationAvailabilityFunctionalTest.php
+ namespace App\Tests;
+
+ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
+
+ class ApplicationAvailabilityFunctionalTest extends WebTestCase
+ {
+ /**
+ * @dataProvider urlProvider
+ */
+ public function testPageIsSuccessful($url): void
+ {
+ $client = self::createClient();
+ $client->request('GET', $url);
+
+ $this->assertResponseIsSuccessful();
+ }
+
+ public function urlProvider(): \Generator
+ {
+ yield ['/'];
+ yield ['/posts'];
+ yield ['/post/fixture-post-1'];
+ yield ['/blog/category/fixture-category'];
+ yield ['/archives'];
+ // ...
+ }
+ }
+
+Add this test while creating your application because it requires little effort
+and checks that none of your pages returns an error. Later, you'll add more
+specific tests for each page.
+
+.. _hardcode-urls-in-a-functional-test:
+
+Hard-code URLs in a Functional Test
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In Symfony applications, it's recommended to :ref:`generate URLs `
+using routes to automatically update all links when a URL changes. However, if a
+public URL changes, users won't be able to browse it unless you set up a
+redirection to the new URL.
+
+That's why it's recommended to use raw URLs in tests instead of generating them
+from routes. Whenever a route changes, tests will 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/
+.. _`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/best_practices/business-logic.rst b/best_practices/business-logic.rst
deleted file mode 100644
index 8feb8163bf7..00000000000
--- a/best_practices/business-logic.rst
+++ /dev/null
@@ -1,273 +0,0 @@
-Organizing Your Business Logic
-==============================
-
-In computer software, **business logic** or domain logic is "the part of the
-program that encodes the real-world business rules that determine how data can
-be created, displayed, stored, and changed" (read `full definition`_).
-
-In Symfony applications, business logic is all the custom code you write for
-your app that's not specific to the framework (e.g. routing and controllers).
-Domain classes, Doctrine entities and regular PHP classes that are used as
-services are good examples of business logic.
-
-For most projects, you should store all your code inside the ``src/`` directory.
-Inside here, you can create whatever directories you want to organize things:
-
-.. code-block:: text
-
- symfony-project/
- ├─ config/
- ├─ public/
- ├─ src/
- │ └─ Utils/
- │ └─ MyClass.php
- ├─ tests/
- ├─ var/
- └─ vendor/
-
-.. _services-naming-and-format:
-
-Services: Naming and Configuration
-----------------------------------
-
-.. best-practice::
-
- Use autowiring to automate the configuration of application services.
-
-:doc:`Service autowiring ` is a feature provided
-by Symfony's Service Container to manage services with minimal configuration. It
-reads the type-hints on your constructor (or other methods) and automatically
-passes the correct services to each method. It can also add
-:doc:`service tags ` to the services needing them, such
-as Twig extensions, event subscribers, etc.
-
-The blog application needs a utility that can transform a post title (e.g.
-"Hello World") into a slug (e.g. "hello-world") to include it as part of the
-post URL. Let's create a new ``Slugger`` class inside ``src/Utils/``::
-
- // src/Utils/Slugger.php
- namespace App\Utils;
-
- class Slugger
- {
- public function slugify(string $value): string
- {
- // ...
- }
- }
-
-If you're using the :ref:`default services.yaml configuration `,
-this class is auto-registered as a service with the ID ``App\Utils\Slugger`` (to
-prevent against typos, import the class and write ``Slugger::class`` in your code).
-
-.. best-practice::
-
- The id of your application's services should be equal to their class name,
- except when you have multiple services configured for the same class (in that
- case, use a snake case id).
-
-Now you can use the custom slugger in any other service or controller class,
-such as the ``AdminController``::
-
- use App\Utils\Slugger;
-
- public function create(Request $request, Slugger $slugger)
- {
- // ...
-
- if ($form->isSubmitted() && $form->isValid()) {
- $slug = $slugger->slugify($post->getTitle());
- $post->setSlug($slug);
-
- // ...
- }
- }
-
-Services can also be :ref:`public or private `. If you use the
-:ref:`default services.yaml configuration `,
-all services are private by default.
-
-.. best-practice::
-
- Services should be ``private`` whenever possible. This will prevent you from
- accessing that service via ``$container->get()``. Instead, you will need to use
- dependency injection.
-
-Service Format: YAML
---------------------
-
-If you use the :ref:`default services.yaml configuration `,
-most services will be configured automatically. However, in some edge cases
-you'll need to configure services (or parts of them) manually.
-
-.. best-practice::
-
- Use the YAML format to configure your own services.
-
-This is controversial, and in our experience, YAML and XML usage is evenly
-distributed among developers, with a slight preference towards YAML.
-Both formats have the same performance, so this is ultimately a matter of
-personal taste.
-
-We recommend YAML because it's friendly to newcomers and concise. You can
-use any of the other formats if you prefer another format.
-
-Using a Persistence Layer
--------------------------
-
-Symfony is an HTTP framework that only cares about generating an HTTP response
-for each HTTP request. That's why Symfony doesn't provide a way to talk to
-a persistence layer (e.g. database, external API). You can choose whatever
-library or strategy you want for this.
-
-In practice, many Symfony applications rely on the independent
-`Doctrine project`_ to define their model using entities and repositories.
-Just like with business logic, we recommend storing Doctrine entities in the
-``src/Entity/`` directory.
-
-The three entities defined by our sample blog application are a good example:
-
-.. code-block:: text
-
- symfony-project/
- ├─ ...
- └─ src/
- └─ Entity/
- ├─ Comment.php
- ├─ Post.php
- └─ User.php
-
-Doctrine Mapping Information
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Doctrine entities are plain PHP objects that you store in some "database".
-Doctrine only knows about your entities through the mapping metadata configured
-for your model classes. Doctrine supports four metadata formats: YAML, XML,
-PHP and annotations.
-
-.. best-practice::
-
- Use annotations to define the mapping information of the Doctrine entities.
-
-Annotations are by far the most convenient and agile way of setting up and
-looking for mapping information::
-
- namespace App\Entity;
-
- use Doctrine\ORM\Mapping as ORM;
- use Doctrine\Common\Collections\ArrayCollection;
-
- /**
- * @ORM\Entity
- */
- class Post
- {
- const NUMBER_OF_ITEMS = 10;
-
- /**
- * @ORM\Id
- * @ORM\GeneratedValue
- * @ORM\Column(type="integer")
- */
- private $id;
-
- /**
- * @ORM\Column(type="string")
- */
- private $title;
-
- /**
- * @ORM\Column(type="string")
- */
- private $slug;
-
- /**
- * @ORM\Column(type="text")
- */
- private $content;
-
- /**
- * @ORM\Column(type="string")
- */
- private $authorEmail;
-
- /**
- * @ORM\Column(type="datetime")
- */
- private $publishedAt;
-
- /**
- * @ORM\OneToMany(
- * targetEntity="App\Entity\Comment",
- * mappedBy="post",
- * orphanRemoval=true
- * )
- * @ORM\OrderBy({"publishedAt"="ASC"})
- */
- private $comments;
-
- public function __construct()
- {
- $this->publishedAt = new \DateTime();
- $this->comments = new ArrayCollection();
- }
-
- // getters and setters ...
- }
-
-All formats have the same performance, so this is once again ultimately a
-matter of taste.
-
-Data Fixtures
-~~~~~~~~~~~~~
-
-As fixtures support is not enabled by default in Symfony, you should execute
-the following command to install the Doctrine fixtures bundle:
-
-.. code-block:: terminal
-
- $ composer require "doctrine/doctrine-fixtures-bundle"
-
-Then, this bundle is enabled automatically, but only for the ``dev`` and
-``test`` environments::
-
- // config/bundles.php
- return [
- // ...
- Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
- ];
-
-We recommend creating just *one* `fixture class`_ for simplicity, though
-you're welcome to have more if that class gets quite large.
-
-Assuming you have at least one fixtures class and that the database access
-is configured properly, you can load your fixtures by executing the following
-command:
-
-.. code-block:: terminal
-
- $ php bin/console doctrine:fixtures:load
-
- Careful, database will be purged. Do you want to continue Y/N ? Y
- > purging database
- > loading App\DataFixtures\ORM\LoadFixtures
-
-Coding Standards
-----------------
-
-The Symfony source code follows the `PSR-1`_ and `PSR-2`_ coding standards that
-were defined by the PHP community. You can learn more about
-:doc:`the Symfony Coding standards ` and even
-use the `PHP-CS-Fixer`_, which is a command-line utility that can fix the
-coding standards of an entire codebase in a matter of seconds.
-
-----
-
-Next: :doc:`/best_practices/controllers`
-
-.. _`full definition`: https://en.wikipedia.org/wiki/Business_logic
-.. _`Doctrine project`: http://www.doctrine-project.org/
-.. _`fixture class`: https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html#writing-simple-fixtures
-.. _`PSR-1`: https://www.php-fig.org/psr/psr-1/
-.. _`PSR-2`: https://www.php-fig.org/psr/psr-2/
-.. _`PHP-CS-Fixer`: https://github.com/FriendsOfPHP/PHP-CS-Fixer
diff --git a/best_practices/configuration.rst b/best_practices/configuration.rst
deleted file mode 100644
index d5d0f443673..00000000000
--- a/best_practices/configuration.rst
+++ /dev/null
@@ -1,190 +0,0 @@
-Configuration
-=============
-
-Configuration usually involves different application parts (such as infrastructure
-and security credentials) and different environments (development, production).
-That's why Symfony recommends that you split the application configuration into
-three parts.
-
-Infrastructure-Related Configuration
-------------------------------------
-
-These are the options that change from one machine to another (e.g. from your
-development machine to the production server) but which don't change the
-application behavior.
-
-.. best-practice::
-
- Define the infrastructure-related configuration options as
- :doc:`environment variables `. During
- development, use the ``.env`` and ``.env.local`` files at the root of your project
- to set these.
-
-By default, Symfony adds these types of options to the ``.env`` file when
-installing new dependencies in the app:
-
-.. code-block:: bash
-
- # .env
- ###> doctrine/doctrine-bundle ###
- DATABASE_URL=sqlite:///%kernel.project_dir%/var/data/blog.sqlite
- ###< doctrine/doctrine-bundle ###
-
- ###> symfony/swiftmailer-bundle ###
- MAILER_URL=smtp://localhost?encryption=ssl&auth_mode=login&username=&password=
- ###< symfony/swiftmailer-bundle ###
-
- # ...
-
-These options aren't defined inside the ``config/services.yaml`` file because
-they have nothing to do with the application's behavior. In other words, your
-application doesn't care about the location of your database or the credentials
-to access to it, as long as the database is correctly configured.
-
-To override these variables with machine-specific or sensitive values, create a
-``.env.local`` file. This file should not be added to version control.
-
-.. caution::
-
- Beware that dumping the contents of the ``$_SERVER`` and ``$_ENV`` variables
- or outputting the ``phpinfo()`` contents will display the values of the
- environment variables, exposing sensitive information such as the database
- credentials.
-
-Canonical Parameters
-~~~~~~~~~~~~~~~~~~~~
-
-.. best-practice::
-
- Define all your application's env vars in the ``.env`` file.
-
-Symfony includes a configuration file called ``.env`` at the project root, which
-stores the canonical list of environment variables for the application. This
-file should be stored in version control and so should only contain non-sensitive
-default values.
-
-.. 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`.
-
-Application-Related Configuration
----------------------------------
-
-.. best-practice::
-
- Define the application behavior related configuration options in the
- ``config/services.yaml`` file.
-
-The ``services.yaml`` file contains the options used by the application to
-modify its behavior, such as the sender of email notifications, or the enabled
-`feature toggles`_. Defining these values in ``.env`` file would add an extra
-layer of configuration that's not needed because you don't need or want these
-configuration values to change on each server.
-
-The configuration options defined in the ``services.yaml`` may vary from one
-:doc:`environment ` to another. That's why Symfony
-supports defining ``config/services_dev.yaml`` and ``config/services_prod.yaml``
-files so that you can override specific values for each environment.
-
-Constants vs Configuration Options
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-One of the most common errors when defining application configuration is to
-create new options for values that never change, such as the number of items for
-paginated results.
-
-.. best-practice::
-
- Use constants to define configuration options that rarely change.
-
-The traditional approach for defining configuration options has caused many
-Symfony apps to include an option like the following, which would be used
-to control the number of posts to display on the blog homepage:
-
-.. code-block:: yaml
-
- # config/services.yaml
- parameters:
- homepage.number_of_items: 10
-
-If you've done something like this in the past, it's likely that you've in fact
-*never* actually needed to change that value. Creating a configuration
-option for a value that you are never going to configure just isn't necessary.
-Our recommendation is to define these values as constants in your application.
-You could, for example, define a ``NUMBER_OF_ITEMS`` constant in the ``Post`` entity::
-
- // src/Entity/Post.php
- namespace App\Entity;
-
- class Post
- {
- const NUMBER_OF_ITEMS = 10;
-
- // ...
- }
-
-The main advantage of defining constants is that you can use their values
-everywhere in your application. When using parameters, they are only available
-from places with access to the Symfony container.
-
-Constants can be used for example in your Twig templates thanks to the
-`constant() function`_:
-
-.. code-block:: html+twig
-
-
- Displaying the {{ constant('NUMBER_OF_ITEMS', post) }} most recent results.
-
-
-And Doctrine entities and repositories can now easily access these values,
-whereas they cannot access the container parameters::
-
- namespace App\Repository;
-
- use App\Entity\Post;
- use Doctrine\ORM\EntityRepository;
-
- class PostRepository extends EntityRepository
- {
- public function findLatest($limit = Post::NUMBER_OF_ITEMS)
- {
- // ...
- }
- }
-
-The only notable disadvantage of using constants for this kind of configuration
-values is that you cannot redefine them easily in your tests.
-
-Parameter Naming
-----------------
-
-.. best-practice::
-
- The name of your configuration parameters should be as short as possible and
- should include a common prefix for the entire application.
-
-Using ``app.`` as the prefix of your parameters is a common practice to avoid
-collisions with Symfony and third-party bundles/libraries parameters. Then, use
-just one or two words to describe the purpose of the parameter:
-
-.. code-block:: yaml
-
- # config/services.yaml
- parameters:
- # don't do this: 'dir' is too generic and it doesn't convey any meaning
- app.dir: '...'
- # do this: short but easy to understand names
- app.contents_dir: '...'
- # it's OK to use dots, underscores, dashes or nothing, but always
- # be consistent and use the same format for all the parameters
- app.dir.contents: '...'
- app.contents-dir: '...'
-
-----
-
-Next: :doc:`/best_practices/business-logic`
-
-.. _`feature toggles`: https://en.wikipedia.org/wiki/Feature_toggle
-.. _`constant() function`: https://twig.symfony.com/doc/2.x/functions/constant.html
diff --git a/best_practices/controllers.rst b/best_practices/controllers.rst
deleted file mode 100644
index 0ef6fd1d3bc..00000000000
--- a/best_practices/controllers.rst
+++ /dev/null
@@ -1,236 +0,0 @@
-Controllers
-===========
-
-Symfony follows the philosophy of *"thin controllers and fat models"*. This
-means that controllers should hold just the thin layer of *glue-code*
-needed to coordinate the different parts of the application.
-
-Your controller methods should just call to other services, trigger some events
-if needed and then return a response, but they should not contain any actual
-business logic. If they do, refactor it out of the controller and into a service.
-
-.. best-practice::
-
- Make your controller extend the ``AbstractController`` base controller
- provided by Symfony and use annotations to configure routing, caching and
- security whenever possible.
-
-Coupling the controllers to the underlying framework allows you to leverage
-all of its features and increases your productivity.
-
-And since your controllers should be thin and contain nothing more than a
-few lines of *glue-code*, spending hours trying to decouple them from your
-framework doesn't benefit you in the long run. The amount of time *wasted*
-isn't worth the benefit.
-
-In addition, using annotations for routing, caching and security simplifies
-configuration. You don't need to browse tens of files created with different
-formats (YAML, XML, PHP): all the configuration is just where you need it
-and it only uses one format.
-
-Overall, this means you should aggressively decouple your business logic
-from the framework while, at the same time, aggressively coupling your controllers
-and routing *to* the framework in order to get the most out of it.
-
-Controller Action Naming
-------------------------
-
-.. best-practice::
-
- Don't add the ``Action`` suffix to the methods of the controller actions.
-
-The first Symfony versions required that controller method names ended in
-``Action`` (e.g. ``newAction()``, ``showAction()``). This suffix became optional
-when annotations were introduced for controllers. In modern Symfony applications
-this suffix is neither required nor recommended, so you can safely remove it.
-
-Routing Configuration
----------------------
-
-To load routes defined as annotations in your controllers, add the following
-configuration to the main routing configuration file:
-
-.. code-block:: yaml
-
- # config/routes.yaml
- controllers:
- resource: '../src/Controller/'
- type: annotation
-
-This configuration will load annotations from any controller stored inside the
-``src/Controller/`` directory and even from its subdirectories. So if your application
-defines lots of controllers, it's perfectly ok to reorganize them into subdirectories:
-
-.. code-block:: text
-
- /
- ├─ ...
- └─ src/
- ├─ ...
- └─ Controller/
- ├─ DefaultController.php
- ├─ ...
- ├─ Api/
- │ ├─ ...
- │ └─ ...
- └─ Backend/
- ├─ ...
- └─ ...
-
-Template Configuration
-----------------------
-
-.. best-practice::
-
- Don't use the ``@Template`` annotation to configure the template used by
- the controller.
-
-The ``@Template`` annotation is useful, but also involves some magic. We
-don't think its benefit is worth the magic, and so recommend against using
-it.
-
-Most of the time, ``@Template`` is used without any parameters, which makes
-it more difficult to know which template is being rendered. It also makes
-it less obvious to beginners that a controller should always return a Response
-object (unless you're using a view layer).
-
-What does the Controller look like
-----------------------------------
-
-Considering all this, here is an example of what the controller should look like
-for the homepage of our app::
-
- namespace App\Controller;
-
- use App\Entity\Post;
- use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
- use Symfony\Component\Routing\Annotation\Route;
-
- class DefaultController extends AbstractController
- {
- /**
- * @Route("/", name="homepage")
- */
- public function index()
- {
- $posts = $this->getDoctrine()
- ->getRepository(Post::class)
- ->findLatest();
-
- return $this->render('default/index.html.twig', [
- 'posts' => $posts,
- ]);
- }
- }
-
-Fetching Services
------------------
-
-If you extend the base ``AbstractController`` class, you can't access services
-directly from the container via ``$this->container->get()`` or ``$this->get()``.
-Instead, you must use dependency injection to fetch services by
-:ref:`type-hinting action method arguments `:
-
-.. best-practice::
-
- Don't use ``$this->get()`` or ``$this->container->get()`` to fetch services
- from the container. Instead, use dependency injection.
-
-By not fetching services directly from the container, you can make your services
-*private*, which has :ref:`several advantages `.
-
-.. _best-practices-paramconverter:
-
-Using the ParamConverter
-------------------------
-
-If you're using Doctrine, then you can *optionally* use the `ParamConverter`_
-to automatically query for an entity and pass it as an argument to your controller.
-
-.. best-practice::
-
- Use the ParamConverter trick to automatically query for Doctrine entities
- when it's simple and convenient.
-
-For example::
-
- use App\Entity\Post;
- use Symfony\Component\Routing\Annotation\Route;
-
- /**
- * @Route("/{id}", name="admin_post_show")
- */
- public function show(Post $post)
- {
- $deleteForm = $this->createDeleteForm($post);
-
- return $this->render('admin/post/show.html.twig', [
- 'post' => $post,
- 'delete_form' => $deleteForm->createView(),
- ]);
- }
-
-Normally, you'd expect a ``$id`` argument to ``show()``. Instead, by creating a
-new argument (``$post``) and type-hinting it with the ``Post`` class (which is a
-Doctrine entity), the ParamConverter automatically queries for an object whose
-``$id`` property matches the ``{id}`` value. It will also show a 404 page if no
-``Post`` can be found.
-
-When Things Get More Advanced
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The above example works without any configuration because the wildcard name
-``{id}`` matches the name of the property on the entity. If this isn't true, or
-if you have even more complex logic, your best choice is to query for
-the entity manually. In our application, we have this situation in
-``CommentController``::
-
- /**
- * @Route("/comment/{postSlug}/new", name="comment_new")
- */
- public function new(Request $request, $postSlug)
- {
- $post = $this->getDoctrine()
- ->getRepository(Post::class)
- ->findOneBy(['slug' => $postSlug]);
-
- if (!$post) {
- throw $this->createNotFoundException();
- }
-
- // ...
- }
-
-You can also use the ``@ParamConverter`` configuration, which is infinitely
-flexible::
-
- use App\Entity\Post;
- use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
- use Symfony\Component\HttpFoundation\Request;
- use Symfony\Component\Routing\Annotation\Route;
-
- /**
- * @Route("/comment/{postSlug}/new", name="comment_new")
- * @ParamConverter("post", options={"mapping"={"postSlug"="slug"}})
- */
- public function new(Request $request, Post $post)
- {
- // ...
- }
-
-The point is this: the ParamConverter shortcut is great for most situations.
-However, there is nothing wrong with querying for entities directly if the
-ParamConverter would get complicated.
-
-Pre and Post Hooks
-------------------
-
-If you need to execute some code before or after the execution of your controllers,
-you can use the EventDispatcher component to
-:doc:`set up before and after filters `.
-
-----
-
-Next: :doc:`/best_practices/templates`
-
-.. _`ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html
diff --git a/best_practices/creating-the-project.rst b/best_practices/creating-the-project.rst
deleted file mode 100644
index 6ea123c0b6f..00000000000
--- a/best_practices/creating-the-project.rst
+++ /dev/null
@@ -1,101 +0,0 @@
-Creating the Project
-====================
-
-Installing Symfony
-------------------
-
-.. best-practice::
-
- Use Composer and Symfony Flex to create and manage Symfony applications.
-
-`Composer`_ is the package manager used by modern PHP applications to manage
-their dependencies. `Symfony Flex`_ is a Composer plugin designed to automate
-some of the most common tasks performed in Symfony applications. Using Flex is
-optional but recommended because it improves your productivity significantly.
-
-.. best-practice::
-
- Use the Symfony Skeleton to create new Symfony-based projects.
-
-The `Symfony Skeleton`_ is a minimal and empty Symfony project which you can
-base your new projects on. Unlike past Symfony versions, this skeleton installs
-the absolute bare minimum amount of dependencies to make a fully working Symfony
-project. Read the :doc:`/setup` article to learn more about installing Symfony.
-
-.. _linux-and-mac-os-x-systems:
-.. _windows-systems:
-
-Creating the Blog Application
------------------------------
-
-In your command console, browse to a directory where you have permission to
-create files and execute the following commands:
-
-.. code-block:: terminal
-
- $ cd projects/
- $ composer create-project symfony/skeleton blog
-
-This command creates a new directory called ``blog`` that contains a fresh new
-project based on the most recent stable Symfony version available.
-
-.. tip::
-
- The technical requirements to run Symfony are simple. If you want to check
- if your system meets those requirements, read :doc:`/reference/requirements`.
-
-Structuring the Application
----------------------------
-
-After creating the application, enter the ``blog/`` directory and you'll see a
-number of files and directories generated automatically. These are the most
-important ones:
-
-.. code-block:: text
-
- blog/
- ├─ bin/
- │ └─ console
- ├─ config/
- └─ public/
- │ └─ index.php
- ├─ src/
- │ └─ Kernel.php
- ├─ var/
- │ ├─ cache/
- │ └─ log/
- └─ vendor/
-
-This file and directory hierarchy is the convention proposed by Symfony to
-structure your applications. It's recommended to keep this structure because it's
-easy to navigate and most directory names are self-explanatory, but you can
-:doc:`override the location of any Symfony directory `:
-
-Application Bundles
-~~~~~~~~~~~~~~~~~~~
-
-When Symfony 2.0 was released, most developers naturally adopted the symfony
-1.x way of dividing applications into logical modules. That's why many Symfony
-apps used bundles to divide their code into logical features: UserBundle,
-ProductBundle, InvoiceBundle, etc.
-
-But a bundle is *meant* to be something that can be reused as a stand-alone
-piece of software. If UserBundle cannot be used *"as is"* in other Symfony
-apps, then it shouldn't be its own bundle. Moreover, if InvoiceBundle depends on
-ProductBundle, then there's no advantage to having two separate bundles.
-
-.. best-practice::
-
- Don't create any bundle to organize your application logic.
-
-Symfony applications can still use third-party bundles (installed in ``vendor/``)
-to add features, but you should use PHP namespaces instead of bundles to organize
-your own code.
-
-----
-
-Next: :doc:`/best_practices/configuration`
-
-.. _`Composer`: https://getcomposer.org/
-.. _`Symfony Flex`: https://github.com/symfony/flex
-.. _`Symfony Skeleton`: https://github.com/symfony/skeleton
diff --git a/best_practices/forms.rst b/best_practices/forms.rst
deleted file mode 100644
index 31ffa4d5707..00000000000
--- a/best_practices/forms.rst
+++ /dev/null
@@ -1,221 +0,0 @@
-Forms
-=====
-
-Forms are one of the most misused Symfony components due to its vast scope and
-endless list of features. In this chapter we'll show you some of the best
-practices so you can leverage forms but get work done quickly.
-
-Building Forms
---------------
-
-.. best-practice::
-
- Define your forms as PHP classes.
-
-The Form component allows you to build forms right inside your controller code.
-This is perfectly fine if you don't need to reuse the form somewhere else. But
-for organization and reuse, we recommend that you define each form in its own
-PHP class::
-
- namespace App\Form;
-
- use App\Entity\Post;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- use Symfony\Component\Form\Extension\Core\Type\TextareaType;
- use Symfony\Component\Form\Extension\Core\Type\EmailType;
- use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
-
- class PostType extends AbstractType
- {
- public function buildForm(FormBuilderInterface $builder, array $options)
- {
- $builder
- ->add('title')
- ->add('summary', TextareaType::class)
- ->add('content', TextareaType::class)
- ->add('authorEmail', EmailType::class)
- ->add('publishedAt', DateTimeType::class)
- ;
- }
-
- public function configureOptions(OptionsResolver $resolver)
- {
- $resolver->setDefaults([
- 'data_class' => Post::class,
- ]);
- }
- }
-
-.. best-practice::
-
- Put the form type classes in the ``App\Form`` namespace, unless you
- use other custom form classes like data transformers.
-
-To use the class, use ``createForm()`` and pass the fully qualified class name::
-
- // ...
- use App\Form\PostType;
-
- // ...
- public function new(Request $request)
- {
- $post = new Post();
- $form = $this->createForm(PostType::class, $post);
-
- // ...
- }
-
-Form Button Configuration
--------------------------
-
-Form classes should try to be agnostic to *where* they will be used. This
-makes them easier to re-use later.
-
-.. best-practice::
-
- Add buttons in the templates, not in the form classes or the controllers.
-
-The Symfony Form component allows you to add buttons as fields on your form.
-This is a nice way to simplify the template that renders your form. But if you
-add the buttons directly in your form class, this would effectively limit the
-scope of that form::
-
- class PostType extends AbstractType
- {
- public function buildForm(FormBuilderInterface $builder, array $options)
- {
- $builder
- // ...
- ->add('save', SubmitType::class, ['label' => 'Create Post'])
- ;
- }
-
- // ...
- }
-
-This form *may* have been designed for creating posts, but if you wanted
-to reuse it for editing posts, the button label would be wrong. Instead,
-some developers configure form buttons in the controller::
-
- namespace App\Controller\Admin;
-
- use App\Entity\Post;
- use App\Form\PostType;
- use Symfony\Component\HttpFoundation\Request;
- use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
- use Symfony\Component\Form\Extension\Core\Type\SubmitType;
-
- class PostController extends AbstractController
- {
- // ...
-
- public function new(Request $request)
- {
- $post = new Post();
- $form = $this->createForm(PostType::class, $post);
- $form->add('submit', SubmitType::class, [
- 'label' => 'Create',
- 'attr' => ['class' => 'btn btn-default pull-right'],
- ]);
-
- // ...
- }
- }
-
-This is also an important error, because you are mixing presentation markup
-(labels, CSS classes, etc.) with pure PHP code. Separation of concerns is
-always a good practice to follow, so put all the view-related things in the
-view layer:
-
-.. code-block:: html+twig
-
- {{ form_start(form) }}
- {{ form_widget(form) }}
-
-
- {{ form_end(form) }}
-
-Validation
-----------
-
-The :ref:`constraints ` option allows you to
-attach :doc:`validation constraints ` to any form field.
-However, doing that prevents the validation from being reused in other forms or
-other places where the mapped object is used.
-
-.. best-practice::
-
- Do not define your validation constraints in the form but on the object the
- form is mapped to.
-
-For example, to validate that the title of the post edited with a form is not
-blank, add the following in the ``Post`` object::
-
- // src/Entity/Post.php
-
- // ...
- use Symfony\Component\Validator\Constraints as Assert;
-
- class Post
- {
- /**
- * @Assert\NotBlank
- */
- public $title;
- }
-
-Rendering the Form
-------------------
-
-There are a lot of ways to render your form, ranging from rendering the entire
-thing in one line to rendering each part of each field independently. The
-best way depends on how much customization you need.
-
-One of the simplest ways - which is especially useful during development -
-is to render the form tags and use the ``form_widget()`` function to render
-all of the fields:
-
-.. code-block:: html+twig
-
- {{ form_start(form, {attr: {class: 'my-form-class'} }) }}
- {{ form_widget(form) }}
- {{ form_end(form) }}
-
-If you need more control over how your fields are rendered, then you should
-remove the ``form_widget(form)`` function and render your fields individually.
-See :doc:`/form/form_customization` for more information on this and how you
-can control *how* the form renders at a global level using form theming.
-
-Handling Form Submits
----------------------
-
-Handling a form submit usually follows a similar template::
-
- public function new(Request $request)
- {
- // build the form ...
-
- $form->handleRequest($request);
-
- if ($form->isSubmitted() && $form->isValid()) {
- $entityManager = $this->getDoctrine()->getManager();
- $entityManager->persist($post);
- $entityManager->flush();
-
- return $this->redirectToRoute('admin_post_show', [
- 'id' => $post->getId()
- ]);
- }
-
- // render the template
- }
-
-We recommend that you use a single action for both rendering the form and
-handling the form submit. For example, you *could* have a ``new()`` action that
-*only* renders the form and a ``create()`` action that *only* processes the form
-submit. Both those actions will be almost identical. So it's much simpler to let
-``new()`` handle everything.
-
-Next: :doc:`/best_practices/i18n`
diff --git a/best_practices/i18n.rst b/best_practices/i18n.rst
deleted file mode 100644
index ca4dd0c4c4b..00000000000
--- a/best_practices/i18n.rst
+++ /dev/null
@@ -1,82 +0,0 @@
-Internationalization
-====================
-
-Internationalization and localization adapt the applications and their contents
-to the specific region or language of the users. In Symfony this is an opt-in
-feature that needs to be installed before using it (``composer require symfony/translation``).
-
-Translation Source File Location
---------------------------------
-
-.. best-practice::
-
- Store the translation files in the ``translations/`` directory at the root
- of your project.
-
-Your translators' lives will be much easier if all the application translations
-are in one central location.
-
-Translation Source File Format
-------------------------------
-
-The Symfony Translation component supports lots of different translation
-formats: PHP, Qt, ``.po``, ``.mo``, JSON, CSV, INI, etc.
-
-.. best-practice::
-
- Use the XLIFF format for your translation files.
-
-Of all the available translation formats, only XLIFF and gettext have broad
-support in the tools used by professional translators. And since it's based
-on XML, you can validate XLIFF file contents as you write them.
-
-Symfony supports notes in XLIFF files, making them more user-friendly for
-translators. At the end, good translations are all about context, and these
-XLIFF notes allow you to define that context.
-
-.. tip::
-
- The `PHP Translation Bundle`_ includes advanced extractors that can read
- your project and automatically update the XLIFF files.
-
-Translation Keys
-----------------
-
-.. best-practice::
-
- Always use keys for translations instead of content strings.
-
-Using keys simplifies the management of the translation files because you can
-change the original contents without having to update all of the translation
-files.
-
-Keys should always describe their *purpose* and *not* their location. For
-example, if a form has a field with the label "Username", then a nice key
-would be ``label.username``, *not* ``edit_form.label.username``.
-
-Example Translation File
-------------------------
-
-Applying all the previous best practices, the sample translation file for
-English in the application would be:
-
-.. code-block:: xml
-
-
-
-
-
-
-
- title.post_list
- Post List
-
-
-
-
-
-----
-
-Next: :doc:`/best_practices/security`
-
-.. _`PHP Translation Bundle`: https://github.com/php-translation/symfony-bundle
diff --git a/best_practices/index.rst b/best_practices/index.rst
deleted file mode 100644
index 8df4abb1364..00000000000
--- a/best_practices/index.rst
+++ /dev/null
@@ -1,19 +0,0 @@
-Official Symfony Best Practices
-===============================
-
-.. toctree::
- :hidden:
-
- introduction
- creating-the-project
- configuration
- business-logic
- controllers
- templates
- forms
- i18n
- security
- web-assets
- tests
-
-.. include:: /best_practices/map.rst.inc
diff --git a/best_practices/introduction.rst b/best_practices/introduction.rst
deleted file mode 100644
index 3bce1615273..00000000000
--- a/best_practices/introduction.rst
+++ /dev/null
@@ -1,106 +0,0 @@
-.. index::
- single: Symfony Framework Best Practices
-
-The Symfony Framework Best Practices
-====================================
-
-The Symfony Framework is well-known for being *really* flexible and is used
-to build micro-sites, enterprise applications that handle billions of connections
-and even as the basis for *other* frameworks. Since its release in July 2011,
-the community has learned a lot about what's possible and how to do things *best*.
-
-These community resources - like blog posts or presentations - have created
-an unofficial set of recommendations for developing Symfony applications.
-Unfortunately, a lot of these recommendations are unneeded for web applications.
-Much of the time, they unnecessarily overcomplicate things and don't follow the
-original pragmatic philosophy of Symfony.
-
-What is this Guide About?
--------------------------
-
-This guide aims to fix that by describing the **best practices for developing
-web apps with the Symfony full-stack Framework**. These are best practices that
-fit the philosophy of the framework as envisioned by its original creator
-`Fabien Potencier`_.
-
-.. note::
-
- **Best practice** is a noun that means *"a well defined procedure that is
- known to produce near-optimum results"*. And that's exactly what this
- guide aims to provide. Even if you don't agree with every recommendation,
- we believe these will help you build great applications with less complexity.
-
-This guide is **specially suited** for:
-
-* Websites and web applications developed with the full-stack Symfony Framework.
-
-For other situations, this guide might be a good **starting point** that you can
-then **extend and fit to your specific needs**:
-
-* Bundles shared publicly to the Symfony community;
-* Advanced developers or teams who have created their own standards;
-* Some complex applications that have highly customized requirements;
-* Bundles that may be shared internally within a company.
-
-We know that old habits die hard and some of you will be shocked by some
-of these best practices. But by following these, you'll be able to develop
-apps faster, with less complexity and with the same or even higher quality.
-It's also a moving target that will continue to improve.
-
-Keep in mind that these are **optional recommendations** that you and your
-team may or may not follow to develop Symfony applications. If you want to
-continue using your own best practices and methodologies, you can still do
-that. Symfony is flexible enough to adapt to your needs. That will never
-change.
-
-Who this Book Is for (Hint: It's not a Tutorial)
-------------------------------------------------
-
-Any Symfony developer, whether you are an expert or a newcomer, can read this
-guide. But since this isn't a tutorial, you'll need some basic knowledge of
-Symfony to follow everything. If you are totally new to Symfony, welcome! and
-read the :doc:`Getting Started guides ` first.
-
-We've deliberately kept this guide short. We won't repeat explanations that
-you can find in the vast Symfony documentation, like discussions about Dependency
-Injection or front controllers. We'll solely focus on explaining how to do
-what you already know.
-
-The Application
----------------
-
-In addition to this guide, a sample application called `Symfony Demo`_ has been
-developed with all these best practices in mind. Execute this command to download
-the demo application:
-
-.. code-block:: terminal
-
- $ composer create-project symfony/symfony-demo
-
-**The demo application is a simple blog engine**, because that will allow us to
-focus on the Symfony concepts and features without getting buried in difficult
-implementation details. Instead of developing the application step by step in
-this guide, you'll find selected snippets of code through the chapters.
-
-Don't Update Your Existing Applications
----------------------------------------
-
-After reading this handbook, some of you may be considering refactoring your
-existing Symfony applications. Our recommendation is sound and clear: you may
-use these best practices for **new applications** but **you should not refactor
-your existing applications to comply with these best practices**. The reasons
-for not doing it are various:
-
-* Your existing applications are not wrong, they just follow another set of
- guidelines;
-* A full codebase refactorization is prone to introduce errors in your
- applications;
-* The amount of work spent on this could be better dedicated to improving
- your tests or adding features that provide real value to the end users.
-
-----
-
-Next: :doc:`/best_practices/creating-the-project`
-
-.. _`Fabien Potencier`: https://connect.symfony.com/profile/fabpot
-.. _`Symfony Demo`: https://github.com/symfony/demo
diff --git a/best_practices/map.rst.inc b/best_practices/map.rst.inc
deleted file mode 100644
index f9dfd0c3e9d..00000000000
--- a/best_practices/map.rst.inc
+++ /dev/null
@@ -1,11 +0,0 @@
-* :doc:`/best_practices/introduction`
-* :doc:`/best_practices/creating-the-project`
-* :doc:`/best_practices/configuration`
-* :doc:`/best_practices/business-logic`
-* :doc:`/best_practices/controllers`
-* :doc:`/best_practices/templates`
-* :doc:`/best_practices/forms`
-* :doc:`/best_practices/i18n`
-* :doc:`/best_practices/security`
-* :doc:`/best_practices/web-assets`
-* :doc:`/best_practices/tests`
diff --git a/best_practices/security.rst b/best_practices/security.rst
deleted file mode 100644
index f746303d347..00000000000
--- a/best_practices/security.rst
+++ /dev/null
@@ -1,381 +0,0 @@
-Security
-========
-
-Authentication and Firewalls (i.e. Getting the User's Credentials)
-------------------------------------------------------------------
-
-You can configure Symfony to authenticate your users using any method you
-want and to load user information from any source. This is a complex topic, but
-the :doc:`Security guide ` has a lot of information about this.
-
-Regardless of your needs, authentication is configured in ``security.yaml``,
-primarily under the ``firewalls`` key.
-
-.. best-practice::
-
- Unless you have two legitimately different authentication systems and
- users (e.g. form login for the main site and a token system for your
- API only), we recommend having only *one* firewall entry with the ``anonymous``
- key enabled.
-
-Most applications only have one authentication system and one set of users.
-For this reason, you only need *one* firewall entry. There are exceptions
-of course, especially if you have separated web and API sections on your
-site. But the point is to keep things simple.
-
-Additionally, you should use the ``anonymous`` key under your firewall. If
-you need to require users to be logged in for different sections of your
-site (or maybe nearly *all* sections), use the ``access_control`` area.
-
-.. best-practice::
-
- Use the ``bcrypt`` encoder for hashing your users' passwords.
-
-If your users have a password, then we recommend hashing it using the ``bcrypt``
-encoder, instead of the traditional SHA-512 hashing encoder. The main advantages
-of ``bcrypt`` are the inclusion of a *salt* value to protect against rainbow
-table attacks, and its adaptive nature, which allows to make it slower to
-remain resistant to brute-force search attacks.
-
-.. note::
-
- :ref:`Argon2i ` is the hashing algorithm as
- recommended by industry standards, but this won't be available to you unless
- you are using PHP 7.2+ or have the `libsodium`_ extension installed.
- ``bcrypt`` is sufficient for most applications.
-
-With this in mind, here is the authentication setup from our application,
-which uses a login form to load users from the database:
-
-.. code-block:: yaml
-
- # config/packages/security.yaml
- security:
- encoders:
- App\Entity\User: bcrypt
-
- providers:
- database_users:
- entity: { class: App\Entity\User, property: username }
-
- firewalls:
- secured_area:
- pattern: ^/
- anonymous: true
- form_login:
- check_path: login
- login_path: login
-
- logout:
- path: security_logout
- target: homepage
-
- # ... access_control exists, but is not shown here
-
-.. tip::
-
- The source code for our project contains comments that explain each part.
-
-Authorization (i.e. Denying Access)
------------------------------------
-
-Symfony gives you several ways to enforce authorization, including the ``access_control``
-configuration in :doc:`security.yaml `, the
-:ref:`@Security annotation ` and using
-:ref:`isGranted ` on the ``security.authorization_checker``
-service directly.
-
-.. best-practice::
-
- * For protecting broad URL patterns, use ``access_control``;
- * Whenever possible, use the ``@Security`` annotation;
- * Check security directly on the ``security.authorization_checker`` service
- whenever you have a more complex situation.
-
-There are also different ways to centralize your authorization logic, like
-with a custom security voter:
-
-.. best-practice::
-
- Define a custom security voter to implement fine-grained restrictions.
-
-.. _best-practices-security-annotation:
-
-The @Security Annotation
-------------------------
-
-For controlling access on a controller-by-controller basis, use the ``@Security``
-annotation whenever possible. Placing it above each action makes it consistent and readable.
-
-In our application, you need the ``ROLE_ADMIN`` in order to create a new post.
-Using ``@Security``, this looks like::
-
- use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
- use Symfony\Component\Routing\Annotation\Route;
- // ...
-
- /**
- * Displays a form to create a new Post entity.
- *
- * @Route("/new", name="admin_post_new")
- * @Security("is_granted('ROLE_ADMIN')")
- */
- public function new()
- {
- // ...
- }
-
-Using Expressions for Complex Security Restrictions
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-If your security logic is a little bit more complex, you can use an :doc:`expression `
-inside ``@Security``. In the following example, a user can only access the
-controller if their email matches the value returned by the ``getAuthorEmail()``
-method on the ``Post`` object::
-
- use App\Entity\Post;
- use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
- use Symfony\Component\Routing\Annotation\Route;
-
- /**
- * @Route("/{id}/edit", name="admin_post_edit")
- * @Security("user.getEmail() == post.getAuthorEmail()")
- */
- public function edit(Post $post)
- {
- // ...
- }
-
-Notice that this requires the use of the `ParamConverter`_, which automatically
-queries for the ``Post`` object and puts it on the ``$post`` argument. This
-is what makes it possible to use the ``post`` variable in the expression.
-
-This has one major drawback: an expression in an annotation cannot
-be reused in other parts of the application. Imagine that you want to add
-a link in a template that will only be seen by authors. Right now you'll
-need to repeat the expression code using Twig syntax:
-
-.. code-block:: html+twig
-
- {% if app.user and app.user.email == post.authorEmail %}
- ...
- {% endif %}
-
-A good solution - if your logic is simple enough - can be to add a new method
-to the ``Post`` entity that checks if a given user is its author::
-
- // src/Entity/Post.php
- // ...
-
- class Post
- {
- // ...
-
- /**
- * Is the given User the author of this Post?
- *
- * @return bool
- */
- public function isAuthor(User $user = null)
- {
- return $user && $user->getEmail() === $this->getAuthorEmail();
- }
- }
-
-Now you can reuse this method both in the template and in the security expression::
-
- use App\Entity\Post;
- use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
- use Symfony\Component\Routing\Annotation\Route;
-
- /**
- * @Route("/{id}/edit", name="admin_post_edit")
- * @Security("post.isAuthor(user)")
- */
- public function edit(Post $post)
- {
- // ...
- }
-
-.. code-block:: html+twig
-
- {% if post.isAuthor(app.user) %}
- ...
- {% endif %}
-
-.. _best-practices-directly-isGranted:
-.. _checking-permissions-without-security:
-.. _manually-checking-permissions:
-
-Checking Permissions without @Security
---------------------------------------
-
-The above example with ``@Security`` only works because we're using the
-:ref:`ParamConverter `, which gives the expression
-access to the ``post`` variable. If you don't use this, or have some other
-more advanced use-case, you can always do the same security check in PHP::
-
- /**
- * @Route("/{id}/edit", name="admin_post_edit")
- */
- public function edit($id)
- {
- $post = $this->getDoctrine()
- ->getRepository(Post::class)
- ->find($id);
-
- if (!$post) {
- throw $this->createNotFoundException();
- }
-
- if (!$post->isAuthor($this->getUser())) {
- $this->denyAccessUnlessGranted('edit', $post);
- }
- // equivalent code without using the "denyAccessUnlessGranted()" shortcut:
- //
- // use Symfony\Component\Security\Core\Exception\AccessDeniedException;
- // use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface
- //
- // ...
- //
- // public function __construct(AuthorizationCheckerInterface $authorizationChecker) {
- // $this->authorizationChecker = $authorizationChecker;
- // }
- //
- // ...
- //
- // if (!$this->authorizationChecker->isGranted('edit', $post)) {
- // throw $this->createAccessDeniedException();
- // }
- //
- // ...
- }
-
-Security Voters
----------------
-
-If your security logic is complex and can't be centralized into a method like
-``isAuthor()``, you should leverage custom voters. These are much easier than
-:doc:`ACLs ` and will give you the flexibility you need in almost
-all cases.
-
-First, create a voter class. The following example shows a voter that implements
-the same ``getAuthorEmail()`` logic you used above::
-
- namespace App\Security;
-
- use App\Entity\Post;
- use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
- use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
- use Symfony\Component\Security\Core\Authorization\Voter\Voter;
- use Symfony\Component\Security\Core\User\UserInterface;
-
- class PostVoter extends Voter
- {
- const CREATE = 'create';
- const EDIT = 'edit';
-
- private $decisionManager;
-
- public function __construct(AccessDecisionManagerInterface $decisionManager)
- {
- $this->decisionManager = $decisionManager;
- }
-
- protected function supports($attribute, $subject)
- {
- if (!in_array($attribute, [self::CREATE, self::EDIT])) {
- return false;
- }
-
- if (!$subject instanceof Post) {
- return false;
- }
-
- return true;
- }
-
- protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
- {
- $user = $token->getUser();
- /** @var Post */
- $post = $subject; // $subject must be a Post instance, thanks to the supports method
-
- if (!$user instanceof UserInterface) {
- return false;
- }
-
- switch ($attribute) {
- // if the user is an admin, allow them to create new posts
- case self::CREATE:
- if ($this->decisionManager->decide($token, ['ROLE_ADMIN'])) {
- return true;
- }
-
- break;
-
- // if the user is the author of the post, allow them to edit the posts
- case self::EDIT:
- if ($user->getEmail() === $post->getAuthorEmail()) {
- return true;
- }
-
- break;
- }
-
- return false;
- }
- }
-
-If you're using the :ref:`default services.yaml configuration `,
-your application will :ref:`autoconfigure ` your security
-voter and inject an ``AccessDecisionManagerInterface`` instance into it thanks to
-:doc:`autowiring `.
-
-Now, you can use the voter with the ``@Security`` annotation::
-
- /**
- * @Route("/{id}/edit", name="admin_post_edit")
- * @Security("is_granted('edit', post)")
- */
- public function edit(Post $post)
- {
- // ...
- }
-
-You can also use this directly with the ``security.authorization_checker`` service or
-via the even easier shortcut in a controller::
-
- /**
- * @Route("/{id}/edit", name="admin_post_edit")
- */
- public function edit($id)
- {
- $post = ...; // query for the post
-
- $this->denyAccessUnlessGranted('edit', $post);
-
- // use Symfony\Component\Security\Core\Exception\AccessDeniedException;
- // use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface
- //
- // ...
- //
- // public function __construct(AuthorizationCheckerInterface $authorizationChecker) {
- // $this->authorizationChecker = $authorizationChecker;
- // }
- //
- // ...
- //
- // if (!$this->authorizationChecker->isGranted('edit', $post)) {
- // throw $this->createAccessDeniedException();
- // }
- //
- // ...
- }
-
-Next: :doc:`/best_practices/web-assets`
-
-.. _`ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html
-.. _`@Security annotation`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/security.html
-.. _`FOSUserBundle`: https://github.com/FriendsOfSymfony/FOSUserBundle
-.. _`libsodium`: https://pecl.php.net/package/libsodium
diff --git a/best_practices/templates.rst b/best_practices/templates.rst
deleted file mode 100644
index e9970b9a8d7..00000000000
--- a/best_practices/templates.rst
+++ /dev/null
@@ -1,122 +0,0 @@
-Templates
-=========
-
-When PHP was created 20 years ago, developers loved its simplicity and how
-well it blended HTML and dynamic code. But as time passed, other template
-languages - like `Twig`_ - were created to make templating even better.
-
-.. best-practice::
-
- Use Twig templating format for your templates.
-
-Generally speaking, PHP templates are more verbose than Twig templates because
-they lack native support for lots of modern features needed by templates,
-like inheritance, automatic escaping and named arguments for filters and
-functions.
-
-Twig is the default templating format in Symfony and has the largest community
-support of all non-PHP template engines (it's used in high profile projects
-such as Drupal 8).
-
-Template Locations
-------------------
-
-.. best-practice::
-
- Store the application templates in the ``templates/`` directory at the root
- of your project.
-
-Centralizing your templates in a single location simplifies the work of your
-designers. In addition, using this directory simplifies the notation used when
-referring to templates (e.g. ``$this->render('admin/post/show.html.twig')``
-instead of ``$this->render('@SomeTwigNamespace/Admin/Posts/show.html.twig')``).
-
-.. best-practice::
-
- Use lowercased snake_case for directory and template names.
-
-This recommendation aligns with Twig best practices, where variables and template
-names use lowercased snake_case too (e.g. ``user_profile`` instead of ``userProfile``
-and ``edit_form.html.twig`` instead of ``EditForm.html.twig``).
-
-.. best-practice::
-
- Use a prefixed underscore for partial templates in template names.
-
-You often want to reuse template code using the ``include`` function to avoid
-redundant code. To determine those partials in the filesystem you should
-prefix partials and any other template without HTML body or ``extends`` tag
-with a single underscore.
-
-Twig Extensions
----------------
-
-.. best-practice::
-
- Define your Twig extensions in the ``src/Twig/`` directory. Your
- application will automatically detect them and configure them.
-
-Our application needs a custom ``md2html`` Twig filter so that we can transform
-the Markdown contents of each post into HTML. To do this, create a new
-``Markdown`` class that will be used later by the Twig extension. It needs
-to define one single method to transform Markdown content into HTML::
-
- namespace App\Utils;
-
- class Markdown
- {
- // ...
-
- public function toHtml(string $text): string
- {
- return $this->parser->text($text);
- }
- }
-
-Next, create a new Twig extension and define a filter called ``md2html`` using
-the ``Twig\TwigFilter`` class. Inject the newly defined ``Markdown`` class in the
-constructor of the Twig extension::
-
- namespace App\Twig;
-
- use App\Utils\Markdown;
- use Twig\Extension\AbstractExtension;
- use Twig\TwigFilter;
-
- class AppExtension extends AbstractExtension
- {
- private $parser;
-
- public function __construct(Markdown $parser)
- {
- $this->parser = $parser;
- }
-
- public function getFilters()
- {
- return [
- new TwigFilter('md2html', [$this, 'markdownToHtml'], [
- 'is_safe' => ['html'],
- 'pre_escape' => 'html',
- ]),
- ];
- }
-
- public function markdownToHtml($content)
- {
- return $this->parser->toHtml($content);
- }
- }
-
-And that's it!
-
-If you're using the :ref:`default services.yaml configuration `,
-you're done! Symfony will automatically know about your new service and tag it to
-be used as a Twig extension.
-
-----
-
-Next: :doc:`/best_practices/forms`
-
-.. _`Twig`: https://twig.symfony.com/
-.. _`Parsedown`: http://parsedown.org/
diff --git a/best_practices/tests.rst b/best_practices/tests.rst
deleted file mode 100644
index 2c1230cc452..00000000000
--- a/best_practices/tests.rst
+++ /dev/null
@@ -1,124 +0,0 @@
-Tests
-=====
-
-Of all the different types of test available, these best practices focus solely
-on unit and functional tests. Unit testing allows you to test the input and
-output of specific functions. Functional testing allows you to command a
-"browser" where you browse to pages on your site, click links, fill out forms
-and assert that you see certain things on the page.
-
-Unit Tests
-----------
-
-Unit tests are used to test your "business logic", which should live in classes
-that are independent of Symfony. For that reason, Symfony doesn't really
-have an opinion on what tools you use for unit testing. However, the most
-popular tools are `PHPUnit`_ and `PHPSpec`_.
-
-Functional Tests
-----------------
-
-Creating really good functional tests can be tough so some developers skip
-these completely. Don't skip the functional tests! By defining some *simple*
-functional tests, you can quickly spot any big errors before you deploy them:
-
-.. best-practice::
-
- Define a functional test that at least checks if your application pages
- are successfully loading.
-
-:ref:`PHPUnit data providers ` help you implement
-functional tests::
-
- // tests/ApplicationAvailabilityFunctionalTest.php
- namespace App\Tests;
-
- use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
-
- class ApplicationAvailabilityFunctionalTest extends WebTestCase
- {
- /**
- * @dataProvider urlProvider
- */
- public function testPageIsSuccessful($url)
- {
- $client = self::createClient();
- $client->request('GET', $url);
-
- $this->assertTrue($client->getResponse()->isSuccessful());
- }
-
- public function urlProvider()
- {
- yield ['/'];
- yield ['/posts'];
- yield ['/post/fixture-post-1'];
- yield ['/blog/category/fixture-category'];
- yield ['/archives'];
- // ...
- }
- }
-
-This code checks that all the given URLs load successfully, which means that
-their HTTP response status code is between ``200`` and ``299``. This may
-not look that useful, but given how little effort this took, it's worth
-having it in your application.
-
-In computer software, this kind of test is called `smoke testing`_ and consists
-of *"preliminary testing to reveal simple failures severe enough to reject a
-prospective software release"*.
-
-Hardcode URLs in a Functional Test
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Some of you may be asking why the previous functional test doesn't use the URL
-generator service:
-
-.. best-practice::
-
- Hardcode the URLs used in the functional tests instead of using the URL
- generator.
-
-Consider the following functional test that uses the ``router`` service to
-generate the URL of the tested page::
-
- // ...
- private $router; // consider that this holds the Symfony router service
-
- public function testBlogArchives()
- {
- $client = self::createClient();
- $url = $this->router->generate('blog_archives');
- $client->request('GET', $url);
-
- // ...
- }
-
-This will work, but it has one *huge* drawback. If a developer mistakenly
-changes the path of the ``blog_archives`` route, the test will still pass,
-but the original (old) URL won't work! This means that any bookmarks for
-that URL will be broken and you'll lose any search engine page ranking.
-
-Testing JavaScript Functionality
---------------------------------
-
-The built-in functional testing client is great, but it can't be used to
-test any JavaScript behavior on your pages. If you need to test this, consider
-using the `Mink`_ library from within PHPUnit.
-
-Of course, if you have a heavy JavaScript front-end, you should consider using
-pure JavaScript-based testing tools.
-
-Learn More about Functional Tests
----------------------------------
-
-Consider using the `HautelookAliceBundle`_ to generate real-looking data for
-your test fixtures using `Faker`_ and `Alice`_.
-
-.. _`PHPUnit`: https://phpunit.de/
-.. _`PHPSpec`: https://www.phpspec.net/
-.. _`smoke testing`: https://en.wikipedia.org/wiki/Smoke_testing_(software)
-.. _`Mink`: http://mink.behat.org
-.. _`HautelookAliceBundle`: https://github.com/hautelook/AliceBundle
-.. _`Faker`: https://github.com/fzaninotto/Faker
-.. _`Alice`: https://github.com/nelmio/alice
diff --git a/best_practices/web-assets.rst b/best_practices/web-assets.rst
deleted file mode 100644
index 271a1fa3eeb..00000000000
--- a/best_practices/web-assets.rst
+++ /dev/null
@@ -1,34 +0,0 @@
-Web Assets
-==========
-
-Web assets are things like CSS, JavaScript and image files that make the
-frontend of your site look and work great.
-
-.. best-practice::
-
- Store your assets in the ``assets/`` directory at the root of your project.
-
-Your designers' and front-end developers' lives will be much easier if all the
-application assets are in one central location.
-
-.. best-practice::
-
- Use `Webpack Encore`_ to compile, combine and minimize web assets.
-
-`Webpack`_ is the leading JavaScript module bundler that compiles, transforms
-and packages assets for usage in a browser. Webpack Encore is a JavaScript
-library that gets rid of most of Webpack complexity without hiding any of its
-features or distorting its usage and philosophy.
-
-Webpack Encore was designed to bridge the gap between Symfony applications and
-the JavaScript-based tools used in modern web applications. Check out the
-`official Webpack Encore documentation`_ to learn more about all the available
-features.
-
-----
-
-Next: :doc:`/best_practices/tests`
-
-.. _`Webpack Encore`: https://github.com/symfony/webpack-encore
-.. _`Webpack`: https://webpack.js.org/
-.. _`official Webpack Encore documentation`: https://symfony.com/doc/current/frontend.html
diff --git a/bundles.rst b/bundles.rst
index 2f0fb608c04..878bee3af4a 100644
--- a/bundles.rst
+++ b/bundles.rst
@@ -1,15 +1,12 @@
-.. 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 ` 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
@@ -18,26 +15,27 @@ SecurityBundle, DebugBundle, etc.) They are also used to add new features in
your application via `third-party bundles`_.
Bundles used in your applications must be enabled per
-:doc:`environment ` in the ``config/bundles.php``
+:ref:`environment ` in the ``config/bundles.php``
file::
// config/bundles.php
return [
// 'all' means that the bundle is enabled for any Symfony environment
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
- Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
- Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
- Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
- Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true],
- Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
- Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
- // this bundle is enabled only in 'dev' and 'test', so you can't use it in 'prod'
+ // ...
+
+ // 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::
- In a default Symfony application that uses :doc:`Symfony Flex `,
+ In a default Symfony application that uses :ref:`Symfony Flex `,
bundles are enabled/disabled automatically for you when installing/removing
them, so you don't need to look at or edit this ``bundles.php`` file.
@@ -45,28 +43,32 @@ 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 just a
-dummy name that should be replaced by some "vendor" name that represents you or
-your organization (e.g. ABCTestBundle for some company named ``ABC``).
+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. 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 `. 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,48 +77,85 @@ 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
--------------------------
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. Take a look at AcmeDemoBundle, as it contains some
-of the most common elements of a bundle:
+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
----------
@@ -128,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 a38dceefe32..023b58af162 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,11 +16,12 @@ 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 the first letter uppercased);
+* Use a StudlyCaps name (i.e. camelCase with an uppercase first letter);
* Use a descriptive and short name (no more than two words);
* Prefix the name with the concatenation of the vendor (and optionally the
category namespaces);
@@ -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
/
- ├─ 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 `.
@@ -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 `.
+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;
@@ -171,82 +195,67 @@ Continuous Integration
Testing bundle code continuously, including all its commits and pull requests,
is a good practice called Continuous Integration. There are several services
-providing this feature for free for open source projects. The most popular
-service for Symfony bundles is called `Travis CI`_.
-
-Here is the recommended configuration file (``.travis.yml``) for Symfony bundles,
-which test the two latest :doc:`LTS versions `
-of Symfony and the latest beta release:
-
-.. code-block:: yaml
-
- language: php
- sudo: false
- cache:
- directories:
- - $HOME/.composer/cache/files
- - $HOME/symfony-bridge/.phpunit
-
- env:
- global:
- - PHPUNIT_FLAGS="-v"
- - SYMFONY_PHPUNIT_DIR="$HOME/symfony-bridge/.phpunit"
-
- matrix:
- fast_finish: true
- include:
- # Minimum supported dependencies with the latest and oldest PHP version
- - php: 7.2
- env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" SYMFONY_DEPRECATIONS_HELPER="weak_vendors"
- - php: 7.0
- env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" SYMFONY_DEPRECATIONS_HELPER="weak_vendors"
-
- # Test the latest stable release
- - php: 7.0
- - 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:
- # To be removed when this issue will be resolved: https://github.com/composer/composer/issues/5355
- - if [[ "$COMPOSER_FLAGS" == *"--prefer-lowest"* ]]; then composer update --prefer-dist --no-interaction --prefer-stable --quiet; fi
- - 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
------------
Bundles should set ``"type": "symfony-bundle"`` in their ``composer.json`` file.
-With this, :doc:`Symfony Flex ` will be able to automatically
+With this, :ref:`Symfony Flex ` will be able to automatically
enable your bundle when it's installed.
If your bundle requires any setup (e.g. configuration, new files, changes to
@@ -257,13 +266,13 @@ 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) ` is the format
-used to render the documentation on symfony.com.
+used to render the documentation on the Symfony website.
Installation Instructions
~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -278,13 +287,17 @@ following standardized instructions in your ``README.md`` file.
Installation
============
+ Make sure Composer is installed globally, as explained in the
+ [installation chapter](https://getcomposer.org/doc/00-intro.md)
+ of the Composer documentation.
+
Applications that use Symfony Flex
----------------------------------
Open a command console, enter your project directory and execute:
```console
- $ composer require
+ composer require
```
Applications that don't use Symfony Flex
@@ -296,36 +309,21 @@ following standardized instructions in your ``README.md`` file.
following command to download the latest stable version of this bundle:
```console
- $ composer require
+ composer require
```
- This command requires you to have Composer installed globally, as explained
- in the [installation chapter](https://getcomposer.org/doc/00-intro.md)
- of the Composer documentation.
-
### Step 2: Enable the Bundle
Then, enable the bundle by adding it to the list of registered bundles
- in the `app/AppKernel.php` file of your project:
+ in the `config/bundles.php` file of your project:
```php
- // app/AppKernel.php
-
- // ...
- class AppKernel extends Kernel
- {
- public function registerBundles()
- {
- $bundles = [
- // ...
- new \\(),
- ];
-
- // ...
- }
+ // config/bundles.php
+ return [
// ...
- }
+ \\::class => ['all' => true],
+ ];
```
.. code-block:: rst
@@ -333,14 +331,16 @@ following standardized instructions in your ``README.md`` file.
Installation
============
- Applications that use Symfony Flex
+ Make sure Composer is installed globally, as explained in the
+ `installation chapter`_ of the Composer documentation.
+
----------------------------------
Open a command console, enter your project directory and execute:
- .. code-block:: bash
+ .. code-block:: terminal
- $ composer require
+ composer require
Applications that don't use Symfony Flex
----------------------------------------
@@ -353,38 +353,19 @@ following standardized instructions in your ``README.md`` file.
.. code-block:: terminal
- $ composer require
-
- This command requires you to have Composer installed globally, as explained
- in the `installation chapter`_ of the Composer documentation.
+ composer require
Step 2: Enable the Bundle
~~~~~~~~~~~~~~~~~~~~~~~~~
Then, enable the bundle by adding it to the list of registered bundles
- in the ``app/AppKernel.php`` file of your project:
-
- .. code-block:: php
-
- \\(),
- ];
-
- // ...
- }
+ in the ``config/bundles.php`` file of your project::
+ // config/bundles.php
+ return [
// ...
- }
+ \\::class => ['all' => true],
+ ];
.. _`installation chapter`: https://getcomposer.org/doc/00-intro.md
@@ -415,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
-------------
@@ -449,8 +434,8 @@ The end user can provide values in any configuration file:
-
+ https://symfony.com/schema/dic/services/services-1.0.xsd"
+ >
fabien@example.com
@@ -460,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::
@@ -478,8 +469,11 @@ Bundles must be versioned following the `Semantic Versioning Standard`_.
Services
--------
-If the bundle defines services, they must be prefixed with the bundle alias.
-For example, AcmeBlogBundle services must be prefixed with ``acme_blog``.
+If the bundle defines services, they must be prefixed with the bundle alias
+instead of using fully qualified class names like you do in your project
+services. For example, AcmeBlogBundle services must be prefixed with ``acme_blog``.
+The reason is that bundles shouldn't rely on features such as service autowiring
+or autoconfiguration to not impose an overhead when compiling application services.
In addition, services not meant to be used by the application directly, should
be :ref:`defined as private `. For public services,
@@ -491,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:
@@ -505,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``.
@@ -520,7 +521,24 @@ The ``composer.json`` file should include at least the following metadata:
``autoload``
This information is used by Symfony to load the classes of the bundle. It's
- recommended to use the `PSR-4`_ autoload standard.
+ 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/"
+ }
+ }
+ }
In order to make it easier for developers to find your bundle, register it on
`Packagist`_, the official repository for Composer packages.
@@ -529,16 +547,12 @@ 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 ` provided by the Symfony
+kernel, but this is no longer a recommended practice.
Learn more
----------
@@ -552,5 +566,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 c6b00bce2d4..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,35 +16,140 @@ as integration of other related components:
.. code-block:: yaml
+ # config/packages/framework.yaml
framework:
form: true
.. code-block:: xml
+
-
+ https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"
+ >
-
+
.. 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 `:
+ this is recommended for new bundles and for bundles following the
+ :ref:`recommended directory structure `;
+#. :ref:`Using the Bundle extension class `:
+ this was the traditional way of doing it, but nowadays it's only recommended for
+ bundles following the :ref:`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 `,
+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,27 +165,30 @@ can add some configuration that looks like this:
.. code-block:: xml
-
+
-
-
-
-
-
-
+ https://symfony.com/schema/dic/services/services-1.0.xsd"
+ >
+
+
+
.. code-block:: php
// config/packages/acme_social.php
- $container->loadFromExtension('acme_social', [
- '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,
@@ -95,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::
@@ -105,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.
@@ -170,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;
@@ -178,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');
@@ -197,10 +301,6 @@ The ``Configuration`` class to handle the sample configuration looks like::
}
}
-.. versionadded:: 4.2
-
- Not passing the root node name to ``TreeBuilder`` was deprecated in Symfony 4.2.
-
.. seealso::
The ``Configuration`` class can be much more complicated than shown here,
@@ -215,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();
@@ -235,15 +335,15 @@ For example, imagine your bundle has the following example config:
.. code-block:: xml
-
+
-
+ https://symfony.com/schema/dic/services/services-1.0.xsd"
+ >
-
+
@@ -252,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\DependencyInjection\Loader\XmlFileLoader;
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');
@@ -266,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']);
}
@@ -278,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;
@@ -287,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
{
// ...
}
@@ -303,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
@@ -329,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
+(``/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() `
+:method:`Extension::getConfiguration() `
method and return an instance of your ``Configuration``.
Supporting XML
@@ -359,18 +459,19 @@ In XML, the `XML namespace`_ is used to determine which elements belong to the
configuration of a specific bundle. The namespace is returned from the
:method:`Extension::getNamespace() `
method. By convention, the namespace is a URL (it doesn't have to be a valid
-URL nor does it need to exists). By default, the namespace for a bundle is
+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';
}
@@ -381,7 +482,7 @@ Providing an XML Schema
XML has a very useful feature called `XML schema`_. This allows you to
describe all possible elements and attributes and their values in an XML Schema
-Definition (an xsd file). This XSD file is used by IDEs for auto completion and
+Definition (an XSD file). This XSD file is used by IDEs for auto completion and
it is used by the Config component to validate the elements.
In order to use the schema, the XML configuration file must provide an
@@ -392,35 +493,38 @@ 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';
}
}
Assuming the XSD file is called ``hello-1.0.xsd``, the schema location will be
-``http://acme_company.com/schema/dic/hello/hello-1.0.xsd``:
+``https://acme_company.com/schema/dic/hello/hello-1.0.xsd``:
.. code-block:: xml
-
+
-
+ 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"
+ >
diff --git a/bundles/extension.rst b/bundles/extension.rst
index adef497bcf8..d2792efc477 100644
--- a/bundles/extension.rst
+++ b/bundles/extension.rst
@@ -1,20 +1,79 @@
-.. index::
- single: Configuration; Semantic
- single: Bundle; Extension configuration
-
How to Load Service Configuration inside a Bundle
=================================================
Services created by bundles are not defined in the main ``config/services.yaml``
file used by the application but in the bundles themselves. This article
-explains how to create and load those bundle services files.
+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 `:
+ this is recommended for new bundles and for bundles following the
+ :ref:`recommended directory structure `;
+#. :ref:`Create an extension class to load the service configuration files `:
+ this was the traditional way of doing it, but nowadays it's only recommended for
+ bundles following the :ref:`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 `,
+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;
@@ -23,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;
@@ -37,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
}
@@ -53,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();
}
@@ -72,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
@@ -86,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\DependencyInjection\Loader\XmlFileLoader;
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');
}
@@ -118,19 +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::
- use App\Manager\UserManager;
- use App\Utils\Slugger;
-
- // ...
- 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\\**',
// ...
]);
@@ -145,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 78dd8c6d4fb..58bcd13761e 100644
--- a/bundles/index.rst
+++ b/bundles/index.rst
@@ -1,5 +1,3 @@
-:orphan:
-
Bundles
=======
@@ -7,7 +5,6 @@ Bundles
:maxdepth: 2
override
- inheritance
best_practices
configuration
extension
diff --git a/bundles/inheritance.rst b/bundles/inheritance.rst
deleted file mode 100644
index d8ce372adb4..00000000000
--- a/bundles/inheritance.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-.. index::
- single: Bundle; Inheritance
-
-How to Use Bundle Inheritance to Override Parts of a Bundle
-===========================================================
-
-.. caution::
-
- Bundle inheritance was removed in Symfony 4.0, but you can
- :doc:`override any part of a bundle ` without
- using bundle inheritance.
diff --git a/bundles/override.rst b/bundles/override.rst
index b73b245d13b..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 `
- 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
``/templates/bundles//`` directory. The new templates
-must use the same name and path (relative to ``/Resources/views/``) as
+must use the same name and path (relative to ``/templates/``) as
the original templates.
-For example, to override the ``Resources/views/Registration/confirmed.html.twig``
-template from the FOSUserBundle, create this template:
-``/templates/bundles/FOSUserBundle/Registration/confirmed.html.twig``
+For example, to override the ``templates/registration/confirmed.html.twig``
+template from the AcmeUserBundle, create this template:
+``/templates/bundles/AcmeUserBundle/registration/confirmed.html.twig``
-.. caution::
+.. 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 "@!FOSUserBundle/Registration/confirmed.html.twig" %}
+ {% extends "@!AcmeUser/registration/confirmed.html.twig" %}
{% block some_block %}
...
@@ -92,14 +81,10 @@ inside a :doc:`compiler pass `.
Entities & Entity Mapping
-------------------------
-If a bundle defines its entity mapping in configuration files instead of
-annotations, you can override them as any other regular bundle configuration
-file. The only caveat is that you must override all those mapping configuration
-files and not just the ones you actually want to override.
-
-If a bundle provides a mapped superclass (such as the ``User`` entity in the
-FOSUserBundle) you can override its attributes and associations. Learn more
-about this feature and its limitations in `the Doctrine documentation`_.
+Overriding entity mapping is only possible if a bundle provides a mapped
+superclass (such as the ``User`` entity in the FOSUserBundle). It's possible to
+override attributes and associations in this way. Learn more about this feature
+and its limitations in `the Doctrine documentation`_.
Forms
-----
@@ -143,8 +128,8 @@ to a new validation group:
-
+ https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"
+ >
@@ -172,12 +157,12 @@ instead of the original ones.
Translations
------------
-Translations are not related to bundles, but to :ref:`translation domains `.
+Translations are not related to bundles, but to translation domains.
For this reason, you can override any bundle translation file from the main
``translations/`` directory, as long as the new file uses the same domain.
For example, to override the translations defined in the
-``Resources/translations/FOSUserBundle.es.yml`` file of the FOSUserBundle,
-create a``/translations/FOSUserBundle.es.yml`` file.
+``translations/AcmeUserBundle.es.yaml`` file of the AcmeUserBundle,
+create a ``/translations/AcmeUserBundle.es.yaml`` file.
-.. _`the Doctrine documentation`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#overrides
+.. _`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 dff8f2dea1f..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.
@@ -27,15 +23,15 @@ To give an Extension the power to do this, it needs to implement
// src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php
namespace Acme\HelloBundle\DependencyInjection;
- use Symfony\Component\HttpKernel\DependencyInjection\Extension;
- use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
+ use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
+ use Symfony\Component\HttpKernel\DependencyInjection\Extension;
class AcmeHelloExtension extends Extension implements PrependExtensionInterface
{
// ...
- 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'],
+ ]);
+ }
}
}
@@ -122,28 +117,103 @@ registered and the ``entity_manager_name`` setting for ``acme_hello`` is set to
xmlns:acme-something="http://example.org/schema/dic/acme_something"
xmlns:acme-other="http://example.org/schema/dic/acme_other"
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://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"
+ >
+
non_default
-
+
+
+
.. 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
new file mode 100644
index 00000000000..35f1404d42a
--- /dev/null
+++ b/cache.rst
@@ -0,0 +1,982 @@
+Cache
+=====
+
+Using a cache is a great way of making your application run quicker. The Symfony cache
+component ships with many adapters to different storages. Every adapter is
+developed for high performance.
+
+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): string {
+ $item->expiresAfter(3600);
+
+ // ... do some HTTP request or heavy computations
+ $computedValue = 'foobar';
+
+ return $computedValue;
+ });
+
+ echo $value; // 'foobar'
+
+ // ... and to remove the cache key
+ $pool->delete('my_cache_key');
+
+Symfony supports Cache Contracts and PSR-6/16 interfaces.
+You can read more about these at the :doc:`component documentation `.
+
+.. _cache-configuration-with-frameworkbundle:
+
+Configuring Cache with FrameworkBundle
+--------------------------------------
+
+When configuring the cache component there are a few concepts you should know
+of:
+
+**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.
+**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 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
+adapter (template) they use by using the ``app`` and ``system`` key like:
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # config/packages/cache.yaml
+ framework:
+ cache:
+ app: cache.adapter.filesystem
+ system: cache.adapter.system
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+ .. code-block:: php
+
+ // config/packages/cache.php
+ 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 `
+* :doc:`cache.adapter.array `
+* :doc:`cache.adapter.doctrine_dbal `
+* :doc:`cache.adapter.filesystem `
+* :doc:`cache.adapter.memcached `
+* :doc:`cache.adapter.pdo `
+* :doc:`cache.adapter.psr6 `
+* :doc:`cache.adapter.redis `
+* :ref:`cache.adapter.redis_tag_aware ` (Redis adapter optimized to work with tags)
+
+.. note::
+
+ There's also a special ``cache.adapter.system`` adapter. It's recommended to
+ use it for the :ref:`system cache `. 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.
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # config/packages/cache.yaml
+ framework:
+ cache:
+ directory: '%kernel.cache_dir%/pools' # Only used with cache.adapter.filesystem
+
+ 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'
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+
+ .. code-block:: php
+
+ // config/packages/cache.php
+ use Symfony\Config\FrameworkConfig;
+
+ return static function (FrameworkConfig $framework): void {
+ $framework->cache()
+ // Only used with cache.adapter.filesystem
+ ->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
+----------------------------------
+
+You can also create more customized pools:
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # config/packages/cache.yaml
+ framework:
+ cache:
+ default_memcached_provider: 'memcached://localhost'
+
+ pools:
+ # creates a "custom_thing.cache" service
+ # autowireable via "CacheInterface $customThingCache"
+ # uses the "app" cache configuration
+ custom_thing.cache:
+ adapter: cache.app
+
+ # creates a "my_cache_pool" service
+ # autowireable via "CacheInterface $myCachePool"
+ my_cache_pool:
+ adapter: cache.adapter.filesystem
+
+ # 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
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ .. code-block:: php
+
+ // config/packages/cache.php
+ 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 cache adapter class and a :ref:`configurable 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
+using the camel case version of its name - e.g. ``custom_thing.cache`` can be
+injected automatically by naming the argument ``$customThingCache`` and type-hinting it
+with either :class:`Symfony\\Contracts\\Cache\\CacheInterface` or
+``Psr\Cache\CacheItemPoolInterface``::
+
+ use Symfony\Contracts\Cache\CacheInterface;
+ // ...
+
+ // from a controller method
+ public function listProducts(CacheInterface $customThingCache): Response
+ {
+ // ...
+ }
+
+ // in a service
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+ .. 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
+-----------------------
+
+Some providers have specific options that can be configured. The
+:doc:`RedisAdapter ` allows you to
+create providers with the options ``timeout``, ``retry_interval``. etc. To use these
+options with non-default values you need to create your own ``\Redis`` provider
+and use that when configuring the pool.
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # config/packages/cache.yaml
+ framework:
+ cache:
+ pools:
+ cache.my_redis:
+ adapter: cache.adapter.redis
+ provider: app.my_custom_redis_provider
+
+ services:
+ app.my_custom_redis_provider:
+ class: \Redis
+ factory: ['Symfony\Component\Cache\Adapter\RedisAdapter', 'createConnection']
+ arguments:
+ - 'redis://localhost'
+ - { retry_interval: 2, timeout: 10 }
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+ redis://localhost
+
+ 2
+ 10
+
+
+
+
+
+ .. code-block:: php
+
+ // config/packages/cache.php
+ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
+
+ 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
+----------------------
+
+Different cache adapters have different strengths and weaknesses. Some might be
+really quick but optimized to store small items and some may be able to contain
+a lot of data but are quite slow. To get the best of both worlds you may use a
+chain of adapters.
+
+A cache chain combines several cache pools into a single one. When storing an
+item in a cache chain, Symfony stores it in all pools sequentially. When
+retrieving an item, Symfony tries to get it from the first pool. If it's not
+found, it tries the next pools until the item is found or an exception is thrown.
+Because of this behavior, it's recommended to define the adapters in the chain
+in order from fastest to slowest.
+
+If an error happens when storing an item in a pool, Symfony stores it in the
+other pools and no exception is thrown. Later, when the item is retrieved,
+Symfony stores the item automatically in all the missing pools.
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # config/packages/cache.yaml
+ 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'}
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ .. code-block:: php
+
+ // config/packages/cache.php
+ 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
+----------------
+
+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 tag could be invalidated with one function call::
+
+ use Symfony\Contracts\Cache\ItemInterface;
+ use Symfony\Contracts\Cache\TagAwareCacheInterface;
+
+ class SomeClass
+ {
+ // using autowiring to inject the cache pool
+ public function __construct(
+ private TagAwareCacheInterface $myCachePool,
+ ) {
+ }
+
+ public function someMethod(): void
+ {
+ $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): string {
+ $item->tag('foo');
+
+ return 'debug';
+ });
+
+ // Remove all cache keys tagged with "bar"
+ $this->myCachePool->invalidateTags(['bar']);
+ }
+ }
+
+The cache adapter needs to implement :class:`Symfony\\Contracts\\Cache\\TagAwareCacheInterface`
+to enable this feature. This could be added by using the following configuration.
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # config/packages/cache.yaml
+ framework:
+ cache:
+ pools:
+ my_cache_pool:
+ adapter: cache.adapter.redis_tag_aware
+ tags: true
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+
+
+ .. code-block:: php
+
+ // config/packages/cache.php
+ 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
+achieved by specifying the adapter.
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # config/packages/cache.yaml
+ framework:
+ cache:
+ pools:
+ my_cache_pool:
+ adapter: cache.adapter.redis
+ tags: tag_pool
+ tag_pool:
+ adapter: cache.adapter.apcu
+
+ .. code-block:: xml
+
+
+
+
+
+
+
+
+
+
+
+
+ .. code-block:: php
+
+ // config/packages/cache.php
+ 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::
+
+ The interface :class:`Symfony\\Contracts\\Cache\\TagAwareCacheInterface` is
+ autowired to the ``cache.app`` service.
+
+Clearing the Cache
+------------------
+
+To clear the cache you can use the ``bin/console cache:pool:clear [pool]`` command.
+That will remove all the entries from your storage and you will have to recalculate
+all the values. You can also group your pools into "cache clearers". There are 3 cache
+clearers by default:
+
+* ``cache.global_clearer``
+* ``cache.system_clearer``
+* ``cache.app_clearer``
+
+The global clearer clears all the cache items in every pool. The system cache clearer
+is used in the ``bin/console cache:clear`` command. The app clearer is the default
+clearer.
+
+To see all available cache pools:
+
+.. code-block:: terminal
+
+ $ php bin/console cache:pool:list
+
+Clear one pool:
+
+.. code-block:: terminal
+
+ $ php bin/console cache:pool:clear my_cache_pool
+
+Clear all custom pools:
+
+.. code-block:: terminal
+
+ $ 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 ` 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
+
+
+
+
+
+
+
+
+
+
+ env(base64:CACHE_DECRYPTION_KEY)
+
+
+
+
+
+
+
+
+ .. code-block:: php
+
+ // config/packages/cache.php
+ use Symfony\Component\Cache\Marshaller\SodiumMarshaller;
+ use Symfony\Component\DependencyInjection\ChildDefinition;
+ use Symfony\Component\DependencyInjection\Reference;
+
+ // ...
+ $container->setDefinition(SodiumMarshaller::class, new ChildDefinition('cache.default_marshaller'))
+ ->addArgument(['env(base64:CACHE_DECRYPTION_KEY)'])
+ // use multiple keys in order to rotate them
+ //->addArgument(['env(base64:CACHE_DECRYPTION_KEY)', 'env(base64:OLD_CACHE_DECRYPTION_KEY)'])
+ ->addArgument(new Reference('.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 ` 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 `. 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
+
+
+
+
+
+
+
+
+
+
+ %env(MESSENGER_TRANSPORT_DSN)%
+
+
+
+
+
+
+
+ .. code-block:: php
+
+ // config/framework/framework.php
+ use function Symfony\Component\DependencyInjection\Loader\Configurator\env;
+ use Symfony\Component\Cache\Messenger\EarlyExpirationMessage;
+ use Symfony\Config\FrameworkConfig;
+
+ 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 474a1d03704..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
@@ -17,7 +13,7 @@ For example:
-
+
This practice is no longer recommended unless the web application is extremely
simple. Hardcoding URLs can be a disadvantage because:
@@ -30,7 +26,7 @@ simple. Hardcoding URLs can be a disadvantage because:
is essential for some applications because it allows you to control how
the assets are cached. The Asset component allows you to define different
versioning strategies for each package;
-* **Moving assets location** is cumbersome and error-prone: it requires you to
+* **Moving assets' location** is cumbersome and error-prone: it requires you to
carefully update the URLs of all assets included in all templates. The Asset
component allows to move assets effortlessly just by changing the base path
value associated with the package of assets;
@@ -46,13 +42,13 @@ Installation
$ composer require symfony/asset
-Alternatively, you can clone the ``_ repository.
-
.. include:: /components/require_autoload.rst.inc
Usage
-----
+.. _asset-packages:
+
Asset Packages
~~~~~~~~~~~~~~
@@ -121,8 +117,9 @@ suffix to any asset path::
echo $package->getUrl('image.png');
// result: image.png?v1
-In case you want to modify the version format, pass a sprintf-compatible format
-string as the second argument of the ``StaticVersionStrategy`` constructor::
+In case you want to modify the version format, pass a ``sprintf``-compatible
+format string as the second argument of the ``StaticVersionStrategy``
+constructor::
// puts the 'version' word before the version value
$package = new Package(new StaticVersionStrategy('v1', '%s?version=%s'));
@@ -148,7 +145,6 @@ corresponding output file:
.. code-block:: json
- // rev-manifest.json
{
"css/app.css": "build/css/app.b916426ea1d10021f3f17ce8031f93c2.css",
"js/app.js": "build/js/app.13630905267b809161e71d0f8a0c017b.js",
@@ -161,11 +157,40 @@ In those cases, use the
use Symfony\Component\Asset\Package;
use Symfony\Component\Asset\VersionStrategy\JsonManifestVersionStrategy;
+ // assumes the JSON file above is called "rev-manifest.json"
$package = new Package(new JsonManifestVersionStrategy(__DIR__.'/rev-manifest.json'));
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\\JsonManifestVersionStrategy`
+with the :doc:`HttpClient component `::
+
+ use Symfony\Component\Asset\Package;
+ 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 JsonManifestVersionStrategy($manifestUrl, $httpClient));
+
Custom Version Strategies
.........................
@@ -178,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));
}
@@ -225,8 +250,8 @@ If you are also using the :doc:`HttpFoundation `
component in your project (for instance, in a Symfony application), the ``PathPackage``
class can take into account the context of the current request::
- use Symfony\Component\Asset\PathPackage;
use Symfony\Component\Asset\Context\RequestStackContext;
+ use Symfony\Component\Asset\PathPackage;
// ...
$pathPackage = new PathPackage(
@@ -261,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::
@@ -293,18 +318,18 @@ 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 be always served by the same
+is deterministic, meaning that each asset will always be served by the same
domain. This behavior simplifies the management of HTTP cache.
Request Context Aware Assets
@@ -315,8 +340,8 @@ account the context of the current request. In this case, only the request
scheme is considered, in order to select the appropriate base URL (HTTPs or
protocol-relative URLs for HTTPs requests, any base URL for HTTP requests)::
- use Symfony\Component\Asset\UrlPackage;
use Symfony\Component\Asset\Context\RequestStackContext;
+ use Symfony\Component\Asset\UrlPackage;
// ...
$urlPackage = new UrlPackage(
@@ -341,9 +366,9 @@ In the following example, all packages use the same versioning strategy, but
they all have different base paths::
use Symfony\Component\Asset\Package;
+ use Symfony\Component\Asset\Packages;
use Symfony\Component\Asset\PathPackage;
use Symfony\Component\Asset\UrlPackage;
- use Symfony\Component\Asset\Packages;
// ...
$versionStrategy = new StaticVersionStrategy('v1');
@@ -351,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::
@@ -367,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
@@ -401,5 +426,7 @@ improve performance::
Learn more
----------
-.. _Packagist: https://packagist.org/packages/symfony/asset
+* :doc:`How to manage CSS and JavaScript assets in Symfony applications `
+* :doc:`WebLink component ` to preload assets using HTTP/2.
+
.. _`Webpack`: https://webpack.js.org/
diff --git a/components/browser_kit.rst b/components/browser_kit.rst
index ce1cbec9077..8cf0772298c 100644
--- a/components/browser_kit.rst
+++ b/components/browser_kit.rst
@@ -1,19 +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::
-
- The BrowserKit component can only make internal requests to your application.
- If you need to make requests to external sites and applications, consider
- using `Goutte`_, a simple web scraper based on Symfony Components.
-
Installation
------------
@@ -21,8 +11,6 @@ Installation
$ composer require symfony/browser-kit
-Alternatively, you can clone the ``_ repository.
-
.. include:: /components/require_autoload.rst.inc
Basic Usage
@@ -38,20 +26,19 @@ Creating a Client
~~~~~~~~~~~~~~~~~
The component only provides an abstract client and does not provide any backend
-ready to use for the HTTP layer.
-
-To create your own client, you must extend the abstract ``Client`` class and
-implement the :method:`Symfony\\Component\\BrowserKit\\Client::doRequest` method.
+ready to use for the HTTP layer. To create your own client, you must extend the
+``AbstractBrowser`` class and implement the
+:method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::doRequest` method.
This method accepts a request and should return a response::
namespace Acme;
- use Symfony\Component\BrowserKit\Client as BaseClient;
+ use Symfony\Component\BrowserKit\AbstractBrowser;
use Symfony\Component\BrowserKit\Response;
- class Client extends BaseClient
+ class Client extends AbstractBrowser
{
- protected function doRequest($request)
+ protected function doRequest($request): Response
{
// ... convert request into a response
@@ -60,14 +47,15 @@ This method accepts a request and should return a response::
}
For a simple implementation of a browser based on the HTTP layer, have a look
-at `Goutte`_. For an implementation based on ``HttpKernelInterface``, have
-a look at the :class:`Symfony\\Component\\HttpKernel\\Client` provided by
-the :doc:`HttpKernel component `.
+at the :class:`Symfony\\Component\\BrowserKit\\HttpBrowser` provided by
+:ref:`this component `. For an implementation based
+on ``HttpKernelInterface``, have a look at the :class:`Symfony\\Component\\HttpKernel\\HttpClientKernel`
+provided by the :doc:`HttpKernel component `.
Making Requests
~~~~~~~~~~~~~~~
-Use the :method:`Symfony\\Component\\BrowserKit\\Client::request` method to
+Use the :method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::request` method to
make HTTP requests. The first two arguments are the HTTP method and the requested
URL::
@@ -81,7 +69,17 @@ The value returned by the ``request()`` method is an instance of the
:doc:`DomCrawler component `, which allows accessing
and traversing HTML elements programmatically.
-The :method:`Symfony\\Component\\BrowserKit\\Client::xmlHttpRequest` method,
+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::
@@ -94,7 +92,7 @@ make AJAX requests::
Clicking Links
~~~~~~~~~~~~~~
-The ``Client`` object is capable of simulating link clicks. Pass the text
+The ``AbstractBrowser`` is capable of simulating link clicks. Pass the text
content of the link and the client will perform the needed HTTP GET request to
simulate the link click::
@@ -107,17 +105,35 @@ simulate the link click::
If you need the :class:`Symfony\\Component\\DomCrawler\\Link` object that
provides access to the link properties (e.g. ``$link->getMethod()``,
-``$link->getUri()``), use this other method:
+``$link->getUri()``), use this other method::
// ...
$crawler = $client->request('GET', '/product/123');
$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
~~~~~~~~~~~~~~~~
-The ``Client`` object is also capable of submitting forms. First, select the
+The ``AbstractBrowser`` is also capable of submitting forms. First, select the
form using any of its buttons and then override any of its properties (method,
field values, etc.) before submitting it::
@@ -127,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
@@ -290,17 +310,27 @@ config files:
.. code-block:: php
- use Symfony\Component\DependencyInjection\Reference;
+ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
- // ...
- $container->setParameter('mailer.transport', 'sendmail');
- $container
- ->register('mailer', 'Mailer')
- ->addArgument('%mailer.transport%');
+ return static function (ContainerConfigurator $container): void {
+ $container->parameters()
+ // ...
+ ->set('mailer.transport', 'sendmail')
+ ;
+
+ $services = $container->services();
+ $services->set('mailer', 'Mailer')
+ ->args(['%mailer.transport%'])
+ ;
+
+ $services->set('mailer', 'Mailer')
+ ->args([param('mailer.transport')])
+ ;
- $container
- ->register('newsletter_manager', 'NewsletterManager')
- ->addMethodCall('setMailer', [new Reference('mailer')]);
+ $services->set('newsletter_manager', 'NewsletterManager')
+ ->call('setMailer', [service('mailer')])
+ ;
+ };
Learn More
----------
@@ -312,5 +342,4 @@ Learn More
/components/dependency_injection/*
/service_container/*
-.. _`PSR-11`: http://www.php-fig.org/psr/psr-11/
-.. _Packagist: https://packagist.org/packages/symfony/dependency-injection
+.. _`PSR-11`: https://www.php-fig.org/psr/psr-11/
diff --git a/components/dependency_injection/_imports-parameters-note.rst.inc b/components/dependency_injection/_imports-parameters-note.rst.inc
index abb01a6ef13..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,14 +19,18 @@
-
+ https://symfony.com/schema/dic/services/services-1.0.xsd"
+ >
-
+
.. 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 83fc81d57aa..7f991e85b72 100644
--- a/components/dependency_injection/compilation.rst
+++ b/components/dependency_injection/compilation.rst
@@ -1,6 +1,3 @@
-.. index::
- single: DependencyInjection; Compilation
-
Compiling the Container
=======================
@@ -57,14 +54,14 @@ added but are processed when the container's ``compile()`` method is called.
A very simple extension may just load configuration files into the container::
+ use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
- use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
- use Symfony\Component\Config\FileLocator;
+ use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
class AcmeDemoExtension implements ExtensionInterface
{
- public function load(array $configs, ContainerBuilder $container)
+ public function load(array $configs, ContainerBuilder $container): void
{
$loader = new XmlFileLoader(
$container,
@@ -78,7 +75,7 @@ A very simple extension may just load configuration files into the container::
This does not gain very much compared to loading the file directly into
the overall container being built. It just allows the files to be split
-up amongst the modules/bundles. Being able to affect the configuration
+up among the modules/bundles. Being able to affect the configuration
of a module from configuration files outside of the module/bundle is needed
to make a complex application configurable. This can be done by specifying
sections of config files loaded directly into the container as being for
@@ -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';
}
@@ -113,18 +110,18 @@ If this file is loaded into the configuration then the values in it are
only processed when the container is compiled at which point the Extensions
are loaded::
- use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
+ use Symfony\Component\DependencyInjection\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 ` 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
-
+
-
-
+ 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"
+ >
+ fooValuebarValue
-
+
.. 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,9 +350,9 @@ compilation::
class AcmeDemoExtension implements ExtensionInterface, CompilerPassInterface
{
- public function process(ContainerBuilder $container)
+ public function process(ContainerBuilder $container): void
{
- // ... do something during the compilation
+ // ... do something during the compilation
}
// ...
@@ -341,7 +368,7 @@ methods described in :doc:`/service_container/definitions`.
.. note::
Please note that the ``process()`` method in the extension class is
- called during the optimization step. You can read
+ called during the ``PassConfig::TYPE_BEFORE_OPTIMIZATION`` step. You can read
:ref:`the next section ` if you
need to edit the container during another step.
@@ -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 `
+that have a certain tag, in order to dynamically plug each one into other services.
+See the section on :ref:`service tags `
for an example.
.. _components-di-separate-compiler-passes:
@@ -377,9 +404,9 @@ class implementing the ``CompilerPassInterface``::
class CustomPass implements CompilerPassInterface
{
- public function process(ContainerBuilder $container)
+ public function process(ContainerBuilder $container): void
{
- // ... do something during the compilation
+ // ... 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,16 +487,24 @@ 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. That could cause issues
+ in a production environment with multiple concurrent requests. Instead, use
+ the :ref:`dumpFile() method ` from Symfony Filesystem
+ component or other methods provided by Symfony (e.g. ``$containerConfigCache->write()``)
+ which are atomic.
+
``ProjectServiceContainer`` is the default name given to the dumped container
-class. However you can change this with the ``class`` option when you
+class. However, you can change this with the ``class`` option when you
dump it::
// ...
@@ -479,11 +514,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 +546,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 +581,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,8 +599,8 @@ 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::
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 `
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 `
diff --git a/components/dom_crawler.rst b/components/dom_crawler.rst
index e0f573c5393..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
========================
@@ -19,8 +15,6 @@ Installation
$ composer require symfony/dom-crawler
-Alternatively, you can clone the ``_ repository.
-
.. include:: /components/require_autoload.rst.inc
Usage
@@ -36,7 +30,7 @@ The :class:`Symfony\\Component\\DomCrawler\\Crawler` class provides methods
to query and manipulate HTML and XML documents.
An instance of the Crawler represents a set of :phpclass:`DOMElement` objects,
-which are basically nodes that you can traverse::
+which are nodes that can be traversed as follows::
use Symfony\Component\DomCrawler\Crawler;
@@ -83,8 +77,8 @@ Using XPath expressions, you can select specific nodes within the document::
``DOMXPath::query`` is used internally to actually perform an XPath query.
-If you prefer CSS selectors over XPath, install the CssSelector component.
-It allows you to use jQuery-like selectors to traverse::
+If you prefer CSS selectors over XPath, install :doc:`/components/css_selector`.
+It allows you to use jQuery-like selectors::
$crawler = $crawler->filter('body > p');
@@ -95,17 +89,18 @@ 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.
+To remove a node, the anonymous function must return ``false``.
.. note::
All filter methods return a new :class:`Symfony\\Component\\DomCrawler\\Crawler`
- instance with filtered content.
+ instance with the filtered content. To check if the filter actually
+ found something, use ``$crawler->count() > 0`` on this new crawler.
Both the :method:`Symfony\\Component\\DomCrawler\\Crawler::filterXPath` and
:method:`Symfony\\Component\\DomCrawler\\Crawler::filter` methods work with
@@ -116,7 +111,7 @@ Consider the XML below:
.. code-block:: xml
-
+
registerNamespace('m', 'http://search.yahoo.com/mrss/');
$crawler = $crawler->filterXPath('//m:group//yt:aspectRatio');
+Verify if the current node matches a selector::
+
+ $crawler->matches('p.lorem');
+
Node Traversing
~~~~~~~~~~~~~~~
@@ -177,15 +176,19 @@ 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::
$crawler->filter('body')->children('p.lorem');
+Get the first parent (heading toward the document root) of the element that matches the provided selector::
+
+ $crawler->closest('p.lorem');
+
.. note::
All the traversal methods return a new :class:`Symfony\\Component\\DomCrawler\\Crawler`
@@ -194,7 +197,7 @@ Get all the direct child nodes matching a CSS selector::
Accessing Node Values
~~~~~~~~~~~~~~~~~~~~~
-Access the node name (HTML tag name) of the first node of the current selection (eg. "p" or "div")::
+Access the node name (HTML tag name) of the first node of the current selection (e.g. "p" or "div")::
// returns the node name (HTML tag name) of the first child element under
$tag = $crawler->filterXPath('//body/*')->nodeName();
@@ -207,14 +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');
-.. versionadded:: 4.3
+ // 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
Foo Bar
or
Bar Foo
+ // 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
+ //
Foo Bar Baz
+ // innerText() returns only the first text node 'Foo'
- The default argument of ``text()`` was introduced in Symfony 4.3.
+ // 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
@@ -227,37 +252,47 @@ Extract attribute and/or node values from the list of nodes::
Special attribute ``_text`` represents a node value, while ``_name``
represents the element name (the HTML tag name).
- .. versionadded:: 4.3
-
- The special attribute ``_name`` was introduced in Symfony 4.3.
-
Call an anonymous function on each node of the list::
use Symfony\Component\DomCrawler\Crawler;
// ...
- $nodeValues = $crawler->filter('p')->each(function (Crawler $node, $i) {
+ $nodeValues = $crawler->filter('p')->each(function (Crawler $node, $i): string {
return $node->text();
});
The anonymous function receives the node (as a Crawler) and the position as arguments.
The result is an array of values returned by the anonymous function calls.
+When using nested crawler, beware that ``filterXPath()`` is evaluated in the
+context of the crawler::
+
+ $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');
+
+ // DO THIS: specify the parent tag too
+ $subCrawler = $parentCrawler->filterXPath('parent/sub-tag/sub-child-tag');
+ $subCrawler = $parentCrawler->filterXPath('node()/sub-tag/sub-child-tag');
+ });
+
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('');
+ $crawler = new Crawler('');
- $crawler->addHtmlContent('');
- $crawler->addXmlContent('');
+ $crawler->addHtmlContent('');
+ $crawler->addXmlContent('');
- $crawler->addContent('');
- $crawler->addContent('', 'text/xml');
+ $crawler->addContent('');
+ $crawler->addContent('', 'text/xml');
- $crawler->add('');
- $crawler->add('');
+ $crawler->add('');
+ $crawler->add('');
.. note::
@@ -275,7 +310,7 @@ to interact with native :phpclass:`DOMDocument`, :phpclass:`DOMNodeList`
and :phpclass:`DOMNode` objects::
$domDocument = new \DOMDocument();
- $domDocument->loadXml('');
+ $domDocument->loadXml('');
$nodeList = $domDocument->getElementsByTagName('node');
$node = $domDocument->getElementsByTagName('node')->item(0);
@@ -312,9 +347,10 @@ and :phpclass:`DOMNode` objects::
// avoid the exception passing an argument that html() returns when node does not exist
$html = $crawler->html('Default HTML content');
- .. versionadded:: 4.3
+ Or you can get the outer HTML of the first node using
+ :method:`Symfony\\Component\\DomCrawler\\Crawler::outerHtml`::
- The default argument of ``html()`` was introduced in Symfony 4.3.
+ $html = $crawler->outerHtml();
Expression Evaluation
~~~~~~~~~~~~~~~~~~~~~
@@ -341,32 +377,36 @@ This behavior is best illustrated with examples::
$crawler->addHtmlContent($html);
$crawler->filterXPath('//span[contains(@id, "article-")]')->evaluate('substring-after(@id, "-")');
- /* array:3 [
- 0 => "100"
- 1 => "101"
- 2 => "102"
- ]
- */
+ /* Result:
+ [
+ 0 => '100',
+ 1 => '101',
+ 2 => '102',
+ ];
+ */
$crawler->evaluate('substring-after(//span[contains(@id, "article-")]/@id, "-")');
- /* array:1 [
- 0 => "100"
- ]
- */
+ /* Result:
+ [
+ 0 => '100',
+ ]
+ */
$crawler->filterXPath('//span[@class="article"]')->evaluate('count(@id)');
- /* array:3 [
- 0 => 1.0
- 1 => 1.0
- 2 => 1.0
- ]
- */
+ /* Result:
+ [
+ 0 => 1.0,
+ 1 => 1.0,
+ 2 => 1.0,
+ ]
+ */
$crawler->evaluate('count(//span[@class="article"])');
- /* array:1 [
- 0 => 3.0
- ]
- */
+ /* Result:
+ [
+ 0 => 3.0,
+ ]
+ */
$crawler->evaluate('//span[1]');
// A Symfony\Component\DomCrawler\Crawler instance
@@ -374,16 +414,26 @@ This behavior is best illustrated with examples::
Links
~~~~~
-To find a link by name (or a clickable image by its ``alt`` attribute), use
-the ``selectLink()`` method on an existing crawler. This returns a ``Crawler``
-instance with just the selected link(s). Calling ``link()`` gives you a special
-:class:`Symfony\\Component\\DomCrawler\\Link` object::
+Use the ``filter()`` method to find links by their ``id`` or ``class``
+attributes and use the ``selectLink()`` method to find links by their content
+(it also finds clickable images with that content in its ``alt`` attribute).
- $linksCrawler = $crawler->selectLink('Go elsewhere...');
- $link = $linksCrawler->link();
+Both methods return a ``Crawler`` instance with just the selected link. Use the
+``link()`` method to get the :class:`Symfony\\Component\\DomCrawler\\Link` object
+that represents the link::
- // or do this all at once
- $link = $crawler->selectLink('Go elsewhere...')->link();
+ // first, select the link by id, class or content...
+ $linkCrawler = $crawler->filter('#sign-up');
+ $linkCrawler = $crawler->filter('.user-profile');
+ $linkCrawler = $crawler->selectLink('Log in');
+
+ // ...then, get the Link object:
+ $link = $linkCrawler->link();
+
+ // or do all this at once:
+ $link = $crawler->filter('#sign-up')->link();
+ $link = $crawler->filter('.user-profile')->link();
+ $link = $crawler->selectLink('Log in')->link();
The :class:`Symfony\\Component\\DomCrawler\\Link` object has several useful
methods to get more information about the selected link itself::
@@ -450,8 +500,8 @@ The :class:`Symfony\\Component\\DomCrawler\\Form` object has lots of very
useful methods for working with forms::
$uri = $form->getUri();
-
$method = $form->getMethod();
+ $name = $form->getName();
The :method:`Symfony\\Component\\DomCrawler\\Form::getUri` method does more
than just return the ``action`` attribute of the form. If the form method
@@ -480,12 +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
Pass an array of values::
@@ -499,6 +554,11 @@ Pass an array of values::
'dimensional' => 'an other value',
]]);
+ // tick multiple checkboxes at once
+ $form->setValues(['multi' => [
+ 'dimensional' => [1, 3] // it uses the input value to determine which checkbox to tick
+ ]]);
+
This is great, but it gets better! The ``Form`` object allows you to interact
with your form like a browser, selecting radio values, ticking checkboxes,
and uploading files::
@@ -538,15 +598,18 @@ of the information you need to create a POST request for the form::
// now use some HTTP client and post using this information
-One great example of an integrated system that uses all of this is `Goutte`_.
-Goutte understands the Symfony Crawler object and can use it to submit forms
+One great example of an integrated system that uses all of this is
+the :class:`Symfony\\Component\\BrowserKit\\HttpBrowser` provided by
+the :doc:`BrowserKit component `.
+It understands the Symfony Crawler object and can use it to submit forms
directly::
- use Goutte\Client;
+ use Symfony\Component\BrowserKit\HttpBrowser;
+ use Symfony\Component\HttpClient\HttpClient;
// makes a real request to an external site
- $client = new Client();
- $crawler = $client->request('GET', 'https://github.com/login');
+ $browser = new HttpBrowser(HttpClient::create());
+ $crawler = $browser->request('GET', 'https://github.com/login');
// select the form and fill in some values
$form = $crawler->selectButton('Sign in')->form();
@@ -554,7 +617,7 @@ directly::
$form['password'] = 'anypass';
// submits the given form
- $crawler = $client->submit($form);
+ $crawler = $browser->submit($form);
.. _components-dom-crawler-invalid:
@@ -573,11 +636,36 @@ the whole form or specific field(s)::
$form->disableValidation();
$form['country']->select('Invalid value');
-.. _`Goutte`: https://github.com/FriendsOfPHP/Goutte
-.. _Packagist: https://packagist.org/packages/symfony/dom-crawler
+Resolving a 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::
+
+ use Symfony\Component\DomCrawler\UriResolver;
+
+ UriResolver::resolve('/foo', 'http://localhost/bar/foo/'); // http://localhost/foo
+ UriResolver::resolve('?a=b', 'http://localhost/bar#foo'); // http://localhost/bar?a=b
+ UriResolver::resolve('../../', 'http://localhost/'); // http://localhost/
+
+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`
+
+.. _`masterminds/html5`: https://packagist.org/packages/masterminds/html5
diff --git a/components/dotenv.rst b/components/dotenv.rst
deleted file mode 100644
index a14d5bcfebb..00000000000
--- a/components/dotenv.rst
+++ /dev/null
@@ -1,111 +0,0 @@
-.. index::
- single: Dotenv
- single: Components; Dotenv
-
-The Dotenv Component
-====================
-
- The Dotenv Component parses ``.env`` files to make environment variables
- stored in them accessible via ``getenv()``, ``$_ENV`` or ``$_SERVER``.
-
-Installation
-------------
-
-.. code-block:: terminal
-
- $ composer require --dev symfony/dotenv
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-Usage
------
-
-Sensitive information and environment-dependent settings should be defined as
-environment variables (as recommended for `twelve-factor applications`_). Using
-a ``.env`` file to store those environment variables eases development and CI
-management by keeping them in one "standard" place and agnostic of the
-technology stack you are using (Nginx vs PHP built-in server for instance).
-
-.. note::
-
- PHP has a lot of different implementations of this "pattern". This
- implementation's goal is to replicate what ``source .env`` would do. It
- tries to be as similar as possible with the standard shell's behavior (so
- no value validation for instance).
-
-Load a ``.env`` file in your PHP application via ``Dotenv::load()``::
-
- use Symfony\Component\Dotenv\Dotenv;
-
- $dotenv = new Dotenv();
- $dotenv->load(__DIR__.'/.env');
-
- // You can also load several files
- $dotenv->load(__DIR__.'/.env', __DIR__.'/.env.dev');
-
-Given the following ``.env`` file content:
-
-.. code-block:: bash
-
- # .env
- DB_USER=root
- DB_PASS=pass
-
-Access the value with ``getenv()`` in your code::
-
- $dbUser = getenv('DB_USER');
- // you can also use ``$_ENV`` or ``$_SERVER``
-
-The ``load()`` method never overwrites existing environment variables. Use the
-``overload()`` method if you need to overwrite them::
-
- // ...
- $dotenv->overload(__DIR__.'/.env');
-
-You should never store a ``.env`` file in your code repository as it might
-contain sensitive information; create a ``.env.dist`` file with sensible
-defaults instead.
-
-.. note::
-
- Symfony Dotenv can be used in any environment of your application:
- development, testing, staging and even production. However, in production
- it's recommended to configure real environment variables to avoid the
- performance overhead of parsing the ``.env`` file for every request.
-
-As a ``.env`` file is a regular shell script, you can ``source`` it in your own
-shell scripts:
-
-.. code-block:: terminal
-
- source .env
-
-Add comments by prefixing them with ``#``:
-
-.. code-block:: bash
-
- # Database credentials
- DB_USER=root
- DB_PASS=pass # This is the secret password
-
-Use environment variables in values by prefixing variables with ``$``:
-
-.. code-block:: bash
-
- DB_USER=root
- DB_PASS=${DB_USER}pass # Include the user as a password prefix
-
-Embed commands via ``$()`` (not supported on Windows):
-
-.. code-block:: bash
-
- START_TIME=$(date)
-
-.. note::
-
- Note that using ``$()`` might not work depending on your shell.
-
-.. _Packagist: https://packagist.org/packages/symfony/dotenv
-.. _twelve-factor applications: http://www.12factor.net/
diff --git a/components/event_dispatcher.rst b/components/event_dispatcher.rst
index 67aa53ef165..8cd676dd5fe 100644
--- a/components/event_dispatcher.rst
+++ b/components/event_dispatcher.rst
@@ -1,7 +1,3 @@
-.. index::
- single: EventDispatcher
- single: Components; EventDispatcher
-
The EventDispatcher Component
=============================
@@ -13,7 +9,7 @@ Introduction
------------
Object-oriented code has gone a long way to ensuring code extensibility.
-By creating classes that have well defined responsibilities, your code becomes
+By creating classes that have well-defined responsibilities, your code becomes
more flexible and a developer can extend them with subclasses to modify
their behaviors. But if they want to share the changes with other developers
who have also made their own subclasses, code inheritance is no longer the
@@ -32,7 +28,7 @@ truly extensible.
Take an example from :doc:`the HttpKernel component `.
Once a ``Response`` object has been created, it may be useful to allow other
elements in the system to modify it (e.g. add some cache headers) before
-it's actually used. To make this possible, the Symfony kernel throws an
+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
------------
@@ -56,8 +49,6 @@ Installation
$ composer require symfony/event-dispatcher
-Alternatively, you can clone the ``_ repository.
-
.. include:: /components/require_autoload.rst.inc
Usage
@@ -74,27 +65,10 @@ Events
When an event is dispatched, it's identified by a unique name (e.g.
``kernel.response``), which any number of listeners might be listening to.
-An :class:`Symfony\\Component\\EventDispatcher\\Event` instance is also
+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
.............................
@@ -113,7 +87,7 @@ Often times, data about a specific event needs to be passed along with the
case, a special subclass that has additional methods for retrieving and
overriding information can be passed when dispatching an event. For example,
the ``kernel.response`` event uses a
-:class:`Symfony\\Component\\HttpKernel\\Event\\FilterResponseEvent`, which
+:class:`Symfony\\Component\\HttpKernel\\Event\\ResponseEvent`, which
contains methods to get and even replace the ``Response`` object.
The Dispatcher
@@ -128,9 +102,6 @@ listeners registered with that event::
$dispatcher = new EventDispatcher();
-.. index::
- single: EventDispatcher; Listeners
-
Connecting Listeners
~~~~~~~~~~~~~~~~~~~~
@@ -163,9 +134,9 @@ The ``addListener()`` method takes up to three arguments:
So far, you've seen how PHP objects can be registered as listeners.
You can also register PHP `Closures`_ as event listeners::
- use Symfony\Component\EventDispatcher\Event;
+ 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
});
@@ -174,13 +145,13 @@ is notified. In the above example, when the ``acme.foo.action`` event is dispatc
the dispatcher calls the ``AcmeListener::onFooAction()`` method and passes
the ``Event`` object as the single argument::
- use Symfony\Component\EventDispatcher\Event;
+ use Symfony\Contracts\EventDispatcher\Event;
class AcmeListener
{
// ...
- public function onFooAction(Event $event)
+ public function onFooAction(Event $event): void
{
// ... do something
}
@@ -200,39 +171,68 @@ 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\EventDispatcher;
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');
- By default, the listeners pass assumes that the event dispatcher's service
+ ``RegisterListenersPass`` resolves aliased class names which for instance
+ allows to refer to an event via the fully qualified class name (FQCN) of the
+ event class. The pass will read the alias mapping from a dedicated container
+ parameter. This parameter can be extended by registering another compiler pass,
+ ``AddEventAliasesPass``::
+
+ use Symfony\Component\DependencyInjection\Compiler\PassConfig;
+ use Symfony\Component\DependencyInjection\ContainerBuilder;
+ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
+ use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass;
+ use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
+ use Symfony\Component\EventDispatcher\EventDispatcher;
+
+ $container = new ContainerBuilder(new ParameterBag());
+ $container->addCompilerPass(new AddEventAliasesPass([
+ \AcmeFooActionEvent::class => 'acme.foo.action',
+ ]));
+ $container->addCompilerPass(new RegisterListenersPass(), PassConfig::TYPE_BEFORE_REMOVING);
+
+ $container->register('event_dispatcher', EventDispatcher::class);
+
+ // registers an event listener
+ $container->register('listener_service_id', \AcmeListener::class)
+ ->addTag('kernel.event_listener', [
+ // will be translated to 'acme.foo.action' by RegisterListenersPass.
+ 'event' => \AcmeFooActionEvent::class,
+ 'method' => 'onFooAction',
+ ]);
+
+ .. note::
+
+ Note that ``AddEventAliasesPass`` has to be processed before ``RegisterListenersPass``.
+
+ 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 and that event subscribers are tagged
- with the ``kernel.event_subscriber`` tag. You can change these default
- values by passing custom values to the constructor of ``RegisterListenersPass``.
+ ``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``.
.. _event_dispatcher-closures-as-listeners:
-.. index::
- single: EventDispatcher; Creating and dispatching an event
-
Creating and Dispatching an Event
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -246,32 +246,25 @@ 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::
namespace Acme\Store\Event;
- use Symfony\Component\EventDispatcher\Event;
use Acme\Store\Order;
+ 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;
}
@@ -279,25 +272,16 @@ 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\\Component\\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 name of the event to dispatch and the ``Event`` instance to pass to
-each listener of that event::
+the ``Event`` instance to pass to each listener of that event and the name
+of the event to dispatch::
- use Acme\Store\Order;
use Acme\Store\Event\OrderPlacedEvent;
+ use Acme\Store\Order;
// the order is somehow created or retrieved
$order = new Order();
@@ -305,14 +289,37 @@ each listener of that event::
// creates the OrderPlacedEvent and dispatches it
$event = new OrderPlacedEvent($order);
- $dispatcher->dispatch(OrderPlacedEvent::NAME, $event);
+ $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:
@@ -330,40 +337,41 @@ 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;
+ use Acme\Store\Event\OrderPlacedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
- use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+ use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
- use Acme\Store\Event\OrderPlacedEvent;
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(FilterResponseEvent $event)
+ public function onKernelResponsePre(ResponseEvent $event): void
{
// ...
}
- public function onKernelResponsePost(FilterResponseEvent $event)
+ public function onKernelResponsePost(ResponseEvent $event): void
{
// ...
}
- public function onStoreOrder(OrderPlacedEvent $event)
+ public function onPlacedOrder(OrderPlacedEvent $event): void
{
+ $order = $event->getOrder();
// ...
}
}
@@ -393,9 +401,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
@@ -406,33 +411,30 @@ from being called. In other words, the listener needs to be able to tell
the dispatcher to stop all propagation of the event to future listeners
(i.e. to not notify any more listeners). This can be accomplished from
inside a listener via the
-:method:`Symfony\\Component\\EventDispatcher\\Event::stopPropagation` method::
+:method:`Symfony\\Contracts\\EventDispatcher\\Event::stopPropagation` method::
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
-:method:`Symfony\\Component\\EventDispatcher\\Event::isPropagationStopped`
+:method:`Symfony\\Contracts\\EventDispatcher\\Event::isPropagationStopped`
method which returns a boolean value::
// ...
- $dispatcher->dispatch('foo.event', $event);
+ $dispatcher->dispatch($event, 'foo.event');
if ($event->isPropagationStopped()) {
// ...
}
-.. index::
- single: EventDispatcher; EventDispatcher aware events and listeners
-
.. _event_dispatcher-dispatcher-aware-events:
EventDispatcher Aware Events and Listeners
@@ -443,39 +445,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; Dispatcher shortcuts
-
-.. _event_dispatcher-shortcuts:
-
-Dispatcher Shortcuts
-~~~~~~~~~~~~~~~~~~~~
-
-If you do not need a custom event object, you can rely on a plain
-:class:`Symfony\\Component\\EventDispatcher\\Event` object. You do not even
-need to pass this to the dispatcher as it will create one by default unless you
-specifically pass one::
-
- $dispatcher->dispatch('order.placed');
-
-Moreover, the event dispatcher always returns whichever event object that
-was dispatched, i.e. either the event that was passed or the event that
-was created internally by the dispatcher. This allows for nice shortcuts::
-
- if (!$dispatcher->dispatch('foo.event')->isPropagationStopped()) {
- // ...
- }
-
-Or::
-
- $event = new OrderPlacedEvent($order);
- $order = $dispatcher->dispatch('bar.event', $event)->getOrder();
-
-and so on.
-
-.. index::
- single: EventDispatcher; Event name introspection
-
.. _event_dispatcher-event-name-introspection:
Event Name Introspection
@@ -484,12 +453,12 @@ Event Name Introspection
The ``EventDispatcher`` instance, as well as the name of the event that
is dispatched, are passed as arguments to the listener::
- use Symfony\Component\EventDispatcher\Event;
- use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+ 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
}
@@ -501,25 +470,17 @@ Other Dispatchers
Besides the commonly used ``EventDispatcher``, the component comes
with some other dispatchers:
-* :doc:`/components/event_dispatcher/container_aware_dispatcher`
* :doc:`/components/event_dispatcher/immutable_dispatcher`
* :doc:`/components/event_dispatcher/traceable_dispatcher`
Learn More
----------
-.. toctree::
- :maxdepth: 1
- :glob:
-
- /components/event_dispatcher/*
- /event_dispatcher/*
-
+* :doc:`/components/event_dispatcher/generic_event`
* :ref:`The kernel.event_listener tag `
* :ref:`The kernel.event_subscriber tag `
.. _Mediator: https://en.wikipedia.org/wiki/Mediator_pattern
.. _Observer: https://en.wikipedia.org/wiki/Observer_pattern
-.. _Closures: https://php.net/manual/en/functions.anonymous.php
-.. _PHP callable: https://php.net/manual/en/language.pseudo-types.php#language.types.callback
-.. _Packagist: https://packagist.org/packages/symfony/event-dispatcher
+.. _Closures: https://www.php.net/manual/en/functions.anonymous.php
+.. _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 cc00d26ac20..41d0a9d66a4 100644
--- a/components/event_dispatcher/generic_event.rst
+++ b/components/event_dispatcher/generic_event.rst
@@ -1,10 +1,7 @@
-.. index::
- single: EventDispatcher
-
The Generic Event Object
========================
-The base :class:`Symfony\\Component\\EventDispatcher\\Event` class provided
+The base :class:`Symfony\\Contracts\\EventDispatcher\\Event` class provided
by the EventDispatcher component is deliberately sparse to allow the creation
of API specific event objects by inheritance using OOP. This allows for
elegant and readable code in complex applications.
@@ -18,7 +15,7 @@ arguments.
:class:`Symfony\\Component\\EventDispatcher\\GenericEvent` adds some more
methods in addition to the base class
-:class:`Symfony\\Component\\EventDispatcher\\Event`
+:class:`Symfony\\Contracts\\EventDispatcher\\Event`
* :method:`Symfony\\Component\\EventDispatcher\\GenericEvent::__construct`:
Constructor takes the event subject and any arguments;
@@ -53,11 +50,11 @@ Passing a subject::
use Symfony\Component\EventDispatcher\GenericEvent;
$event = new GenericEvent($subject);
- $dispatcher->dispatch('foo', $event);
+ $dispatcher->dispatch($event, 'foo');
class FooListener
{
- public function handler(GenericEvent $event)
+ public function handler(GenericEvent $event): void
{
if ($event->getSubject() instanceof Foo) {
// ...
@@ -74,13 +71,13 @@ access the event arguments::
$subject,
['type' => 'foo', 'counter' => 0]
);
- $dispatcher->dispatch('foo', $event);
+ $dispatcher->dispatch($event, 'foo');
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
}
@@ -93,13 +90,12 @@ Filtering data::
use Symfony\Component\EventDispatcher\GenericEvent;
$event = new GenericEvent($subject, ['data' => 'Foo']);
- $dispatcher->dispatch('foo', $event);
+ $dispatcher->dispatch($event, 'foo');
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 57f05ba6e0d..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
==============================
@@ -38,13 +34,13 @@ to register event listeners and dispatch events::
// dispatches an event
$event = ...;
- $traceableEventDispatcher->dispatch('event.the_name', $event);
+ $traceableEventDispatcher->dispatch($event, 'event.the_name');
After your application has been processed, you can use the
-:method:`Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcherInterface::getCalledListeners`
+:method:`Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher::getCalledListeners`
method to retrieve an array of event listeners that have been called in
your application. Similarly, the
-:method:`Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcherInterface::getNotCalledListeners`
+:method:`Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher::getNotCalledListeners`
method returns an array of event listeners that have not been called::
// ...
diff --git a/components/expression_language.rst b/components/expression_language.rst
index 1e75bdeb78f..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
================================
@@ -16,16 +12,16 @@ Installation
$ composer require symfony/expression-language
-Alternatively, you can clone the ``_ repository.
-
.. 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*.
@@ -45,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
-----
@@ -75,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 `
+ 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();
-See :doc:`/components/expression_language/syntax` to learn the syntax of the
-ExpressionLanguage component.
+ // 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
+
+ The support for flags in the ``parse()`` and ``lint()`` methods
+ was introduced in Symfony 7.1.
Passing in Variables
--------------------
@@ -93,7 +145,7 @@ PHP type (including objects)::
class Apple
{
- public $variety;
+ public string $variety;
}
$apple = new Apple();
@@ -104,32 +156,264 @@ PHP type (including objects)::
[
'fruit' => $apple,
]
- ));
+ )); // displays "Honeycrisp"
+
+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.):
-This will print "Honeycrisp". For more information, see the :doc:`/components/expression_language/syntax`
-entry, especially :ref:`component-expression-objects` and :ref:`component-expression-arrays`.
+* :doc:`Variables available in security expressions `;
+* :doc:`Variables available in service container expressions `;
+* :ref:`Variables available in routing expressions `.
+
+.. _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::
-Learn More
-----------
+ use Psr\Cache\CacheItemPoolInterface;
+ use Symfony\Component\ExpressionLanguage\ExpressionLanguage as BaseExpressionLanguage;
-.. toctree::
- :maxdepth: 1
- :glob:
+ 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());
- /components/expression_language/*
- /service_container/expression_language
- /reference/constraints/Expression
+ parent::__construct($cache, $providers);
+ }
+ }
-.. _Packagist: https://packagist.org/packages/symfony/expression-language
+.. _`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 5bfaf15133a..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\ExpressionLanguage\ExpressionLanguage;
- use Symfony\Component\Cache\Adapter\RedisAdapter;
-
- $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 7d1d758f855..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.
-
-.. code-block:: php
-
- 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 ``compile()`` (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 $parser = null, array $providers = [])
- {
- // prepends the default provider to let users override it
- array_unshift($providers, new StringExpressionLanguageProvider());
-
- parent::__construct($parser, $providers);
- }
- }
-
diff --git a/components/expression_language/syntax.rst b/components/expression_language/syntax.rst
deleted file mode 100644
index 4ba134fbf38..00000000000
--- a/components/expression_language/syntax.rst
+++ /dev/null
@@ -1,310 +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``
-
-.. 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' : ''``)
diff --git a/components/filesystem.rst b/components/filesystem.rst
index 5aee48f7c23..dabf3f81872 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
------------
@@ -13,47 +11,39 @@ Installation
$ composer require symfony/filesystem
-Alternatively, you can clone the ``_ repository.
-
.. include:: /components/require_autoload.rst.inc
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\Filesystem;
use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
+ use Symfony\Component\Filesystem\Filesystem;
+ use Symfony\Component\Filesystem\Path;
- $fileSystem = new Filesystem();
+ $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
-~~~~~
+``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);
+ $filesystem->mkdir('/tmp/photos', 0700);
.. note::
@@ -67,31 +57,31 @@ On POSIX filesystems, directories are created with a default mode value
.. note::
The directory permissions are affected by the current `umask`_.
- Set the umask for your webserver, use PHP's :phpfunction:`umask`
+ Set the ``umask`` for your webserver, use PHP's :phpfunction:`umask`
function or use the :phpfunction:`chmod` function after the
directory has been created.
-exists
-~~~~~~
+``exists``
+~~~~~~~~~~
:method:`Symfony\\Component\\Filesystem\\Filesystem::exists` checks for the
presence of one or more files or directories and returns ``false`` if any of
them is missing::
// if this absolute directory exists, returns true
- $fileSystem->exists('/tmp/photos');
+ $filesystem->exists('/tmp/photos');
// if rabbit.jpg exists and bottle.png does not exist, returns false
// non-absolute paths are relative to the directory where the running PHP script is stored
- $fileSystem->exists(['rabbit.jpg', 'bottle.png']);
+ $filesystem->exists(['rabbit.jpg', 'bottle.png']);
.. note::
You can pass an array or any :phpclass:`Traversable` object as the first
argument.
-copy
-~~~~
+``copy``
+~~~~~~~~
:method:`Symfony\\Component\\Filesystem\\Filesystem::copy` makes a copy of a
single file (use :method:`Symfony\\Component\\Filesystem\\Filesystem::mirror` to
@@ -100,217 +90,420 @@ source modification date is later than the target. This behavior can be overridd
by the third boolean argument::
// works only if image-ICC has been modified after image.jpg
- $fileSystem->copy('image-ICC.jpg', 'image.jpg');
+ $filesystem->copy('image-ICC.jpg', 'image.jpg');
// image.jpg will be overridden
- $fileSystem->copy('image-ICC.jpg', 'image.jpg', true);
+ $filesystem->copy('image-ICC.jpg', 'image.jpg', true);
-touch
-~~~~~
+``touch``
+~~~~~~~~~
:method:`Symfony\\Component\\Filesystem\\Filesystem::touch` sets access and
modification time for a file. The current time is used by default. You can set
your own with the second argument. The third argument is the access time::
// sets modification time to the current timestamp
- $fileSystem->touch('file.txt');
+ $filesystem->touch('file.txt');
// sets modification time 10 seconds in the future
- $fileSystem->touch('file.txt', time() + 10);
+ $filesystem->touch('file.txt', time() + 10);
// sets access time 10 seconds in the past
- $fileSystem->touch('file.txt', time(), time() - 10);
+ $filesystem->touch('file.txt', time(), time() - 10);
.. note::
You can pass an array or any :phpclass:`Traversable` object as the first
argument.
-chown
-~~~~~
+``chown``
+~~~~~~~~~
:method:`Symfony\\Component\\Filesystem\\Filesystem::chown` changes the owner of
a file. The third argument is a boolean recursive option::
// sets the owner of the lolcat video to www-data
- $fileSystem->chown('lolcat.mp4', 'www-data');
+ $filesystem->chown('lolcat.mp4', 'www-data');
// changes the owner of the video directory recursively
- $fileSystem->chown('/video', 'www-data', true);
+ $filesystem->chown('/video', 'www-data', true);
.. note::
You can pass an array or any :phpclass:`Traversable` object as the first
argument.
-chgrp
-~~~~~
+``chgrp``
+~~~~~~~~~
:method:`Symfony\\Component\\Filesystem\\Filesystem::chgrp` changes the group of
a file. The third argument is a boolean recursive option::
// sets the group of the lolcat video to nginx
- $fileSystem->chgrp('lolcat.mp4', 'nginx');
+ $filesystem->chgrp('lolcat.mp4', 'nginx');
// changes the group of the video directory recursively
- $fileSystem->chgrp('/video', 'nginx', true);
+ $filesystem->chgrp('/video', 'nginx', true);
.. note::
You can pass an array or any :phpclass:`Traversable` object as the first
argument.
-chmod
-~~~~~
+``chmod``
+~~~~~~~~~
:method:`Symfony\\Component\\Filesystem\\Filesystem::chmod` changes the mode or
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
- $fileSystem->chmod('src', 0700, 0000, true);
+ $filesystem->chmod('video.ogg', 0600);
+ // changes the mode of the src directory recursively
+ $filesystem->chmod('src', 0700, 0000, true);
.. note::
You can pass an array or any :phpclass:`Traversable` object as the first
argument.
-remove
-~~~~~~
+``remove``
+~~~~~~~~~~
:method:`Symfony\\Component\\Filesystem\\Filesystem::remove` deletes files,
directories and symlinks::
- $fileSystem->remove(['symlink', '/path/to/directory', 'activity.log']);
+ $filesystem->remove(['symlink', '/path/to/directory', 'activity.log']);
.. note::
You can pass an array or any :phpclass:`Traversable` object as the first
argument.
-rename
-~~~~~~
+``rename``
+~~~~~~~~~~
:method:`Symfony\\Component\\Filesystem\\Filesystem::rename` changes the name
of a single file or directory::
// renames a file
- $fileSystem->rename('/tmp/processed_video.ogg', '/path/to/store/video_647.ogg');
+ $filesystem->rename('/tmp/processed_video.ogg', '/path/to/store/video_647.ogg');
// renames a directory
- $fileSystem->rename('/tmp/files', '/path/to/store/files');
+ $filesystem->rename('/tmp/files', '/path/to/store/files');
+ // if the target already exists, a third boolean argument is available to overwrite.
+ $filesystem->rename('/tmp/processed_video2.ogg', '/path/to/store/video_647.ogg', true);
-symlink
-~~~~~~~
+``symlink``
+~~~~~~~~~~~
:method:`Symfony\\Component\\Filesystem\\Filesystem::symlink` creates a
symbolic link from the target to the destination. If the filesystem does not
support symbolic links, a third boolean argument is available::
// creates a symbolic link
- $fileSystem->symlink('/path/to/source', '/path/to/destination');
+ $filesystem->symlink('/path/to/source', '/path/to/destination');
// duplicates the source directory if the filesystem
// does not support symbolic links
- $fileSystem->symlink('/path/to/source', '/path/to/destination', true);
+ $filesystem->symlink('/path/to/source', '/path/to/destination', true);
-readlink
-~~~~~~~~
+``readlink``
+~~~~~~~~~~~~
:method:`Symfony\\Component\\Filesystem\\Filesystem::readlink` read links targets.
-PHP's ``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');
+ $filesystem->readlink('/path/to/link');
// returns its absolute fully resolved final version of the target (if there are nested links, they are resolved)
- $fileSystem->readlink('/path/to/link', true);
+ $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.
-makePathRelative
-~~~~~~~~~~~~~~~~
+ * 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``
+~~~~~~~~~~~~~~~~~~~~
:method:`Symfony\\Component\\Filesystem\\Filesystem::makePathRelative` takes two
absolute paths and returns the relative path from the second path to the first one::
// returns '../'
- $fileSystem->makePathRelative(
+ $filesystem->makePathRelative(
'/var/lib/symfony/src/Symfony/',
'/var/lib/symfony/src/Symfony/Component'
);
// returns 'videos/'
- $fileSystem->makePathRelative('/tmp/videos', '/tmp')
+ $filesystem->makePathRelative('/tmp/videos', '/tmp');
-mirror
-~~~~~~
+``mirror``
+~~~~~~~~~~
:method:`Symfony\\Component\\Filesystem\\Filesystem::mirror` copies all the
contents of the source directory into the target one (use the
:method:`Symfony\\Component\\Filesystem\\Filesystem::copy` method to copy single
files)::
- $fileSystem->mirror('/path/to/source', '/path/to/target');
+ $filesystem->mirror('/path/to/source', '/path/to/target');
-isAbsolutePath
-~~~~~~~~~~~~~~
+``isAbsolutePath``
+~~~~~~~~~~~~~~~~~~
:method:`Symfony\\Component\\Filesystem\\Filesystem::isAbsolutePath` returns
``true`` if the given path is absolute, ``false`` otherwise::
// returns true
- $fileSystem->isAbsolutePath('/tmp');
+ $filesystem->isAbsolutePath('/tmp');
// returns true
- $fileSystem->isAbsolutePath('c:\\Windows');
+ $filesystem->isAbsolutePath('c:\\Windows');
// returns false
- $fileSystem->isAbsolutePath('tmp');
+ $filesystem->isAbsolutePath('tmp');
// returns false
- $fileSystem->isAbsolutePath('../dir');
+ $filesystem->isAbsolutePath('../dir');
-tempnam
-~~~~~~~
+``tempnam``
+~~~~~~~~~~~
-:method:`Symfony\\Component\\Filesystem\\Filesystem::tempnam` creates a temporary file with a unique filename, and returns its path, or throw an exception on failure::
+:method:`Symfony\\Component\\Filesystem\\Filesystem::tempnam` creates a
+temporary file with a unique filename, and returns its path, or throw an
+exception on failure::
// returns a path like : /tmp/prefix_wyjgtF
$filesystem->tempnam('/tmp', 'prefix_');
+ // returns a path like : /tmp/prefix_wyjgtF.png
+ $filesystem->tempnam('/tmp', 'prefix_', '.png');
-dumpFile
-~~~~~~~~
+.. _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');
+ $filesystem->dumpFile('file.txt', 'Hello World');
The ``file.txt`` file contains ``Hello World`` now.
-appendToFile
-~~~~~~~~~~~~
+``appendToFile``
+~~~~~~~~~~~~~~~~
:method:`Symfony\\Component\\Filesystem\\Filesystem::appendToFile` adds new
contents at the end of some file::
- $fileSystem->appendToFile('logs.txt', 'Email sent to user@example.com');
+ $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.
+- Absolute/relative paths: web applications frequently need to deal with absolute
+ and relative paths. Converting one to the other properly is tricky and repetitive.
+
+:class:`Symfony\\Component\\Filesystem\\Path` provides utility methods to tackle
+those issues.
+
+Canonicalization
+~~~~~~~~~~~~~~~~
+
+Returns the shortest path name equivalent to the given path. It applies the
+following rules iteratively until no further processing can be done:
+
+- "." segments are removed;
+- ".." segments are resolved;
+- backslashes ("\\") are converted into forward slashes ("/");
+- root paths ("/" and "C:/") always terminate with a slash;
+- non-root paths never terminate with a slash;
+- schemes (such as "phar://") are kept;
+- replace ``~`` with the user's home directory.
+
+You can canonicalize a path with :method:`Symfony\\Component\\Filesystem\\Path::canonicalize`::
+
+ echo Path::canonicalize('/var/www/vhost/webmozart/../config.ini');
+ // => /var/www/vhost/config.ini
+
+You can pass absolute paths and relative paths to the
+:method:`Symfony\\Component\\Filesystem\\Path::canonicalize` method. When a
+relative path is passed, ".." segments at the beginning of the path are kept::
+
+ echo Path::canonicalize('../uploads/../config/config.yaml');
+ // => ../config/config.yaml
+
+Malformed paths are returned unchanged::
+
+ echo Path::canonicalize('C:Programs/PHP/php.ini');
+ // => C:Programs/PHP/php.ini
+
+Converting Absolute/Relative Paths
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Absolute/relative paths can be converted with the methods
+:method:`Symfony\\Component\\Filesystem\\Path::makeAbsolute`
+and :method:`Symfony\\Component\\Filesystem\\Path::makeRelative`.
+
+:method:`Symfony\\Component\\Filesystem\\Path::makeAbsolute` method expects a
+relative path and a base path to base that relative path upon::
+
+ echo Path::makeAbsolute('config/config.yaml', '/var/www/project');
+ // => /var/www/project/config/config.yaml
+
+If an absolute path is passed in the first argument, the absolute path is
+returned unchanged::
+
+ echo Path::makeAbsolute('/usr/share/lib/config.ini', '/var/www/project');
+ // => /usr/share/lib/config.ini
+
+The method resolves ".." segments, if there are any::
+
+ echo Path::makeAbsolute('../config/config.yaml', '/var/www/project/uploads');
+ // => /var/www/project/config/config.yaml
+
+This method is very useful if you want to be able to accept relative paths (for
+example, relative to the root directory of your project) and absolute paths at
+the same time.
+
+:method:`Symfony\\Component\\Filesystem\\Path::makeRelative` is the inverse
+operation to :method:`Symfony\\Component\\Filesystem\\Path::makeAbsolute`::
+
+ echo Path::makeRelative('/var/www/project/config/config.yaml', '/var/www/project');
+ // => config/config.yaml
+
+If the path is not within the base path, the method will prepend ".." segments
+as necessary::
+
+ echo Path::makeRelative('/var/www/project/config/config.yaml', '/var/www/project/uploads');
+ // => ../config/config.yaml
+
+Use :method:`Symfony\\Component\\Filesystem\\Path::isAbsolute` and
+:method:`Symfony\\Component\\Filesystem\\Path::isRelative` to check whether a
+path is absolute or relative::
+
+ Path::isAbsolute('C:\Programs\PHP\php.ini')
+ // => true
+
+All four methods internally canonicalize the passed path.
+
+Finding Longest Common Base Paths
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When you store absolute file paths on the file system, this leads to a lot of
+duplicated information::
+
+ return [
+ '/var/www/vhosts/project/httpdocs/config/config.yaml',
+ '/var/www/vhosts/project/httpdocs/config/routing.yaml',
+ '/var/www/vhosts/project/httpdocs/config/services.yaml',
+ '/var/www/vhosts/project/httpdocs/images/banana.gif',
+ '/var/www/vhosts/project/httpdocs/uploads/images/nicer-banana.gif',
+ ];
+
+Especially when storing many paths, the amount of duplicated information is
+noticeable. You can use :method:`Symfony\\Component\\Filesystem\\Path::getLongestCommonBasePath`
+to check a list of paths for a common base path::
+
+ $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
--------------
@@ -323,5 +516,4 @@ Whenever something wrong happens, an exception implementing
An :class:`Symfony\\Component\\Filesystem\\Exception\\IOException` is
thrown if directory creation fails.
-.. _`Packagist`: https://packagist.org/packages/symfony/filesystem
.. _`umask`: https://en.wikipedia.org/wiki/Umask
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 ` instead.
diff --git a/components/finder.rst b/components/finder.rst
index 25cefdb2370..cecc597ac64 100644
--- a/components/finder.rst
+++ b/components/finder.rst
@@ -1,12 +1,8 @@
-.. index::
- single: Finder
- single: Components; Finder
-
The Finder Component
====================
- The Finder component finds files and directories via an intuitive fluent
- interface.
+ The Finder component finds files and directories based on different criteria
+ (name, file size, modification time, etc.) via an intuitive fluent interface.
Installation
------------
@@ -15,8 +11,6 @@ Installation
$ composer require symfony/finder
-Alternatively, you can clone the ``_ repository.
-
.. include:: /components/require_autoload.rst.inc
Usage
@@ -28,57 +22,36 @@ directories::
use Symfony\Component\Finder\Finder;
$finder = new Finder();
+ // find all files in the current directory
$finder->files()->in(__DIR__);
- foreach ($finder as $file) {
- // dumps the absolute path
- var_dump($file->getRealPath());
-
- // dumps the relative path to the file, omitting the filename
- var_dump($file->getRelativePath());
-
- // dumps the relative path to the file
- var_dump($file->getRelativePathname());
+ // check if there are any search results
+ if ($finder->hasResults()) {
+ // ...
}
-The ``$file`` is an instance of :class:`Symfony\\Component\\Finder\\SplFileInfo`
-which extends PHP's own :phpclass:`SplFileInfo` to provide methods to work with relative
-paths.
-
-The above code prints the names of all the files in the current directory
-recursively. The Finder class uses a fluent interface, so all methods return
-the Finder instance.
+ foreach ($finder as $file) {
+ $absoluteFilePath = $file->getRealPath();
+ $fileNameWithExtension = $file->getRelativePathname();
-.. tip::
+ // ...
+ }
- A Finder instance is a PHP :phpclass:`IteratorAggregate`. So, in addition to iterating over the
- Finder with ``foreach``, you can also convert it to an array with the
- :phpfunction:`iterator_to_array` function, or get the number of items with
- :phpfunction:`iterator_count`.
+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
- get mixed results.
-
-.. caution::
+ to get mixed results.
- When searching through multiple locations passed to the
- :method:`Symfony\\Component\\Finder\\Finder::in` method, a separate iterator
- is created internally for every location. This means we have multiple result
- sets aggregated into one.
- Since :phpfunction:`iterator_to_array` uses keys of result sets by default,
- when converting to an array, some keys might be duplicated and their values
- overwritten. This can be avoided by passing ``false`` as a second parameter
- to :phpfunction:`iterator_to_array`.
+Searching for Files and Directories
+-----------------------------------
-Criteria
---------
-
-There are lots of ways to filter and sort your results. You can also use the
-:method:`Symfony\\Component\\Finder\\Finder::hasResults` method to check if
-there's any file or directory matching the search criteria.
+The component provides lots of methods to define the search criteria. They all
+can be chained because they implement a `fluent interface`_.
Location
~~~~~~~~
@@ -97,12 +70,11 @@ Search in several locations by chaining calls to
// same as above
$finder->in(__DIR__)->in('/elsewhere');
-Use wildcard characters to search in the directories matching a pattern::
+Use ``*`` as a wildcard character to search in the directories matching a
+pattern (each pattern has to resolve to at least one directory path)::
$finder->in('src/Symfony/*/*/Resources');
-Each pattern has to resolve to at least one directory path.
-
Exclude directories from matching with the
:method:`Symfony\\Component\\Finder\\Finder::exclude` method::
@@ -114,7 +86,7 @@ It's also possible to ignore directories that you don't have permission to read:
$finder->ignoreUnreadableDirs()->in(__DIR__);
As the Finder uses PHP iterators, you can pass any URL with a supported
-`protocol`_::
+`PHP wrapper for URL-style protocols`_ (``ftp://``, ``zlib://``, etc.)::
// always add a trailing slash when looking for in the FTP root dir
$finder->in('ftp://example.com/');
@@ -126,8 +98,9 @@ And it also works with user-defined streams::
use Symfony\Component\Finder\Finder;
- $s3 = new \Zend_Service_Amazon_S3($key, $secret);
- $s3->registerStreamWrapper('s3');
+ // register a 's3://' wrapper with the official AWS SDK
+ $s3Client = new Aws\S3\S3Client([/* config options */]);
+ $s3Client->registerStreamWrapper();
$finder = new Finder();
$finder->name('photos*')->size('< 100K')->date('since 1 hour ago');
@@ -135,74 +108,79 @@ And it also works with user-defined streams::
// ... do something with the file
}
-.. note::
+.. seealso::
- Read the `Streams`_ documentation to learn how to create your own streams.
+ Read the `PHP streams`_ documentation to learn how to create your own streams.
Files or Directories
~~~~~~~~~~~~~~~~~~~~
-By default, the Finder returns files and directories; but the
-:method:`Symfony\\Component\\Finder\\Finder::files` and
-:method:`Symfony\\Component\\Finder\\Finder::directories` methods control that::
+By default, the Finder returns both files and directories. If you need to find either files or directories only, use the :method:`Symfony\\Component\\Finder\\Finder::files` and :method:`Symfony\\Component\\Finder\\Finder::directories` methods::
+ // look for files only; ignore directories
$finder->files();
+ // look for directories only; ignore files
$finder->directories();
-If you want to follow links, use the ``followLinks()`` method::
+If you want to follow `symbolic links`_, use the ``followLinks()`` method::
$finder->files()->followLinks();
-By default, the iterator ignores popular VCS files. This can be changed with
-the ``ignoreVCS()`` method::
-
- $finder->ignoreVCS(false);
-
-Sorting
-~~~~~~~
-
-Sort the result by name or by type (directories first, then files)::
-
- $finder->sortByName();
-
- $finder->sortByType();
+Note that this method follows links but it doesn't resolve them. Consider
+the following structure of files of directories:
-.. tip::
-
- By default, the ``sortByName()`` method uses the :phpfunction:`strcmp` PHP
- function (e.g. ``file1.txt``, ``file10.txt``, ``file2.txt``). Pass ``true``
- as its argument to use PHP's `natural sort order`_ algorithm instead (e.g.
- ``file1.txt``, ``file2.txt``, ``file10.txt``).
-
-Sort the files and directories by the last accessed, changed or modified time::
-
- $finder->sortByAccessedTime();
-
- $finder->sortByChangedTime();
+.. code-block:: text
- $finder->sortByModifiedTime();
+ ├── 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
+~~~~~~~~~~~~~~~~~~~~~
+
+`Version Control Systems`_ (or "VCS" for short), such as Git and Mercurial,
+create some special files to store their metadata. Those files are ignored by
+default when looking for files and directories, but you can change this with the
+``ignoreVCS()`` method::
-You can also define your own sorting algorithm with ``sort()`` method::
+ $finder->ignoreVCS(false);
- $finder->sort(function (\SplFileInfo $a, \SplFileInfo $b) {
- return strcmp($a->getRealPath(), $b->getRealPath());
- });
+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::
-You can reverse any sorting by using the ``reverseSorting()`` method::
+ // excludes files/directories matching the .gitignore patterns
+ $finder->ignoreVCSIgnored(true);
- // results will be sorted "Z to A" instead of the default "A to Z"
- $finder->sortByName()->reverseSorting();
+The rules of a directory always override the rules of its parent directories.
.. note::
- Notice that the ``sort*`` methods need to get all matching elements to do
- their jobs. For large iterators, it is slow.
+ 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
~~~~~~~~~
-Restrict files by name with the
+Find files by name with the
:method:`Symfony\\Component\\Finder\\Finder::name` method::
$finder->files()->name('*.php');
@@ -233,7 +211,7 @@ Multiple filenames can be excluded by chaining calls or passing an array::
File Contents
~~~~~~~~~~~~~
-Restrict files by contents with the
+Find files by content with the
:method:`Symfony\\Component\\Finder\\Finder::contains` method::
$finder->files()->contains('lorem ipsum');
@@ -249,7 +227,7 @@ The ``notContains()`` method excludes files containing given pattern::
Path
~~~~
-Restrict files and directories by path with the
+Find files and directories by path with the
:method:`Symfony\\Component\\Finder\\Finder::path` method::
// matches files that contain "data" anywhere in their paths (files or directories)
@@ -257,10 +235,11 @@ Restrict files and directories by path with the
// for example this will match data/*.xml and data.xml if they exist
$finder->path('data')->name('*.xml');
-On all platforms slash (i.e. ``/``) should be used as the directory separator.
+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/');
@@ -275,12 +254,15 @@ Multiple paths can be defined by chaining calls or passing an array::
Internally, strings are converted into regular expressions by escaping slashes
and adding delimiters:
-.. code-block:: text
-
- dirname ===> /dirname/
- a/b/c ===> /a\/b\/c/
+===================== =======================
+Original Given String Regular Expression Used
+===================== =======================
+``dirname`` ``/dirname/``
+``a/b/c`` ``/a\/b\/c/``
+===================== =======================
-The :method:`Symfony\\Component\\Finder\\Finder::notPath` method excludes files by path::
+The :method:`Symfony\\Component\\Finder\\Finder::notPath` method excludes files
+by path::
$finder->notPath('other/dir');
@@ -291,15 +273,10 @@ Multiple paths can be excluded by chaining calls or passing an array::
// same as above
$finder->notPath(['first/dir', 'other/dir']);
-.. versionadded:: 4.2
-
- Support for passing arrays to ``notPath()`` was introduced in Symfony
- 4.2
-
File Size
~~~~~~~~~
-Restrict files by size with the
+Find files by size with the
:method:`Symfony\\Component\\Finder\\Finder::size` method::
$finder->files()->size('< 1.5K');
@@ -311,8 +288,8 @@ Restrict by a size range by chaining calls or passing an array::
// same as above
$finder->files()->size(['>= 1K', '<= 2K']);
-The comparison operator can be any of the following: ``>``, ``>=``, ``<``, ``<=``,
-``==``, ``!=``.
+The comparison operator can be any of the following: ``>``, ``>=``, ``<``,
+``<=``, ``==``, ``!=``.
The target value may use magnitudes of kilobytes (``k``, ``ki``), megabytes
(``m``, ``mi``), or gigabytes (``g``, ``gi``). Those suffixed with an ``i`` use
@@ -321,7 +298,7 @@ the appropriate ``2**n`` version in accordance with the `IEC standard`_.
File Date
~~~~~~~~~
-Restrict files by last modified dates with the
+Find files by last modified dates with the
:method:`Symfony\\Component\\Finder\\Finder::date` method::
$finder->date('since yesterday');
@@ -333,18 +310,19 @@ Restrict by a date range by chaining calls or passing an array::
// same as above
$finder->date(['>= 2018-01-01', '<= 2018-12-31']);
-The comparison operator can be any of the following: ``>``, ``>=``, ``<``, ``<=``,
-``==``. You can also use ``since`` or ``after`` as an alias for ``>``, and
-``until`` or ``before`` as an alias for ``<``.
+The comparison operator can be any of the following: ``>``, ``>=``, ``<``,
+``<=``, ``==``. You can also use ``since`` or ``after`` as an alias for ``>``,
+and ``until`` or ``before`` as an alias for ``<``.
-The target value can be any date supported by the `strtotime`_ function.
+The target value can be any date supported by :phpfunction:`strtotime`.
Directory Depth
~~~~~~~~~~~~~~~
-By default, the Finder recursively traverse directories. Restrict the depth of
+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');
@@ -358,7 +336,7 @@ Restrict by a depth range by chaining calls or passing an array::
Custom Filtering
~~~~~~~~~~~~~~~~
-To restrict the matching file with your own strategy, use
+To filter results with your own strategy, use
:method:`Symfony\\Component\\Finder\\Finder::filter`::
$filter = function (\SplFileInfo $file)
@@ -375,8 +353,78 @@ 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, extension, size or type (directories first, then files)::
+
+ $finder->sortByName();
+ $finder->sortByCaseInsensitiveName();
+ $finder->sortByExtension();
+ $finder->sortBySize();
+ $finder->sortByType();
+
+.. tip::
+
+ By default, the ``sortByName()`` method uses the :phpfunction:`strcmp` PHP
+ function (e.g. ``file1.txt``, ``file10.txt``, ``file2.txt``). Pass ``true``
+ as its argument to use PHP's `natural sort order`_ algorithm instead (e.g.
+ ``file1.txt``, ``file2.txt``, ``file10.txt``).
+
+ The ``sortByCaseInsensitiveName()`` method uses the case insensitive
+ :phpfunction:`strcasecmp` PHP function. Pass ``true`` as its argument to use
+ PHP's case insensitive `natural sort order`_ algorithm instead (i.e. the
+ :phpfunction:`strnatcasecmp` PHP function)
+
+Sort the files and directories by the last accessed, changed or modified time::
+
+ $finder->sortByAccessedTime();
+
+ $finder->sortByChangedTime();
+
+ $finder->sortByModifiedTime();
+
+You can also define your own sorting algorithm with the ``sort()`` method::
+
+ $finder->sort(function (\SplFileInfo $a, \SplFileInfo $b): int {
+ return strcmp($a->getRealPath(), $b->getRealPath());
+ });
+
+You can reverse any sorting by using the ``reverseSorting()`` method::
+
+ // results will be sorted "Z to A" instead of the default "A to Z"
+ $finder->sortByName()->reverseSorting();
+
+.. note::
+
+ Notice that the ``sort*`` methods need to get all matching elements to do
+ their jobs. For large iterators, it is slow.
+
+Transforming Results into Arrays
+--------------------------------
+
+A Finder instance is an :phpclass:`IteratorAggregate` PHP class. So, in addition
+to iterating over the Finder results with ``foreach``, you can also convert it
+to an array with the :phpfunction:`iterator_to_array` function, or get the
+number of items with :phpfunction:`iterator_count`.
+
+If you call to the :method:`Symfony\\Component\\Finder\\Finder::in` method more
+than once to search through multiple locations, pass ``false`` as a second
+parameter to :phpfunction:`iterator_to_array` to avoid issues (a separate
+iterator is created for each location and, if you don't pass ``false`` to
+:phpfunction:`iterator_to_array`, keys of result sets are used and some of them
+might be duplicated and their values overwritten).
+
Reading Contents of Returned Files
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+----------------------------------
The contents of returned files can be read with
:method:`Symfony\\Component\\Finder\\SplFileInfo::getContents`::
@@ -392,9 +440,10 @@ The contents of returned files can be read with
// ...
}
-.. _strtotime: https://php.net/manual/en/datetime.formats.php
-.. _protocol: https://php.net/manual/en/wrappers.php
-.. _Streams: https://php.net/streams
-.. _IEC standard: https://physics.nist.gov/cuu/Units/binary.html
-.. _Packagist: https://packagist.org/packages/symfony/finder
+.. _`fluent interface`: https://en.wikipedia.org/wiki/Fluent_interface
+.. _`symbolic links`: https://en.wikipedia.org/wiki/Symbolic_link
+.. _`Version Control Systems`: https://en.wikipedia.org/wiki/Version_control
+.. _`PHP wrapper for URL-style protocols`: https://www.php.net/manual/en/wrappers.php
+.. _`PHP streams`: https://www.php.net/streams
+.. _`IEC standard`: https://physics.nist.gov/cuu/Units/binary.html
.. _`natural sort order`: https://en.wikipedia.org/wiki/Natural_sort_order
diff --git a/components/form.rst b/components/form.rst
index 40aadef63f8..44f407e4c8e 100644
--- a/components/form.rst
+++ b/components/form.rst
@@ -1,7 +1,3 @@
-.. index::
- single: Forms
- single: Components; Form
-
The Form Component
==================
@@ -20,8 +16,6 @@ Installation
$ composer require symfony/form
-Alternatively, you can clone the ``_ repository.
-
.. include:: /components/require_autoload.rst.inc
Configuration
@@ -57,7 +51,7 @@ support for very important features:
The Symfony Form component relies on other libraries to solve these problems.
Most of the time you will use Twig and the Symfony
:doc:`HttpFoundation `,
-:doc:`Translation ` and :doc:`Validator `
+:doc:`Translation ` and :doc:`Validator `
components, but you can replace any of these with a different library of your choice.
The following sections explain how to plug these libraries into the form
@@ -82,8 +76,8 @@ object to read data off of the correct PHP superglobals (i.e. ``$_POST`` or
.. seealso::
If you need more control over exactly when your form is submitted or which
- data is passed to it, you can use the :method:`Symfony\\Component\\Form\\FormInterface::submit`
- for this. Read more about it :ref:`form-call-submit-directly`.
+ data is passed to it,
+ :doc:`use the submit() method to handle form submissions `.
.. sidebar:: Integration with the HttpFoundation Component
@@ -91,8 +85,8 @@ object to read data off of the correct PHP superglobals (i.e. ``$_POST`` or
:class:`Symfony\\Component\\Form\\Extension\\HttpFoundation\\HttpFoundationExtension`
to your form factory::
- use Symfony\Component\Form\Forms;
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationExtension;
+ use Symfony\Component\Form\Forms;
$formFactory = Forms::createFormFactoryBuilder()
->addExtension(new HttpFoundationExtension())
@@ -121,18 +115,18 @@ use the built-in support, first install the Security CSRF component:
The following snippet adds CSRF protection to the form factory::
- use Symfony\Component\Form\Forms;
- use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Form\Extension\Csrf\CsrfExtension;
- use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage;
- use Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator;
+ use Symfony\Component\Form\Forms;
+ 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()
@@ -140,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.
@@ -157,7 +156,7 @@ the CSRF generator and validated when binding the form.
You can disable CSRF protection per form using the ``csrf_protection`` option::
- use Symfony\\Component\\Form\\Extension\\Core\\Type\\FormType
+ use Symfony\Component\Form\Extension\Core\Type\FormType;
$form = $formFactory->createBuilder(FormType::class, null, ['csrf_protection' => false])
->getForm();
@@ -165,10 +164,10 @@ You can disable CSRF protection per form using the ``csrf_protection`` option::
Twig Templating
~~~~~~~~~~~~~~~
-If you're using the Form component to process HTML forms, you'll need a way
-to render your form as HTML form fields (complete with field values,
-errors, and labels). If you use `Twig`_ as your template engine, the Form
-component offers a rich integration.
+If you're using the Form component to process HTML forms, you'll need a way to
+render your form as HTML form fields (complete with field values, errors, and
+labels). If you use `Twig`_ as your template engine, the Form component offers a
+rich integration.
To use the integration, you'll need the twig bridge, which provides integration
between Twig and several Symfony components:
@@ -183,10 +182,10 @@ that help you render the HTML widget, label, help and errors for each field
(as well as a few other things). To configure the integration, you'll need
to bootstrap or access Twig and add the :class:`Symfony\\Bridge\\Twig\\Extension\\FormExtension`::
- use Symfony\Component\Form\Forms;
use Symfony\Bridge\Twig\Extension\FormExtension;
- use Symfony\Component\Form\FormRenderer;
use Symfony\Bridge\Twig\Form\TwigRendererEngine;
+ use Symfony\Component\Form\FormRenderer;
+ use Symfony\Component\Form\Forms;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
use Twig\RuntimeLoader\FactoryRuntimeLoader;
@@ -209,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);
},
]));
@@ -224,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.
@@ -248,13 +243,13 @@ Translation
~~~~~~~~~~~
If you're using the Twig integration with one of the default form theme files
-(e.g. ``form_div_layout.html.twig``), there are 2 Twig filters (``trans``
-and ``transChoice``) that are used for translating form labels, errors, option
+(e.g. ``form_div_layout.html.twig``), there is a Twig filter (``trans``)
+that is used for translating form labels, errors, option
text and other strings.
-To add these Twig filters, you can either use the built-in
+To add the ``trans`` Twig filter, you can either use the built-in
:class:`Symfony\\Bridge\\Twig\\Extension\\TranslationExtension` that integrates
-with Symfony's Translation component, or add the 2 Twig filters yourself,
+with Symfony's Translation component, or add the Twig filter yourself,
via your own Twig extension.
To use the built-in integration, be sure that your project has Symfony's
@@ -266,12 +261,12 @@ installed:
$ composer require symfony/translation symfony/config
Next, add the :class:`Symfony\\Bridge\\Twig\\Extension\\TranslationExtension`
-to your ``Twig\\Environment`` instance::
+to your ``Twig\Environment`` instance::
+ use Symfony\Bridge\Twig\Extension\TranslationExtension;
use Symfony\Component\Form\Forms;
- use Symfony\Component\Translation\Translator;
use Symfony\Component\Translation\Loader\XliffFileLoader;
- use Symfony\Bridge\Twig\Extension\TranslationExtension;
+ use Symfony\Component\Translation\Translator;
// creates the Translator
$translator = new Translator('en');
@@ -283,7 +278,7 @@ to your ``Twig\\Environment`` instance::
'en'
);
- // adds the TranslationExtension (gives us trans and transChoice filters)
+ // adds the TranslationExtension (it gives us trans filter)
$twig->addExtension(new TranslationExtension($translator));
$formFactory = Forms::createFormFactoryBuilder()
@@ -318,8 +313,8 @@ errors are then mapped to the correct field and rendered.
Your integration with the Validation component will look something like this::
- use Symfony\Component\Form\Forms;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
+ use Symfony\Component\Form\Forms;
use Symfony\Component\Validator\Validation;
$vendorDirectory = realpath(__DIR__.'/../vendor');
@@ -372,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
@@ -384,8 +375,9 @@ 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
- base controller class has a :method:`Symfony\\Bundle\\FrameworkBundle\\Controller::createFormBuilder`
+ 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.
@@ -395,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
@@ -439,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
@@ -455,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'),
@@ -498,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
@@ -519,11 +513,11 @@ done by passing a special form "view" object to your template (notice the
{{ form_start(form) }}
{{ form_widget(form) }}
-
+
{{ 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
@@ -537,23 +531,10 @@ Changing a Form's Method and Action
By default, a form is submitted to the same URI that rendered the form with
an HTTP POST request. This behavior can be changed using the :ref:`form-option-action`
and :ref:`form-option-method` options (the ``method`` option is also used
-by ``handleRequest()`` to determine whether a form has been submitted):
+by :method:`Symfony\\Component\\Form\\Form::handleRequest` to determine whether a form has been submitted):
.. 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
@@ -561,10 +542,11 @@ by ``handleRequest()`` to determine whether a form has been submitted):
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',
@@ -575,46 +557,28 @@ by ``handleRequest()`` to determine whether a form has been submitted):
}
}
-.. _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
@@ -624,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)
@@ -648,20 +613,58 @@ 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 `, 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 ``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.
-Pass the current request to the ``handleRequest()`` method. Then, the Form
-component will do all the necessary work for you.
+Just pass the current request to the :method:`Symfony\\Component\\Form\\Form::handleRequest`
+method. Then, the Form component will do all the necessary work for you.
.. _component-form-intro-validation:
@@ -673,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, [
@@ -715,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.
@@ -749,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
@@ -765,8 +769,9 @@ method. This is useful when you'd like to validate the form without showing
validation errors to the user (i.e. during a partial AJAX submission or
:doc:`dynamic form modification `).
-Because clearing the errors makes the form valid, ``clearErrors()`` should only
-be called after testing whether the form is valid.
+Because clearing the errors makes the form valid,
+:method:`Symfony\\Component\\Form\\ClearableErrorsInterface::clearErrors`
+should only be called after testing whether the form is valid.
Learn more
----------
@@ -777,6 +782,5 @@ Learn more
/form/*
-.. _Packagist: https://packagist.org/packages/symfony/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 f7dad004250..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
============================
@@ -23,8 +18,6 @@ Installation
$ composer require symfony/http-foundation
-Alternatively, you can clone the ``_ repository.
-
.. include:: /components/require_autoload.rst.inc
.. seealso::
@@ -59,6 +52,8 @@ which is almost equivalent to the more verbose, but also more flexible,
$_SERVER
);
+.. _accessing-request-data:
+
Accessing Request Data
~~~~~~~~~~~~~~~~~~~~~~
@@ -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,9 +200,21 @@ Finally, the raw data sent with the request body can be accessed using
$content = $request->getContent();
-For instance, this may be useful to process a JSON string sent to the
+For instance, this may be useful to process an XML string sent to the
application by a remote service using the HTTP POST method.
+If the request body is a JSON string, it can be accessed using
+:method:`Symfony\\Component\\HttpFoundation\\Request::toArray`::
+
+ $data = $request->toArray();
+
+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::
+
+ $data = $request->getPayload();
+
Identifying a Request
~~~~~~~~~~~~~~~~~~~~~
@@ -236,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.
@@ -272,6 +296,10 @@ this complexity and defines some methods for the most common tasks::
HeaderUtils::unquote('"foo \"bar\""');
// => 'foo "bar"'
+ // Parses a query string but maintains dots (PHP parse_str() replaces '.' by '_')
+ HeaderUtils::parseQuery('foo[bar.baz]=qux');
+ // => ['foo' => ['bar.baz' => 'qux']]
+
Accessing ``Accept-*`` Headers Data
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -316,6 +344,126 @@ are also supported::
$quality = $accept->get('text/xml')->getQuality(); // $quality = 0.8
$quality = $accept->get('application/xml')->getQuality(); // $quality = 0.3
+Anonymizing IP Addresses
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+An increasingly common need for applications to comply with user protection
+regulations is to anonymize IP addresses before logging and storing them for
+analysis purposes. Use the ``anonymize()`` method from the
+:class:`Symfony\\Component\\HttpFoundation\\IpUtils` to do that::
+
+ use Symfony\Component\HttpFoundation\IpUtils;
+
+ $ipv4 = '123.234.235.236';
+ $anonymousIpv4 = IpUtils::anonymize($ipv4);
+ // $anonymousIpv4 = '123.234.235.0'
+
+ $ipv6 = '2a01:198:603:10:396e:4789:8e99:890f';
+ $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
~~~~~~~~~~~~~~~~~~~~
@@ -407,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 `.
+
Setting Cookies
~~~~~~~~~~~~~~~
@@ -415,7 +571,7 @@ attribute::
use Symfony\Component\HttpFoundation\Cookie;
- $response->headers->setCookie(new Cookie('foo', 'bar'));
+ $response->headers->setCookie(Cookie::create('foo', 'bar'));
The
:method:`Symfony\\Component\\HttpFoundation\\ResponseHeaderBag::setCookie`
@@ -425,9 +581,27 @@ method takes an instance of
You can clear a cookie via the
:method:`Symfony\\Component\\HttpFoundation\\ResponseHeaderBag::clearCookie` method.
-Note you can create a
-:class:`Symfony\\Component\\HttpFoundation\\Cookie` object from a raw header
-value using :method:`Symfony\\Component\\HttpFoundation\\Cookie::fromString`.
+In addition to the ``Cookie::create()`` method, you can create a ``Cookie``
+object from a raw header value using :method:`Symfony\\Component\\HttpFoundation\\Cookie::fromString`
+method. You can also use the ``with*()`` methods to change some Cookie property (or
+to build the entire Cookie using a fluent interface). Each ``with*()`` method returns
+a new object with the modified property::
+
+ $cookie = Cookie::create('foo')
+ ->withValue('bar')
+ ->withExpires(strtotime('Fri, 20-May-2011 15:25:52 GMT'))
+ ->withDomain('.example.com')
+ ->withSecure(true);
+
+It is possible to define partitioned cookies, also known as `CHIPS`_, by using the
+:method:`Symfony\\Component\\HttpFoundation\\Cookie::withPartitioned` method::
+
+ $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
~~~~~~~~~~~~~~~~~~~~~~~
@@ -435,17 +609,19 @@ Managing the HTTP Cache
The :class:`Symfony\\Component\\HttpFoundation\\Response` class has a rich set
of methods to manipulate the HTTP headers related to the cache:
-* :method:`Symfony\\Component\\HttpFoundation\\Response::setPublic`;
-* :method:`Symfony\\Component\\HttpFoundation\\Response::setPrivate`;
-* :method:`Symfony\\Component\\HttpFoundation\\Response::expire`;
-* :method:`Symfony\\Component\\HttpFoundation\\Response::setExpires`;
-* :method:`Symfony\\Component\\HttpFoundation\\Response::setMaxAge`;
-* :method:`Symfony\\Component\\HttpFoundation\\Response::setSharedMaxAge`;
-* :method:`Symfony\\Component\\HttpFoundation\\Response::setTtl`;
-* :method:`Symfony\\Component\\HttpFoundation\\Response::setClientTtl`;
-* :method:`Symfony\\Component\\HttpFoundation\\Response::setLastModified`;
-* :method:`Symfony\\Component\\HttpFoundation\\Response::setEtag`;
-* :method:`Symfony\\Component\\HttpFoundation\\Response::setVary`;
+* :method:`Symfony\\Component\\HttpFoundation\\Response::setPublic`
+* :method:`Symfony\\Component\\HttpFoundation\\Response::setPrivate`
+* :method:`Symfony\\Component\\HttpFoundation\\Response::expire`
+* :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`
+* :method:`Symfony\\Component\\HttpFoundation\\Response::setEtag`
+* :method:`Symfony\\Component\\HttpFoundation\\Response::setVary`
.. note::
@@ -459,12 +635,20 @@ can be used to set the most commonly used cache information in one method
call::
$response->setCache([
- 'etag' => 'abcdef',
- 'last_modified' => new \DateTime(),
- 'max_age' => 600,
- 's_maxage' => 600,
- 'private' => false,
- 'public' => true,
+ 'must_revalidate' => false,
+ 'no_cache' => false,
+ 'no_store' => false,
+ 'no_transform' => false,
+ 'public' => true,
+ 'private' => false,
+ 'proxy_revalidate' => false,
+ 'max_age' => 600,
+ 's_maxage' => 600,
+ 'stale_if_error' => 86400,
+ 'stale_while_revalidate' => 60,
+ 'immutable' => true,
+ 'last_modified' => new \DateTime(),
+ 'etag' => 'abcdef',
]);
To check if the Response validators (``ETag``, ``Last-Modified``) match a
@@ -497,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->setCallback(function () {
+ $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 (): void {
var_dump('Hello World');
flush();
sleep(2);
@@ -520,11 +715,107 @@ represented by a PHP callable instead of a string::
Additionally, PHP isn't the only layer that can buffer output. Your web
server might also buffer based on its configuration. Some servers, such as
- Nginx, let you disable buffering at the config level or by adding a special HTTP
+ nginx, let you disable buffering at the config level or by adding a special HTTP
header in the response::
- // disables FastCGI buffering in Nginx only for this response
- $response->headers->set('X-Accel-Buffering', 'no')
+ // disables FastCGI buffering in nginx only for this response
+ $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:
@@ -533,7 +824,7 @@ Serving Files
When sending a file, you must add a ``Content-Disposition`` header to your
response. While creating this header for basic file downloads is straightforward,
-using non-ASCII filenames is more involving. The
+using non-ASCII filenames is more involved. The
:method:`Symfony\\Component\\HttpFoundation\\HeaderUtils::makeDisposition`
abstracts the hard work behind a simple API::
@@ -561,13 +852,34 @@ 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();
+.. note::
+
+ The ``BinaryFileResponse`` will only handle ``X-Sendfile`` if the particular header is present.
+ For Apache, this is not the default case.
+
+ To add the header use the ``mod_headers`` Apache module and add the following to the Apache configuration:
+
+ .. code-block:: apache
+
+
+ # This is already present somewhere...
+ XSendFile on
+ XSendFilePath ...some path...
+
+ # This needs to be added:
+
+ RequestHeader set X-Sendfile-Type X-Sendfile
+
+
+
With the ``BinaryFileResponse``, you can still set the ``Content-Type`` of the sent file,
or change its ``Content-Disposition``::
@@ -578,10 +890,27 @@ or change its ``Content-Disposition``::
'filename.txt'
);
-It is possible to delete the file after the request is sent with the
+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``
@@ -590,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::
@@ -625,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
@@ -636,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.
@@ -647,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
~~~~~~~~~~~~~~
@@ -665,7 +1006,64 @@ 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
+-----------------------
+
+Some web sites have a "safe" mode to assist those who don't want to be exposed
+to content to which they might object. The `RFC 8674`_ specification defines a
+way for user agents to ask for safe content to a server.
+
+The specification does not define what content might be considered objectionable,
+so the concept of "safe" is not precisely defined. Rather, the term is interpreted
+by the server and within the scope of each web site that chooses to act upon this information.
+
+Symfony offers two methods to interact with this preference:
+
+* :method:`Symfony\\Component\\HttpFoundation\\Request::preferSafeContent`;
+* :method:`Symfony\\Component\\HttpFoundation\\Response::setContentSafe`;
+
+The following example shows how to detect if the user agent prefers "safe" content::
+
+ if ($request->preferSafeContent()) {
+ $response = new Response($alternativeContent);
+ // this informs the user we respected their preferences
+ $response->setContentSafe();
+
+ return $response;
+
+Generating Relative and Absolute URLs
+-------------------------------------
+
+Generating absolute and relative URLs for a given path is a common need
+in some applications. In Twig templates you can use the
+:ref:`absolute_url() ` and
+:ref:`relative_path() ` functions to do that.
+
+The :class:`Symfony\\Component\\HttpFoundation\\UrlHelper` class provides the
+same functionality for PHP code via the ``getAbsoluteUrl()`` and ``getRelativePath()``
+methods. You can inject this as a service anywhere in your application::
+
+ // src/Normalizer/UserApiNormalizer.php
+ namespace App\Normalizer;
+
+ use Symfony\Component\HttpFoundation\UrlHelper;
+
+ class UserApiNormalizer
+ {
+ public function __construct(
+ private UrlHelper $urlHelper,
+ ) {
+ }
+
+ public function normalize($user): array
+ {
+ return [
+ 'avatar' => $this->urlHelper->getAbsoluteUrl($user->avatar()->path()),
+ ];
+ }
+ }
Learn More
----------
@@ -674,14 +1072,17 @@ Learn More
:maxdepth: 1
:glob:
- /components/http_foundation/*
/controller
/controller/*
- /session/*
+ /session
/http_cache/*
-.. _Packagist: https://packagist.org/packages/symfony/http-foundation
-.. _Nginx: https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/
-.. _Apache: https://tn123.org/mod_xsendfile/
-.. _`JSON Hijacking`: http://haacked.com/archive/2009/06/25/json-hijacking.aspx
-.. _OWASP guidelines: https://www.owasp.org/index.php/OWASP_AJAX_Security_Guidelines#Always_return_JSON_with_an_Object_on_the_outside
+.. _`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 26600980f02..00000000000
--- a/components/http_foundation/session_configuration.rst
+++ /dev/null
@@ -1,291 +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 `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\NativeSessionStorage;
- use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler;
-
- $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\NativeSessionStorage;
- use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler;
-
- $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://php.net/session.customhandler
-.. _`php.net/session.configuration`: https://php.net/session.configuration
-.. _`php.net/memcached.setoption`: https://php.net/memcached.setoption
diff --git a/components/http_foundation/session_php_bridge.rst b/components/http_foundation/session_php_bridge.rst
deleted file mode 100644
index 295c0976854..00000000000
--- a/components/http_foundation/session_php_bridge.rst
+++ /dev/null
@@ -1,49 +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 54a3363a4d2..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 make your code which utilizes session easily testable, we provide two separate
-mock storage mechanisms for both unit testing and functional testing.
-
-Testing code using real sessions is tricky because PHP's workflow state is global
-and it is not possible to have multiple concurrent sessions in the same PHP
-process.
-
-The mock storage engines simulate the PHP session workflow without actually
-starting one allowing you to test your code without complications. You may also
-run multiple instances in the same PHP process.
-
-The mock storage drivers do not read or write the system globals
-``session_id()`` or ``session_name()``. Methods are provided to simulate this if
-required:
-
-* :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\Storage\MockArraySessionStorage;
- use Symfony\Component\HttpFoundation\Session\Session;
-
- $session = new Session(new MockArraySessionStorage());
-
-Functional Testing
-------------------
-
-For functional testing where you may need to persist session data across
-separate PHP processes, 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 fef88d6a10f..00000000000
--- a/components/http_foundation/sessions.rst
+++ /dev/null
@@ -1,341 +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 `.
-
-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 '
'.$message.'
';
- }
-
-.. 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.
-
-.. _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
-.....................
-
-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 '
';
- }
-
-Compact method to process display all flashes at once::
-
- foreach ($session->getFlashBag()->all() as $type => $messages) {
- foreach ($messages as $message) {
- echo '
'.$message.'
';
- }
- }
diff --git a/components/http_kernel.rst b/components/http_kernel.rst
index 54eb6ab70af..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
------------
@@ -18,12 +13,12 @@ Installation
$ composer require symfony/http-kernel
-Alternatively, you can clone the ``_ repository.
-
.. include:: /components/require_autoload.rst.inc
-The Workflow of a Request
--------------------------
+.. _the-workflow-of-a-request:
+
+The Request-Response Lifecycle
+------------------------------
.. seealso::
@@ -33,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**;
@@ -67,21 +61,23 @@ that system::
*/
public function handle(
Request $request,
- $type = self::MASTER_REQUEST,
- $catch = true
- );
+ int $type = self::MAIN_REQUEST,
+ bool $catch = true
+ ): Response;
}
Internally, :method:`HttpKernel::handle() ` -
the concrete implementation of :method:`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
-
+
-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
@@ -103,12 +99,12 @@ not take many steps. You create an
(explained below). To complete your working kernel, you'll add more event
listeners to the events discussed below::
- use Symfony\Component\HttpFoundation\Request;
- use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\EventDispatcher\EventDispatcher;
+ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
+ use Symfony\Component\HttpKernel\HttpKernel;
// create the Request object
$request = Request::createFromGlobals();
@@ -133,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 `" 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 `.
.. seealso::
@@ -202,8 +191,8 @@ attributes).
is the :class:`Symfony\\Component\\HttpKernel\\EventListener\\RouterListener`.
This class executes the routing layer, which returns an *array* of information
about the matched request, including the ``_controller`` and any placeholders
- that are in the route's pattern (e.g. ``{slug}``). See
- :doc:`Routing component `.
+ that are in the route's pattern (e.g. ``{slug}``). See the
+ :doc:`Routing documentation `.
This array of information is stored in the :class:`Symfony\\Component\\HttpFoundation\\Request`
object's ``attributes`` array. Adding the routing information here doesn't
@@ -238,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
@@ -251,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``.
@@ -272,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
@@ -291,26 +275,27 @@ 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:`FilterControllerEvent::setController `
+by calling :method:`ControllerEvent::setController `
on the event object that's passed to listeners on this event.
.. sidebar:: ``kernel.controller`` in the Symfony Framework
- There are a few minor listeners to the ``kernel.controller`` event in
- the Symfony Framework, and many deal with collecting profiler data when
- the profiler is enabled.
+ An interesting listener to ``kernel.controller`` in the Symfony
+ Framework is :class:`Symfony\\Component\\HttpKernel\\EventListener\\CacheAttributeListener`.
+ This class fetches ``#[Cache]`` attribute configuration from the
+ controller and uses it to configure :doc:`HTTP caching `
+ on the response.
- One interesting listener comes from the `SensioFrameworkExtraBundle`_. This
- listener's `@ParamConverter`_ functionality allows you to pass a full object
- (e.g. a ``Post`` object) to your controller instead of a scalar value (e.g.
- an ``id`` parameter that was on your route). The listener -
- ``ParamConverterListener`` - uses reflection to look at each of the
- arguments of the controller and tries to use different methods to convert
- those to objects, which are then stored in the ``attributes`` property of
- the ``Request`` object. Read the next section to see why this is important.
+ There are a few other minor listeners to the ``kernel.controller`` event in
+ the Symfony Framework that deal with collecting profiler data when the
+ profiler is enabled.
4) Getting the Controller Arguments
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -349,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:
@@ -360,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
@@ -411,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 ` 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
@@ -486,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.
@@ -500,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 ` 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
@@ -517,28 +496,41 @@ Handling Exceptions: the ``kernel.exception`` Event
:ref:`Kernel Events Information Table `
If an exception is thrown at any point inside ``HttpKernel::handle()``, another
-event - ``kernel.exception`` is thrown. Internally, the body of the ``handle()``
-function is wrapped in a try-catch block. When any exception is thrown, the
+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
-
+
-Each listener to this event is passed a :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent`
+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
-:method:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent::getException`
+:method:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent::getThrowable`
method. A typical listener on this event will check for a certain type of
exception and create an appropriate error ``Response``.
For example, to generate a 404 page, you might throw a special type of exception
and then add a listener on this event that looks for this exception and
creates and returns a 404 ``Response``. In fact, the HttpKernel component
-comes with an :class:`Symfony\\Component\\HttpKernel\\EventListener\\ExceptionListener`,
+comes with an :class:`Symfony\\Component\\HttpKernel\\EventListener\\ErrorListener`,
which if you choose to use, will do this and more by default (see the sidebar
below for more details).
+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
@@ -549,14 +541,14 @@ below for more details).
There are two main listeners to ``kernel.exception`` when using the
Symfony Framework.
- **ExceptionListener in the HttpKernel Component**
+ **ErrorListener in the HttpKernel Component**
The first comes core to the HttpKernel component
- and is called :class:`Symfony\\Component\\HttpKernel\\EventListener\\ExceptionListener`.
+ and is called :class:`Symfony\\Component\\HttpKernel\\EventListener\\ErrorListener`.
The listener has several goals:
1) The thrown exception is converted into a
- :class:`Symfony\\Component\\Debug\\Exception\\FlattenException`
+ :class:`Symfony\\Component\\ErrorHandler\\Exception\\FlattenException`
object, which contains all the information about the request, but which
can be printed and serialized.
@@ -598,23 +590,24 @@ 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:
.. _component-http-kernel-event-table:
-===================== ================================ ===================================================================================
-Name ``KernelEvents`` Constant Argument passed to the listener
-===================== ================================ ===================================================================================
-kernel.request ``KernelEvents::REQUEST`` :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseEvent`
-kernel.controller ``KernelEvents::CONTROLLER`` :class:`Symfony\\Component\\HttpKernel\\Event\\FilterControllerEvent`
-kernel.view ``KernelEvents::VIEW`` :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForControllerResultEvent`
-kernel.response ``KernelEvents::RESPONSE`` :class:`Symfony\\Component\\HttpKernel\\Event\\FilterResponseEvent`
-kernel.finish_request ``KernelEvents::FINISH_REQUEST`` :class:`Symfony\\Component\\HttpKernel\\Event\\FinishRequestEvent`
-kernel.terminate ``KernelEvents::TERMINATE`` :class:`Symfony\\Component\\HttpKernel\\Event\\PostResponseEvent`
-kernel.exception ``KernelEvents::EXCEPTION`` :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent`
-===================== ================================ ===================================================================================
+=========================== ====================================== ========================================================================
+Name ``KernelEvents`` Constant Argument passed to the listener
+=========================== ====================================== ========================================================================
+kernel.request ``KernelEvents::REQUEST`` :class:`Symfony\\Component\\HttpKernel\\Event\\RequestEvent`
+kernel.controller ``KernelEvents::CONTROLLER`` :class:`Symfony\\Component\\HttpKernel\\Event\\ControllerEvent`
+kernel.controller_arguments ``KernelEvents::CONTROLLER_ARGUMENTS`` :class:`Symfony\\Component\\HttpKernel\\Event\\ControllerArgumentsEvent`
+kernel.view ``KernelEvents::VIEW`` :class:`Symfony\\Component\\HttpKernel\\Event\\ViewEvent`
+kernel.response ``KernelEvents::RESPONSE`` :class:`Symfony\\Component\\HttpKernel\\Event\\ResponseEvent`
+kernel.finish_request ``KernelEvents::FINISH_REQUEST`` :class:`Symfony\\Component\\HttpKernel\\Event\\FinishRequestEvent`
+kernel.terminate ``KernelEvents::TERMINATE`` :class:`Symfony\\Component\\HttpKernel\\Event\\TerminateEvent`
+kernel.exception ``KernelEvents::EXCEPTION`` :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent`
+=========================== ====================================== ========================================================================
.. _http-kernel-working-example:
@@ -644,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'))
);
@@ -674,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
@@ -682,7 +675,9 @@ your controller).
.. raw:: html
-
+
To execute a sub request, use ``HttpKernel::handle()``, but change the second
argument as follows::
@@ -702,50 +697,52 @@ 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\GetResponseEvent;
+ use Symfony\Component\HttpKernel\Event\RequestEvent;
// ...
- public function onKernelRequest(GetResponseEvent $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::
- use Symfony\Component\HttpKernel\HttpKernel;
-
- // ...
- $kernel = new HttpKernel($dispatcher, $resolver);
$path = $kernel->locateResource('@FooBundle/Resources/config/services.xml');
Learn more
@@ -757,13 +754,8 @@ Learn more
/reference/events
-.. _Packagist: https://packagist.org/packages/symfony/http-kernel
-.. _reflection: https://php.net/manual/en/book.reflection.php
+.. _reflection: https://www.php.net/manual/en/book.reflection.php
.. _FOSRestBundle: https://github.com/friendsofsymfony/FOSRestBundle
-.. _`Create your own framework... on top of the Symfony2 Components`: http://fabien.potencier.org/article/50/create-your-own-framework-on-top-of-the-symfony2-components-part-1
-.. _`PHP FPM`: https://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: http://php.net/manual/en/functions.arguments.php
+.. _`PHP FPM`: https://www.php.net/manual/en/install.fpm.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 14f7e7b6f51..00000000000
--- a/components/index.rst
+++ /dev/null
@@ -1,9 +0,0 @@
-The Components
-==============
-
-.. toctree::
- :maxdepth: 1
- :glob:
-
- using_components
- *
diff --git a/components/intl.rst b/components/intl.rst
index 6a5fc5e3d7f..ba3cbdcb959 100644
--- a/components/intl.rst
+++ b/components/intl.rst
@@ -1,17 +1,7 @@
-.. index::
- single: Intl
- single: Components; Intl
-
The Intl Component
==================
- A PHP replacement layer for the C `intl extension`_ that also provides
- access to the localization data of the `ICU library`_.
-
-.. caution::
-
- The replacement layer is limited to the locale "en". If you want to use
- other locales, you should `install the intl extension`_ instead.
+ This component provides access to the localization data of the `ICU library`_.
.. seealso::
@@ -26,310 +16,390 @@ Installation
$ composer require symfony/intl
-Alternatively, you can clone the ``_ repository.
-
.. 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
+------------------
-Writing and Reading Resource Bundles
-------------------------------------
+This component provides the following ICU data:
-The :phpclass:`ResourceBundle` class is not currently supported by this component.
-Instead, it includes a set of readers and writers for reading and writing
-arrays (or array-like objects) from/to resource bundle files. The following
-classes are supported:
+* `Language and Script Names`_
+* `Country Names`_
+* `Locales`_
+* `Currencies`_
+* `Timezones`_
-* `TextBundleWriter`_
-* `PhpBundleWriter`_
-* `BinaryBundleReader`_
-* `PhpBundleReader`_
-* `BufferedBundleReader`_
-* `StructuredBundleReader`_
+Language and Script Names
+~~~~~~~~~~~~~~~~~~~~~~~~~
-Continue reading if you are interested in how to use these classes. Otherwise
-skip this section and jump to `Accessing ICU Data`_.
+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::
-TextBundleWriter
-~~~~~~~~~~~~~~~~
+ use Symfony\Component\Intl\Languages;
-The :class:`Symfony\\Component\\Intl\\ResourceBundle\\Writer\\TextBundleWriter`
-writes an array or an array-like object to a plain-text resource bundle. The
-resulting .txt file can be converted to a binary .res file with the
-:class:`Symfony\\Component\\Intl\\ResourceBundle\\Compiler\\BundleCompiler`
-class::
+ \Locale::setDefault('en');
- use Symfony\Component\Intl\ResourceBundle\Writer\TextBundleWriter;
- use Symfony\Component\Intl\ResourceBundle\Compiler\BundleCompiler;
+ $languages = Languages::getNames();
+ // ('languageCode' => 'languageName')
+ // => ['ab' => 'Abkhazian', 'ace' => 'Achinese', ...]
- $writer = new TextBundleWriter();
- $writer->write('/path/to/bundle', 'en', [
- 'Data' => [
- 'entry1',
- 'entry2',
- // ...
- ],
- ]);
+ $languages = Languages::getAlpha3Names();
+ // ('languageCode' => 'languageName')
+ // => ['abk' => 'Abkhazian', 'ace' => 'Achinese', ...]
- $compiler = new BundleCompiler();
- $compiler->compile('/path/to/bundle', '/path/to/binary/bundle');
+ $language = Languages::getName('fr');
+ // => 'French'
-The command "genrb" must be available for the
-:class:`Symfony\\Component\\Intl\\ResourceBundle\\Compiler\\BundleCompiler` to
-work. If the command is located in a non-standard location, you can pass its
-path to the
-:class:`Symfony\\Component\\Intl\\ResourceBundle\\Compiler\\BundleCompiler`
-constructor.
+ $language = Languages::getAlpha3Name('fra');
+ // => 'French'
-PhpBundleWriter
-~~~~~~~~~~~~~~~
+All methods accept the translation locale as the last, optional parameter,
+which defaults to the current default locale::
-The :class:`Symfony\\Component\\Intl\\ResourceBundle\\Writer\\PhpBundleWriter`
-writes an array or an array-like object to a .php resource bundle::
+ $languages = Languages::getNames('de');
+ // => ['ab' => 'Abchasisch', 'ace' => 'Aceh', ...]
- use Symfony\Component\Intl\ResourceBundle\Writer\PhpBundleWriter;
+ $languages = Languages::getAlpha3Names('de');
+ // => ['abk' => 'Abchasisch', 'ace' => 'Aceh', ...]
- $writer = new PhpBundleWriter();
- $writer->write('/path/to/bundle', 'en', [
- 'Data' => [
- 'entry1',
- 'entry2',
- // ...
- ],
- ]);
+ $language = Languages::getName('fr', 'de');
+ // => 'Französisch'
-BinaryBundleReader
-~~~~~~~~~~~~~~~~~~
+ $language = Languages::getAlpha3Name('fra', 'de');
+ // => 'Französisch'
-The :class:`Symfony\\Component\\Intl\\ResourceBundle\\Reader\\BinaryBundleReader`
-reads binary resource bundle files and returns an array or an array-like object.
-This class currently only works with the `intl extension`_ installed::
+If the given locale doesn't exist, the methods trigger a
+:class:`Symfony\\Component\\Intl\\Exception\\MissingResourceException`. In addition
+to catching the exception, you can also check if a given language code is valid::
- use Symfony\Component\Intl\ResourceBundle\Reader\BinaryBundleReader;
+ $isValidLanguage = Languages::exists($languageCode);
- $reader = new BinaryBundleReader();
- $data = $reader->read('/path/to/bundle', 'en');
+Or if you have an alpha3 language code you want to check::
- var_dump($data['Data']['entry1']);
+ $isValidLanguage = Languages::alpha3CodeExists($alpha3Code);
-PhpBundleReader
-~~~~~~~~~~~~~~~
+You may convert codes between two-letter alpha2 and three-letter alpha3 codes::
-The :class:`Symfony\\Component\\Intl\\ResourceBundle\\Reader\\PhpBundleReader`
-reads resource bundles from .php files and returns an array or an array-like
-object::
+ $alpha3Code = Languages::getAlpha3Code($alpha2Code);
- use Symfony\Component\Intl\ResourceBundle\Reader\PhpBundleReader;
+ $alpha2Code = Languages::getAlpha2Code($alpha3Code);
- $reader = new PhpBundleReader();
- $data = $reader->read('/path/to/bundle', 'en');
+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)::
- var_dump($data['Data']['entry1']);
+ use Symfony\Component\Intl\Scripts;
-BufferedBundleReader
-~~~~~~~~~~~~~~~~~~~~
+ \Locale::setDefault('en');
-The :class:`Symfony\\Component\\Intl\\ResourceBundle\\Reader\\BufferedBundleReader`
-wraps another reader, but keeps the last N reads in a buffer, where N is a
-buffer size passed to the constructor::
+ $scripts = Scripts::getNames();
+ // ('scriptCode' => 'scriptName')
+ // => ['Adlm' => 'Adlam', 'Afak' => 'Afaka', ...]
- use Symfony\Component\Intl\ResourceBundle\Reader\BinaryBundleReader;
- use Symfony\Component\Intl\ResourceBundle\Reader\BufferedBundleReader;
+ $script = Scripts::getName('Hans');
+ // => 'Simplified'
- $reader = new BufferedBundleReader(new BinaryBundleReader(), 10);
+All methods accept the translation locale as the last, optional parameter,
+which defaults to the current default locale::
- // actually reads the file
- $data = $reader->read('/path/to/bundle', 'en');
+ $scripts = Scripts::getNames('de');
+ // => ['Adlm' => 'Adlam', 'Afak' => 'Afaka', ...]
- // returns data from the buffer
- $data = $reader->read('/path/to/bundle', 'en');
+ $script = Scripts::getName('Hans', 'de');
+ // => 'Vereinfacht'
- // actually reads the file
- $data = $reader->read('/path/to/bundle', 'fr');
+If the given script code doesn't exist, the methods trigger a
+:class:`Symfony\\Component\\Intl\\Exception\\MissingResourceException`. In addition
+to catching the exception, you can also check if a given script code is valid::
-StructuredBundleReader
-~~~~~~~~~~~~~~~~~~~~~~
+ $isValidScript = Scripts::exists($scriptCode);
-The :class:`Symfony\\Component\\Intl\\ResourceBundle\\Reader\\StructuredBundleReader`
-wraps another reader and offers a
-:method:`Symfony\\Component\\Intl\\ResourceBundle\\Reader\\StructuredBundleReaderInterface::readEntry`
-method for reading an entry of the resource bundle without having to worry
-whether array keys are set or not. If a path cannot be resolved, ``null`` is
-returned::
+Country Names
+~~~~~~~~~~~~~
- use Symfony\Component\Intl\ResourceBundle\Reader\BinaryBundleReader;
- use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReader;
+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::
- $reader = new StructuredBundleReader(new BinaryBundleReader());
+ use Symfony\Component\Intl\Countries;
- $data = $reader->read('/path/to/bundle', 'en');
+ \Locale::setDefault('en');
- // produces an error if the key "Data" does not exist
- var_dump($data['Data']['entry1']);
+ $countries = Countries::getNames();
+ // ('alpha2Code' => 'countryName')
+ // => ['AF' => 'Afghanistan', 'AX' => 'Åland Islands', ...]
- // returns null if the key "Data" does not exist
- var_dump($reader->readEntry('/path/to/bundle', 'en', ['Data', 'entry1']));
+ $countries = Countries::getAlpha3Names();
+ // ('alpha3Code' => 'countryName')
+ // => ['AFG' => 'Afghanistan', 'ALA' => 'Åland Islands', ...]
-Additionally, the
-:method:`Symfony\\Component\\Intl\\ResourceBundle\\Reader\\StructuredBundleReaderInterface::readEntry`
-method resolves fallback locales. For example, the fallback locale of "en_GB" is
-"en". For single-valued entries (strings, numbers etc.), the entry will be read
-from the fallback locale if it cannot be found in the more specific locale. For
-multi-valued entries (arrays), the values of the more specific and the fallback
-locale will be merged. In order to suppress this behavior, the last parameter
-``$fallback`` can be set to ``false``::
+ $country = Countries::getName('GB');
+ // => 'United Kingdom'
- var_dump($reader->readEntry(
- '/path/to/bundle',
- 'en',
- ['Data', 'entry1'],
- false
- ));
+ $country = Countries::getAlpha3Name('NOR');
+ // => 'Norway'
-Accessing ICU Data
-------------------
+All methods accept the translation locale as the last, optional parameter,
+which defaults to the current default locale::
-The ICU data is located in several "resource bundles". You can access a PHP
-wrapper of these bundles through the static
-:class:`Symfony\\Component\\Intl\\Intl` class. At the moment, the following
-data is supported:
+ $countries = Countries::getNames('de');
+ // => ['AF' => 'Afghanistan', 'EG' => 'Ägypten', ...]
-* `Language and Script Names`_
-* `Country Names`_
-* `Locales`_
-* `Currencies`_
+ $countries = Countries::getAlpha3Names('de');
+ // => ['AFG' => 'Afghanistan', 'EGY' => 'Ägypten', ...]
-Language and Script Names
-~~~~~~~~~~~~~~~~~~~~~~~~~
+ $country = Countries::getName('GB', 'de');
+ // => 'Vereinigtes Königreich'
-The translations of language and script names can be found in the language
-bundle::
+ $country = Countries::getAlpha3Name('GBR', 'de');
+ // => 'Vereinigtes Königreich'
- use Symfony\Component\Intl\Intl;
+If the given country code doesn't exist, the methods trigger a
+:class:`Symfony\\Component\\Intl\\Exception\\MissingResourceException`. In addition
+to catching the exception, you can also check if a given country code is valid::
- \Locale::setDefault('en');
+ $isValidCountry = Countries::exists($alpha2Code);
- $languages = Intl::getLanguageBundle()->getLanguageNames();
- // => ['ab' => 'Abkhazian', ...]
+Or if you have an alpha3 country code you want to check::
- $language = Intl::getLanguageBundle()->getLanguageName('de');
- // => 'German'
+ $isValidCountry = Countries::alpha3CodeExists($alpha3Code);
- $language = Intl::getLanguageBundle()->getLanguageName('de', 'AT');
- // => 'Austrian German'
+You may convert codes between two-letter alpha2 and three-letter alpha3 codes::
- $scripts = Intl::getLanguageBundle()->getScriptNames();
- // => ['Arab' => 'Arabic', ...]
+ $alpha3Code = Countries::getAlpha3Code($alpha2Code);
- $script = Intl::getLanguageBundle()->getScriptName('Hans');
- // => 'Simplified'
+ $alpha2Code = Countries::getAlpha2Code($alpha3Code);
-All methods accept the translation locale as the last, optional parameter,
-which defaults to the current default locale::
+Numeric Country Codes
+~~~~~~~~~~~~~~~~~~~~~
- $languages = Intl::getLanguageBundle()->getLanguageNames('de');
- // => ['ab' => 'Abchasisch', ...]
+The `ISO 3166-1 numeric`_ standard defines three-digit country codes to represent
+countries, dependent territories, and special areas of geographical interest.
-Country Names
-~~~~~~~~~~~~~
+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 translations of country names can be found in the region bundle::
+The :class:`Symfony\\Component\\Intl\\Countries` class provides access to these
+numeric country codes::
- use Symfony\Component\Intl\Intl;
+ use Symfony\Component\Intl\Countries;
\Locale::setDefault('en');
- $countries = Intl::getRegionBundle()->getCountryNames();
- // => ['AF' => 'Afghanistan', ...]
+ $numericCodes = Countries::getNumericCodes();
+ // ('alpha2Code' => 'numericCode')
+ // => ['AA' => '958', 'AD' => '020', ...]
- $country = Intl::getRegionBundle()->getCountryName('GB');
- // => 'United Kingdom'
+ $numericCode = Countries::getNumericCode('FR');
+ // => '250'
-All methods accept the translation locale as the last, optional parameter,
-which defaults to the current default locale::
+ $alpha2 = Countries::getAlpha2FromNumeric('250');
+ // => 'FR'
- $countries = Intl::getRegionBundle()->getCountryNames('de');
- // => ['AF' => 'Afghanistan', ...]
+ $exists = Countries::numericCodeExists('250');
+ // => true
Locales
~~~~~~~
-The translations of locale names can be found in the locale bundle::
+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 :class:`Symfony\\Component\\Intl\\Locales`
+class provides access to the name of all locales::
- use Symfony\Component\Intl\Intl;
+ use Symfony\Component\Intl\Locales;
\Locale::setDefault('en');
- $locales = Intl::getLocaleBundle()->getLocaleNames();
- // => ['af' => 'Afrikaans', ...]
+ $locales = Locales::getNames();
+ // ('localeCode' => 'localeName')
+ // => ['af' => 'Afrikaans', 'af_NA' => 'Afrikaans (Namibia)', ...]
- $locale = Intl::getLocaleBundle()->getLocaleName('zh_Hans_MO');
+ $locale = Locales::getName('zh_Hans_MO');
// => 'Chinese (Simplified, Macau SAR China)'
All methods accept the translation locale as the last, optional parameter,
which defaults to the current default locale::
- $locales = Intl::getLocaleBundle()->getLocaleNames('de');
- // => ['af' => 'Afrikaans', ...]
+ $locales = Locales::getNames('de');
+ // => ['af' => 'Afrikaans', 'af_NA' => 'Afrikaans (Namibia)', ...]
+
+ $locale = Locales::getName('zh_Hans_MO', 'de');
+ // => 'Chinesisch (Vereinfacht, Sonderverwaltungsregion Macau)'
+
+If the given locale code doesn't exist, the methods trigger a
+:class:`Symfony\\Component\\Intl\\Exception\\MissingResourceException`. In addition
+to catching the exception, you can also check if a given locale code is valid::
+
+ $isValidLocale = Locales::exists($localeCode);
Currencies
~~~~~~~~~~
-The translations of currency names and other currency-related information can
-be found in the currency bundle::
+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\Intl;
+ use Symfony\Component\Intl\Currencies;
\Locale::setDefault('en');
- $currencies = Intl::getCurrencyBundle()->getCurrencyNames();
- // => ['AFN' => 'Afghan Afghani', ...]
+ $currencies = Currencies::getNames();
+ // ('currencyCode' => 'currencyName')
+ // => ['AFN' => 'Afghan Afghani', 'ALL' => 'Albanian Lek', ...]
- $currency = Intl::getCurrencyBundle()->getCurrencyName('INR');
+ $currency = Currencies::getName('INR');
// => 'Indian Rupee'
- $symbol = Intl::getCurrencyBundle()->getCurrencySymbol('INR');
+ $symbol = Currencies::getSymbol('INR');
// => '₹'
- $fractionDigits = Intl::getCurrencyBundle()->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
+
+ // Swedish krona defines different values
+ $fractionDigits = Currencies::getFractionDigits('SEK'); // returns: 2
+ $cashFractionDigits = Currencies::getCashFractionDigits('SEK'); // returns: 0
+
+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', ...]
+
+ $currency = Currencies::getName('INR', 'de');
+ // => 'Indische Rupie'
+
+If the given currency code doesn't exist, the methods trigger a
+:class:`Symfony\\Component\\Intl\\Exception\\MissingResourceException`. In addition
+to catching the exception, you can also check if a given currency code is valid::
- $roundingIncrement = Intl::getCurrencyBundle()->getRoundingIncrement('INR');
- // => 0
+ $isValidCurrency = Currencies::exists($currencyCode);
+
+.. _component-intl-timezones:
+
+Timezones
+~~~~~~~~~
+
+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;
+
+ \Locale::setDefault('en');
+
+ $timezones = Timezones::getNames();
+ // ('timezoneID' => 'timezoneValue')
+ // => ['America/Eirunepe' => 'Acre Time (Eirunepe)', 'America/Rio_Branco' => 'Acre Time (Rio Branco)', ...]
+
+ $timezone = Timezones::getName('Africa/Nairobi');
+ // => 'East Africa Time (Nairobi)'
+
+All methods accept the translation locale as the last, optional parameter,
+which defaults to the current default locale::
-All methods (except for
-:method:`Symfony\\Component\\Intl\\ResourceBundle\\CurrencyBundleInterface::getFractionDigits`
-and
-:method:`Symfony\\Component\\Intl\\ResourceBundle\\CurrencyBundleInterface::getRoundingIncrement`)
-accept the translation locale as the last, optional parameter, which defaults
-to the current default locale::
+ $timezones = Timezones::getNames('de');
+ // => ['America/Eirunepe' => 'Acre-Zeit (Eirunepe)', 'America/Rio_Branco' => 'Acre-Zeit (Rio Branco)', ...]
- $currencies = Intl::getCurrencyBundle()->getCurrencyNames('de');
- // => ['AFN' => 'Afghanische Afghani', ...]
+ $timezone = Timezones::getName('Africa/Nairobi', 'de');
+ // => 'Ostafrikanische Zeit (Nairobi)'
-That's all you need to know for now. Have fun coding!
+You can also get all the timezones that exist in a given country. The
+``forCountryCode()`` method returns one or more timezone IDs, which you can
+translate into any locale with the ``getName()`` method shown earlier::
+
+ // unlike language codes, country codes are always uppercase (CL = Chile)
+ $timezones = Timezones::forCountryCode('CL');
+ // => ['America/Punta_Arenas', 'America/Santiago', 'Pacific/Easter']
+
+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 = 'CA' (CA = Canada)
+
+The `UTC/GMT time offsets`_ of all timezones are provided by ``getRawOffset()``
+(which returns an integer representing the offset in seconds) and
+``getGmtOffset()`` (which returns a string representation of the offset to
+display it to users)::
+
+ $offset = Timezones::getRawOffset('Etc/UTC'); // $offset = 0
+ $offset = Timezones::getRawOffset('America/Buenos_Aires'); // $offset = -10800
+ $offset = Timezones::getRawOffset('Asia/Katmandu'); // $offset = 20700
+
+ $offset = Timezones::getGmtOffset('Etc/UTC'); // $offset = 'GMT+00:00'
+ $offset = Timezones::getGmtOffset('America/Buenos_Aires'); // $offset = 'GMT-03:00'
+ $offset = Timezones::getGmtOffset('Asia/Katmandu'); // $offset = 'GMT+05:45'
+
+The timezone offset can vary in time because of the `daylight saving time (DST)`_
+practice. By default these methods use the ``time()`` PHP function to get the
+current timezone offset value, but you can pass a timestamp as their second
+arguments to get the offset at any given point in time::
+
+ // In 2019, the DST period in Madrid (Spain) went from March 31 to October 27
+ $offset = Timezones::getRawOffset('Europe/Madrid', strtotime('March 31, 2019')); // $offset = 3600
+ $offset = Timezones::getRawOffset('Europe/Madrid', strtotime('April 1, 2019')); // $offset = 7200
+ $offset = Timezones::getGmtOffset('Europe/Madrid', strtotime('October 27, 2019')); // $offset = 'GMT+02:00'
+ $offset = Timezones::getGmtOffset('Europe/Madrid', strtotime('October 28, 2019')); // $offset = 'GMT+01:00'
+
+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'
+
+If the given timezone ID doesn't exist, the methods trigger a
+:class:`Symfony\\Component\\Intl\\Exception\\MissingResourceException`. In addition
+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 `
+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
----------
@@ -342,9 +412,14 @@ Learn more
/reference/forms/types/currency
/reference/forms/types/language
/reference/forms/types/locale
-
-.. _Packagist: https://packagist.org/packages/symfony/intl
-.. _Icu component: https://packagist.org/packages/symfony/icu
-.. _intl extension: https://php.net/manual/en/book.intl.php
-.. _install the intl extension: https://php.net/manual/en/intl.setup.php
-.. _ICU library: http://site.icu-project.org/
+ /reference/forms/types/timezone
+
+.. _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 (2T)`: https://en.wikipedia.org/wiki/ISO_639-2
diff --git a/components/ldap.rst b/components/ldap.rst
index 065a0aacba2..e52a341986c 100644
--- a/components/ldap.rst
+++ b/components/ldap.rst
@@ -1,7 +1,3 @@
-.. index::
- single: Ldap
- single: Components; Ldap
-
The Ldap Component
==================
@@ -14,8 +10,6 @@ Installation
$ composer require symfony/ldap
-Alternatively, you can clone the ``_ repository.
-
.. include:: /components/require_autoload.rst.inc
Usage
@@ -42,7 +36,7 @@ following options:
The encryption protocol: ``ssl``, ``tls`` or ``none`` (default)
``connection_string``
- You may use this option instead of ``host`` and ``port`` to connect to the
+ You may use this option instead of ``host`` and ``port`` to connect to the
LDAP server
``optReferrals``
@@ -76,6 +70,23 @@ distinguished name (DN) and the password of a user::
$ldap->bind($dn, $password);
+.. 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::
@@ -103,15 +114,19 @@ array, you may use the
// Do something with the results array
-By default, LDAP queries use the ``Symfony\Component\Ldap\Adapter::SCOPE_SUB``
+By default, LDAP queries use the ``Symfony\Component\Ldap\Adapter\QueryInterface::SCOPE_SUB``
scope, which corresponds to the ``LDAP_SCOPE_SUBTREE`` scope of the
:phpfunction:`ldap_search` function. You can also use ``SCOPE_BASE`` (related
to the ``LDAP_SCOPE_BASE`` scope of :phpfunction:`ldap_read`) and ``SCOPE_ONE``
(related to the ``LDAP_SCOPE_ONELEVEL`` scope of :phpfunction:`ldap_list`)::
- use Symfony\Component\Ldap\Adapter;
+ use Symfony\Component\Ldap\Adapter\QueryInterface;
- $query = $ldap->query('dc=symfony,dc=com', '...', ['scope' => Adapter::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
----------------------------
@@ -119,8 +134,8 @@ Creating or Updating Entries
The Ldap component provides means to create new LDAP entries, update or even
delete existing ones::
- use Symfony\Component\Ldap\Ldap;
use Symfony\Component\Ldap\Entry;
+ use Symfony\Component\Ldap\Ldap;
// ...
$entry = new Entry('cn=Fabien Potencier,dc=symfony,dc=com', [
@@ -137,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);
@@ -147,15 +169,14 @@ delete existing ones::
// Removing an existing entry
$entryManager->remove(new Entry('cn=Test User,dc=symfony,dc=com'));
-
Batch Updating
______________
Use the entry manager's :method:`Symfony\\Component\\Ldap\\Adapter\\ExtLdap\\EntryManager::applyOperations`
method to update multiple attributes at once::
- use Symfony\Component\Ldap\Ldap;
use Symfony\Component\Ldap\Entry;
+ use Symfony\Component\Ldap\Ldap;
// ...
$entry = new Entry('cn=Fabien Potencier,dc=symfony,dc=com', [
@@ -176,4 +197,4 @@ Possible operation types are ``LDAP_MODIFY_BATCH_ADD``, ``LDAP_MODIFY_BATCH_REMO
``$values`` must be ``NULL`` when using ``LDAP_MODIFY_BATCH_REMOVE_ALL``
operation type.
-.. _Packagist: https://packagist.org/packages/symfony/ldap
+.. _`SASL`: https://en.wikipedia.org/wiki/Simple_Authentication_and_Security_Layer
diff --git a/components/lock.rst b/components/lock.rst
index a72cd68ec84..b8ba38c8fc7 100644
--- a/components/lock.rst
+++ b/components/lock.rst
@@ -1,13 +1,12 @@
-.. index::
- single: Lock
- single: Components; Lock
-
The Lock Component
==================
The Lock Component creates and manages `locks`_, a mechanism to provide
exclusive access to a shared resource.
+If you're using the Symfony Framework, read the
+:doc:`Symfony Framework Lock documentation `.
+
Installation
------------
@@ -15,8 +14,6 @@ Installation
$ composer require symfony/lock
-Alternatively, you can clone the ``_ repository.
-
.. include:: /components/require_autoload.rst.inc
Usage
@@ -26,26 +23,26 @@ Locks are used to guarantee exclusive access to some shared resource. In
Symfony applications, you can use locks for example to ensure that a command is
not executed more than once at the same time (on the same or different servers).
-Locks are created using a :class:`Symfony\\Component\\Lock\\Factory` class,
+Locks are created using a :class:`Symfony\\Component\\Lock\\LockFactory` class,
which in turn requires another class to manage the storage of locks::
- use Symfony\Component\Lock\Factory;
+ use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\Store\SemaphoreStore;
$store = new SemaphoreStore();
- $factory = new Factory($store);
+ $factory = new LockFactory($store);
-The lock is created by calling the :method:`Symfony\\Component\\Lock\\Factory::createLock`
+The lock is created by calling the :method:`Symfony\\Component\\Lock\\LockFactory::createLock`
method. Its first argument is an arbitrary string that represents the locked
resource. Then, a call to the :method:`Symfony\\Component\\Lock\\LockInterface::acquire`
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();
}
@@ -55,48 +52,116 @@ 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 ``Factory::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 :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 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;
+
+ $key = new Key('article.'.$article->getId());
+ $lock = $factory->createLockFromKey(
+ $key,
+ 300, // ttl
+ false // autoRelease
+ );
+ $lock->acquire(true);
+
+ $this->bus->dispatch(new RefreshTaxonomy($article, $key));
+
+.. note::
+
+ 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
+:ref:`SemaphoreStore ` store. If you use an incompatible
+store (see :ref:`lock stores ` for supported stores), an
+exception will be thrown when the application tries to serialize the key.
+
+.. _lock-blocking-locks:
+
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\Factory;
- use Symfony\Component\Lock\Store\RedisStore;
- use Symfony\Component\Lock\Store\RetryTillSaveStore;
+ use Symfony\Component\Lock\LockFactory;
+ use Symfony\Component\Lock\Store\FlockStore;
- $store = new RedisStore(new \Predis\Client('tcp://localhost:6379'));
- $store = new RetryTillSaveStore($store);
- $factory = new Factory($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 store does not support blocking locks by implementing the
+:class:`Symfony\\Component\\Lock\\BlockingStoreInterface` interface (see
+:ref:`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
@@ -110,10 +175,12 @@ 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);
- $lock->acquire();
+ if (!$lock->acquire()) {
+ return;
+ }
try {
// perform a job during less than 30 seconds
} finally {
@@ -122,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
@@ -130,9 +197,11 @@ 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);
- $lock->acquire();
+ if (!$lock->acquire()) {
+ return;
+ }
try {
while (!$finished) {
// perform a small part of the job.
@@ -149,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();
@@ -157,23 +226,190 @@ to reset the TTL to its original value::
// refresh the lock for 600 seconds (next refresh() call will be 30 seconds again)
$lock->refresh(600);
+This component also provides two useful methods related to expiring locks:
+``getRemainingLifetime()`` (which returns ``null`` or a ``float``
+as seconds) and ``isExpired()`` (which returns a boolean).
+
+Automatically Releasing The Lock
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+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::
+
+ // ...
+ $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.
+
+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
+:method:`Symfony\\Component\\Lock\\LockInterface::acquire` method to acquire a
+write lock::
+
+ $lock = $factory->createLock('user-'.$user->id);
+ if (!$lock->acquireRead()) {
+ return;
+ }
+
+Similar to the ``acquire()`` method, pass ``true`` as the argument of ``acquireRead()``
+to acquire the lock in a blocking mode::
+
+ $lock = $factory->createLock('user-'.$user->id);
+ $lock->acquireRead(true);
+
+.. note::
+
+ The `priority policy`_ of Symfony's shared locks depends on the underlying
+ store (e.g. Redis store prioritizes readers vs writers).
+
+When a read-only lock is acquired with the ``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->acquireRead(true);
+
+ if (!$this->shouldUpdate($userId)) {
+ return;
+ }
+
+ $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 (see
+:ref:`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 :ref:`owned ` 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::
+
+ if ($lock->isAcquired()) {
+ // We (still) own the lock
+ }
+
+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()) {
+ return;
+ }
+
+ $this->beginTransaction();
+
+ // Perform a very long process that might exceed TTL of the lock
+
+ if ($lock->isAcquired()) {
+ // Still all good, no other instance has acquired the lock in the meantime, we're safe
+ $this->commit();
+ } else {
+ // Bummer! Our lock has apparently exceeded TTL and another process has started in
+ // the meantime so it's not safe for us to commit.
+ $this->rollback();
+ throw new \Exception('Process failed');
+ }
+
+.. 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.
+
+.. _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
----------------
Locks are created and managed in ``Stores``, which are classes that implement
-:class:`Symfony\\Component\\Lock\\StoreInterface`. The component includes the
-following built-in store types:
-
-============================================ ====== ======== ========
-Store Scope Blocking Expiring
-============================================ ====== ======== ========
-:ref:`FlockStore ` local yes no
-:ref:`MemcachedStore ` remote no yes
-:ref:`PdoStore ` remote no yes
-:ref:`RedisStore ` remote no yes
-:ref:`SemaphoreStore ` local yes no
-:ref:`ZookeeperStore ` remote no no
-============================================ ====== ======== ========
+:class:`Symfony\\Component\\Lock\\PersistingStoreInterface` and, optionally,
+:class:`Symfony\\Component\\Lock\\BlockingStoreInterface`.
+
+The component includes the following built-in store types:
+
+========================================================== ====== ======== ======== ======= =============
+Store Scope Blocking Expiring Sharing Serialization
+========================================================== ====== ======== ======== ======= =============
+:ref:`FlockStore ` local yes no yes no
+:ref:`MemcachedStore ` remote no yes no yes
+:ref:`MongoDbStore ` remote no yes no yes
+:ref:`PdoStore ` remote no yes no yes
+:ref:`DoctrineDbalStore ` remote no yes no yes
+:ref:`PostgreSqlStore ` remote yes no yes no
+:ref:`DoctrineDbalPostgreSqlStore ` remote yes no yes no
+:ref:`RedisStore ` remote no yes yes yes
+:ref:`SemaphoreStore ` local yes no no no
+:ref:`ZookeeperStore ` remote no no no no
+========================================================== ====== ======== ======== ======= =============
+
+.. tip::
+
+ Symfony includes two other special stores that are mostly useful for testing:
+ ``InMemoryStore``, which saves locks in memory during a process, and ``NullStore``,
+ which doesn't persist anything.
+
+.. versionadded:: 7.2
+
+ The :class:`Symfony\\Component\\Lock\\Store\\NullStore` was introduced in Symfony 7.2.
.. _lock-store-flock:
@@ -182,18 +418,20 @@ FlockStore
The FlockStore uses the file system on the local computer to create the locks.
It does not support expiration, but the lock is automatically released when the
-PHP process is terminated::
+lock object goes out of scope and is freed by the garbage collector (for example
+when the PHP process ends)::
use Symfony\Component\Lock\Store\FlockStore;
// the argument is the path of the directory where the locks are created
- $store = new FlockStore(sys_get_temp_dir());
+ // 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:
@@ -215,44 +453,158 @@ support blocking, and expects a TTL to avoid stalled locks::
Memcached does not support TTL lower than 1 second.
+.. _lock-store-mongodb:
+
+MongoDbStore
+~~~~~~~~~~~~
+
+The MongoDbStore saves locks on a MongoDB server ``>=2.2``, it requires a
+``\MongoDB\Collection`` or ``\MongoDB\Client`` from `mongodb/mongodb`_ or a
+`MongoDB Connection String`_.
+This store does not support blocking and expects a TTL to
+avoid stalled locks::
+
+ use Symfony\Component\Lock\Store\MongoDbStore;
+
+ $mongo = 'mongodb://localhost/database?collection=lock';
+ $options = [
+ 'gcProbability' => 0.001,
+ 'database' => 'myapp',
+ 'collection' => 'lock',
+ 'uriOptions' => [],
+ 'driverOptions' => [],
+ ];
+ $store = new MongoDbStore($mongo, $options);
+
+The ``MongoDbStore`` takes the following ``$options`` (depending on the first parameter type):
+
+============= ================================================================================================
+Option Description
+============= ================================================================================================
+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`_
+driverOptions Array of driver options for `MongoDBClient::__construct`_
+============= ================================================================================================
+
+When the first parameter is a:
+
+``MongoDB\Collection``:
+
+- ``$options['database']`` is ignored
+- ``$options['collection']`` is ignored
+
+``MongoDB\Client``:
+
+- ``$options['database']`` is mandatory
+- ``$options['collection']`` is mandatory
+
+MongoDB Connection String:
+
+- ``$options['database']`` is used otherwise ``/path`` from the DSN, at least one is mandatory
+- ``$options['collection']`` is used otherwise ``?collection=`` from the DSN, at least one is mandatory
+
+.. note::
+
+ The ``collection`` querystring parameter is not part of the `MongoDB Connection String`_ definition.
+ It is used to allow constructing a ``MongoDbStore`` using a `Data Source Name (DSN)`_ without ``$options``.
+
.. _lock-store-pdo:
PdoStore
~~~~~~~~
-The PdoStore saves locks in an SQL database. It requires a `PDO`_ connection, a
-`Doctrine DBAL Connection`_, or a `Data Source Name (DSN)`_. This store does not
-support blocking, and expects a TTL to avoid stalled locks::
+The PdoStore saves locks in an SQL database. It requires a `PDO`_ connection or a `Data Source Name (DSN)`_.
+This store does not support blocking, and expects a TTL to avoid stalled locks::
use Symfony\Component\Lock\Store\PdoStore;
- // a PDO, a Doctrine DBAL connection or DSN for lazy connecting through PDO
- $databaseConnectionOrDSN = 'mysql:host=127.0.0.1;dbname=lock';
+ // a PDO 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']);
.. note::
This store does not support TTL lower than 1 second.
-Before storing locks in the database, you must create the table that stores
-the information. The store provides a method called
-:method:`Symfony\\Component\\Lock\\Store\\PdoStore::createTable`
-to set up this table for you according to the database engine used::
+The table where values are stored is created automatically on the first call to
+the :method:`Symfony\\Component\\Lock\\Store\\PdoStore::save` method.
+You can also create this table explicitly by calling the
+:method:`Symfony\\Component\\Lock\\Store\\PdoStore::createTable` method in
+your code.
- try {
- $store->createTable();
- } catch (\PDOException $exception) {
- // the table could not be created for some reason
- }
+.. _lock-store-dbal:
+
+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.
-A great way to set up the table in production is to call the ``createTable()``
-method in your local computer and then generate a
-:ref:`database migration `:
+The table where values are stored will be automatically generated when your run
+the command:
.. code-block:: terminal
- $ php bin/console doctrine:migrations:diff
- $ php bin/console doctrine:migrations:migrate
+ $ 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 or a `Data Source Name (DSN)`_. It supports native blocking, as well as sharing
+locks::
+
+ use Symfony\Component\Lock\Store\PostgreSqlStore;
+
+ // a PDO instance or DSN for lazy connecting through PDO
+ $databaseConnectionOrDSN = 'pgsql:host=localhost;port=5634;dbname=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 it does not expire.
+
+.. _lock-store-dbal-pgsql:
+
+DoctrineDbalPostgreSqlStore
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The DoctrineDbalPostgreSqlStore uses `Advisory Locks`_ provided by PostgreSQL.
+It is identical to PostgreSqlStore but requires a `Doctrine DBAL Connection`_ or
+a `Doctrine DBAL URL`_. It supports native blocking, as well as sharing locks::
+
+ use Symfony\Component\Lock\Store\DoctrineDbalPostgreSqlStore;
+
+ // a Doctrine Connection or DSN
+ $databaseConnectionOrDSN = 'postgresql+advisory://myuser:mypassword@127.0.0.1:5634/lock';
+ $store = new DoctrineDbalPostgreSqlStore($databaseConnectionOrDSN);
+
+In opposite to the ``DoctrineDbalStore``, the ``DoctrineDbalPostgreSqlStore`` does not need a table to
+store locks and does not expire.
.. _lock-store-redis:
@@ -260,7 +612,7 @@ RedisStore
~~~~~~~~~~
The RedisStore saves locks on a Redis server, it requires a Redis connection
-implementing the ``\Redis``, ``\RedisArray``, ``\RedisCluster`` or
+implementing the ``\Redis``, ``\RedisArray``, ``\RedisCluster``, ``\Relay\Relay`` or
``\Predis`` classes. This store does not support blocking, and expects a TTL to
avoid stalled locks::
@@ -288,14 +640,14 @@ 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\Strategy\ConsensusStrategy;
use Symfony\Component\Lock\Store\CombinedStore;
use Symfony\Component\Lock\Store\RedisStore;
+ use Symfony\Component\Lock\Strategy\ConsensusStrategy;
$stores = [];
foreach (['server1', 'server2', 'server3'] as $server) {
@@ -309,14 +661,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;
+
+ $store = new CombinedStore($stores, new UnanimousStrategy());
-.. caution::
+.. 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:
@@ -344,28 +701,26 @@ 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
~~~~~~~~~~~~~
Remote stores (:ref:`MemcachedStore `,
-:ref:`PdoStore `, :ref:`RedisStore `, and
+:ref:`MongoDbStore `,
+:ref:`PdoStore `,
+:ref:`PostgreSqlStore `,
+:ref:`RedisStore ` and
:ref:`ZookeeperStore `) use a unique token to recognize
the true owner of the lock. This token is stored in the
: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, ...).
+the ``Lock``.
-.. caution::
-
- Do not share a key between processes.
-
-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
@@ -375,21 +730,25 @@ Expiring Stores
~~~~~~~~~~~~~~~
Expiring stores (:ref:`MemcachedStore `,
-:ref:`PdoStore ` and :ref:`RedisStore `)
+:ref:`MongoDbStore `,
+:ref:`PdoStore ` and
+:ref:`RedisStore `)
guarantee that the lock is acquired only for the defined duration of time. If
the task takes longer to be accomplished, then the lock can be released by the
store and acquired by someone else.
The ``Lock`` provides several methods to check its health. The ``isExpired()``
-method checks whether or not it lifetime is over and the ``getRemainingLifetime()``
+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);
- $lock->acquire();
+ if (!$lock->acquire()) {
+ return;
+ }
while (!$finished) {
if ($lock->getRemainingLifetime() <= 5) {
if ($lock->isExpired()) {
@@ -400,24 +759,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 leave is enough to perform the task.
+ 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.
@@ -426,11 +785,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
@@ -439,22 +798,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.
@@ -464,12 +822,12 @@ MemcachedStore
The way Memcached works is to store items in memory. That means that by using
the :ref:`MemcachedStore ` 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.
@@ -477,7 +835,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
@@ -487,22 +845,63 @@ 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.
+MongoDbStore
+~~~~~~~~~~~~
+
+.. warning::
+
+ The locked resource name is indexed in the ``_id`` field of the lock
+ 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.createIndex(
+ { "expires_at": 1 },
+ { "expireAfterSeconds": 0 }
+ )
+
+Alternatively, the method ``MongoDbStore::createTtlIndex(int $expireAfterSeconds = 0)``
+can be called once to create the TTL index during database setup. Read more
+about `Expire Data from Collections by Setting TTL`_ in MongoDB.
+
+.. tip::
+
+ ``MongoDbStore`` will attempt to automatically create a TTL index. It's
+ recommended to set constructor option ``gcProbability`` to ``0.0`` to
+ disable this behavior if you have manually dealt with TTL index creation.
+
+.. 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`` 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;``.
@@ -511,22 +910,36 @@ 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.
+PostgreSqlStore
+~~~~~~~~~~~~~~~
+
+The PostgreSqlStore relies on the `Advisory Locks`_ properties of the PostgreSQL
+database. That means that by using :ref:`PostgreSqlStore `
+the locks will be automatically released at the end of the session in case the
+client cannot unlock for any reason.
+
+If the PostgreSQL service or the machine hosting it restarts, every lock would
+be lost without notifying the running processes.
+
+If the TCP connection is lost, the PostgreSQL may release locks without
+notifying the application.
+
RedisStore
~~~~~~~~~~
The way Redis works is to store items in memory. That means that by using
the :ref:`RedisStore ` 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.
@@ -540,7 +953,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.
@@ -548,13 +961,13 @@ removed by mistake.
CombinedStore
~~~~~~~~~~~~~
-Combined stores allow to store locks across several backends. It's a common
-mistake to think that the lock mechanism will be more reliable. This is wrong
+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.
@@ -572,12 +985,20 @@ 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.
+.. 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.
+ Check that process is run with a system user (UID <= SYS_UID_MAX) with
+ ``SYS_UID_MAX`` defined in ``/etc/login.defs``, or set the option
+ ``RemoveIPC=off`` in ``/etc/systemd/logind.conf``.
+
ZookeeperStore
~~~~~~~~~~~~~~
@@ -610,11 +1031,21 @@ instance, during the deployment of a new version. Processes with new
configuration must not be started while old processes with old configuration
are still running.
+.. _`a maximum of 1024 bytes in length`: https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit
.. _`ACID`: https://en.wikipedia.org/wiki/ACID
-.. _`locks`: https://en.wikipedia.org/wiki/Lock_(computer_science)
-.. _Packagist: https://packagist.org/packages/symfony/lock
-.. _`PHP semaphore functions`: http://php.net/manual/en/book.sem.php
-.. _`PDO`: https://php.net/pdo
-.. _`Doctrine DBAL Connection`: https://github.com/doctrine/dbal/blob/master/lib/Doctrine/DBAL/Connection.php
+.. _`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/
+.. _`mongodb/mongodb`: https://packagist.org/packages/mongodb/mongodb
+.. _`MongoDBClient::__construct`: https://docs.mongodb.com/php-library/current/reference/method/MongoDBClient__construct/
+.. _`PDO`: https://www.php.net/pdo
+.. _`PHP semaphore functions`: https://www.php.net/manual/en/book.sem.php
+.. _`Replica Set Read and Write Semantics`: https://docs.mongodb.com/manual/applications/replication/
.. _`ZooKeeper`: https://zookeeper.apache.org/
+.. _`readers-writer lock`: https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock
+.. _`priority policy`: https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock#Priority_policies
+.. _`PCNTL`: https://www.php.net/manual/book.pcntl.php
diff --git a/components/mercure.rst b/components/mercure.rst
deleted file mode 100644
index 384bea479b2..00000000000
--- a/components/mercure.rst
+++ /dev/null
@@ -1,48 +0,0 @@
-.. index::
- single: Mercure
- single: Components; Mercure
-
-The Mercure Component
-=====================
-
- `Mercure`_ is an open protocol allowing to push data updates to web
- browsers and other HTTP clients in a convenient, fast, reliable
- and battery-friendly way.
- It is especially useful to publish real-time updates of resources served
- through web APIs, to reactive web and mobile apps.
-
-The Mercure Component implements the "publisher" part of the Mercure Protocol.
-
-Installation
-------------
-
-.. code-block:: terminal
-
- $ composer require symfony/mercure
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-Usage
------
-
-The following example shows the component in action::
-
- // change these values accordingly to your hub installation
- define('HUB_URL', 'https://demo.mercure.rocks/hub');
- define('JWT', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyJmb28iLCJiYXIiXSwicHVibGlzaCI6WyJmb28iXX19.LRLvirgONK13JgacQ_VbcjySbVhkSmHy3IznH3tA9PM');
-
- use Symfony\Component\Mercure\Publisher;
- use Symfony\Component\Mercure\Update;
- use Symfony\Component\Mercure\Jwt\StaticJwtProvide;
-
- $publisher = new Publisher(HUB_URL, new StaticJwtProvide(JWT));
- // Serialize the update, and dispatch it to the hub, that will broadcast it to the clients
- $id = $publisher(new Update('https://example.com/books/1.jsonld', 'Hi from Symfony!', ['target1', 'target2']));
-
-Read the full :doc:`Mercure integration documentation ` to learn
-about all the features of this component and its integration with the Symfony
-framework.
-
-.. _`Mercure`: https://mercure.rocks
diff --git a/components/messenger.rst b/components/messenger.rst
index 5710e37dc37..8d6652fb160 100644
--- a/components/messenger.rst
+++ b/components/messenger.rst
@@ -1,15 +1,11 @@
-.. index::
- single: Messenger
- single: Components; Messenger
-
The Messenger Component
=======================
The Messenger component helps applications send and receive messages to/from
other applications or via message queues.
- The component is greatly inspired by Matthias Noback's series of `blog posts
- about command buses`_ and the `SimpleBus project`_.
+ The component is greatly inspired by Matthias Noback's series of
+ `blog posts about command buses`_ and the `SimpleBus project`_.
.. seealso::
@@ -24,8 +20,6 @@ Installation
$ composer require symfony/messenger
-Alternatively, you can clone the ``_ repository.
-
.. include:: /components/require_autoload.rst.inc
Concepts
@@ -33,7 +27,9 @@ Concepts
.. raw:: html
-
+
**Sender**:
Responsible for serializing and sending messages to *something*. This
@@ -56,14 +52,16 @@ Concepts
For instance: logging, validating a message, starting a transaction, ...
They are also responsible for calling the next middleware in the chain,
which means they can tweak the envelope, by adding stamps to it or even
- replacing it, as well as interrupt the middleware chain.
+ 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.
-**Envelope**
+**Envelope**:
Messenger specific concept, it gives full flexibility inside the message bus,
by wrapping the messages into it, allowing to add useful information inside
through *envelope stamps*.
-**Envelope Stamps**
+**Envelope Stamps**:
Piece of information you need to attach to your message: serializer context
to use for transport, markers identifying a received message or any sort of
metadata your middleware or transport layer may use.
@@ -77,20 +75,22 @@ middleware stack. The component comes with a set of middleware that you can use.
When using the message bus with Symfony's FrameworkBundle, the following middleware
are configured for you:
-#. :class:`Symfony\\Component\\Messenger\\Middleware\\LoggingMiddleware` (logs the processing of your messages)
-#. :class:`Symfony\\Component\\Messenger\\Middleware\\SendMessageMiddleware` (enables asynchronous processing)
+#. :class:`Symfony\\Component\\Messenger\\Middleware\\SendMessageMiddleware` (enables asynchronous processing, logs the processing of your messages if you provide a logger)
#. :class:`Symfony\\Component\\Messenger\\Middleware\\HandleMessageMiddleware` (calls the registered handler(s))
Example::
use App\Message\MyMessage;
- use Symfony\Component\Messenger\MessageBus;
+ use App\MessageHandler\MyMessageHandler;
use Symfony\Component\Messenger\Handler\HandlersLocator;
+ use Symfony\Component\Messenger\MessageBus;
use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware;
+ $handler = new MyMessageHandler();
+
$bus = new MessageBus([
new HandleMessageMiddleware(new HandlersLocator([
- MyMessage::class => ['dummy' => $handler],
+ MyMessage::class => [$handler],
])),
]);
@@ -113,12 +113,14 @@ that will do the required processing for your message::
class MyMessageHandler
{
- public function __invoke(MyMessage $message)
- {
- // Message processing...
- }
+ public function __invoke(MyMessage $message): void
+ {
+ // Message processing...
+ }
}
+.. _messenger-envelopes:
+
Adding Metadata to Messages (Envelopes)
---------------------------------------
@@ -132,34 +134,58 @@ through the transport layer, use the ``SerializerStamp`` stamp::
$bus->dispatch(
(new Envelope($message))->with(new SerializerStamp([
+ // groups are applied to the whole message, so make sure
+ // to define the group for every embedded object
'groups' => ['my_serialization_groups'],
]))
);
-At the moment, the Symfony Messenger has the following built-in envelope stamps:
-
-#. :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\\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\\HandledStamp`,
- a stamp that marks the message as handled by a specific handler.
- Allows accessing the handler returned value, the handler callable name
- and its alias if available from the :class:`Symfony\\Component\\Messenger\\Handler\\HandlersLocator`.
+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 :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 `.
+
+.. 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::
use App\Message\Stamp\AnotherStamp;
- use Symfony\Component\Messenger\Stamp\ReceivedStamp;
+ use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackInterface;
+ use Symfony\Component\Messenger\Stamp\ReceivedStamp;
class MyOwnMiddleware implements MiddlewareInterface
{
@@ -170,6 +196,8 @@ Hence you can inspect the envelope content and its stamps, or add any::
// You could for example add another stamp.
$envelope = $envelope->with(new AnotherStamp(/* ... */));
+ } else {
+ // Message was just originally dispatched
}
return $stack->next()->handle($envelope, $stack);
@@ -201,50 +229,47 @@ transport will be responsible for communicating with your message broker or 3rd
Your own Sender
~~~~~~~~~~~~~~~
-Using the :class:`Symfony\\Component\\Messenger\\Transport\\Sender\\SenderInterface`,
-you can create your own message sender.
Imagine that you already have an ``ImportantAction`` message going through the
message bus and being handled by a handler. Now, you also want to send this
-message as an email.
+message as an email (using the :doc:`Mime ` and
+:doc:`Mailer ` components).
-First, create your sender::
+Using the :class:`Symfony\\Component\\Messenger\\Transport\\Sender\\SenderInterface`,
+you can create your own message sender::
namespace App\MessageSender;
use App\Message\ImportantAction;
- use Symfony\Component\Messenger\Transport\Sender\SenderInterface;
+ use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\Envelope;
+ use Symfony\Component\Messenger\Transport\Sender\SenderInterface;
+ use Symfony\Component\Mime\Email;
class ImportantActionToEmailSender implements SenderInterface
{
- private $mailer;
- private $toEmail;
-
- public function __construct(\Swift_Mailer $mailer, string $toEmail)
- {
- $this->mailer = $mailer;
- $this->toEmail = $toEmail;
- }
-
- public function send(Envelope $envelope): Envelope
- {
- $message = $envelope->getMessage();
-
- if (!$message instanceof ImportantAction) {
- throw new \InvalidArgumentException(sprintf('This transport only supports "%s" messages.', ImportantAction::class));
- }
-
- $this->mailer->send(
- (new \Swift_Message('Important action made'))
- ->setTo($this->toEmail)
- ->setBody(
- '
Important action
Made by '.$message->getUsername().'
',
- 'text/html'
- )
- );
-
- return $envelope;
- }
+ public function __construct(
+ private MailerInterface $mailer,
+ private string $toEmail,
+ ) {
+ }
+
+ public function send(Envelope $envelope): Envelope
+ {
+ $message = $envelope->getMessage();
+
+ if (!$message instanceof ImportantAction) {
+ throw new \InvalidArgumentException(sprintf('This transport only supports "%s" messages.', ImportantAction::class));
+ }
+
+ $this->mailer->send(
+ (new Email())
+ ->to($this->toEmail)
+ ->subject('Important action made')
+ ->html('
Important action
Made by '.$message->getUsername().'
')
+ );
+
+ return $envelope;
+ }
}
Your own Receiver
@@ -259,43 +284,63 @@ application but you can't use an API and need to use a shared CSV file with new
orders.
You will read this CSV file and dispatch a ``NewOrder`` message. All you need to
-do is to write your custom CSV receiver and Symfony will do the rest.
-
-First, create your receiver::
+do is to write your own CSV receiver::
namespace App\MessageReceiver;
use App\Message\NewOrder;
+ use Symfony\Component\Messenger\Envelope;
+ use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
use Symfony\Component\Serializer\SerializerInterface;
- use Symfony\Component\Messenger\Envelope;
class NewOrdersFromCsvFileReceiver implements ReceiverInterface
{
- private $serializer;
- private $filePath;
-
- public function __construct(SerializerInterface $serializer, string $filePath)
- {
- $this->serializer = $serializer;
- $this->filePath = $filePath;
- }
-
- public function receive(callable $handler): void
- {
- $ordersFromCsv = $this->serializer->deserialize(file_get_contents($this->filePath), 'csv');
-
- foreach ($ordersFromCsv as $orderFromCsv) {
- $order = new NewOrder($orderFromCsv['id'], $orderFromCsv['account_id'], $orderFromCsv['amount']);
-
- $handler(new Envelope($order));
- }
- }
-
- public function stop(): void
- {
- // noop
- }
+ 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 [];
+ }
+
+ try {
+ $envelope = $this->serializer->decode([
+ 'body' => $yourEnvelope['body'],
+ 'headers' => $yourEnvelope['headers'],
+ ]);
+ } catch (MessageDecodingFailedException $exception) {
+ $this->connection->reject($yourEnvelope['id']);
+ throw $exception;
+ }
+
+ return [$envelope->with(new CustomStamp($yourEnvelope['id']))];
+ }
+
+ public function ack(Envelope $envelope): void
+ {
+ // Add information about the handled message
+ }
+
+ public function reject(Envelope $envelope): void
+ {
+ // In the case of a custom connection
+ $id = /* get the message id thanks to information or stamps present in the envelope */;
+
+ $this->connection->reject($id);
+ }
}
Receiver and Sender on the same Bus
@@ -308,6 +353,7 @@ middleware will know it should not route these messages again to a transport.
Learn more
----------
+
.. toctree::
:maxdepth: 1
:glob:
@@ -315,5 +361,5 @@ Learn more
/messenger
/messenger/*
-.. _blog posts about command buses: https://matthiasnoback.nl/tags/command%20bus/
-.. _SimpleBus project: http://simplebus.io
+.. _`blog posts about command buses`: https://matthiasnoback.nl/tags/command%20bus/
+.. _`SimpleBus project`: https://docs.simplebus.io/en/latest/
diff --git a/components/mime.rst b/components/mime.rst
new file mode 100644
index 00000000000..c043b342ebc
--- /dev/null
+++ b/components/mime.rst
@@ -0,0 +1,298 @@
+The Mime Component
+==================
+
+ The Mime component allows manipulating the MIME messages used to send emails
+ and provides utilities related to MIME types.
+
+Installation
+------------
+
+.. code-block:: terminal
+
+ $ composer require symfony/mime
+
+.. include:: /components/require_autoload.rst.inc
+
+Introduction
+------------
+
+`MIME`_ (Multipurpose Internet Mail Extensions) is an Internet standard that
+extends the original basic format of emails to support features like:
+
+* Headers and text contents using non-ASCII characters;
+* Message bodies with multiple parts (e.g. HTML and plain text contents);
+* Non-text attachments: audio, video, images, PDF, etc.
+
+The entire MIME standard is complex and huge, but Symfony abstracts all that
+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 absolute control over every single part of the email message.
+
+Usage
+-----
+
+Use the :class:`Symfony\\Component\\Mime\\Email` class and their *chainable*
+methods to compose the entire email message::
+
+ use Symfony\Component\Mime\Email;
+
+ $email = (new Email())
+ ->from('fabien@symfony.com')
+ ->to('foo@example.com')
+ ->cc('bar@example.com')
+ ->bcc('baz@example.com')
+ ->replyTo('fabien@symfony.com')
+ ->priority(Email::PRIORITY_HIGH)
+ ->subject('Important Notification')
+ ->text('Lorem ipsum...')
+ ->html('
Lorem ipsum
...
')
+ ;
+
+The only purpose of this component is to create the email messages. Use the
+:doc:`Mailer component ` to actually send them.
+
+Twig Integration
+----------------
+
+The Mime component comes with excellent integration with Twig, allowing you to
+create messages from Twig templates, embed images, inline CSS and more. Details
+on how to use those features can be found in the Mailer documentation:
+:ref:`Twig: HTML & CSS `.
+
+But if you're using the Mime component without the Symfony framework, you'll need
+to handle a few setup details.
+
+Twig Setup
+~~~~~~~~~~
+
+To integrate with Twig, use the :class:`Symfony\\Bridge\\Twig\\Mime\\BodyRenderer`
+class to render the template and update the email message contents with the results::
+
+ // ...
+ use Symfony\Bridge\Twig\Mime\BodyRenderer;
+ use Twig\Environment;
+ use Twig\Loader\FilesystemLoader;
+
+ // when using the Mime component inside a full-stack Symfony application, you
+ // don't need to do this Twig setup. You only have to inject the 'twig' service
+ $loader = new FilesystemLoader(__DIR__.'/templates');
+ $twig = new Environment($loader);
+
+ $renderer = new BodyRenderer($twig);
+ // this updates the $email object contents with the result of rendering
+ // the template defined earlier with the given context
+ $renderer->render($email);
+
+Inlining CSS Styles (and other Extensions)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To use the :ref:`inline_css ` filter, first install the Twig
+extension:
+
+.. code-block:: terminal
+
+ $ composer require twig/cssinliner-extra
+
+Now, enable the extension::
+
+ // ...
+ use Twig\Extra\CssInliner\CssInlinerExtension;
+
+ $loader = new FilesystemLoader(__DIR__.'/templates');
+ $twig = new Environment($loader);
+ $twig->addExtension(new CssInlinerExtension());
+
+The same process should be used for enabling other extensions, like the
+:ref:`MarkdownExtension ` and :ref:`InkyExtension `.
+
+Creating Raw Email Messages
+---------------------------
+
+This is useful for advanced applications that need absolute control over every
+email part. It's not recommended for applications with regular email
+requirements because it adds complexity for no real gain.
+
+Before continuing, it's important to have a look at the low level structure of
+an email message. Consider a message which includes some content as both text
+and HTML, a single PNG image embedded in those contents and a PDF file attached
+to it. The MIME standard allows structuring this message in different ways, but
+the following tree is the one that works on most email clients:
+
+.. code-block:: text
+
+ multipart/mixed
+ ├── multipart/related
+ │ ├── multipart/alternative
+ │ │ ├── text/plain
+ │ │ └── text/html
+ │ └── image/png
+ └── application/pdf
+
+This is the purpose of each MIME message part:
+
+* ``multipart/alternative``: used when two or more parts are alternatives of the
+ same (or very similar) content. The preferred format must be added last.
+* ``multipart/mixed``: used to send different content types in the same message,
+ such as when attaching files.
+* ``multipart/related``: used to indicate that each message part is a component
+ of an aggregate whole. The most common usage is to display images embedded
+ in the message contents.
+
+When using the low-level :class:`Symfony\\Component\\Mime\\Message` class to
+create the email message, you must keep all the above in mind to define the
+different parts of the email by hand::
+
+ use Symfony\Component\Mime\Header\Headers;
+ use Symfony\Component\Mime\Message;
+ use Symfony\Component\Mime\Part\Multipart\AlternativePart;
+ use Symfony\Component\Mime\Part\TextPart;
+
+ $headers = (new Headers())
+ ->addMailboxListHeader('From', ['fabien@symfony.com'])
+ ->addMailboxListHeader('To', ['foo@example.com'])
+ ->addTextHeader('Subject', 'Important Notification')
+ ;
+
+ $textContent = new TextPart('Lorem ipsum...');
+ $htmlContent = new TextPart('
Lorem ipsum
...
', null, 'html');
+ $body = new AlternativePart($textContent, $htmlContent);
+
+ $email = new Message($headers, $body);
+
+Embedding images and attaching files is possible by creating the appropriate
+email multiparts::
+
+ // ...
+ use Symfony\Component\Mime\Part\DataPart;
+ use Symfony\Component\Mime\Part\Multipart\MixedPart;
+ use Symfony\Component\Mime\Part\Multipart\RelatedPart;
+
+ // ...
+ $embeddedImage = new DataPart(fopen('/path/to/images/logo.png', 'r'), null, 'image/png');
+ $imageCid = $embeddedImage->getContentId();
+
+ $attachedFile = new DataPart(fopen('/path/to/documents/terms-of-use.pdf', 'r'), null, 'application/pdf');
+
+ $textContent = new TextPart('Lorem ipsum...');
+ $htmlContent = new TextPart(sprintf(
+ '
Lorem ipsum
...
', $imageCid
+ ), null, 'html');
+ $bodyContent = new AlternativePart($textContent, $htmlContent);
+ $body = new RelatedPart($bodyContent, $embeddedImage);
+
+ $messageParts = new MixedPart($body, $attachedFile);
+
+ $email = new Message($headers, $messageParts);
+
+Serializing Email Messages
+--------------------------
+
+Email messages created with either the ``Email`` or ``Message`` classes can be
+serialized because they are simple data objects::
+
+ $email = (new Email())
+ ->from('fabien@symfony.com')
+ // ...
+ ;
+
+ $serializedEmail = serialize($email);
+
+A common use case is to store serialized email messages, include them in a
+message sent with the :doc:`Messenger component ` and
+recreate them later when sending them. Use the
+:class:`Symfony\\Component\\Mime\\RawMessage` class to recreate email messages
+from their serialized contents::
+
+ use Symfony\Component\Mime\RawMessage;
+
+ // ...
+ $serializedEmail = serialize($email);
+
+ // later, recreate the original message to actually send it
+ $message = new RawMessage(unserialize($serializedEmail));
+
+MIME Types Utilities
+--------------------
+
+Although MIME was designed mainly for creating emails, the content types (also
+known as `MIME types`_ and "media types") defined by MIME standards are also of
+importance in communication protocols outside of email, such as HTTP. That's
+why this component also provides utilities to work with MIME types.
+
+The :class:`Symfony\\Component\\Mime\\MimeTypes` class transforms between
+MIME types and file name extensions::
+
+ use Symfony\Component\Mime\MimeTypes;
+
+ $mimeTypes = new MimeTypes();
+ $exts = $mimeTypes->getExtensions('application/javascript');
+ // $exts = ['js', 'jsm', 'mjs']
+ $exts = $mimeTypes->getExtensions('image/jpeg');
+ // $exts = ['jpeg', 'jpg', 'jpe']
+
+ $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.
+
+.. _components-mime-type-guess:
+
+Guessing the MIME Type
+~~~~~~~~~~~~~~~~~~~~~~
+
+Another useful utility allows to guess the MIME type of any given file::
+
+ use Symfony\Component\Mime\MimeTypes;
+
+ $mimeTypes = new MimeTypes();
+ $mimeType = $mimeTypes->guessMimeType('/some/path/to/image.gif');
+ // Guessing is not based on the file name, so $mimeType will be 'image/gif'
+ // only if the given file is truly a GIF image
+
+Guessing the MIME type is a time-consuming process that requires inspecting
+part of the file contents. Symfony applies multiple guessing mechanisms, one
+of them based on the PHP `fileinfo extension`_. It's recommended to install
+that extension to improve the guessing performance.
+
+Adding a MIME Type Guesser
+..........................
+
+You can add your own MIME type guesser by creating a class that implements
+:class:`Symfony\\Component\\Mime\\MimeTypeGuesserInterface`::
+
+ namespace App;
+
+ use Symfony\Component\Mime\MimeTypeGuesserInterface;
+
+ class SomeMimeTypeGuesser implements MimeTypeGuesserInterface
+ {
+ public function isGuesserSupported(): bool
+ {
+ // return true when the guesser is supported (might depend on the OS for instance)
+ return true;
+ }
+
+ public function guessMimeType(string $path): ?string
+ {
+ // inspect the contents of the file stored in $path to guess its
+ // type and return a valid MIME type ... or null if unknown
+
+ return '...';
+ }
+ }
+
+MIME type guessers must be :ref:`registered as services `
+and :doc:`tagged ` with the ``mime.mime_type_guesser`` tag.
+If you're using the
+:ref:`default services.yaml configuration `,
+this is already done for you, thanks to :ref:`autoconfiguration `.
+
+.. _`MIME`: https://en.wikipedia.org/wiki/MIME
+.. _`MIME types`: https://en.wikipedia.org/wiki/Media_type
+.. _`fileinfo extension`: https://www.php.net/fileinfo
diff --git a/components/options_resolver.rst b/components/options_resolver.rst
index 4b2526872d6..6f3a6751f28 100644
--- a/components/options_resolver.rst
+++ b/components/options_resolver.rst
@@ -1,13 +1,10 @@
-.. index::
- single: OptionsResolver
- single: Components; OptionsResolver
-
The OptionsResolver Component
=============================
- The OptionsResolver component is :phpfunction:`array_replace` on steroids.
- It allows you to create an options system with required options, defaults,
- validation (type, value), normalization and more.
+ The OptionsResolver component is an improved replacement for the
+ :phpfunction:`array_replace` PHP function. It allows you to create an
+ options system with required options, defaults, validation (type, value),
+ normalization and more.
Installation
------------
@@ -16,8 +13,6 @@ Installation
$ composer require symfony/options-resolver
-Alternatively, you can clone the ``_ repository.
-
.. include:: /components/require_autoload.rst.inc
Usage
@@ -28,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 = [])
{
@@ -42,7 +37,7 @@ check which options are set::
class Mailer
{
// ...
- public function sendMail($from, $to)
+ public function sendMail($from, $to): void
{
$mail = ...;
@@ -56,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
{
@@ -73,13 +68,11 @@ code. Use the :phpfunction:`array_replace` to fix that::
}
}
-Now all four options are guaranteed to be set. But what happens if the user of
-the ``Mailer`` class makes a mistake?
-
-.. code-block:: php
+Now all four options are guaranteed to be set, but you could still make an error
+like the following when using the ``Mailer`` class::
$mailer = new Mailer([
- 'usernme' => 'johndoe', // usernme misspelled (instead of username)
+ 'usernme' => 'johndoe', // 'username' is wrongly spelled as 'usernme'
]);
No error will be shown. In the best case, the bug will appear during testing,
@@ -118,7 +111,7 @@ is thrown if an unknown option is passed::
]);
// UndefinedOptionsException: The option "usernme" does not exist.
- // Known options are: "host", "password", "port", "username"
+ // Defined options are: "host", "password", "port", "username"
The rest of your code can access the values of the options without boilerplate
code::
@@ -128,7 +121,7 @@ code::
{
// ...
- public function sendMail($from, $to)
+ public function sendMail($from, $to): void
{
$mail = ...;
$mail->setHost($this->options['host']);
@@ -154,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',
@@ -173,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);
@@ -196,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');
@@ -220,7 +213,7 @@ one required option::
{
// ...
- public function configureOptions(OptionsResolver $resolver)
+ public function configureOptions(OptionsResolver $resolver): void
{
// ...
$resolver->setRequired(['host', 'username', 'password']);
@@ -235,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);
@@ -258,7 +251,7 @@ been set::
{
// ...
- public function configureOptions(OptionsResolver $resolver)
+ public function configureOptions(OptionsResolver $resolver): void
{
// ...
$resolver->setRequired('host');
@@ -268,7 +261,7 @@ been set::
// ...
class GoogleMailer extends Mailer
{
- public function configureOptions(OptionsResolver $resolver)
+ public function configureOptions(OptionsResolver $resolver): void
{
parent::configureOptions($resolver);
@@ -288,7 +281,7 @@ been set::
}
}
-The method :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::getMissingOptions`
+The :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::getMissingOptions` method
lets you access the names of all missing options.
Type Validation
@@ -303,7 +296,7 @@ correctly. To validate the types of the options, call
{
// ...
- public function configureOptions(OptionsResolver $resolver)
+ public function configureOptions(OptionsResolver $resolver): void
{
// ...
@@ -312,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_()`` 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
@@ -333,11 +334,13 @@ is thrown::
]);
// InvalidOptionsException: The option "host" with value "25" is
- // expected to be of type "string"
+ // expected to be of type "string", but is of type "int"
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
~~~~~~~~~~~~~~~~
@@ -352,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');
@@ -368,17 +371,32 @@ is thrown::
'transport' => 'send-mail',
]);
- // InvalidOptionsException: The option "transport" has the value
- // "send-mail", but is expected to be one of "sendmail", "mail", "smtp"
+ // InvalidOptionsException: The option "transport" with value "send-mail"
+ // is invalid. Accepted values are: "sendmail", "mail", "smtp"
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 ` 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.
@@ -398,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;
}
@@ -420,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 {
@@ -437,6 +455,12 @@ if you need to use other options during normalization::
}
}
+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.
+
Default Values that Depend on another Option
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -445,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;
@@ -454,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;
}
@@ -469,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.
@@ -477,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::
@@ -486,7 +510,7 @@ the closure::
class Mailer
{
// ...
- public function configureOptions(OptionsResolver $resolver)
+ public function configureOptions(OptionsResolver $resolver): void
{
// ...
$resolver->setDefaults([
@@ -498,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
@@ -529,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?
@@ -556,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!';
@@ -590,7 +614,7 @@ options in one go::
class Mailer
{
// ...
- public function configureOptions(OptionsResolver $resolver)
+ public function configureOptions(OptionsResolver $resolver): void
{
// ...
$resolver->setDefined(['port', 'encryption']);
@@ -606,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);
@@ -636,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',
@@ -648,7 +672,7 @@ default value::
});
}
- public function sendMail($from, $to)
+ public function sendMail(string $from, string $to): void
{
if ('memory' === $this->options['spool']['type']) {
// ...
@@ -662,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
@@ -671,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',
// ...
@@ -683,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
@@ -695,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'];
});
}
@@ -714,6 +748,51 @@ In same way, parent options can access to the nested options as normal arrays::
The fact that an option is defined as nested means that you must pass
an array of values to resolve it at runtime.
+Prototype Options
+~~~~~~~~~~~~~~~~~
+
+There are situations where you will have to resolve and validate a set of
+options that may repeat many times within another option. Let's imagine a
+``connections`` option that will accept an array of database connections
+with ``host``, ``database``, ``user`` and ``password`` each.
+
+The best way to implement this is to define the ``connections`` option as prototype::
+
+ $resolver->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::
+
+ A prototype option can only be defined inside a nested option and
+ during its resolution it will expect an array of arrays.
+
Deprecating the Option
~~~~~~~~~~~~~~~~~~~~~~
@@ -723,12 +802,21 @@ method::
$resolver
->setDefined(['hostname', 'host'])
- // this outputs the following generic deprecation message:
- // The option "hostname" is deprecated.
- ->setDeprecated('hostname')
- // you can also pass a custom deprecation message
- ->setDeprecated('hostname', 'The option "hostname" is deprecated, use "host" instead.')
+ // this outputs the following generic deprecation message:
+ // Since acme/package 1.2: The option "hostname" is deprecated.
+ ->setDeprecated('hostname', 'acme/package', '1.2')
+
+ // you can also pass a custom deprecation message (%name% placeholder is available)
+ // %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 "%name%" is deprecated, use "host" instead.'
+ )
;
.. note::
@@ -737,6 +825,17 @@ method::
somewhere, either its value is provided by the user or the option is evaluated
within closures of lazy options and normalizers.
+.. note::
+
+ 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\\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
@@ -746,7 +845,7 @@ the option::
->setDefault('encryption', null)
->setDefault('port', null)
->setAllowedTypes('port', ['null', 'int'])
- ->setDeprecated('port', function (Options $options, $value) {
+ ->setDeprecated('port', 'acme/package', '1.2', function (Options $options, ?int $value): string {
if (null === $value) {
return 'Passing "null" to option "port" is deprecated, pass an integer instead.';
}
@@ -768,6 +867,56 @@ 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
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In many cases you may need to define multiple configurations for each option.
+For example, suppose the ``InvoiceMailer`` class has an ``host`` option that is required
+and a ``transport`` option which can be one of ``sendmail``, ``mail`` and ``smtp``.
+You can improve the readability of the code avoiding to duplicate option name for
+each configuration using the :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::define`
+method::
+
+ // ...
+ class InvoiceMailer
+ {
+ // ...
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ // ...
+ $resolver->define('host')
+ ->required()
+ ->default('smtp.example.org')
+ ->allowedTypes('string')
+ ->info('The IP address or hostname');
+
+ $resolver->define('transport')
+ ->required()
+ ->default('transport')
+ ->allowedValues('sendmail', 'mail', 'smtp');
+ }
+ }
+
Performance Tweaks
~~~~~~~~~~~~~~~~~~
@@ -780,9 +929,9 @@ 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 = [])
{
@@ -798,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
{
// ...
}
@@ -813,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 = [];
}
@@ -826,5 +975,20 @@ method ``clearOptionsConfig()`` and call it periodically::
That's it! You now have all the tools and knowledge needed to process
options in your code.
-.. _Packagist: https://packagist.org/packages/symfony/options-resolver
-.. _CHANGELOG: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/OptionsResolver/CHANGELOG.md#260
+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 ec5a5be132c..5ce4c003a11 100644
--- a/components/phpunit_bridge.rst
+++ b/components/phpunit_bridge.rst
@@ -1,16 +1,14 @@
-.. index::
- single: PHPUnitBridge
- single: Components; PHPUnitBridge
-
The PHPUnit Bridge
==================
The PHPUnit Bridge provides utilities to report legacy tests and usage of
- deprecated code and a helper for time-sensitive tests.
+ deprecated code and helpers for mocking native functions related to time,
+ DNS and class existence.
It comes with the following features:
-* Forces the tests to use a consistent locale (``C``);
+* 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);
@@ -18,19 +16,25 @@ It comes with the following features:
* Displays the stack trace of a deprecation on-demand;
-* Provides a ``ClockMock`` and ``DnsMock`` helper classes for time or network-sensitive tests.
+* Provides a ``ClockMock``, ``DnsMock`` and ``ClassExistsMock`` classes for tests
+ sensitive to time, network or class existence;
+
+* 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;
-* Provides a modified version of PHPUnit that does not embed ``symfony/yaml`` nor
- ``prophecy`` to prevent any conflicts with these dependencies.
+* It allows to create tests that are compatible with multiple PHPUnit versions
+ (because it provides polyfills for missing methods, namespaced aliases for
+ non-namespaced classes, etc.).
Installation
------------
.. code-block:: terminal
- $ composer require --dev "symfony/phpunit-bridge:*"
-
-Alternatively, you can clone the ``_ repository.
+ $ composer require --dev symfony/phpunit-bridge
.. include:: /components/require_autoload.rst.inc
@@ -41,21 +45,21 @@ Alternatively, you can clone the ``_
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 ` 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
-
+
-
+
@@ -122,21 +126,46 @@ The summary includes:
-
+
+Running Tests in Parallel
+-------------------------
+
+The modified PHPUnit script allows running tests in parallel by providing
+a directory containing multiple test suites with their own ``phpunit.xml.dist``.
+
+.. code-block:: terminal
+
+ ├── tests/
+ │ ├── Functional/
+ │ │ ├── ...
+ │ │ └── phpunit.xml.dist
+ │ ├── Unit/
+ │ │ ├── ...
+ │ │ └── phpunit.xml.dist
+
+.. code-block:: terminal
+
+ $ ./vendor/bin/simple-phpunit tests/
+
+The modified PHPUnit script will recursively go through the provided directory,
+up to a depth of 3 subdirectories or the value specified by the environment variable
+``SYMFONY_PHPUNIT_MAX_DEPTH``, looking for ``phpunit.xml.dist`` files and then
+running each suite it finds in parallel, collecting their output and displaying
+each test suite's results in their own section.
+
Trigger Deprecation Notices
---------------------------
-Deprecation notices can be triggered by using::
+Deprecation notices can be triggered by using ``trigger_deprecation`` from
+the ``symfony/deprecation-contracts`` package::
- @trigger_error('Your deprecation message', E_USER_DEPRECATED);
+ // indicates something is deprecated since version 1.3 of vendor-name/packagename
+ trigger_deprecation('vendor-name/package-name', '1.3', 'Your deprecation message');
-Without the `@-silencing operator`_, users would need to opt-out from deprecation
-notices. Silencing by default swaps this behavior and allows users to opt-in
-when they are ready to cope with them (by adding a custom error handler like the
-one provided by this bridge). When not silenced, deprecation notices will appear
-in the **Unsilenced** section of the deprecation report.
+ // you can also use printf format (all arguments after the message will be used)
+ trigger_deprecation('...', '1.3', 'Value "%s" is deprecated, use ... instead.', $value);
Mark Tests as Legacy
--------------------
@@ -171,67 +200,172 @@ message, enclosed with ``/``. For example, with:
.. code-block:: xml
-
+
-
-
+
+
-PHPUnit_ will stop your test suite once a deprecation notice is triggered whose
+`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`_ deprecation notices
-will make tests fail. Alternatively, setting ``SYMFONY_DEPRECATIONS_HELPER`` to
-an arbitrary value (ex: ``320``) will make the tests fails only if a higher
-number of deprecation notices is reached (``0`` is the default value). You can
-also set the value ``"weak"`` which will make the bridge ignore any deprecation
-notices. This is useful to projects that must use deprecated interfaces for
-backward compatibility reasons.
+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 fail only if a
+higher number of deprecation notices is reached (``0`` is the default
+value).
+
+You can have even finer-grained control by using other keys of the ``max``
+array, which are ``self``, ``direct``, and ``indirect``. The
+``SYMFONY_DEPRECATIONS_HELPER`` environment variable accepts a URL-encoded
+string, meaning you can combine thresholds and any other configuration setting,
+like this: ``SYMFONY_DEPRECATIONS_HELPER='max[total]=42&max[self]=0&verbose=0'``
+
+Internal deprecations
+.....................
When you maintain a library, having the test suite fail as soon as a dependency
introduces a new deprecation is not desirable, because it shifts the burden of
-fixing that deprecation to any contributor that happens to submit a pull
-request shortly after a new vendor release is made with that deprecation. To
-mitigate this, you can either use tighter requirements, in the hope that
+fixing that deprecation to any contributor that happens to submit a pull request
+shortly after a new vendor release is made with that deprecation.
+
+To mitigate this, you can either use tighter requirements, in the hope that
dependencies will not introduce deprecations in a patch version, or even commit
-the Composer lock file, which would create another class of issues. Libraries
-will often use ``SYMFONY_DEPRECATIONS_HELPER=weak`` because of this. This has
-the drawback of allowing contributions that introduce deprecations but:
+the ``composer.lock`` file, which would create another class of issues.
+Libraries will often use ``SYMFONY_DEPRECATIONS_HELPER=max[total]=999999``
+because of this. This has the drawback of allowing contributions that introduce
+deprecations but:
* forget to fix the deprecated calls if there are any;
* forget to mark appropriate tests with the ``@group legacy`` annotations.
-By using the ``"weak_vendors"`` value, deprecations that are triggered outside
-the ``vendors`` directory will make the test suite fail, while deprecations
-triggered from a library inside it will not, giving you the best of both
-worlds.
+By using ``SYMFONY_DEPRECATIONS_HELPER=max[self]=0``, deprecations that are
+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.
+
+Direct and Indirect Deprecations
+................................
+
+When working on a project, you might be more interested in ``max[direct]``.
+Let's say you want to fix deprecations as soon as they appear. A problem many
+developers experience is that some dependencies they have tend to lag behind
+their own dependencies, meaning they do not fix deprecations as soon as
+possible, which means you should create a pull request on the outdated vendor,
+and ignore these deprecations until your pull request is merged.
+
+The ``max[direct]`` config allows you to put a threshold on direct deprecations
+only, allowing you to notice when *your code* is using deprecated APIs, and to
+keep up with the changes. You can still use ``max[indirect]`` if you want to
+keep indirect deprecations under a given threshold.
+
+Here is a summary that should help you pick the right configuration:
+
++------------------------+-----------------------------------------------------+
+| Value | Recommended situation |
++========================+=====================================================+
+| max[total]=0 | Recommended for actively maintained projects |
+| | with robust/no dependencies |
++------------------------+-----------------------------------------------------+
+| max[direct]=0 | Recommended for projects with dependencies |
+| | that fail to keep up with new deprecations. |
++------------------------+-----------------------------------------------------+
+| max[self]=0 | Recommended for libraries that use |
+| | the deprecation system themselves and |
+| | 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
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+By default, the bridge will display a detailed output with the number of
+deprecations and where they arise. If this is too much for you, you can use
+``SYMFONY_DEPRECATIONS_HELPER=verbose=0`` to turn the verbose output off.
+
+It's also possible to change verbosity per deprecation type. For example, using
+``quiet[]=indirect&quiet[]=other`` will hide details for deprecations of types
+"indirect" and "other".
+
+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 `
+is for, and both settings are orthogonal.
Disabling the Deprecation Helper
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Set the ``SYMFONY_DEPRECATIONS_HELPER`` environment variable to ``disabled`` to
-completely disable the deprecation helper. This is useful to make use of the
+Set the ``SYMFONY_DEPRECATIONS_HELPER`` environment variable to ``disabled=1``
+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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
By default, the PHPUnit Bridge uses ``DebugClassLoader`` from the
-:doc:`Debug component ` to throw deprecation notices at
-class autoloading time. This can be disabled with the ``debug-class-loader`` option.
+`ErrorHandler component`_ to throw deprecation notices at class autoloading
+time. This can be disabled with the ``debug-class-loader`` option.
.. code-block:: xml
@@ -248,26 +382,90 @@ class autoloading time. This can be disabled with the ``debug-class-loader`` opt
+Compile-time Deprecations
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Use the ``debug:container`` command to list the deprecations generated during
+the compiling and warming up of the container:
+
+.. code-block:: terminal
+
+ $ php bin/console debug:container --deprecations
+
+Log Deprecations
+~~~~~~~~~~~~~~~~
+
+For turning the verbose output off and write it to a log file instead you can use
+``SYMFONY_DEPRECATIONS_HELPER='logFile=/path/deprecations.log'``.
+
+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
+
+
+
+
+
+
+
+
+
+
+
+
+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
-----------------------------------
When adding deprecations to your code, you might like writing tests that verify
that they are triggered as required. To do so, the bridge provides the
-``@expectedDeprecation`` annotation that you can use on your test methods.
+``expectDeprecation()`` method that you can use on your test methods.
It requires you to pass the expected message, given in the same format as for
the `PHPUnit's assertStringMatchesFormat()`_ method. If you expect more than one
-deprecation message for a given test method, you can use the annotation several
+deprecation message for a given test method, you can use the method several
times (order matters)::
- /**
- * @group legacy
- * @expectedDeprecation This "%s" method is deprecated.
- * @expectedDeprecation The second argument of the "%s" method is deprecated.
- */
- public function testDeprecatedCode()
+ use PHPUnit\Framework\TestCase;
+ use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
+
+ class MyTest extends TestCase
{
- @trigger_error('This "Foo" method is deprecated.', E_USER_DEPRECATED);
- @trigger_error('The second argument of the "Bar" method is deprecated.', E_USER_DEPRECATED);
+ use ExpectDeprecationTrait;
+
+ /**
+ * @group legacy
+ */
+ public function testDeprecatedCode(): void
+ {
+ // test some code that triggers the following deprecation:
+ // trigger_deprecation('vendor-name/package-name', '5.1', 'This "Foo" method is deprecated.');
+ $this->expectDeprecation('Since vendor-name/package-name 5.1: This "%s" method is deprecated');
+
+ // ...
+
+ // test some code that triggers the following deprecation:
+ // trigger_deprecation('vendor-name/package-name', '4.4', 'The second argument of the "Bar" method is deprecated.');
+ $this->expectDeprecation('Since vendor-name/package-name 4.4: The second argument of the "%s" method is deprecated.');
+ }
}
Display the Full Stack Trace
@@ -277,7 +475,9 @@ By default, the PHPUnit Bridge displays only deprecation messages.
To show the full stack trace related to a deprecation, set the value of ``SYMFONY_DEPRECATIONS_HELPER``
to a regular expression matching the deprecation message.
-For example, if the following deprecation notice is thrown::
+For example, if the following deprecation notice is thrown:
+
+.. code-block:: bash
1x: Doctrine\Common\ClassLoader is deprecated.
1x in EntityTypeTest::setUp from Symfony\Bridge\Doctrine\Tests\Form\Type
@@ -288,6 +488,45 @@ Running the following command will display the full stack trace:
$ SYMFONY_DEPRECATIONS_HELPER='/Doctrine\\Common\\ClassLoader is deprecated\./' ./vendor/bin/simple-phpunit
+Testing with Multiple PHPUnit Versions
+--------------------------------------
+
+When testing a library that has to be compatible with several versions of PHP,
+the test suite cannot use the latest versions of PHPUnit because:
+
+* PHPUnit 8 deprecated several methods in favor of other methods which are not
+ available in older versions (e.g. PHPUnit 4);
+* PHPUnit 8 added the ``void`` return type to the ``setUp()`` method, which is
+ not compatible with PHP 5.5;
+* PHPUnit switched to namespaced classes starting from PHPUnit 6, so tests must
+ work with and without namespaces.
+
+Polyfills for the Unavailable Methods
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When using the ``simple-phpunit`` script, PHPUnit Bridge injects polyfills for
+most methods of the ``TestCase`` and ``Assert`` classes (e.g. ``expectException()``,
+``expectExceptionMessage()``, ``assertContainsEquals()``, etc.). This allows writing
+test cases using the latest best practices while still remaining compatible with
+older PHPUnit versions.
+
+Removing the Void Return Type
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When running the ``simple-phpunit`` script with the ``SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT``
+environment variable set to ``1``, the PHPUnit bridge will alter the code of
+PHPUnit to remove the return type (introduced in PHPUnit 8) from ``setUp()``,
+``tearDown()``, ``setUpBeforeClass()`` and ``tearDownAfterClass()`` methods.
+This allows you to write a test compatible with both PHP 5 and PHPUnit 8.
+
+Using Namespaced PHPUnit Classes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The PHPUnit bridge adds namespaced class aliases for most of the PHPUnit classes
+declared without namespaces (e.g. ``PHPUnit_Framework_Assert``), allowing you to
+always use the namespaced class declaration even when the test is executed with
+PHPUnit 4.
+
Time-sensitive Tests
--------------------
@@ -301,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();
@@ -313,8 +552,8 @@ If you have this kind of time-related tests::
}
}
-You used the :doc:`Symfony Stopwatch Component ` to
-calculate the duration time of your process, here 10 seconds. However, depending
+You calculated the duration time of your process using the Stopwatch utilities to
+:ref:`profile Symfony applications `. However, depending
on the load of the server or the processes running on your local machine, the
``$duration`` could for example be ``10.000023s`` instead of ``10s``.
@@ -326,11 +565,16 @@ 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()`` and ``usleep()``. Additionally the function
-``date()`` is mocked so it uses the mocked time if no timestamp is specified.
+allows you to mock the PHP's built-in time functions ``time()``, ``microtime()``,
+``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.
+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', (string) time())`` to use the mocked
+``time()`` function.
To use the ``ClockMock`` class in your test, add the ``@group time-sensitive``
annotation to its class or methods. This annotation only works when executing
@@ -342,7 +586,7 @@ following listener in your PHPUnit configuration:
-
+
.. note::
@@ -363,7 +607,7 @@ test::
*/
class MyTest extends TestCase
{
- public function testSomething()
+ public function testSomething(): void
{
$stopwatch = new Stopwatch();
@@ -377,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.
@@ -391,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;
}
@@ -402,13 +646,14 @@ different class, do it explicitly using ``ClockMock::register(MyClass::class)``:
use App\MyClass;
use PHPUnit\Framework\TestCase;
+ use Symfony\Bridge\PhpUnit\ClockMock;
/**
* @group time-sensitive
*/
class MyTest extends TestCase
{
- public function testGetTimeInHours()
+ public function testGetTimeInHours(): void
{
ClockMock::register(MyClass::class);
@@ -447,46 +692,48 @@ functions:
Use Case
~~~~~~~~
-Consider the following example that uses the ``checkMX`` option of the ``Email``
-constraint to test the validity of the email domain::
+Consider the following example that tests a custom class called ``DomainValidator``
+which defines a ``checkDnsRecord`` option to also validate that a domain is
+associated to a valid host::
+ use App\Validator\DomainValidator;
use PHPUnit\Framework\TestCase;
- use Symfony\Component\Validator\Constraints\Email;
class MyTest extends TestCase
{
- public function testEmail()
+ public function testEmail(): void
{
- $validator = ...
- $constraint = new Email(['checkMX' => true]);
-
- $result = $validator->validate('foo@example.com', $constraint);
+ $validator = new DomainValidator(['checkDnsRecord' => true]);
+ $isValid = $validator->validate('example.com');
// ...
+ }
}
-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::
+ use App\Validator\DomainValidator;
use PHPUnit\Framework\TestCase;
- use Symfony\Component\Validator\Constraints\Email;
+ use Symfony\Bridge\PhpUnit\DnsMock;
/**
* @group dns-sensitive
*/
- class MyTest extends TestCase
+ class DomainValidatorTest extends TestCase
{
- public function testEmails()
+ public function testEmails(): void
{
- DnsMock::withMockedHosts(['example.com' => [['type' => 'MX']]]);
-
- $validator = ...
- $constraint = new Email(['checkMX' => true]);
+ DnsMock::withMockedHosts([
+ 'example.com' => [['type' => 'A', 'ip' => '1.2.3.4']],
+ ]);
- $result = $validator->validate('foo@example.com', $constraint);
+ $validator = new DomainValidator(['checkDnsRecord' => true]);
+ $isValid = $validator->validate('example.com');
// ...
+ }
}
The ``withMockedHosts()`` method configuration is defined as an array. The keys
@@ -507,13 +754,96 @@ conditions::
],
]);
+Class Existence Based Tests
+---------------------------
+
+Tests that behave differently depending on existing classes, for example Composer's
+development dependencies, are often hard to test for the alternate case. For that
+reason, this component also provides mocks for these PHP functions:
+
+* :phpfunction:`class_exists`
+* :phpfunction:`interface_exists`
+* :phpfunction:`trait_exists`
+* :phpfunction:`enum_exists`
+
+Use Case
+~~~~~~~~
+
+Consider the following example that relies on the ``Vendor\DependencyClass`` to
+toggle a behavior::
+
+ use Vendor\DependencyClass;
+
+ class MyClass
+ {
+ public function hello(): string
+ {
+ if (class_exists(DependencyClass::class)) {
+ return 'The dependency behavior.';
+ }
+
+ return 'The default behavior.';
+ }
+ }
+
+A regular test case for ``MyClass`` (assuming the development dependencies
+are installed during tests) would look like::
+
+ use MyClass;
+ use PHPUnit\Framework\TestCase;
+
+ class MyClassTest extends TestCase
+ {
+ public function testHello(): void
+ {
+ $class = new MyClass();
+ $result = $class->hello(); // "The dependency behavior."
+
+ // ...
+ }
+ }
+
+In order to test the default behavior instead use the
+``ClassExistsMock::withMockedClasses()`` to configure the expected
+classes, interfaces and/or traits for the code to run::
+
+ use MyClass;
+ use PHPUnit\Framework\TestCase;
+ use Vendor\DependencyClass;
+
+ class MyClassTest extends TestCase
+ {
+ // ...
+
+ public function testHelloDefault(): void
+ {
+ ClassExistsMock::register(MyClass::class);
+ ClassExistsMock::withMockedClasses([DependencyClass::class => false]);
+
+ $class = new MyClass();
+ $result = $class->hello(); // "The default behavior."
+
+ // ...
+ }
+ }
+
+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
---------------
The ``@group time-sensitive`` and ``@group dns-sensitive`` annotations work
"by convention" and assume that the namespace of the tested class can be
obtained just by removing the ``Tests\`` part from the test namespace. I.e.
-that if the your test case fully-qualified class name (FQCN) is
+if your test cases fully-qualified class name (FQCN) is
``App\Tests\Watch\DummyWatchTest``, it assumes the tested class namespace
is ``App\Watch``.
@@ -523,9 +853,9 @@ namespaces in the ``phpunit.xml`` file, as done for example in the
.. code-block:: xml
-
+
@@ -543,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
@@ -562,16 +898,24 @@ the namespaces of the tested classes in your phpunit.xml.dist
+::
+
+ // config/bootstrap.php
+ use Symfony\Bridge\PhpUnit\ClockMock;
+
+ // ...
+ if ('test' === $_SERVER['APP_ENV']) {
+ ClockMock::register('Acme\\MyClassTest\\');
+ }
+
Modified PHPUnit script
-----------------------
This bridge provides a modified version of PHPUnit that you can call by using
its ``bin/simple-phpunit`` command. It has the following features:
-* Does not embed ``symfony/yaml`` nor ``prophecy`` to prevent any conflicts with
- these dependencies;
-* Uses PHPUnit 4.8 when run with PHP <=5.5, PHPUnit 5.7 when run with PHP >=5.6
- and PHPUnit 6.5 when run with PHP >=7.2;
+* Works with a standalone vendor directory that doesn't conflict with yours;
+* Does not embed ``prophecy`` to prevent any conflicts with its dependencies;
* Collects and replays skipped tests when the ``SYMFONY_PHPUNIT_SKIPPED_TESTS``
env var is defined: the env var should specify a file name that will be used for
storing skipped tests on a first run, and replay them on the second run;
@@ -581,22 +925,28 @@ its ``bin/simple-phpunit`` command. It has the following features:
The script writes the modified PHPUnit it builds in a directory that can be
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.
+the ``simple-phpunit`` if it is not provided. It's also possible to set this
+env var in the ``phpunit.xml.dist`` file.
If you have installed the bridge through Composer, you can run it by calling e.g.:
-.. code-block:: bash
+.. code-block:: terminal
$ vendor/bin/simple-phpunit
.. tip::
- Set the ``SYMFONY_PHPUNIT_VERSION`` env var to e.g. ``5.5`` to change the
- base version of PHPUnit to ``5.5`` instead of the default ``5.3``.
+ It's possible to change the PHPUnit version by setting the
+ ``SYMFONY_PHPUNIT_VERSION`` env var in the ``phpunit.xml.dist`` file (e.g.
+ ````). This is the
+ preferred method as it can be committed to your version control repository.
- It's also possible to set this env var in the ``phpunit.xml.dist`` file.
+ It's also possible to set ``SYMFONY_PHPUNIT_VERSION`` as a real env var
+ (not defined in a :ref:`dotenv file `).
+
+ In the same way, ``SYMFONY_MAX_PHPUNIT_VERSION`` will set the maximum version
+ of PHPUnit to be considered. This is useful when testing a framework that does
+ not support the latest version(s) of PHPUnit.
.. tip::
@@ -605,11 +955,27 @@ 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.
-Code coverage listener
+.. 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
+
+
+
+
+
+
+
+Code Coverage Listener
----------------------
By default, the code coverage is computed with the following rule: if a line of
-code is executed, then it is marked as covered. And the test which executes a
+code is executed, then it is marked as covered. The test which executes a
line of code is therefore marked as "covering the line of code". This can be
misleading.
@@ -617,7 +983,7 @@ Consider the following example::
class Bar
{
- public function barMethod()
+ public function barMethod(): string
{
return 'bar';
}
@@ -625,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();
@@ -642,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);
@@ -664,19 +1028,19 @@ the ``Test`` part of the classname: ``My\Namespace\Tests\FooTest`` ->
Installation
~~~~~~~~~~~~
-Add the following configuration to the ``phpunit.xml.dist`` file
+Add the following configuration to the ``phpunit.xml.dist`` file:
.. code-block:: xml
-
+
-
+
@@ -694,7 +1058,7 @@ your application, you can use your own SUT (System Under Test) solver:
The ``My\Namespace\SutSolver::solve`` can be any PHP callable and receives the
-current test classname as its first argument.
+current test as its first argument.
Finally, the listener can also display warning messages when the SUT solver does
not find the SUT:
@@ -710,15 +1074,14 @@ not find the SUT:
-.. _PHPUnit: https://phpunit.de
-.. _`PHPUnit event listener`: https://phpunit.de/manual/current/en/extending-phpunit.html#extending-phpunit.PHPUnit_Framework_TestListener
-.. _`PHPUnit's assertStringMatchesFormat()`: https://phpunit.de/manual/current/en/appendixes.assertions.html#appendixes.assertions.assertStringMatchesFormat
-.. _`PHP error handler`: https://php.net/manual/en/book.errorfunc.php
-.. _`environment variable`: https://phpunit.de/manual/current/en/appendixes.configuration.html#appendixes.configuration.php-ini-constants-variables
-.. _Packagist: https://packagist.org/packages/symfony/phpunit-bridge
-.. _`@-silencing operator`: https://php.net/manual/en/language.operators.errorcontrol.php
-.. _`@-silenced`: https://php.net/manual/en/language.operators.errorcontrol.php
+.. _`PHPUnit`: https://phpunit.de
+.. _`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://docs.phpunit.de/en/9.6/assertions.html#assertstringmatchesformat
+.. _`PHP error handler`: https://www.php.net/manual/en/book.errorfunc.php
+.. _`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
-.. _`PHP namespace resolutions rules`: https://php.net/manual/en/language.namespaces.rules.php
+.. _`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/polyfill_apcu.rst b/components/polyfill_apcu.rst
deleted file mode 100644
index b3b60e95566..00000000000
--- a/components/polyfill_apcu.rst
+++ /dev/null
@@ -1,52 +0,0 @@
-.. index::
- single: Polyfill
- single: APC
- single: Components; Polyfill
-
-The Symfony Polyfill / APCu Component
-=====================================
-
- This component provides ``apcu_*`` functions and the ``APCUIterator`` class
- to users of the legacy APC extension.
-
-Installation
-------------
-
-.. code-block:: terminal
-
- $ composer require symfony/polyfill-apcu
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-Usage
------
-
-Once this component is installed in your application, you can use the following
-classes and functions, no matter if the `PHP APCu extension`_ is installed or
-not in your server. The only requirement is to have installed at least the
-`legacy APC extension`_.
-
-Provided Classes
-~~~~~~~~~~~~~~~~
-
-* :phpclass:`APCUIterator`
-
-Provided Functions
-~~~~~~~~~~~~~~~~~~
-
-* :phpfunction:`apcu_add`
-* :phpfunction:`apcu_delete`
-* :phpfunction:`apcu_exists`
-* :phpfunction:`apcu_fetch`
-* :phpfunction:`apcu_store`
-* :phpfunction:`apcu_cache_info`
-* :phpfunction:`apcu_cas`
-* :phpfunction:`apcu_clear_cache`
-* :phpfunction:`apcu_dec`
-* :phpfunction:`apcu_inc`
-* :phpfunction:`apcu_sma_info`
-
-.. _`PHP APCu extension`: https://secure.php.net/manual/en/book.apcu.php
-.. _`legacy APC extension`: https://secure.php.net/manual/en/book.apc.php
diff --git a/components/polyfill_ctype.rst b/components/polyfill_ctype.rst
deleted file mode 100644
index c77af0bc874..00000000000
--- a/components/polyfill_ctype.rst
+++ /dev/null
@@ -1,45 +0,0 @@
-.. index::
- single: Polyfill
- single: Ctype
- single: Components; Polyfill
-
-The Symfony Polyfill / Ctype Component
-======================================
-
- This component provides ``ctype_*`` functions to users who run PHP versions
- without the ctype extension.
-
-Installation
-------------
-
-.. code-block:: terminal
-
- $ composer require symfony/polyfill-ctype
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-Usage
------
-
-Once this component is installed in your application, you can use the following
-functions, no matter if the `PHP Ctype extension`_ is installed or not in your
-server.
-
-Provided Functions
-~~~~~~~~~~~~~~~~~~
-
-* :phpfunction:`ctype_alnum`
-* :phpfunction:`ctype_alpha`
-* :phpfunction:`ctype_cntrl`
-* :phpfunction:`ctype_digit`
-* :phpfunction:`ctype_graph`
-* :phpfunction:`ctype_lower`
-* :phpfunction:`ctype_print`
-* :phpfunction:`ctype_punct`
-* :phpfunction:`ctype_space`
-* :phpfunction:`ctype_upper`
-* :phpfunction:`ctype_xdigit`
-
-.. _`PHP Ctype extension`: https://secure.php.net/manual/en/book.ctype.php
diff --git a/components/polyfill_iconv.rst b/components/polyfill_iconv.rst
deleted file mode 100644
index 56346186c60..00000000000
--- a/components/polyfill_iconv.rst
+++ /dev/null
@@ -1,61 +0,0 @@
-.. index::
- single: Polyfill
- single: Iconv
- single: Components; Polyfill
-
-The Symfony Polyfill / Iconv Component
-======================================
-
- This component provides a native PHP implementation of the ``iconv_*``
- functions to users who run PHP versions without the ``iconv`` extension.
-
-Installation
-------------
-
-.. code-block:: terminal
-
- $ composer require symfony/polyfill-iconv
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-Usage
------
-
-Once this component is installed in your application, you can use the following
-constants and functions, no matter if the `PHP iconv extension`_ is installed or
-not in your server. The only function not implemented in this component is
-:phpfunction:`ob_iconv_handler`.
-
-Provided Constants
-~~~~~~~~~~~~~~~~~~
-
-* ``ICONV_IMPL`` (value = ``'Symfony'``)
-* ``ICONV_VERSION`` (value = ``'1.0'``)
-* ``ICONV_MIME_DECODE_STRICT`` (value = ``1``)
-* ``ICONV_MIME_DECODE_CONTINUE_ON_ERROR`` (value = ``2``)
-
-Provided Functions
-~~~~~~~~~~~~~~~~~~
-
-These functions are always available:
-
-* :phpfunction:`iconv`
-* :phpfunction:`iconv_get_encoding`
-* :phpfunction:`iconv_set_encoding`
-* :phpfunction:`iconv_mime_encode`
-* :phpfunction:`iconv_mime_decode_headers`
-
-These functions are only available when the ``mbstring`` or the ``xml``
-extension are installed:
-
-* :phpfunction:`iconv_strlen`
-* :phpfunction:`iconv_strpos`
-* :phpfunction:`iconv_strrpos`
-* :phpfunction:`iconv_substr`
-* :phpfunction:`iconv_mime_decode`
-
-.. _`PHP iconv extension`: https://secure.php.net/manual/en/book.iconv.php
-.. _`mbstring`: https://secure.php.net/manual/en/book.mbstring.php
-.. _`xml`: https://secure.php.net/manual/en/book.xml.php
diff --git a/components/polyfill_intl_grapheme.rst b/components/polyfill_intl_grapheme.rst
deleted file mode 100644
index bad42f179fc..00000000000
--- a/components/polyfill_intl_grapheme.rst
+++ /dev/null
@@ -1,58 +0,0 @@
-.. index::
- single: Polyfill
- single: Intl
- single: Components; Polyfill
-
-The Symfony Polyfill / Intl Grapheme Component
-==============================================
-
- This component provides a partial, native PHP implementation of the
- ``grapheme_*`` functions to users who run PHP versions without the ``intl``
- extension.
-
-Installation
-------------
-
-.. code-block:: terminal
-
- $ composer require symfony/polyfill-intl-grapheme
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-Usage
------
-
-Once this component is installed in your application, you can use the following
-constants and functions, no matter if the `PHP intl extension`_ is installed or
-not in your server.
-
-Provided Constants
-~~~~~~~~~~~~~~~~~~
-
-* ``GRAPHEME_EXTR_COUNT`` (value = ``0``)
-* ``GRAPHEME_EXTR_MAXBYTES`` (value = ``1``)
-* ``GRAPHEME_EXTR_MAXCHARS`` (value = ``2``)
-
-Provided Functions
-~~~~~~~~~~~~~~~~~~
-
-* :phpfunction:`grapheme_extract`
-* :phpfunction:`grapheme_stripos`
-* :phpfunction:`grapheme_stristr`
-* :phpfunction:`grapheme_strlen`
-* :phpfunction:`grapheme_strpos`
-* :phpfunction:`grapheme_strripos`
-* :phpfunction:`grapheme_strrpos`
-* :phpfunction:`grapheme_strstr`
-* :phpfunction:`grapheme_substr`
-
-.. seealso::
-
- The :doc:`polyfill-intl-icu ` and
- :doc:`polyfill-intl-normalizer `
- components provide polyfills for other classes and functions related to the
- Intl PHP extension.
-
-.. _`PHP intl extension`: https://secure.php.net/manual/en/book.intl.php
diff --git a/components/polyfill_intl_icu.rst b/components/polyfill_intl_icu.rst
deleted file mode 100644
index 4dc7c503afc..00000000000
--- a/components/polyfill_intl_icu.rst
+++ /dev/null
@@ -1,54 +0,0 @@
-.. index::
- single: Polyfill
- single: ICU
- single: Components; Polyfill
-
-The Symfony Polyfill / Intl ICU Component
-=========================================
-
- This component provides a native PHP implementation of several Intl
- functions and classes to users who run PHP versions without the ``intl``
- extension.
-
-Installation
-------------
-
-.. code-block:: terminal
-
- $ composer require symfony/polyfill-intl-icu
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-Usage
------
-
-Once this component is installed in your application, you can use the following
-classes and functions, no matter if the `PHP intl extension`_ is installed or
-not in your server.
-
-Provided Classes
-~~~~~~~~~~~~~~~~
-
-* :phpclass:`Collator`
-* :phpclass:`IntlDateFormatter`
-* :phpclass:`Locale`
-* :phpclass:`NumberFormatter`
-
-Provided Functions
-~~~~~~~~~~~~~~~~~~
-
-* :phpfunction:`intl_error_name`
-* :phpfunction:`intl_get_error_code`
-* :phpfunction:`intl_get_error_message`
-* :phpfunction:`intl_is_failure`
-
-.. seealso::
-
- The :doc:`polyfill-intl-grapheme ` and
- :doc:`polyfill-intl-normalizer `
- components provide polyfills for other classes and functions related to the
- Intl PHP extension.
-
-.. _`PHP intl extension`: https://secure.php.net/manual/en/book.intl.php
diff --git a/components/polyfill_intl_normalizer.rst b/components/polyfill_intl_normalizer.rst
deleted file mode 100644
index 4604f29a259..00000000000
--- a/components/polyfill_intl_normalizer.rst
+++ /dev/null
@@ -1,47 +0,0 @@
-.. index::
- single: Polyfill
- single: Normalizer
- single: Components; Polyfill
-
-The Symfony Polyfill / Intl Normalizer Component
-================================================
-
- This component provides a fallback implementation for the ``Normalizer``
- class to users who run PHP versions without the ``intl`` extension.
-
-Installation
-------------
-
-.. code-block:: terminal
-
- $ composer require symfony/polyfill-intl-normalizer
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-Usage
------
-
-Once this component is installed in your application, you can use the following
-classes and functions, no matter if the `PHP intl extension`_ is installed or
-not in your server.
-
-Provided Classes
-~~~~~~~~~~~~~~~~
-
-* :phpclass:`Normalizer`
-
-Provided Functions
-~~~~~~~~~~~~~~~~~~
-
-* :phpfunction:`normalizer_is_normalized`
-* :phpfunction:`normalizer_normalize`
-
-.. seealso::
-
- The :doc:`polyfill-intl-grapheme ` and
- :doc:`polyfill-intl-icu ` components provide
- polyfills for other classes and functions related to the Intl PHP extension.
-
-.. _`PHP intl extension`: https://secure.php.net/manual/en/book.intl.php
diff --git a/components/polyfill_mbstring.rst b/components/polyfill_mbstring.rst
deleted file mode 100644
index 934301bf432..00000000000
--- a/components/polyfill_mbstring.rst
+++ /dev/null
@@ -1,78 +0,0 @@
-.. index::
- single: Polyfill
- single: Mbstring
- single: Components; Polyfill
-
-The Symfony Polyfill / Mbstring Component
-=========================================
-
- This component provides a partial, native PHP implementation for the
- ``mbstring`` PHP extension.
-
-Installation
-------------
-
-.. code-block:: terminal
-
- $ composer require symfony/polyfill-mbstring
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-Usage
------
-
-Once this component is installed in your application, you can use the following
-constants and functions, no matter if the `PHP mbstring extension`_ is installed
-or not in your server.
-
-Provided Constants
-~~~~~~~~~~~~~~~~~~
-
-* ``MB_CASE_UPPER`` (value = ``0``)
-* ``MB_CASE_LOWER`` (value = ``1``)
-* ``MB_CASE_TITLE`` (value = ``2``)
-
-Provided Functions
-~~~~~~~~~~~~~~~~~~
-
-* :phpfunction:`mb_check_encoding`
-* :phpfunction:`mb_chr`
-* :phpfunction:`mb_convert_case`
-* :phpfunction:`mb_convert_encoding`
-* :phpfunction:`mb_convert_variables`
-* :phpfunction:`mb_decode_mimeheader`
-* :phpfunction:`mb_decode_numericentity`
-* :phpfunction:`mb_detect_encoding`
-* :phpfunction:`mb_detect_order`
-* :phpfunction:`mb_encode_mimeheader`
-* :phpfunction:`mb_encode_numericentity`
-* :phpfunction:`mb_encoding_aliases`
-* :phpfunction:`mb_get_info`
-* :phpfunction:`mb_http_input`
-* :phpfunction:`mb_http_output`
-* :phpfunction:`mb_internal_encoding`
-* :phpfunction:`mb_language`
-* :phpfunction:`mb_list_encodings`
-* :phpfunction:`mb_ord`
-* :phpfunction:`mb_output_handler`
-* :phpfunction:`mb_parse_str`
-* :phpfunction:`mb_scrub`
-* :phpfunction:`mb_stripos`
-* :phpfunction:`mb_stristr`
-* :phpfunction:`mb_strlen`
-* :phpfunction:`mb_strpos`
-* :phpfunction:`mb_strrchr`
-* :phpfunction:`mb_strrichr`
-* :phpfunction:`mb_strripos`
-* :phpfunction:`mb_strrpos`
-* :phpfunction:`mb_strstr`
-* :phpfunction:`mb_strtolower`
-* :phpfunction:`mb_strtoupper`
-* :phpfunction:`mb_strwidth`
-* :phpfunction:`mb_substitute_character`
-* :phpfunction:`mb_substr_count`
-* :phpfunction:`mb_substr`
-
-.. _`PHP mbstring extension`: https://secure.php.net/manual/en/book.mbstring.php
diff --git a/components/polyfill_php54.rst b/components/polyfill_php54.rst
deleted file mode 100644
index e32a00f6f62..00000000000
--- a/components/polyfill_php54.rst
+++ /dev/null
@@ -1,35 +0,0 @@
-.. index::
- single: Polyfill
- single: PHP
- single: Components; Polyfill
-
-The Symfony Polyfill / PHP 5.4 Component
-========================================
-
- This component provides some PHP 5.4 features to applications using earlier
- PHP versions.
-
-Installation
-------------
-
-.. code-block:: terminal
-
- $ composer require symfony/polyfill-php54
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-Usage
------
-
-Once this component is installed in your application, you can use the following
-functions, no matter if your PHP version is earlier than PHP 5.4.
-
-Provided Functions
-~~~~~~~~~~~~~~~~~~
-
-* :phpfunction:`class_uses`
-* :phpfunction:`hex2bin`
-* :phpfunction:`session_register_shutdown`
-* :phpfunction:`trait_exists`
diff --git a/components/polyfill_php55.rst b/components/polyfill_php55.rst
deleted file mode 100644
index 9ac1577dea1..00000000000
--- a/components/polyfill_php55.rst
+++ /dev/null
@@ -1,39 +0,0 @@
-.. index::
- single: Polyfill
- single: PHP
- single: Components; Polyfill
-
-The Symfony Polyfill / PHP 5.5 Component
-========================================
-
- This component provides some PHP 5.5 features to applications using earlier
- PHP versions.
-
-Installation
-------------
-
-.. code-block:: terminal
-
- $ composer require symfony/polyfill-php55
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-Usage
------
-
-Once this component is installed in your application, you can use the following
-functions, no matter if your PHP version is earlier than PHP 5.5.
-
-Provided Functions
-~~~~~~~~~~~~~~~~~~
-
-* :phpfunction:`array_column`
-* :phpfunction:`boolval`
-* :phpfunction:`hash_pbkdf2`
-* :phpfunction:`json_last_error_msg`
-* :phpfunction:`password_get_info`
-* :phpfunction:`password_hash`
-* :phpfunction:`password_needs_rehash`
-* :phpfunction:`password_verify`
diff --git a/components/polyfill_php56.rst b/components/polyfill_php56.rst
deleted file mode 100644
index 44b8f69598b..00000000000
--- a/components/polyfill_php56.rst
+++ /dev/null
@@ -1,39 +0,0 @@
-.. index::
- single: Polyfill
- single: PHP
- single: Components; Polyfill
-
-The Symfony Polyfill / PHP 5.6 Component
-========================================
-
- This component provides some PHP 5.6 features to applications using earlier
- PHP versions.
-
-Installation
-------------
-
-.. code-block:: terminal
-
- $ composer require symfony/polyfill-php56
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-Usage
------
-
-Once this component is installed in your application, you can use the following
-constants and functions, no matter if your PHP version is earlier than PHP 5.6.
-
-Provided Constants
-~~~~~~~~~~~~~~~~~~
-
-* ``LDAP_ESCAPE_FILTER`` (value = ``1``)
-* ``LDAP_ESCAPE_DN`` (value = ``2``)
-
-Provided Functions
-~~~~~~~~~~~~~~~~~~
-
-* :phpfunction:`hash_equals`
-* :phpfunction:`ldap_escape`
diff --git a/components/polyfill_php70.rst b/components/polyfill_php70.rst
deleted file mode 100644
index e95ada92dcc..00000000000
--- a/components/polyfill_php70.rst
+++ /dev/null
@@ -1,53 +0,0 @@
-.. index::
- single: Polyfill
- single: PHP
- single: Components; Polyfill
-
-The Symfony Polyfill / PHP 7.0 Component
-========================================
-
- This component provides some PHP 7.0 features to applications using earlier
- PHP versions.
-
-Installation
-------------
-
-.. code-block:: terminal
-
- $ composer require symfony/polyfill-php70
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-Usage
------
-
-Once this component is installed in your application, you can use the following
-constants, classes and functions, no matter if your PHP version is earlier than
-PHP 7.0.
-
-Provided Constants
-~~~~~~~~~~~~~~~~~~
-
-* ``PHP_INT_MIN`` (value = ``~PHP_INT_MAX``)
-
-Provided Classes
-~~~~~~~~~~~~~~~~
-
-* :phpclass:`ArithmeticError`
-* :phpclass:`AssertionError`
-* :phpclass:`DivisionByZeroError`
-* :phpclass:`Error`
-* :phpclass:`ParseError`
-* :phpclass:`SessionUpdateTimestampHandlerInterface`
-* :phpclass:`TypeError`
-
-Provided Functions
-~~~~~~~~~~~~~~~~~~
-
-* :phpfunction:`error_clear_last`
-* :phpfunction:`intdiv`
-* :phpfunction:`preg_replace_callback_array`
-* :phpfunction:`random_bytes`
-* :phpfunction:`random_int`
diff --git a/components/polyfill_php71.rst b/components/polyfill_php71.rst
deleted file mode 100644
index 6e7f116403e..00000000000
--- a/components/polyfill_php71.rst
+++ /dev/null
@@ -1,32 +0,0 @@
-.. index::
- single: Polyfill
- single: PHP
- single: Components; Polyfill
-
-The Symfony Polyfill / PHP 7.1 Component
-========================================
-
- This component provides some PHP 7.1 features to applications using earlier
- PHP versions.
-
-Installation
-------------
-
-.. code-block:: terminal
-
- $ composer require symfony/polyfill-php71
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-Usage
------
-
-Once this component is installed in your application, you can use the following
-functions, no matter if your PHP version is earlier than PHP 7.1.
-
-Provided Functions
-~~~~~~~~~~~~~~~~~~
-
-* :phpfunction:`is_iterable`
diff --git a/components/polyfill_php72.rst b/components/polyfill_php72.rst
deleted file mode 100644
index b3c6c86198e..00000000000
--- a/components/polyfill_php72.rst
+++ /dev/null
@@ -1,41 +0,0 @@
-.. index::
- single: Polyfill
- single: PHP
- single: Components; Polyfill
-
-The Symfony Polyfill / PHP 7.2 Component
-========================================
-
- This component provides some PHP 7.2 features to applications using earlier
- PHP versions.
-
-Installation
-------------
-
-.. code-block:: terminal
-
- $ composer require symfony/polyfill-php72
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-Usage
------
-
-Once this component is installed in your application, you can use the following
-constants and functions, no matter if your PHP version is earlier than PHP 7.2.
-
-Provided Constants
-~~~~~~~~~~~~~~~~~~
-
-* ``PHP_OS_FAMILY`` (value = depends on your operating system)
-
-Provided Functions
-~~~~~~~~~~~~~~~~~~
-
-* :phpfunction:`sapi_windows_vt100_support` (only on Windows systems)
-* :phpfunction:`spl_object_id`
-* :phpfunction:`stream_isatty`
-* :phpfunction:`utf8_decode`
-* :phpfunction:`utf8_encode`
diff --git a/components/polyfill_php73.rst b/components/polyfill_php73.rst
deleted file mode 100644
index 5f1dac15414..00000000000
--- a/components/polyfill_php73.rst
+++ /dev/null
@@ -1,35 +0,0 @@
-.. index::
- single: Polyfill
- single: PHP
- single: Components; Polyfill
-
-The Symfony Polyfill / PHP 7.3 Component
-========================================
-
- This component provides some PHP 7.3 features to applications using earlier
- PHP versions.
-
-Installation
-------------
-
-.. code-block:: terminal
-
- $ composer require symfony/polyfill-php73
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-Usage
------
-
-Once this component is installed in your application, you can use the following
-functions, no matter if your PHP version is earlier than PHP 7.3.
-
-Provided Functions
-~~~~~~~~~~~~~~~~~~
-
-* :phpfunction:`is_countable`
-* :phpfunction:`hrtime`
-* :phpfunction:`array_key_first`
-* :phpfunction:`array_key_last`
diff --git a/components/process.rst b/components/process.rst
index 9a6f0055bdd..7552537e82e 100644
--- a/components/process.rst
+++ b/components/process.rst
@@ -1,7 +1,3 @@
-.. index::
- single: Process
- single: Components; Process
-
The Process Component
=====================
@@ -14,8 +10,6 @@ Installation
$ composer require symfony/process
-Alternatively, you can clone the ``_ repository.
-
.. include:: /components/require_autoload.rst.inc
Usage
@@ -27,8 +21,8 @@ escaping arguments to prevent security issues. It replaces PHP functions like
:phpfunction:`exec`, :phpfunction:`passthru`, :phpfunction:`shell_exec` and
:phpfunction:`system`::
- use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;
+ use Symfony\Component\Process\Process;
$process = new Process(['ls', '-lsa']);
$process->run();
@@ -52,7 +46,7 @@ the contents of the output and
the contents of the error output.
You can also use the :class:`Symfony\\Component\\Process\\Process` class with the
-foreach construct to get the output while it is generated. By default, the loop waits
+for each construct to get the output while it is generated. By default, the loop waits
for new output before going to the next iteration::
$process = new Process(['ls', '-lsa']);
@@ -97,14 +91,40 @@ with a non-zero code)::
echo $exception->getMessage();
}
+.. tip::
+
+ You can get the last output time in seconds by using the
+ :method:`Symfony\\Component\\Process\\Process::getLastOutputTime` method.
+ This method returns ``null`` if the process wasn't started!
+
+Configuring Process Options
+---------------------------
+
+Symfony uses the PHP :phpfunction:`proc_open` function to run the processes.
+You can configure the options passed to the ``other_options`` argument of
+``proc_open()`` using the ``setOptions()`` method::
+
+ $process = new Process(['...', '...', '...']);
+ // this option allows a subprocess to continue running after the main script exited
+ $process->setOptions(['create_new_console' => true]);
+
+.. 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 before completion)::
+(e.g. to stop processes while they run)::
- $process = new Process(['/path/command', '--flag', 'arg 1', 'etc.']);
+ $process = new Process(['/path/command', '--option', 'argument', 'etc.']);
+ $process = new Process(['/path/to/php', '--define', 'memory_limit=1024M', '/path/to/script.php']);
If you need to use stream redirections, conditional execution, or any other
feature provided by the shell of your operating system, you can also define
@@ -128,6 +148,17 @@ environment variables using the second argument of the ``run()``,
// On both Unix-like and Windows
$process->run(null, ['MESSAGE' => 'Something to output']);
+If you prefer to create portable commands that are independent from the
+operating system, you can write the above command as follows::
+
+ // works the same on Windows , Linux and macOS
+ $process = Process::fromShellCommandline('echo "${:MESSAGE}"');
+
+Portable commands require using a syntax that is specific to the component: when
+enclosing a variable name into ``"${:`` and ``}"`` exactly, the process object
+will replace it with its escaped value, or will fail if the variable is not
+found in the list of environment variables attached to the command.
+
Setting Environment Variables for Processes
-------------------------------------------
@@ -152,7 +183,7 @@ env vars you want to remove::
Getting real-time Process Output
--------------------------------
-When executing a long running command (like rsync-ing files to a remote
+When executing a long running command (like ``rsync`` to a remote
server), you can give feedback to the end user in real-time by passing an
anonymous function to the
:method:`Symfony\\Component\\Process\\Process::run` method::
@@ -160,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 {
@@ -168,6 +199,12 @@ anonymous function to the
}
});
+.. note::
+
+ This feature won't work as expected in servers using PHP output buffering.
+ In those cases, either disable the `output_buffering`_ PHP option or use the
+ :phpfunction:`ob_flush` PHP function to force sending the output buffer.
+
Running Processes Asynchronously
--------------------------------
@@ -217,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
@@ -232,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 {
@@ -251,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...';
});
@@ -263,7 +300,7 @@ Streaming to the Standard Input of a Process
Before a process is started, you can specify its standard input using either the
:method:`Symfony\\Component\\Process\\Process::setInput` method or the 4th argument
of the constructor. The provided input can be a string, a stream resource or a
-Traversable object::
+``Traversable`` object::
$process = new Process(['cat']);
$process->setInput('foobar');
@@ -293,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.
@@ -320,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
------------------
@@ -327,7 +393,7 @@ Any asynchronous process can be stopped at any time with the
:method:`Symfony\\Component\\Process\\Process::stop` method. This method takes
two arguments: a timeout and a signal. Once the timeout is reached, the signal
is sent to the running process. The default signal sent to a process is ``SIGKILL``.
-Please read the :ref:`signal documentation below`
+Please read the :ref:`signal documentation below `
to find out more about signal handling in the Process component::
$process = new Process(['ls', '-lsa']);
@@ -351,6 +417,36 @@ instead::
);
$process->run();
+Executing a PHP Child Process with the Same Configuration
+---------------------------------------------------------
+
+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``.
+
+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::
+
+ use Symfony\Component\Process\Process;
+
+ class MyCommand extends Command
+ {
+ protected function execute(InputInterface $input, OutputInterface $output): 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']);
+ }
+ }
+
Process Timeout
---------------
@@ -364,7 +460,7 @@ a different timeout (in seconds) to the ``setTimeout()`` method::
$process->run();
If the timeout is reached, a
-:class:`Symfony\\Component\\Process\\Exception\\RuntimeException` is thrown.
+:class:`Symfony\\Component\\Process\\Exception\\ProcessTimedOutException` is thrown.
For long running commands, it is your responsibility to perform the timeout
check regularly::
@@ -381,6 +477,10 @@ check regularly::
usleep(200000);
}
+.. tip::
+
+ You can get the process start time using the ``getStartTime()`` method.
+
.. _reference-process-signal:
Process Idle Timeout
@@ -413,6 +513,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
-----------
@@ -440,7 +554,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.
@@ -451,10 +565,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::
@@ -475,11 +610,8 @@ whether `TTY`_ is supported on the current operating system::
$process = (new Process())->setTty(Process::isTtySupported());
-.. _`Symfony Issue#5759`: https://github.com/symfony/symfony/issues/5759
-.. _`PHP Bug#39992`: https://bugs.php.net/bug.php?id=39992
-.. _`exec`: https://en.wikipedia.org/wiki/Exec_(operating_system)
.. _`pid`: https://en.wikipedia.org/wiki/Process_identifier
-.. _`PHP Documentation`: https://php.net/manual/en/pcntl.constants.php
-.. _Packagist: https://packagist.org/packages/symfony/process
-.. _`PHP streams`: http://www.php.net/manual/en/book.stream.php
+.. _`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 2e23043e562..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
@@ -15,15 +11,13 @@ Installation
$ composer require symfony/property-access
-Alternatively, you can clone the ``_ repository.
-
.. include:: /components/require_autoload.rst.inc
Usage
-----
The entry point of this component is the
-:method:`PropertyAccess::createPropertyAccessor`
+:method:`Symfony\\Component\\PropertyAccess\\PropertyAccess::createPropertyAccessor`
factory. This factory will create a new instance of the
:class:`Symfony\\Component\\PropertyAccess\\PropertyAccessor` class with the
default configuration::
@@ -32,12 +26,14 @@ default configuration::
$propertyAccessor = PropertyAccess::createPropertyAccessor();
+.. _property-access-reading-arrays:
+
Reading from Arrays
-------------------
You can read an array with the
-:method:`PropertyAccessor::getValue`
-method. This is done using the index notation that is used in PHP::
+:method:`Symfony\\Component\\PropertyAccess\\PropertyAccessor::getValue` method.
+This is done using the index notation that is used in PHP::
// ...
$person = [
@@ -65,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::
// ...
@@ -74,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
--------------------
@@ -103,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
@@ -114,16 +125,16 @@ Using Getters
~~~~~~~~~~~~~
The ``getValue()`` method also supports reading using getters. The method will
-be created using common naming conventions for getters. It camelizes the
-property name (``first_name`` becomes ``FirstName``) and prefixes it with
-``get``. So the actual method becomes ``getFirstName()``::
+be created using common naming conventions for getters. It transforms the
+property name to camelCase (``first_name`` becomes ``FirstName``) and prefixes
+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;
}
@@ -143,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);
}
@@ -160,13 +171,69 @@ getters, this means that you can do something like this::
$person = new Person();
if ($propertyAccessor->getValue($person, 'author')) {
- var_dump('He is an author');
+ var_dump('This person is an author');
}
if ($propertyAccessor->getValue($person, 'children')) {
- var_dump('He has children');
+ var_dump('This person has children');
+ }
+
+This will produce: ``This person is an author``
+
+Accessing a non Existing Property Path
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+By default a :class:`Symfony\\Component\\PropertyAccess\\Exception\\NoSuchPropertyException`
+is thrown if the property path passed to :method:`Symfony\\Component\\PropertyAccess\\PropertyAccessor::getValue`
+does not exist. You can change this behavior using the
+:method:`Symfony\\Component\\PropertyAccess\\PropertyAccessorBuilder::disableExceptionOnInvalidPropertyPath`
+method::
+
+ // ...
+ class Person
+ {
+ public string $name;
}
-This will produce: ``He is an author``
+ $person = new Person();
+
+ $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
+ ->disableExceptionOnInvalidPropertyPath()
+ ->getPropertyAccessor();
+
+ // 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:
Magic ``__get()`` Method
~~~~~~~~~~~~~~~~~~~~~~~~
@@ -176,42 +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')); // [...]
+.. warning::
+
+ 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;
@@ -228,10 +303,10 @@ 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:`PropertyAccessorBuilder::enableMagicCall`
+ :method:`Symfony\\Component\\PropertyAccess\\PropertyAccessorBuilder::enableMagicCall`
see `Enable other Features`_.
Writing to Arrays
@@ -239,8 +314,7 @@ Writing to Arrays
The ``PropertyAccessor`` class can do more than just read an array, it can
also write to an array. This can be achieved using the
-:method:`PropertyAccessor::setValue`
-method::
+:method:`Symfony\\Component\\PropertyAccess\\PropertyAccessor::setValue` method::
// ...
$person = [];
@@ -251,6 +325,8 @@ method::
// or
// var_dump($person['first_name']); // 'Wouter'
+.. _components-property-access-writing-to-objects:
+
Writing to Objects
------------------
@@ -260,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;
}
@@ -301,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;
@@ -329,6 +403,11 @@ see `Enable other Features`_::
var_dump($person->getWouter()); // [...]
+.. note::
+
+ The ``__set()`` method support is enabled by default.
+ See `Enable other Features`_ if you want to disable it.
+
Writing to Array Properties
~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -341,7 +420,7 @@ properties through *adder* and *remover* methods::
/**
* @var string[]
*/
- private $children = [];
+ private array $children = [];
public function getChildren(): array
{
@@ -367,19 +446,58 @@ properties through *adder* and *remover* methods::
The PropertyAccess component checks for methods called ``add()``
and ``remove()``. 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()`` and ``remove()``, the PropertyAccess
+component will call ``join()`` and ``leave()`` methods.
+
Checking Property Paths
-----------------------
When you want to check whether
-:method:`PropertyAccessor::getValue`
-can safely be called without actually calling that method, you can use
-:method:`PropertyAccessor::isReadable`
-instead::
+:method:`Symfony\\Component\\PropertyAccess\\PropertyAccessor::getValue` can
+safely be called without actually calling that method, you can use
+:method:`Symfony\\Component\\PropertyAccess\\PropertyAccessor::isReadable` instead::
$person = new Person();
@@ -387,9 +505,8 @@ instead::
// ...
}
-The same is possible for :method:`PropertyAccessor::setValue`:
-Call the
-:method:`PropertyAccessor::isWritable`
+The same is possible for :method:`Symfony\\Component\\PropertyAccess\\PropertyAccessor::setValue`:
+Call the :method:`Symfony\\Component\\PropertyAccess\\PropertyAccessor::isWritable`
method to find out whether a property path can be updated::
$person = new Person();
@@ -406,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;
}
@@ -441,14 +558,20 @@ configured to enable extra features. To do that you could use the
// ...
$propertyAccessorBuilder = PropertyAccess::createPropertyAccessorBuilder();
- // enables magic __call
- $propertyAccessorBuilder->enableMagicCall();
+ $propertyAccessorBuilder->enableMagicCall(); // enables magic __call
+ $propertyAccessorBuilder->enableMagicGet(); // enables magic __get
+ $propertyAccessorBuilder->enableMagicSet(); // enables magic __set
+ $propertyAccessorBuilder->enableMagicMethods(); // enables magic __get, __set and __call
- // disables magic __call
- $propertyAccessorBuilder->disableMagicCall();
+ $propertyAccessorBuilder->disableMagicCall(); // disables magic __call
+ $propertyAccessorBuilder->disableMagicGet(); // disables magic __get
+ $propertyAccessorBuilder->disableMagicSet(); // disables magic __set
+ $propertyAccessorBuilder->disableMagicMethods(); // disables magic __get, __set and __call
- // checks if magic __call handling is enabled
+ // checks if magic __call, __get or __set handling are enabled
$propertyAccessorBuilder->isMagicCallEnabled(); // true or false
+ $propertyAccessorBuilder->isMagicGetEnabled(); // true or false
+ $propertyAccessorBuilder->isMagicSetEnabled(); // true or false
// At the end get the configured property accessor
$propertyAccessor = $propertyAccessorBuilder->getPropertyAccessor();
@@ -460,8 +583,7 @@ configured to enable extra features. To do that you could use the
Or you can pass parameters directly to the constructor (not the recommended way)::
- // ...
- $propertyAccessor = new PropertyAccessor(true); // this enables handling of magic __call
+ // enable handling of magic __call, __set but not __get:
+ $propertyAccessor = new PropertyAccessor(PropertyAccessor::MAGIC_CALL | PropertyAccessor::MAGIC_SET);
-.. _Packagist: https://packagist.org/packages/symfony/property-access
-.. _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 8fac45ae6a0..39019657ced 100644
--- a/components/property_info.rst
+++ b/components/property_info.rst
@@ -1,7 +1,3 @@
-.. index::
- single: PropertyInfo
- single: Components; PropertyInfo
-
The PropertyInfo Component
==========================
@@ -23,8 +19,6 @@ Installation
$ composer require symfony/property-info
-Alternatively, you can clone the ``_ repository.
-
.. include:: /components/require_autoload.rst.inc
Additional dependencies may be required for some of the
@@ -39,10 +33,10 @@ To use this component, create a new
:class:`Symfony\\Component\\PropertyInfo\\PropertyInfoExtractor` instance and
provide it with a set of information extractors::
- use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
+ use Example\Namespace\YourAwesomeCoolClass;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
- use Example\Namespace\YourAwesomeCoolClass;
+ use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
// a full list of extractors is shown further below
$phpDocExtractor = new PhpDocExtractor();
@@ -92,9 +86,7 @@ both provide list and type information it is probably better that:
just mapped properties) are returned.
* The :class:`Symfony\\Bridge\\Doctrine\\PropertyInfo\\DoctrineExtractor`
has priority for type information so that entity metadata is used instead
- of type-hinting to provide more accurate type information.
-
-.. code-block:: php
+ of type-hinting to provide more accurate type information::
use Symfony\Bridge\Doctrine\PropertyInfo\DoctrineExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
@@ -124,11 +116,12 @@ Extractable Information
The :class:`Symfony\\Component\\PropertyInfo\\PropertyInfoExtractor`
class exposes public methods to extract several types of information:
-* :ref:`List of properties `: :method:`Symfony\\Component\\PropertyInfo\\PropertyListExtractorInterface::getProperties()`
-* :ref:`Property type `: :method:`Symfony\\Component\\PropertyInfo\\PropertyTypeExtractorInterface::getTypes()`
-* :ref:`Property description `: :method:`Symfony\\Component\\PropertyInfo\\PropertyDescriptionExtractorInterface::getShortDescription()` and :method:`Symfony\\Component\\PropertyInfo\\PropertyDescriptionExtractorInterface::getLongDescription()`
-* :ref:`Property access details `: :method:`Symfony\\Component\\PropertyInfo\\PropertyAccessExtractorInterface::isReadable()` and :method:`Symfony\\Component\\PropertyInfo\\PropertyAccessExtractorInterface::isWritable()`
-* :ref:`Property initializable through the constructor `: :method:`Symfony\\Component\\PropertyInfo\\PropertyInitializableExtractorInterface::isInitializable()`
+* :ref:`List of properties `: :method:`Symfony\\Component\\PropertyInfo\\PropertyListExtractorInterface::getProperties`
+* :ref:`Property type `: :method:`Symfony\\Component\\PropertyInfo\\PropertyTypeExtractorInterface::getTypes`
+ (including typed properties)
+* :ref:`Property description `: :method:`Symfony\\Component\\PropertyInfo\\PropertyDescriptionExtractorInterface::getShortDescription` and :method:`Symfony\\Component\\PropertyInfo\\PropertyDescriptionExtractorInterface::getLongDescription`
+* :ref:`Property access details `: :method:`Symfony\\Component\\PropertyInfo\\PropertyAccessExtractorInterface::isReadable` and :method:`Symfony\\Component\\PropertyInfo\\PropertyAccessExtractorInterface::isWritable`
+* :ref:`Property initializable through the constructor `: :method:`Symfony\\Component\\PropertyInfo\\PropertyInitializableExtractorInterface::isInitializable`
.. note::
@@ -153,13 +146,13 @@ containing each property name as a string::
$properties = $propertyInfo->getProperties($class);
/*
- Example Result
- --------------
- array(3) {
- [0] => string(8) "username"
- [1] => string(8) "password"
- [2] => string(6) "active"
- }
+ Example Result
+ --------------
+ array(3) {
+ [0] => string(8) "username"
+ [1] => string(8) "password"
+ [2] => string(6) "active"
+ }
*/
.. _property-info-type:
@@ -172,25 +165,44 @@ provide :ref:`extensive data type information `
for a property::
$types = $propertyInfo->getTypes($class, $property);
-
/*
- Example Result
- --------------
- array(1) {
- [0] =>
- class Symfony\Component\PropertyInfo\Type (6) {
- private $builtinType => string(6) "string"
- private $nullable => bool(false)
- private $class => NULL
- private $collection => bool(false)
- private $collectionKeyType => NULL
- private $collectionValueType => NULL
+ Example Result
+ --------------
+ array(1) {
+ [0] =>
+ class Symfony\Component\PropertyInfo\Type (6) {
+ private $builtinType => string(6) "string"
+ private $nullable => bool(false)
+ private $class => NULL
+ private $collection => bool(false)
+ private $collectionKeyType => NULL
+ private $collectionValueType => NULL
+ }
}
- }
*/
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
@@ -202,18 +214,18 @@ strings::
$title = $propertyInfo->getShortDescription($class, $property);
/*
- Example Result
- --------------
- string(41) "This is the first line of the DocComment."
+ Example Result
+ --------------
+ string(41) "This is the first line of the DocComment."
*/
$paragraph = $propertyInfo->getLongDescription($class, $property);
/*
- Example Result
- --------------
- string(79):
- These is the subsequent paragraph in the DocComment.
- It can span multiple lines.
+ Example Result
+ --------------
+ string(79):
+ This is the subsequent paragraph in the DocComment.
+ It can span multiple lines.
*/
.. _property-info-access:
@@ -233,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 `
-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:
@@ -286,8 +300,8 @@ Each object will provide 6 attributes, available in the 6 methods:
.. _`components-property-info-type-builtin`:
-Type::getBuiltInType()
-~~~~~~~~~~~~~~~~~~~~~~
+``Type::getBuiltInType()``
+~~~~~~~~~~~~~~~~~~~~~~~~~~
The :method:`Type::getBuiltinType() `
method returns the built-in PHP data type, which can be one of these
@@ -297,22 +311,22 @@ string values: ``array``, ``bool``, ``callable``, ``float``, ``int``,
Constants inside the :class:`Symfony\\Component\\PropertyInfo\\Type`
class, in the form ``Type::BUILTIN_TYPE_*``, are provided for convenience.
-Type::isNullable()
-~~~~~~~~~~~~~~~~~~
+``Type::isNullable()``
+~~~~~~~~~~~~~~~~~~~~~~
The :method:`Type::isNullable() `
method will return a boolean value indicating whether the property parameter
can be set to ``null``.
-Type::getClassName()
-~~~~~~~~~~~~~~~~~~~~
+``Type::getClassName()``
+~~~~~~~~~~~~~~~~~~~~~~~~
If the :ref:`built-in PHP data type `
is ``object``, the :method:`Type::getClassName() `
method will return the fully-qualified class or interface name accepted.
-Type::isCollection()
-~~~~~~~~~~~~~~~~~~~~
+``Type::isCollection()``
+~~~~~~~~~~~~~~~~~~~~~~~~
The :method:`Type::isCollection() `
method will return a boolean value indicating if the property parameter is
@@ -320,19 +334,27 @@ a collection - a non-scalar value capable of containing other values. Currently
this returns ``true`` if:
* The :ref:`built-in PHP data type `
- is ``array``, or
+ is ``array``;
* The mutator method the property is derived from has a prefix of ``add``
- or ``remove`` (which are defined as the list of array mutator prefixes).
+ or ``remove`` (which are defined as the list of array mutator prefixes);
+* The `phpDocumentor`_ annotation is of type "collection" (e.g.
+ ``@var SomeClass``, ``@var SomeClass``,
+ ``@var Doctrine\Common\Collections\Collection``, 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() `
-and :method:`Type::getCollectionValueType() `
+available), via the :method:`Type::getCollectionKeyTypes() `
+and :method:`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
@@ -356,22 +378,9 @@ ReflectionExtractor
Using PHP reflection, the :class:`Symfony\\Component\\PropertyInfo\\Extractor\\ReflectionExtractor`
provides list, type and access information from setter and accessor methods.
-It can also give the type of a property, and if it is initializable through the
-constructor. It supports return and scalar types for PHP 7.
-
-.. note::
-
- When using the Symfony framework, this service is automatically registered
- when the ``property_info`` feature is enabled:
-
- .. code-block:: yaml
-
- # config/packages/framework.yaml
- framework:
- property_info:
- enabled: true
-
-.. code-block:: php
+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::
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
@@ -390,6 +399,18 @@ constructor. It supports return and scalar types for PHP 7.
// Initializable information
$reflectionExtractor->isInitializable($class, $property);
+.. note::
+
+ When using the Symfony framework, this service is automatically registered
+ when the ``property_info`` feature is enabled:
+
+ .. code-block:: yaml
+
+ # config/packages/framework.yaml
+ framework:
+ property_info:
+ enabled: true
+
PhpDocExtractor
~~~~~~~~~~~~~~~
@@ -412,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
~~~~~~~~~~~~~~~~~~~
@@ -420,24 +489,25 @@ SerializerExtractor
This extractor depends on the `symfony/serializer`_ library.
-Using :ref:`groups metadata `
-from the :doc:`Serializer component `,
-the :class:`Symfony\\Component\\PropertyInfo\\Extractor\\SerializerExtractor`
+Using :ref:`groups metadata ` from the
+:doc:`Serializer component `, the
+:class:`Symfony\\Component\\PropertyInfo\\Extractor\\SerializerExtractor`
provides list information. This extractor is *not* registered automatically
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
~~~~~~~~~~~~~~~~~
@@ -468,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
@@ -475,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,11 +588,20 @@ service by defining it as a service with one or more of the following
* ``property_info.type_extractor`` if it provides type information.
* ``property_info.description_extractor`` if it provides description information.
* ``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.
-.. _Packagist: https://packagist.org/packages/symfony/property-info
+.. _`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
-.. _`Doctrine ORM`: http://www.doctrine-project.org/projects/orm.html
+.. _`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
.. _`doctrine/orm`: https://packagist.org/packages/doctrine/orm
+.. _`phpDocumentor`: https://www.phpdoc.org/
diff --git a/components/psr7.rst b/components/psr7.rst
index 61b2d1b35e3..04a3b9148b5 100644
--- a/components/psr7.rst
+++ b/components/psr7.rst
@@ -1,6 +1,3 @@
-.. index::
- single: PSR-7
-
The PSR-7 Bridge
================
@@ -15,14 +12,16 @@ Installation
$ composer require symfony/psr-http-message-bridge
-Alternatively, you can clone the ``_ repository.
-
.. include:: /components/require_autoload.rst.inc
-The bridge also needs a PSR-7 implementation to allow converting HttpFoundation
-objects to PSR-7 objects. It provides native support for `Zend Diactoros`_.
-Use Composer (`zendframework/zend-diactoros on Packagist `_)
-or refer to the project documentation to install it.
+The bridge also needs a PSR-7 and `PSR-17`_ implementation to convert
+HttpFoundation objects to PSR-7 objects. The following command installs the
+``nyholm/psr7`` library, a lightweight and fast PSR-7 implementation, but you
+can use any of the `libraries that implement psr/http-factory-implementation`_:
+
+.. code-block:: terminal
+
+ $ composer require nyholm/psr7
Usage
-----
@@ -31,41 +30,44 @@ 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.
-It also provide a default implementation using Zend Diactoros internally.
+`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 ``Zend\Diactoros\ServerRequest`` class implementing the
+to a ``Nyholm\Psr7\ServerRequest`` class implementing the
``Psr\Http\Message\ServerRequestInterface`` interface::
- use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
+ use Nyholm\Psr7\Factory\Psr17Factory;
+ use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Component\HttpFoundation\Request;
$symfonyRequest = new Request([], [], [], [], [], ['HTTP_HOST' => 'dunglas.fr'], 'Content');
// The HTTP_HOST server key must be set to avoid an unexpected error
- $psr7Factory = new DiactorosFactory();
- $psrRequest = $psr7Factory->createRequest($symfonyRequest);
+ $psr17Factory = new Psr17Factory();
+ $psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
+ $psrRequest = $psrHttpFactory->createRequest($symfonyRequest);
And now from a :class:`Symfony\\Component\\HttpFoundation\\Response` to a
-``Zend\Diactoros\Response`` class implementing the
+``Nyholm\Psr7\Response`` class implementing the
``Psr\Http\Message\ResponseInterface`` interface::
- use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
+ use Nyholm\Psr7\Factory\Psr17Factory;
+ use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Component\HttpFoundation\Response;
$symfonyResponse = new Response('Content');
- $psr7Factory = new DiactorosFactory();
- $psrResponse = $psr7Factory->createResponse($symfonyResponse);
+ $psr17Factory = new Psr17Factory();
+ $psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
+ $psrResponse = $psrHttpFactory->createResponse($symfonyResponse);
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
@@ -89,5 +91,7 @@ to a :class:`Symfony\\Component\\HttpFoundation\\Response` instance::
$symfonyResponse = $httpFoundationFactory->createResponse($psrResponse);
.. _`PSR-7`: https://www.php-fig.org/psr/psr-7/
-.. _`Zend Diactoros`: https://github.com/zendframework/zend-diactoros
-.. _`symfony/psr-http-message-bridge on Packagist`: https://packagist.org/packages/symfony/psr-http-message-bridge
+.. _`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/routing.rst b/components/routing.rst
deleted file mode 100644
index e326494261c..00000000000
--- a/components/routing.rst
+++ /dev/null
@@ -1,542 +0,0 @@
-.. index::
- single: Routing
- single: Components; Routing
-
-The Routing Component
-=====================
-
- The Routing component maps an HTTP request to a set of configuration
- variables.
-
-Installation
-------------
-
-.. code-block:: terminal
-
- $ composer require symfony/routing
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-Usage
------
-
-.. seealso::
-
- This article explains how to use the Routing features as an independent
- component in any PHP application. Read the :doc:`/routing` article to learn
- about how to use it in Symfony applications.
-
-In order to set up a basic routing system you need three parts:
-
-* A :class:`Symfony\\Component\\Routing\\RouteCollection`, which contains the route definitions (instances of the class :class:`Symfony\\Component\\Routing\\Route`)
-* A :class:`Symfony\\Component\\Routing\\RequestContext`, which has information about the request
-* A :class:`Symfony\\Component\\Routing\\Matcher\\UrlMatcher`, which performs the mapping of the request to a single route
-
-Here is a quick example. Notice that this assumes that you've already configured
-your autoloader to load the Routing component::
-
- use Symfony\Component\Routing\Matcher\UrlMatcher;
- use Symfony\Component\Routing\RequestContext;
- use Symfony\Component\Routing\RouteCollection;
- use Symfony\Component\Routing\Route;
-
- $route = new Route('/foo', ['_controller' => 'MyController']);
- $routes = new RouteCollection();
- $routes->add('route_name', $route);
-
- $context = new RequestContext('/');
-
- $matcher = new UrlMatcher($routes, $context);
-
- $parameters = $matcher->match('/foo');
- // ['_controller' => 'MyController', '_route' => 'route_name']
-
-.. note::
-
- The :class:`Symfony\\Component\\Routing\\RequestContext` parameters can be populated
- with the values stored in ``$_SERVER``, but it's easier to use the HttpFoundation
- component as explained :ref:`below `.
-
-You can add as many routes as you like to a
-:class:`Symfony\\Component\\Routing\\RouteCollection`.
-
-The :method:`RouteCollection::add() `
-method takes two arguments. The first is the name of the route. The second
-is a :class:`Symfony\\Component\\Routing\\Route` object, which expects a
-URL path and some array of custom variables in its constructor. This array
-of custom variables can be *anything* that's significant to your application,
-and is returned when that route is matched.
-
-The :method:`UrlMatcher::match() `
-returns the variables you set on the route as well as the wildcard placeholders
-(see below). Your application can now use this information to continue
-processing the request. In addition to the configured variables, a ``_route``
-key is added, which holds the name of the matched route.
-
-If no matching route can be found, a
-:class:`Symfony\\Component\\Routing\\Exception\\ResourceNotFoundException` will
-be thrown.
-
-Defining Routes
-~~~~~~~~~~~~~~~
-
-A full route definition can contain up to seven parts:
-
-#. The URL path route. This is matched against the URL passed to the `RequestContext`,
- and can contain named wildcard placeholders (e.g. ``{placeholders}``)
- to match dynamic parts in the URL.
-
-#. An array of default values. This contains an array of arbitrary values
- that will be returned when the request matches the route.
-
-#. An array of requirements. These define constraints for the values of the
- placeholders as regular expressions.
-
-#. An array of options. These contain internal settings for the route and
- are the least commonly needed.
-
-#. A host. This is matched against the host of the request. See
- :doc:`/routing/hostname_pattern` for more details.
-
-#. An array of schemes. These enforce a certain HTTP scheme (``http``, ``https``).
-
-#. An array of methods. These enforce a certain HTTP request method (``HEAD``,
- ``GET``, ``POST``, ...).
-
-Take the following route, which combines several of these ideas::
-
- $route = new Route(
- '/archive/{month}', // path
- ['_controller' => 'showArchive'], // default values
- ['month' => '[0-9]{4}-[0-9]{2}', 'subdomain' => 'www|m'], // requirements
- [], // options
- '{subdomain}.example.com', // host
- [], // schemes
- [] // methods
- );
-
- // ...
-
- $parameters = $matcher->match('/archive/2012-01');
- // [
- // '_controller' => 'showArchive',
- // 'month' => '2012-01',
- // 'subdomain' => 'www',
- // '_route' => ...
- // ]
-
- $parameters = $matcher->match('/archive/foo');
- // throws ResourceNotFoundException
-
-In this case, the route is matched by ``/archive/2012-01``, because the ``{month}``
-wildcard matches the regular expression wildcard given. However, ``/archive/foo``
-does *not* match, because "foo" fails the month wildcard.
-
-When using wildcards, these are returned in the array result when calling
-``match``. The part of the path that the wildcard matched (e.g. ``2012-01``) is used
-as value.
-
-.. tip::
-
- If you want to match all URLs which start with a certain path and end in an
- arbitrary suffix you can use the following route definition::
-
- $route = new Route(
- '/start/{suffix}',
- ['suffix' => ''],
- ['suffix' => '.*']
- );
-
-Using Prefixes
-~~~~~~~~~~~~~~
-
-You can add routes or other instances of
-:class:`Symfony\\Component\\Routing\\RouteCollection` to *another* collection.
-This way you can build a tree of routes. Additionally you can define a prefix
-and default values for the parameters, requirements, options, schemes and the
-host to all routes of a subtree using methods provided by the
-``RouteCollection`` class::
-
- $rootCollection = new RouteCollection();
-
- $subCollection = new RouteCollection();
- $subCollection->add(...);
- $subCollection->add(...);
- $subCollection->addPrefix('/prefix');
- $subCollection->addDefaults([...]);
- $subCollection->addRequirements([]);
- $subCollection->addOptions([]);
- $subCollection->setHost('admin.example.com');
- $subCollection->setMethods(['POST']);
- $subCollection->setSchemes(['https']);
-
- $rootCollection->addCollection($subCollection);
-
-Set the Request Parameters
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The :class:`Symfony\\Component\\Routing\\RequestContext` provides information
-about the current request. You can define all parameters of an HTTP request
-with this class via its constructor::
-
- public function __construct(
- $baseUrl = '',
- $method = 'GET',
- $host = 'localhost',
- $scheme = 'http',
- $httpPort = 80,
- $httpsPort = 443,
- $path = '/',
- $queryString = ''
- )
-
-.. _components-routing-http-foundation:
-
-Normally you can pass the values from the ``$_SERVER`` variable to populate the
-:class:`Symfony\\Component\\Routing\\RequestContext`. But if you use the
-:doc:`HttpFoundation ` component, you can use its
-:class:`Symfony\\Component\\HttpFoundation\\Request` class to feed the
-:class:`Symfony\\Component\\Routing\\RequestContext` in a shortcut::
-
- use Symfony\Component\HttpFoundation\Request;
-
- $context = new RequestContext();
- $context->fromRequest(Request::createFromGlobals());
-
-Generate a URL
-~~~~~~~~~~~~~~
-
-While the :class:`Symfony\\Component\\Routing\\Matcher\\UrlMatcher` tries
-to find a route that fits the given request you can also build a URL from
-a certain route::
-
- use Symfony\Component\Routing\Generator\UrlGenerator;
- use Symfony\Component\Routing\RequestContext;
- use Symfony\Component\Routing\Route;
- use Symfony\Component\Routing\RouteCollection;
-
- $routes = new RouteCollection();
- $routes->add('show_post', new Route('/show/{slug}'));
-
- $context = new RequestContext('/');
-
- $generator = new UrlGenerator($routes, $context);
-
- $url = $generator->generate('show_post', [
- 'slug' => 'my-blog-post',
- ]);
- // /show/my-blog-post
-
-.. note::
-
- If you have defined a scheme, an absolute URL is generated if the scheme
- of the current :class:`Symfony\\Component\\Routing\\RequestContext` does
- not match the requirement.
-
-Check if a Route Exists
-~~~~~~~~~~~~~~~~~~~~~~~
-
-In highly dynamic applications, it may be necessary to check whether a route
-exists before using it to generate a URL. In those cases, don't use the
-:method:`Symfony\\Component\\Routing\\Router::getRouteCollection` method because
-that regenerates the routing cache and slows down the application.
-
-Instead, try to generate the URL and catch the
-:class:`Symfony\\Component\\Routing\\Exception\\RouteNotFoundException` thrown
-when the route doesn't exist::
-
- use Symfony\Component\Routing\Exception\RouteNotFoundException;
-
- // ...
-
- try {
- $url = $generator->generate($dynamicRouteName, $parameters);
- } catch (RouteNotFoundException $e) {
- // the route is not defined...
- }
-
-Load Routes from a File
-~~~~~~~~~~~~~~~~~~~~~~~
-
-You've already seen how you can add routes to a collection right inside
-PHP. But you can also load routes from a number of different files.
-
-The Routing component comes with a number of loader classes, each giving
-you the ability to load a collection of route definitions from an external
-file of some format.
-Each loader expects a :class:`Symfony\\Component\\Config\\FileLocator` instance
-as the constructor argument. You can use the :class:`Symfony\\Component\\Config\\FileLocator`
-to define an array of paths in which the loader will look for the requested files.
-If the file is found, the loader returns a :class:`Symfony\\Component\\Routing\\RouteCollection`.
-
-If you're using the ``YamlFileLoader``, then route definitions look like this:
-
-.. code-block:: yaml
-
- # routes.yaml
- route1:
- path: /foo
- controller: MyController::fooAction
- methods: GET|HEAD
-
- route2:
- path: /foo/bar
- controller: FooBarInvokableController
- methods: PUT
-
-To load this file, you can use the following code. This assumes that your
-``routes.yaml`` file is in the same directory as the below code::
-
- use Symfony\Component\Config\FileLocator;
- use Symfony\Component\Routing\Loader\YamlFileLoader;
-
- // looks inside *this* directory
- $fileLocator = new FileLocator([__DIR__]);
- $loader = new YamlFileLoader($fileLocator);
- $routes = $loader->load('routes.yaml');
-
-Besides :class:`Symfony\\Component\\Routing\\Loader\\YamlFileLoader` there are two
-other loaders that work the same way:
-
-* :class:`Symfony\\Component\\Routing\\Loader\\XmlFileLoader`
-* :class:`Symfony\\Component\\Routing\\Loader\\PhpFileLoader`
-
-If you use the :class:`Symfony\\Component\\Routing\\Loader\\PhpFileLoader` you
-have to provide the name of a PHP file which returns a callable handling a :class:`Symfony\\Component\\Routing\\Loader\\Configurator\\RoutingConfigurator`.
-This class allows to chain imports, collections or simple route definition calls::
-
- // RouteProvider.php
- namespace Symfony\Component\Routing\Loader\Configurator;
-
- return function (RoutingConfigurator $routes) {
- $routes->add('route_name', '/foo')
- ->controller('ExampleController')
- // ...
- ;
- };
-
-Routes as Closures
-..................
-
-There is also the :class:`Symfony\\Component\\Routing\\Loader\\ClosureLoader`, which
-calls a closure and uses the result as a :class:`Symfony\\Component\\Routing\\RouteCollection`::
-
- use Symfony\Component\Routing\Loader\ClosureLoader;
-
- $closure = function () {
- return new RouteCollection();
- };
-
- $loader = new ClosureLoader();
- $routes = $loader->load($closure);
-
-Routes as Annotations
-.....................
-
-Last but not least there are
-:class:`Symfony\\Component\\Routing\\Loader\\AnnotationDirectoryLoader` and
-:class:`Symfony\\Component\\Routing\\Loader\\AnnotationFileLoader` to load
-route definitions from class annotations. The specific details are left
-out here.
-
-.. include:: /_includes/_annotation_loader_tip.rst.inc
-
-The all-in-one Router
-~~~~~~~~~~~~~~~~~~~~~
-
-The :class:`Symfony\\Component\\Routing\\Router` class is an all-in-one package
-to use the Routing component. The constructor expects a loader instance,
-a path to the main route definition and some other settings::
-
- public function __construct(
- LoaderInterface $loader,
- $resource,
- array $options = [],
- RequestContext $context = null,
- LoggerInterface $logger = null
- );
-
-With the ``cache_dir`` option you can enable route caching (if you provide a
-path) or disable caching (if it's set to ``null``). The caching is done
-automatically in the background if you want to use it. A basic example of the
-:class:`Symfony\\Component\\Routing\\Router` class would look like::
-
- $fileLocator = new FileLocator([__DIR__]);
- $requestContext = new RequestContext('/');
-
- $router = new Router(
- new YamlFileLoader($fileLocator),
- 'routes.yaml',
- ['cache_dir' => __DIR__.'/cache'],
- $requestContext
- );
- $router->match('/foo/bar');
-
-.. note::
-
- If you use caching, the Routing component will compile new classes which
- are saved in the ``cache_dir``. This means your script must have write
- permissions for that location.
-
-Unicode Routing Support
-~~~~~~~~~~~~~~~~~~~~~~~
-
-The Routing component supports UTF-8 characters in route paths and requirements.
-Thanks to the ``utf8`` route option, you can make Symfony match and generate
-routes with UTF-8 characters:
-
-.. configuration-block::
-
- .. code-block:: php-annotations
-
- namespace App\Controller;
-
- use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
- use Symfony\Component\Routing\Annotation\Route;
-
- class DefaultController extends AbstractController
- {
- /**
- * @Route("/category/{name}", name="route1", options={"utf8": true})
- */
- public function category()
- {
- // ...
- }
-
- .. code-block:: yaml
-
- route1:
- path: /category/{name}
- controller: App\Controller\DefaultController::category
- options:
- utf8: true
-
- .. code-block:: xml
-
-
-
-
-
-
-
-
-
- .. code-block:: php
-
- // config/routes.php
- namespace Symfony\Component\Routing\Loader\Configurator;
-
- use App\Controller\DefaultController;
-
- return function (RoutingConfigurator $routes) {
- $routes->add('route1', '/category/{name}')
- ->controller([DefaultController::class, 'category'])
- ->options([
- 'utf8' => true,
- ])
- ;
- };
-
-In this route, the ``utf8`` option set to ``true`` makes Symfony consider the
-``.`` requirement to match any UTF-8 characters instead of just a single
-byte character. This means that so the following URLs would match:
-``/category/日本語``, ``/category/فارسی``, ``/category/한국어``, etc. In case you
-are wondering, this option also allows to include and match emojis in URLs.
-
-You can also include UTF-8 strings as routing requirements:
-
-.. configuration-block::
-
- .. code-block:: php-annotations
-
- namespace App\Controller;
-
- use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
- use Symfony\Component\Routing\Annotation\Route;
-
- class DefaultController extends AbstractController
- {
- /**
- * @Route(
- * "/category/{name}",
- * name="route2",
- * defaults={"name"="한국어"},
- * options={"utf8": true}
- * )
- */
- public function category()
- {
- // ...
- }
-
- .. code-block:: yaml
-
- route2:
- path: /category/{name}
- controller: 'App\Controller\DefaultController::category'
- defaults:
- name: "한국어"
- options:
- utf8: true
-
- .. code-block:: xml
-
-
-
-
-
- 한국어
-
-
-
-
- .. code-block:: php
-
- // config/routes.php
- namespace Symfony\Component\Routing\Loader\Configurator;
-
- use App\Controller\DefaultController;
-
- return function (RoutingConfigurator $routes) {
- $routes->add('route2', '/category/{name}')
- ->controller([DefaultController::class, 'category'])
- ->defaults([
- 'name' => '한국어',
- ])
- ->options([
- 'utf8' => true,
- ])
- ;
- };
-
-.. tip::
-
- In addition to UTF-8 characters, the Routing component also supports all
- the `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,
- ``\P{Han}`` matches any character not included in the Chinese Han script.
-
-Learn more
-----------
-
-.. toctree::
- :maxdepth: 1
- :glob:
-
- /routing
- /routing/*
- /controller
- /controller/*
-
-.. _Packagist: https://packagist.org/packages/symfony/routing
-.. _PCRE Unicode properties: http://php.net/manual/en/regexp.reference.unicode.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
+ 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 ` of Symfony applications (e.g. to
+ display errors)
+``runtimes``
+ Maps "application types" to a ``GenericRuntime`` implementation that
+ knows how to deal with each of them.
+``error_handler`` (default: :class:`Symfony\\Component\\Runtime\\Internal\\BasicErrorHandler` or :class:`Symfony\\Component\\Runtime\\Internal\\SymfonyErrorHandler` for ``SymfonyRuntime``)
+ Defines the class to use to handle PHP errors.
+``env_var_name`` (default: ``"APP_ENV"``)
+ Defines the name of the env var that stores the name of the
+ :ref:`configuration environment `
+ to use when running the application.
+``debug_var_name`` (default: ``"APP_DEBUG"``)
+ Defines the name of the env var that stores the value of the
+ :ref:`debug mode ` flag to use when running the application.
+
+Create Your Own Runtime
+-----------------------
+
+This is an advanced topic that describes the internals of the Runtime component.
+
+Using the Runtime component will benefit maintainers because the bootstrap
+logic could be versioned as a part of a normal package. If the application
+author decides to use this component, the package maintainer of the Runtime
+class will have more control and can fix bugs and add features.
+
+The Runtime component is designed to be totally generic and able to run any
+application outside of the global state in 6 steps:
+
+#. The main entry point returns a *callable* (the "app") that wraps the application;
+#. The *app callable* is passed to ``RuntimeInterface::getResolver()``, which returns
+ a :class:`Symfony\\Component\\Runtime\\ResolverInterface`. This resolver returns
+ an array with the app callable (or something that decorates this callable) at
+ index 0 and all its resolved arguments at index 1.
+#. The *app callable* is invoked with its arguments, it will return an object that
+ represents the application.
+#. This *application object* is passed to ``RuntimeInterface::getRunner()``, which
+ returns a :class:`Symfony\\Component\\Runtime\\RunnerInterface`: an instance
+ that knows how to "run" the application object.
+#. The ``RunnerInterface::run(object $application)`` is called and it returns the
+ exit status code as ``int``.
+#. The PHP engine is terminated with this status code.
+
+When creating a new runtime, there are two things to consider: First, what arguments
+will the end user use? Second, what will the user's application look like?
+
+For instance, imagine you want to create a runtime for `ReactPHP`_:
+
+**What arguments will the end user use?**
+
+For a generic ReactPHP application, no special arguments are
+typically required. This means that you can use the
+:class:`Symfony\\Component\\Runtime\\GenericRuntime`.
+
+**What will the user's application look like?**
+
+There is also no typical React application, so you might want to rely on
+the `PSR-15`_ interfaces for HTTP request handling.
+
+However, a ReactPHP application will need some special logic to *run*. That logic
+is added in a new class implementing :class:`Symfony\\Component\\Runtime\\RunnerInterface`::
+
+ use Psr\Http\Message\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 98808c33c52..00000000000
--- a/components/security.rst
+++ /dev/null
@@ -1,59 +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
-------------
-
-.. code-block:: terminal
-
- $ composer require symfony/security
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-The Security component is divided into four 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`_.
-
-.. 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
-
-.. _Packagist: https://packagist.org/packages/symfony/security
-.. _`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 ee4e99b6529..00000000000
--- a/components/security/authentication.rst
+++ /dev/null
@@ -1,323 +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 `::
-
- use Symfony\Component\Security\Http\Firewall\ListenerInterface;
- use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
- use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
- use Symfony\Component\HttpKernel\Event\GetResponseEvent;
- use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
-
- class SomeAuthenticationListener implements ListenerInterface
- {
- /**
- * @var TokenStorageInterface
- */
- private $tokenStorage;
-
- /**
- * @var AuthenticationManagerInterface
- */
- private $authenticationManager;
-
- /**
- * @var string Uniquely identifies the secured area
- */
- private $providerKey;
-
- // ...
-
- public function handle(GetResponseEvent $event)
- {
- $request = $event->getRequest();
-
- $username = ...;
- $password = ...;
-
- $unauthenticatedToken = new UsernamePasswordToken(
- $username,
- $password,
- $this->providerKey
- );
-
- $authenticatedToken = $this
- ->authenticationManager
- ->authenticate($unauthenticatedToken);
-
- $this->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 :method:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\AuthenticationProviderInterface::supports`
-by which the ``AuthenticationProviderManager``
-can determine if it supports the given token. If this is the case, the
-manager then calls the provider's method :method:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\AuthenticationProviderInterface::authenticate`.
-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\User\UserChecker;
- use Symfony\Component\Security\Core\User\InMemoryUserProvider;
- use Symfony\Component\Security\Core\Encoder\EncoderFactory;
-
- $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`;
-
-#. 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 4 related authentication events:
-
-=============================== ================================================ ==============================================================================
-Name Event Constant Argument Passed to the Listener
-=============================== ================================================ ==============================================================================
-security.authentication.success ``AuthenticationEvents::AUTHENTICATION_SUCCESS`` :class:`Symfony\\Component\\Security\\Core\\Event\\AuthenticationEvent`
-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`
-=============================== ================================================ ==============================================================================
-
-Authentication Success and Failure Events
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-When a provider authenticates the user, a ``security.authentication.success``
-event is dispatched. But beware - this event will fire, for example, on *every*
-request if you have session-based authentication. 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.
-
-.. 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
-.. _`BasePasswordEncoder::checkPasswordLength`: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Core/Encoder/BasePasswordEncoder.php
diff --git a/components/security/authorization.rst b/components/security/authorization.rst
deleted file mode 100644
index 83860b25060..00000000000
--- a/components/security/authorization.rst
+++ /dev/null
@@ -1,228 +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;
-
-.. code-block:: php
-
- use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
-
- // instances of Symfony\Component\Security\Core\Authorization\Voter\VoterInterface
- $voters = [...];
-
- // one of "affirmative", "consensus", "unanimous"
- $strategy = ...;
-
- // whether or not to grant access when all voters abstain
- $allowIfAllAbstainDecisions = ...;
-
- // whether or not to grant access when there is no majority (applies only to the "consensus" strategy)
- $allowIfEqualGrantedDeniedDecisions = ...;
-
- $accessDecisionManager = new AccessDecisionManager(
- $voters,
- $strategy,
- $allowIfAllAbstainDecisions,
- $allowIfEqualGrantedDeniedDecisions
- );
-
-.. seealso::
-
- You can change the default strategy in the
- :ref:`configuration `.
-
-Voters
-------
-
-Voters are instances
-of :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface`,
-which means they have to implement a few methods which allows the decision
-manager to use them:
-
-``vote(TokenInterface $token, $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``,
-and ``IS_AUTHENTICATED_ANONYMOUSLY`` and grants access based on the current
-level of authentication, i.e. is the user fully authenticated, or only based
-on a "remember-me" cookie, or even authenticated anonymously?
-
-.. code-block:: php
-
- use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver;
- use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
- use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken;
-
- $trustResolver = new AuthenticationTrustResolver(AnonymousToken::class, RememberMeToken::class);
-
- $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 the required ``ROLE_*`` attributes can all 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 subroles
-``ROLE_ADMIN`` and ``ROLE_USER``, so that when a certain object requires the
-user to have the ``ROLE_ADMIN`` role, it grants access to users who in fact
-have the ``ROLE_ADMIN`` role, but also to users having the ``ROLE_SUPER_ADMIN``
-role::
-
- use Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter;
- use Symfony\Component\Security\Core\Role\RoleHierarchy;
-
- $hierarchy = [
- 'ROLE_SUPER_ADMIN' => ['ROLE_ADMIN', 'ROLE_USER'],
- ];
-
- $roleHierarchy = new RoleHierarchy($hierarchy);
-
- $roleHierarchyVoter = new RoleHierarchyVoter($roleHierarchy);
-
-.. 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\Security\Http\AccessMap;
- use Symfony\Component\HttpFoundation\RequestMatcher;
- use Symfony\Component\Security\Http\Firewall\AccessListener;
-
- $accessMap = new AccessMap();
- $requestMatcher = new RequestMatcher('^/admin');
- $accessMap->add($requestMatcher, ['ROLE_ADMIN']);
-
- $accessListener = new AccessListener(
- $securityContext,
- $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 eca13908727..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\Security\Http\FirewallMap;
- use Symfony\Component\HttpFoundation\RequestMatcher;
- use Symfony\Component\Security\Http\Firewall\ExceptionListener;
-
- $firewallMap = new FirewallMap();
-
- $requestMatcher = new RequestMatcher('^/secured-area/');
-
- // instances of Symfony\Component\Security\Http\Firewall\ListenerInterface
- $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\Security\Http\Firewall;
- use Symfony\Component\HttpKernel\KernelEvents;
-
- // 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\\Component\\Security\\Http\\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 implement :class:`Symfony\\Component\\Security\\Http\\Firewall\\ListenerInterface`).
-These listeners will all be asked to handle the current request. This basically
-means: find out if the current request contains any information by which
-the user might be authenticated (for instance the Basic HTTP authentication
-listener checks if the request has a header called ``PHP_AUTH_USER``).
-
-Exception Listener
-~~~~~~~~~~~~~~~~~~
-
-If any of the listeners throws an :class:`Symfony\\Component\\Security\\Core\\Exception\\AuthenticationException`,
-the exception listener that was provided when adding secured areas to the
-firewall map will jump in.
-
-The exception listener determines what happens next, based on the arguments
-it received when it was created. It may start the authentication procedure,
-perhaps ask the user to supply their credentials again (when they have only been
-authenticated based on a "remember-me" cookie), or transform the exception
-into an :class:`Symfony\\Component\\HttpKernel\\Exception\\AccessDeniedHttpException`,
-which will eventually result in an "HTTP/1.1 403: Access Denied" response.
-
-Entry Points
-~~~~~~~~~~~~
-
-When the user is not authenticated at all (i.e. when the token storage
-has no token yet), the firewall's entry point will be called to "start"
-the authentication process. An entry point should implement
-:class:`Symfony\\Component\\Security\\Http\\EntryPoint\\AuthenticationEntryPointInterface`,
-which has only one method: :method:`Symfony\\Component\\Security\\Http\\EntryPoint\\AuthenticationEntryPointInterface::start`.
-This method receives the current :class:`Symfony\\Component\\HttpFoundation\\Request`
-object and the exception by which the exception listener was triggered.
-The method should return a :class:`Symfony\\Component\\HttpFoundation\\Response`
-object. This could be, for instance, the page containing the login form or,
-in the case of Basic HTTP authentication, a response with a ``WWW-Authenticate``
-header, which will prompt the user to supply their username and password.
-
-Flow: Firewall, Authentication, Authorization
----------------------------------------------
-
-Hopefully you can now see a little bit about how the "flow" of the security
-context works:
-
-#. The Firewall is registered as a listener on the ``kernel.request`` event;
-#. At the beginning of the request, the Firewall checks the firewall map
- to see if any firewall should be active for this URL;
-#. If a firewall is found in the map for this URL, its listeners are notified;
-#. Each listener checks to see if the current request contains any authentication
- information - a listener may (a) authenticate a user, (b) throw an
- ``AuthenticationException``, or (c) do nothing (because there is no
- authentication information on the request);
-#. Once a user is authenticated, you'll use :doc:`/components/security/authorization`
- to deny access to certain resources.
-
-Read the next articles to find out more about :doc:`/components/security/authentication`
-and :doc:`/components/security/authorization`.
diff --git a/components/security/secure_tools.rst b/components/security/secure_tools.rst
deleted file mode 100644
index a9d6e0fec3a..00000000000
--- a/components/security/secure_tools.rst
+++ /dev/null
@@ -1,56 +0,0 @@
-Securely Generating Random Values
-=================================
-
-The Symfony Security component comes with a collection of nice utilities
-related to security. These utilities are used by Symfony, but you should
-also use them if you want to solve the problem they address.
-
-.. note::
-
- The functions described in this article were introduced in PHP 5.6 or 7.
- For older PHP versions, a polyfill is provided by the
- `Symfony Polyfill Component`_.
-
-Comparing Strings
-~~~~~~~~~~~~~~~~~
-
-The time it takes to compare two strings depends on their differences. This
-can be used by an attacker when the two strings represent a password for
-instance; it is known as a `Timing attack`_.
-
-When comparing two passwords, you should use the :phpfunction:`hash_equals`
-function::
-
- if (hash_equals($knownString, $userInput)) {
- // ...
- }
-
-Generating a Secure Random String
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Whenever you need to generate a secure random string, you are highly
-encouraged to use the :phpfunction:`random_bytes` function::
-
- $random = random_bytes(10);
-
-The function returns a random string, suitable for cryptographic use, of
-the number bytes passed as an argument (10 in the above example).
-
-.. tip::
-
- The ``random_bytes()`` function returns a binary string which may contain
- the ``\0`` character. This can cause trouble in several common scenarios,
- such as storing this value in a database or including it as part of the
- URL. The solution is to hash the value returned by ``random_bytes()`` with
- a hashing function such as :phpfunction:`md5` or :phpfunction:`sha1`.
-
-Generating a Secure Random Number
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-If you need to generate a cryptographically secure random integer, you should
-use the :phpfunction:`random_int` function::
-
- $random = random_int(1, 10);
-
-.. _`Timing attack`: https://en.wikipedia.org/wiki/Timing_attack
-.. _`Symfony Polyfill Component`: https://github.com/symfony/polyfill
diff --git a/components/semaphore.rst b/components/semaphore.rst
new file mode 100644
index 00000000000..5715b426053
--- /dev/null
+++ b/components/semaphore.rst
@@ -0,0 +1,73 @@
+The Semaphore Component
+=======================
+
+ The Semaphore Component manages `semaphores`_, a mechanism to provide
+ exclusive access to a shared resource.
+
+Installation
+------------
+
+.. code-block:: terminal
+
+ $ composer require symfony/semaphore
+
+.. include:: /components/require_autoload.rst.inc
+
+Usage
+-----
+
+In computer science, a semaphore is a variable or abstract data type used to
+control access to a common resource by multiple processes in a concurrent
+system such as a multitasking operating system. The main difference
+with :doc:`locks ` is that semaphores allow more than one process to
+access a resource, whereas locks only allow one process.
+
+Create semaphores with the :class:`Symfony\\Component\\Semaphore\\SemaphoreFactory`
+class, which in turn requires another class to manage the storage::
+
+ use Symfony\Component\Semaphore\SemaphoreFactory;
+ use Symfony\Component\Semaphore\Store\RedisStore;
+
+ $redis = new Redis();
+ $redis->connect('172.17.0.2');
+
+ $store = new RedisStore($redis);
+ $factory = new SemaphoreFactory($store);
+
+The semaphore is created by calling the
+:method:`Symfony\\Component\\Semaphore\\SemaphoreFactory::createSemaphore`
+method. Its first argument is an arbitrary string that represents the locked
+resource. Its second argument is the maximum number of processes allowed. Then, a
+call to the :method:`Symfony\\Component\\Semaphore\\SemaphoreInterface::acquire`
+method will try to acquire the semaphore::
+
+ // ...
+ $semaphore = $factory->createSemaphore('pdf-invoice-generation', 2);
+
+ if ($semaphore->acquire()) {
+ // The resource "pdf-invoice-generation" is locked.
+ // Here you can safely compute and generate the invoice.
+
+ $semaphore->release();
+ }
+
+If the semaphore can not be acquired, the method returns ``false``. The
+``acquire()`` method can be safely called repeatedly, even if the semaphore is
+already acquired.
+
+.. note::
+
+ Unlike other implementations, the Semaphore component distinguishes
+ semaphores instances even when they are created for the same resource. If a
+ semaphore has to be used by several services, they should share the same
+ ``Semaphore`` instance returned by the ``SemaphoreFactory::createSemaphore``
+ method.
+
+.. tip::
+
+ If you don't release the semaphore explicitly, it will be released
+ automatically on instance destruction. In some cases, it can be useful to
+ lock a resource across several requests. To disable the automatic release
+ behavior, set the fifth argument of the ``createSemaphore()`` method to ``false``.
+
+.. _`semaphores`: https://en.wikipedia.org/wiki/Semaphore_(programming)
diff --git a/components/serializer.rst b/components/serializer.rst
deleted file mode 100644
index 9f5e65306c9..00000000000
--- a/components/serializer.rst
+++ /dev/null
@@ -1,1443 +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
-
-
-
-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
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-To use the ``ObjectNormalizer``, the :doc:`PropertyAccess component `
-must also be installed.
-
-Usage
------
-
-.. seealso::
-
- This article explains how to use the Serializer features as an independent
- component in any PHP application. Read the :doc:`/serializer` article to
- learn about how to use it in Symfony applications.
-
-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\Serializer;
- use Symfony\Component\Serializer\Encoder\XmlEncoder;
- use Symfony\Component\Serializer\Encoder\JsonEncoder;
- use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
-
- $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::
-
- $person = new App\Model\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 = <<
- foo
- 99
- false
-
- 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 ``allow_extra_attributes`` context option to
-``false`` and provide an object that implements ``ClassMetadataFactoryInterface``
-when constructing the normalizer::
-
- $data = <<
- foo
- 99
- Paris
-
- EOF;
-
- // this will throw a Symfony\Component\Serializer\Exception\ExtraAttributesException
- // because "city" is not an attribute of the Person class
- $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
- $normalizer = new ObjectNormalizer($classMetadataFactory);
- $serializer = new Serializer([$normalizer]);
- $person = $serializer->deserialize($data, 'App\Model\Person', 'xml', [
- '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 = <<
- foo
- 69
-
- EOF;
-
- $serializer->deserialize($data, Person::class, 'xml', ['object_to_populate' => $person]);
- // $person = App\Model\Person(name: 'foo', age: '69', sportsperson: true)
-
-This is a common need when working with an ORM.
-
-.. _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.
-
-Initialize the :class:`Symfony\\Component\\Serializer\\Mapping\\Factory\\ClassMetadataFactory`
-like the following::
-
- use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
- // For annotations
- use Doctrine\Common\Annotations\AnnotationReader;
- use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
- // For XML
- // use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader;
- // For YAML
- // use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader;
-
- $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
- // For XML
- // $classMetadataFactory = new ClassMetadataFactory(new XmlFileLoader('/path/to/your/definition.xml'));
- // For YAML
- // $classMetadataFactory = new ClassMetadataFactory(new YamlFileLoader('/path/to/your/definition.yaml'));
-
-.. _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
-
-
-
-
-
- group1
- group2
-
-
-
- group3
-
-
-
-
-You are now able to serialize only attributes in the groups you want::
-
- use Symfony\Component\Serializer\Serializer;
- use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
-
- $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')
-
-.. include:: /_includes/_annotation_loader_tip.rst.inc
-
-.. _ignoring-attributes-when-serializing:
-
-Selecting Specific Attributes
------------------------------
-
-It is also possible to serialize only a set of specific attributes::
-
- use Symfony\Component\Serializer\Serializer;
- use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
-
- 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, ['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
--------------------
-
-.. note::
-
- Using attribute groups instead of the :method:`Symfony\\Component\\Serializer\\Normalizer\\AbstractNormalizer::setIgnoredAttributes`
- method is considered best practice.
-
-As an option, there's a way to ignore attributes from the origin object. To remove
-those attributes use the
-:method:`Symfony\\Component\\Serializer\\Normalizer\\AbstractNormalizer::setIgnoredAttributes`
-method on the normalizer definition::
-
- use Symfony\Component\Serializer\Serializer;
- use Symfony\Component\Serializer\Encoder\JsonEncoder;
- use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
-
- $normalizer = new ObjectNormalizer();
- $normalizer->setIgnoredAttributes(['age']);
- $encoder = new JsonEncoder();
-
- $serializer = new Serializer([$normalizer], [$encoder]);
- $serializer->serialize($person, 'json'); // Output: {"name":"foo","sportsperson":false}
-
-.. _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($propertyName)
- {
- return 'org_'.$propertyName;
- }
-
- public function denormalize($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 `,
-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
-
-
-
-
-
-
-
-
-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();
- $normalizer = new GetSetMethodNormalizer();
-
- // all callback parameters are optional (you can omit the ones you don't use)
- $callback = function ($innerObject, $outerObject, string $attributeName, string $format = null, array $context = []) {
- return $innerObject instanceof \DateTime ? $innerObject->format(\DateTime::ISO8601) : '';
- };
-
- $normalizer->setCallbacks(['createdAt' => $callback]);
-
- $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 `
- to read and write in the object. It means that it can access to properties
- directly and through getters, setters, hassers, 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 removing the ``get``, ``set``, ``has``, ``is`` or ``remove`` prefix from
- the method name and lowercasing the first letter; 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 removing the ``get`` prefix from the method name and lowercasing
- the first letter; 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\\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\\ConstraintViolationListNormalizer`
- This normalizer converts objects that implement
- :class:`Symfony\\Component\\Validator\\ConstraintViolationListInterface`
- into a list of errors according to the `RFC 7807`_ standard.
-
-.. _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\Serializer;
- use Symfony\Component\Serializer\Encoder\XmlEncoder;
- use Symfony\Component\Serializer\Encoder\JsonEncoder;
-
- $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 `.
-
-: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.
-
-The ``CsvEncoder``
-~~~~~~~~~~~~~~~~~~~
-
-The ``CsvEncoder`` encodes to and decodes from CSV.
-
-You can pass the context key ``as_collection`` in order to have the results
-always as a collection.
-
-.. versionadded:: 4.2
-
- Relying on the default value ``false`` is deprecated since Symfony 4.2.
-
-The ``XmlEncoder``
-~~~~~~~~~~~~~~~~~~
-
-This encoder transforms arrays into XML and vice versa.
-
-For example, take an object normalized as following::
-
- ['foo' => [1, 2], 'bar' => true];
-
-The ``XmlEncoder`` will encode this object like that::
-
-
-
- 1
- 2
- 1
-
-
-Be aware that this encoder will consider keys beginning with ``@`` as attributes, 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:
- //
- //
- //
- //
- //
-
-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 ``$decoderIgnoredNodeTypes`` argument of
- the ``XmlEncoder`` class constructor.
-
- 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 ` 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 ``skip_null_values`` context option
-to ``true``::
-
- $dummy = new class {
- public $foo;
- public $bar = 'notNull';
- };
-
- $normalizer = new ObjectNormalizer();
- $result = $normalizer->normalize($dummy, 'json', ['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 ``setCircularReferenceLimit()`` method of this normalizer sets the number
-of times it will serialize the same object before considering it a circular
-reference. Its 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();
- $normalizer = new ObjectNormalizer();
-
- // all callback parameters are optional (you can omit the ones you don't use)
- $normalizer->setCircularReferenceHandler(function ($object, string $format = null, array $context = []) {
- return $object->getName();
- });
-
- $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
-
- use Symfony\Component\Serializer\Annotation\MaxDepth;
-
- namespace Acme;
-
- class MyObj
- {
- /**
- * @MaxDepth(2)
- */
- public $child;
-
- // ...
- }
-
- .. code-block:: yaml
-
- Acme\MyObj:
- attributes:
- child:
- max_depth: 2
-
- .. code-block:: xml
-
-
-
-
-
-
-
-
-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 ` to
-learn how to do that.
-
-The check is only done if the ``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, ['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\Serializer;
- 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\ObjectNormalizer;
-
- 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()));
- $normalizer = new ObjectNormalizer($classMetadataFactory);
- // all callback parameters are optional (you can omit the ones you don't use)
- $normalizer->setMaxDepthHandler(function ($innerObject, $outerObject, string $attributeName, string $format = null, array $context = []) {
- return '/foos/'.$innerObject->id;
- });
-
- $serializer = new Serializer([$normalizer]);
-
- $result = $serializer->normalize($level1, null, [ObjectNormalizer::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
-
-
-
- 1
- 2
- 1
-
-
-The array keys beginning with ``@`` are considered XML attributes::
-
- ['foo' => ['@bar' => 'value']];
-
- // is encoded as follows:
- //
- //
- //
- //
-
-Use the special ``#`` key to define the data of a node::
-
- ['foo' => ['@bar' => 'value', '#' => 'baz']];
-
- // is encoded as follows:
- //
- //
- //
- // baz
- //
- //
-
-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.
-
-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\Serializer;
- use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
-
- 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',
- ['default_constructor_arguments' => [
- 'MyObj' => ['foo' => '', 'bar' => ''],
- ]]
- );
- // $data = new MyObj('Hello', '');
-
-Recursive Denormalization and Type Safety
------------------------------------------
-
-The Serializer component can use the :doc:`PropertyInfo Component ` 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``::
-
- use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
- use Symfony\Component\Serializer\Serializer;
- use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
- use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
-
- namespace Acme;
-
- 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 `,
-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\ClassDiscriminatorMapping;
- use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
- 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
-
-
-
-
-
-
-
-
-
-
-
-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 `
- 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
-.. _Packagist: https://packagist.org/packages/symfony/serializer
-.. _RFC3339: https://tools.ietf.org/html/rfc3339#section-5.8
-.. _JSON: http://www.json.org/
-.. _XML: https://www.w3.org/XML/
-.. _YAML: http://yaml.org/
-.. _CSV: https://tools.ietf.org/html/rfc4180
-.. _`RFC 7807`: https://tools.ietf.org/html/rfc7807
-.. _`Value Objects`: https://en.wikipedia.org/wiki/Value_object
-.. _`API Platform`: https://api-platform.com
diff --git a/components/stopwatch.rst b/components/stopwatch.rst
deleted file mode 100644
index 651a66ef5f0..00000000000
--- a/components/stopwatch.rst
+++ /dev/null
@@ -1,125 +0,0 @@
-.. index::
- single: Stopwatch
- single: Components; Stopwatch
-
-The Stopwatch Component
-=======================
-
- The Stopwatch component provides a way to profile code.
-
-Installation
-------------
-
-.. code-block:: terminal
-
- $ composer require symfony/stopwatch
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-Usage
------
-
-The Stopwatch component provides a consistent way to measure execution
-time of certain parts of code so that you don't constantly have to parse
-microtime by yourself. Instead, use the
-:class:`Symfony\\Component\\Stopwatch\\Stopwatch` class::
-
- use Symfony\Component\Stopwatch\Stopwatch;
-
- $stopwatch = new Stopwatch();
- // starts event named 'eventName'
- $stopwatch->start('eventName');
- // ... some code goes here
- $event = $stopwatch->stop('eventName');
-
-The :class:`Symfony\\Component\\Stopwatch\\StopwatchEvent` object can be retrieved
-from the :method:`Symfony\\Component\\Stopwatch\\Stopwatch::start`,
-:method:`Symfony\\Component\\Stopwatch\\Stopwatch::stop`,
-:method:`Symfony\\Component\\Stopwatch\\Stopwatch::lap` and
-:method:`Symfony\\Component\\Stopwatch\\Stopwatch::getEvent` methods.
-The latter should be used when you need to retrieve the duration of an event
-while it is still running.
-
-.. tip::
-
- By default, the stopwatch truncates any sub-millisecond time measure to ``0``,
- so you can't measure microseconds or nanoseconds. If you need more precision,
- pass ``true`` to the ``Stopwatch`` class constructor to enable full precision::
-
- $stopwatch = new Stopwatch(true);
-
-The stopwatch can be reset to its original state at any given time with the
-:method:`Symfony\\Component\\Stopwatch\\Stopwatch::reset` method, which deletes
-all the data measured so far.
-
-You can also provide a category name to an event::
-
- $stopwatch->start('eventName', 'categoryName');
-
-You can consider categories as a way of tagging events. For example, the
-Symfony Profiler tool uses categories to nicely color-code different events.
-
-.. tip::
-
- Read :ref:`this article ` to learn more about
- integrating the Stopwatch component into the Symfony profiler.
-
-Periods
--------
-
-As you know from the real world, all stopwatches come with two buttons:
-one to start and stop the stopwatch, and another to measure the lap time.
-This is exactly what the :method:`Symfony\\Component\\Stopwatch\\Stopwatch::lap`
-method does::
-
- $stopwatch = new Stopwatch();
- // starts event named 'foo'
- $stopwatch->start('foo');
- // ... some code goes here
- $stopwatch->lap('foo');
- // ... some code goes here
- $stopwatch->lap('foo');
- // ... some other code goes here
- $event = $stopwatch->stop('foo');
-
-Lap information is stored as "periods" within the event. To get lap information
-call::
-
- $event->getPeriods();
-
-In addition to periods, you can get other useful information from the event object.
-For example::
-
- $event->getCategory(); // returns the category the event was started in
- $event->getOrigin(); // returns the event start time in milliseconds
- $event->ensureStopped(); // stops all periods not already stopped
- $event->getStartTime(); // returns the start time of the very first period
- $event->getEndTime(); // returns the end time of the very last period
- $event->getDuration(); // returns the event duration, including all periods
- $event->getMemory(); // returns the max memory usage of all periods
-
-Sections
---------
-
-Sections are a way to logically split the timeline into groups. You can see
-how Symfony uses sections to nicely visualize the framework lifecycle in the
-Symfony Profiler tool. Here is a basic usage example using sections::
-
- $stopwatch = new Stopwatch();
-
- $stopwatch->openSection();
- $stopwatch->start('parsing_config_file', 'filesystem_operations');
- $stopwatch->stopSection('routing');
-
- $events = $stopwatch->getSectionEvents('routing');
-
-You can reopen a closed section by calling the :method:`Symfony\\Component\\Stopwatch\\Stopwatch::openSection`
-method and specifying the id of the section to be reopened::
-
- $stopwatch->openSection('routing');
- $stopwatch->start('building_config_tree');
- $stopwatch->stopSection('routing');
-
-.. _Packagist: https://packagist.org/packages/symfony/stopwatch
diff --git a/components/templating.rst b/components/templating.rst
deleted file mode 100644
index 9639eb39719..00000000000
--- a/components/templating.rst
+++ /dev/null
@@ -1,219 +0,0 @@
-.. index::
- single: Templating
- single: Components; Templating
-
-The Templating Component
-========================
-
- The Templating component provides all the tools needed to build any kind
- of template system.
-
- It provides an infrastructure to load template files and optionally
- monitor them for changes. It also provides a concrete template engine
- implementation using PHP with additional tools for escaping and separating
- templates into blocks and layouts.
-
-Installation
-------------
-
-.. code-block:: terminal
-
- $ composer require symfony/templating
-
-Alternatively, you can clone the ``_ repository.
-
-.. include:: /components/require_autoload.rst.inc
-
-Usage
------
-
-.. seealso::
-
- This article explains how to use the Templating features as an independent
- component in any PHP application. Read the :doc:`/templating` article to
- learn about how to work with templates in Symfony applications.
-
-The :class:`Symfony\\Component\\Templating\\PhpEngine` class is the entry point
-of the component. It needs a
-template name parser (:class:`Symfony\\Component\\Templating\\TemplateNameParserInterface`)
-to convert a template name to a
-template reference (:class:`Symfony\\Component\\Templating\\TemplateReferenceInterface`).
-It also needs a template loader (:class:`Symfony\\Component\\Templating\\Loader\\LoaderInterface`)
-which uses the template reference to actually find and load the template::
-
- use Symfony\Component\Templating\PhpEngine;
- use Symfony\Component\Templating\TemplateNameParser;
- use Symfony\Component\Templating\Loader\FilesystemLoader;
-
- $filesystemLoader = new FilesystemLoader(__DIR__.'/views/%name%');
-
- $templating = new PhpEngine(new TemplateNameParser(), $filesystemLoader);
-
- echo $templating->render('hello.php', ['firstname' => 'Fabien']);
-
-.. code-block:: html+php
-
-
- Hello, = $firstname ?>!
-
-The :method:`Symfony\\Component\\Templating\\PhpEngine::render` method parses
-the ``views/hello.php`` file and returns the output text. The second argument
-of ``render`` is an array of variables to use in the template. In this
-example, the result will be ``Hello, Fabien!``.
-
-.. note::
-
- Templates will be cached in the memory of the engine. This means that if
- you render the same template multiple times in the same request, the
- template will only be loaded once from the file system.
-
-The ``$view`` Variable
-----------------------
-
-In all templates parsed by the ``PhpEngine``, you get access to a mysterious
-variable called ``$view``. That variable holds the current ``PhpEngine``
-instance. That means you get access to a bunch of methods that make your life
-easier.
-
-Including Templates
--------------------
-
-The best way to share a snippet of template code is to create a template that
-can then be included by other templates. As the ``$view`` variable is an
-instance of ``PhpEngine``, you can use the ``render()`` method (which was used
-to render the template originally) inside the template to render another template::
-
-
-
- = $view->render('hello.php', ['firstname' => $name]) ?>
-
-
-Global Variables
-----------------
-
-Sometimes, you need to set a variable which is available in all templates
-rendered by an engine (like the ``$app`` variable when using the Symfony
-Framework). These variables can be set by using the
-:method:`Symfony\\Component\\Templating\\PhpEngine::addGlobal` method and they
-can be accessed in the template as normal variables::
-
- $templating->addGlobal('ga_tracking', 'UA-xxxxx-x');
-
-In a template:
-
-.. code-block:: html+php
-
-
The google tracking code is: = $ga_tracking ?>
-
-.. caution::
-
- The global variables cannot be called ``this`` or ``view``, since they are
- already used by the PHP engine.
-
-.. note::
-
- The global variables can be overridden by a local variable in the template
- with the same name.
-
-Output Escaping
----------------
-
-When you render variables, you should probably escape them so that HTML or
-JavaScript code isn't written out to your page. This will prevent things like
-XSS attacks. To do this, use the
-:method:`Symfony\\Component\\Templating\\PhpEngine::escape` method::
-
- = $view->escape($firstname) ?>
-
-By default, the ``escape()`` method assumes that the variable is outputted
-within an HTML context. The second argument lets you change the context. For
-example, to output something inside JavaScript, use the ``js`` context::
-
- = $view->escape($var, 'js') ?>
-
-The component comes with an HTML and JS escaper. You can register your own
-escaper using the
-:method:`Symfony\\Component\\Templating\\PhpEngine::setEscaper` method::
-
- $templating->setEscaper('css', function ($value) {
- // ... all CSS escaping
-
- return $escapedValue;
- });
-
-Helpers
--------
-
-The Templating component can be extended via helpers. Helpers are PHP objects
-that provide features useful in a template context. The component has one
-built-in helper:
-
-* :doc:`/components/templating/slotshelper`
-
-Before you can use these helpers, you need to register them using
-:method:`Symfony\\Component\\Templating\\PhpEngine::set`::
-
- use Symfony\Component\Templating\Helper\SlotsHelper;
- // ...
-
- $templating->set(new SlotsHelper());
-
-Custom Helpers
-~~~~~~~~~~~~~~
-
-You can create your own helpers by creating a class which implements
-:class:`Symfony\\Component\\Templating\\Helper\\HelperInterface`. However,
-most of the time you'll extend
-:class:`Symfony\\Component\\Templating\\Helper\\Helper`.
-
-The ``Helper`` has one required method:
-:method:`Symfony\\Component\\Templating\\Helper\\HelperInterface::getName`.
-This is the name that is used to get the helper from the ``$view`` object.
-
-Creating a Custom Engine
-------------------------
-
-Besides providing a PHP templating engine, you can also create your own engine
-using the Templating component. To do that, create a new class which
-implements the :class:`Symfony\\Component\\Templating\\EngineInterface`. This
-requires 3 method:
-
-* :method:`render($name, array $parameters = []) `
- - Renders a template
-* :method:`exists($name) `
- - Checks if the template exists
-* :method:`supports($name) `
- - Checks if the given template can be handled by this engine.
-
-Using Multiple Engines
-----------------------
-
-It is possible to use multiple engines at the same time using the
-:class:`Symfony\\Component\\Templating\\DelegatingEngine` class. This class
-takes a list of engines and acts just like a normal templating engine. The
-only difference is that it delegates the calls to one of the other engines. To
-choose which one to use for the template, the
-:method:`EngineInterface::supports() `
-method is used::
-
- use Acme\Templating\CustomEngine;
- use Symfony\Component\Templating\PhpEngine;
- use Symfony\Component\Templating\DelegatingEngine;
-
- $templating = new DelegatingEngine([
- new PhpEngine(...),
- new CustomEngine(...),
- ]);
-
-Learn More
-----------
-
-.. toctree::
- :maxdepth: 1
- :glob:
-
- /components/templating/*
- /templating
- /templating/*
-
-.. _Packagist: https://packagist.org/packages/symfony/templating
diff --git a/components/templating/slotshelper.rst b/components/templating/slotshelper.rst
deleted file mode 100644
index a5c1bd134ff..00000000000
--- a/components/templating/slotshelper.rst
+++ /dev/null
@@ -1,87 +0,0 @@
-.. index::
- single: Templating Helpers; Slots Helper
-
-Slots Helper
-============
-
-More often than not, templates in a project share common elements, like the
-well-known header and footer. Using this helper, the static HTML code can
-be placed in a layout file along with "slots", which represent the dynamic
-parts that will change on a page-by-page basis. These slots are then filled
-in by different children template. In other words, the layout file decorates
-the child template.
-
-Displaying Slots
-----------------
-
-The slots are accessible by using the slots helper (``$view['slots']``). Use
-:method:`Symfony\\Component\\Templating\\Helper\\SlotsHelper::output` to
-display the content of the slot on that place:
-
-.. code-block:: html+php
-
-
-
-
-
- Codestin Search App
-
-
- output('_content') ?>
-
-
-
-The first argument of the method is the name of the slot. The method has an
-optional second argument, which is the default value to use if the slot is not
-available.
-
-The ``_content`` slot is a special slot set by the ``PhpEngine``. It contains
-the content of the subtemplate.
-
-.. caution::
-
- If you're using the standalone component, make sure you registered the
- :class:`Symfony\\Component\\Templating\\Helper\\SlotsHelper`::
-
- use Symfony\Component\Templating\Helper\SlotsHelper;
-
- // ...
- $templateEngine->set(new SlotsHelper());
-
-Extending Templates
--------------------
-
-The :method:`Symfony\\Component\\Templating\\PhpEngine::extend` method is called in the
-sub-template to set its parent template. Then
-:method:`$view['slots']->set() `
-can be used to set the content of a slot. All content which is not explicitly
-set in a slot is in the ``_content`` slot.
-
-.. code-block:: html+php
-
-
- extend('layout.php') ?>
-
- set('title', $page->title) ?>
-
-