diff --git a/.doctor-rst.yaml b/.doctor-rst.yaml index 6ef4959d8f9..f578cb7b737 100644 --- a/.doctor-rst.yaml +++ b/.doctor-rst.yaml @@ -51,18 +51,18 @@ rules: yarn_dev_option_at_the_end: ~ # no_app_bundle: ~ - # 4.x + # master versionadded_directive_major_version: - major_version: 4 + major_version: 6 versionadded_directive_min_version: - min_version: '4.0' + min_version: '6.0' deprecated_directive_major_version: - major_version: 4 + major_version: 6 deprecated_directive_min_version: - min_version: '4.0' + min_version: '6.0' # do not report as violation whitelist: @@ -71,6 +71,7 @@ whitelist: - '/``.yml``/' - '/(.*)\.orm\.yml/' # currently DoctrineBundle only supports .yml - '/rst-class/' + - /docker-compose\.yml/ lines: - 'in config files, so the old ``app/config/config_dev.yml`` goes to' - '#. The most important config file is ``app/config/services.yml``, which now is' @@ -89,12 +90,21 @@ whitelist: - '.. versionadded:: 1.3' # MakerBundle - '.. versionadded:: 1.8' # MakerBundle - '.. versionadded:: 1.18' # Flex in setup/upgrade_minor.rst + - '.. versionadded:: 1.0.0' # Encore - '0 => 123' # assertion for var_dumper - components/var_dumper.rst - '1 => "foo"' # assertion for var_dumper - components/var_dumper.rst + - '123,' # assertion for var_dumper - components/var_dumper.rst + - '"foo",' # assertion for var_dumper - components/var_dumper.rst - '$var .= "Because of this `\xE9` octet (\\xE9),\n";' - "`Deploying Symfony 4 Apps on Heroku`_." - ".. _`Deploying Symfony 4 Apps on Heroku`: https://devcenter.heroku.com/articles/deploying-symfony4" + - "// 224, 165, 141, 224, 164, 164, 224, 165, 135])" - '.. versionadded:: 0.2' # MercureBundle + - 'provides a ``loginUser()`` method to simulate logging in in your functional' - '.. code-block:: twig' + - '.. versionadded:: 3.6' # MonologBundle + - '// bin/console' - 'End to End Tests (E2E)' + - '.. code-block:: php' - '.. _`a feature to test applications using Mercure`: https://github.com/symfony/panther#creating-isolated-browsers-to-test-apps-using-mercure-or-websocket' + - '.. End to End Tests (E2E)' diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0586345396f..17cec7af7c3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,9 +1,9 @@ diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 87ccb0b1d75..b241ec33747 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -23,7 +23,7 @@ jobs: - name: "Set-up PHP" uses: shivammathur/setup-php@v2 with: - php-version: 8.0 + php-version: 8.1 coverage: none tools: "composer:v2" @@ -87,7 +87,7 @@ jobs: - name: Set-up PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.0 + php-version: 8.1 coverage: none - name: Fetch branch from where the PR started diff --git a/README.markdown b/README.markdown index c7758624d24..ff9bfcb894a 100644 --- a/README.markdown +++ b/README.markdown @@ -23,7 +23,7 @@ We love contributors! For more information on how you can contribute, please rea the [Symfony Docs Contributing Guide](https://symfony.com/doc/current/contributing/documentation/overview.html) **Important**: use `4.4` branch as the base of your pull requests, unless you are -documenting a feature that was introduced *after* Symfony 4.4 (e.g. in Symfony 5.2). +documenting a feature that was introduced *after* Symfony 4.4 (e.g. in Symfony 5.4). Build Documentation Locally --------------------------- diff --git a/_build/redirection_map b/_build/redirection_map index f0b726e546f..a44b2f213e8 100644 --- a/_build/redirection_map +++ b/_build/redirection_map @@ -424,6 +424,7 @@ /profiler/storage /profiler /setup/composer /setup /security/security_checker /setup +/setup/built_in_web_server /setup/symfony_server /service_container/parameters /configuration /routing/generate_url_javascript /routing /routing/slash_in_parameter /routing @@ -474,6 +475,7 @@ /components/translation/usage /translation /components/translation/custom_formats https://github.com/symfony/translation /components/translation/custom_message_formatter https://github.com/symfony/translation +/components/notifier https://github.com/symfony/notifier /components/routing https://github.com/symfony/routing /doctrine/pdo_session_storage /session/database /doctrine/mongodb_session_storage /session/database @@ -505,7 +507,34 @@ /messenger/message-recorder /messenger/dispatch_after_current_bus /components/stopwatch https://github.com/symfony/stopwatch /service_container/3.3-di-changes https://symfony.com/doc/3.4/service_container/3.3-di-changes.html +/frontend/encore/shared-entry /frontend/encore/split-chunks +/frontend/encore/page-specific-assets /frontend/encore/simple-example#page-specific-javascript-or-css /testing/functional_tests_assertions /testing#testing-application-assertions /components https://symfony.com/components /components/index https://symfony.com/components /serializer/normalizers /components/serializer#normalizers +/logging/monolog_regex_based_excludes /logging/monolog_exclude_http_codes +/security/named_encoders /security/named_hashers +/components/inflector /components/string#inflector +/security/experimental_authenticators /security +/security/user_provider /security/user_providers +/security/reset_password /security/passwords#reset-password +/security/auth_providers /security#security-authenticators +/security/form_login /security#form-login +/security/form_login_setup /security#form-login +/security/json_login_setup /security#json-login +/security/named_hashers /security/passwords#named-password-hashers +/security/password_migration /security/passwords#security-password-migration +/security/acl https://github.com/symfony/acl-bundle/blob/main/src/Resources/doc/index.rst +/security/securing_services /security#securing-other-services +/security/authenticator_manager /security +/security/multiple_guard_authenticators /security/entry_point +/security/guard_authentication /security/custom_authenticator +/components/security/authentication /security#authenticating-users +/components/security/authorization /security#access-control-authorization +/components/security/firewall /security#the-firewall +/components/security/secure_tools /security/passwords +/components/security /security +/email /mailer +/frontend/assetic /frontend +/frontend/assetic/index /frontend diff --git a/_build/spelling_word_list.txt b/_build/spelling_word_list.txt index 70240ceb6d1..fa05ce9430e 100644 --- a/_build/spelling_word_list.txt +++ b/_build/spelling_word_list.txt @@ -3,7 +3,6 @@ Akamai analytics Ansi Ansible -Assetic async authenticator authenticators diff --git a/_images/components/console/completion.gif b/_images/components/console/completion.gif new file mode 100644 index 00000000000..011ae0b935e 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..a4fd844eb80 Binary files /dev/null and b/_images/components/console/cursor.gif differ diff --git a/_images/components/string/bytes-points-graphemes.png b/_images/components/string/bytes-points-graphemes.png new file mode 100644 index 00000000000..18d971cecf7 Binary files /dev/null and b/_images/components/string/bytes-points-graphemes.png differ diff --git a/_images/components/workflow/blogpost_mermaid.png b/_images/components/workflow/blogpost_mermaid.png new file mode 100644 index 00000000000..b0ffbc984c9 Binary files /dev/null and b/_images/components/workflow/blogpost_mermaid.png differ diff --git a/_images/contributing/docs-github-create-pr.png b/_images/contributing/docs-github-create-pr.png index 29fe22f5dbd..43b6842ffc2 100644 Binary files a/_images/contributing/docs-github-create-pr.png and b/_images/contributing/docs-github-create-pr.png differ diff --git a/_images/contributing/docs-github-edit-page.png b/_images/contributing/docs-github-edit-page.png index c34f13f0889..9ea6c15421a 100644 Binary files a/_images/contributing/docs-github-edit-page.png and b/_images/contributing/docs-github-edit-page.png differ diff --git a/_images/contributing/docs-pull-request-change-base.png b/_images/contributing/docs-pull-request-change-base.png index d824e8ef1bc..791901b8ec6 100644 Binary files a/_images/contributing/docs-pull-request-change-base.png and b/_images/contributing/docs-pull-request-change-base.png differ diff --git a/_images/controller/error_pages/exceptions-in-dev-environment.png b/_images/controller/error_pages/exceptions-in-dev-environment.png index 74128990e57..e1fba2bebf9 100644 Binary files a/_images/controller/error_pages/exceptions-in-dev-environment.png and b/_images/controller/error_pages/exceptions-in-dev-environment.png differ diff --git a/_images/install/deprecations-in-profiler.png b/_images/install/deprecations-in-profiler.png index a8abcae32b7..3d3f9a98a4a 100644 Binary files a/_images/install/deprecations-in-profiler.png and b/_images/install/deprecations-in-profiler.png differ diff --git a/_images/notifier/microsoft_teams/message-card.png b/_images/notifier/microsoft_teams/message-card.png new file mode 100644 index 00000000000..05f505fb3e0 Binary files /dev/null and b/_images/notifier/microsoft_teams/message-card.png differ diff --git a/_images/notifier/microsoft_teams/message.png b/_images/notifier/microsoft_teams/message.png new file mode 100644 index 00000000000..5c4c7f11ed1 Binary files /dev/null and b/_images/notifier/microsoft_teams/message.png differ diff --git a/_images/notifier/slack/field-method.png b/_images/notifier/slack/field-method.png new file mode 100644 index 00000000000..d77a60e6a2e Binary files /dev/null and b/_images/notifier/slack/field-method.png differ diff --git a/_images/notifier/slack/message-reply.png b/_images/notifier/slack/message-reply.png new file mode 100644 index 00000000000..9a60e4573ab Binary files /dev/null and b/_images/notifier/slack/message-reply.png differ diff --git a/_images/notifier/slack/slack-footer.png b/_images/notifier/slack/slack-footer.png new file mode 100644 index 00000000000..a53952c78f6 Binary files /dev/null and b/_images/notifier/slack/slack-footer.png differ diff --git a/_images/notifier/slack/slack-header.png b/_images/notifier/slack/slack-header.png new file mode 100644 index 00000000000..a7caf915d8f Binary files /dev/null and b/_images/notifier/slack/slack-header.png differ diff --git a/_images/profiler/web-interface.png b/_images/profiler/web-interface.png index 2e6c6061892..2a1bc8a0650 100644 Binary files a/_images/profiler/web-interface.png and b/_images/profiler/web-interface.png differ diff --git a/_images/quick_tour/no_routes_page.png b/_images/quick_tour/no_routes_page.png index 382950b6ef5..030953a17b1 100644 Binary files a/_images/quick_tour/no_routes_page.png and b/_images/quick_tour/no_routes_page.png differ diff --git a/_images/quick_tour/web_debug_toolbar.png b/_images/quick_tour/web_debug_toolbar.png deleted file mode 100644 index 465020380cb..00000000000 Binary files a/_images/quick_tour/web_debug_toolbar.png and /dev/null differ diff --git a/_images/rate_limiter/fixed_window.svg b/_images/rate_limiter/fixed_window.svg new file mode 100644 index 00000000000..83d5f6e79ac --- /dev/null +++ b/_images/rate_limiter/fixed_window.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + 10:00 + + + 10:30 + + + 11:00 + + + 11:30 + + + 12:00 + + + + + + + + 12:30 + + + 13:00 + + + + + + + + + + + + + + + + + + + + + + 1 hour window + + + 1 hour window + + + + + + 1 hour window + + + + + 13:15 + + + diff --git a/_images/rate_limiter/sliding_window.svg b/_images/rate_limiter/sliding_window.svg new file mode 100644 index 00000000000..2c565615441 --- /dev/null +++ b/_images/rate_limiter/sliding_window.svg @@ -0,0 +1,65 @@ + + + + + + + + + + 10:00 + + + 10:30 + + + 11:00 + + + 11:30 + + + 12:00 + + + + + + 12:30 + + + 13:00 + + + + + + + + + + + + + + + + + + + + + + 1 hour window + + + + + + 13:15 + + + + + + diff --git a/_images/rate_limiter/token_bucket.svg b/_images/rate_limiter/token_bucket.svg new file mode 100644 index 00000000000..29d6fc8f103 --- /dev/null +++ b/_images/rate_limiter/token_bucket.svg @@ -0,0 +1,83 @@ + + + + 10:00 + + + 10:30 + + + 11:00 + + + 11:30 + + + 12:00 + + + + + + + + 12:30 + + + 13:00 + + + + + + + + + + + + + + + + + + + + + + + + + + + 13:15 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/release-process.jpg b/_images/release-process.jpg deleted file mode 100644 index 9868404b07f..00000000000 Binary files a/_images/release-process.jpg and /dev/null differ diff --git a/_images/security/anonymous_wdt.png b/_images/security/anonymous_wdt.png index 8dbf1cd8298..80736afce39 100644 Binary files a/_images/security/anonymous_wdt.png and b/_images/security/anonymous_wdt.png differ diff --git a/_images/security/login_link_email.png b/_images/security/login_link_email.png new file mode 100644 index 00000000000..8331b878f68 Binary files /dev/null and b/_images/security/login_link_email.png differ diff --git a/_images/security/profiler-badges.png b/_images/security/profiler-badges.png new file mode 100644 index 00000000000..a19f8539581 Binary files /dev/null and b/_images/security/profiler-badges.png differ diff --git a/_images/security/security_events.svg b/_images/security/security_events.svg new file mode 100644 index 00000000000..f1b93923da6 --- /dev/null +++ b/_images/security/security_events.svg @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/sources/rate_limiter/fixed_window.dia b/_images/sources/rate_limiter/fixed_window.dia new file mode 100644 index 00000000000..16282a2dcce Binary files /dev/null and b/_images/sources/rate_limiter/fixed_window.dia differ diff --git a/_images/sources/rate_limiter/sliding_window.dia b/_images/sources/rate_limiter/sliding_window.dia new file mode 100644 index 00000000000..e16275d8995 Binary files /dev/null and b/_images/sources/rate_limiter/sliding_window.dia differ diff --git a/_images/sources/rate_limiter/token_bucket.dia b/_images/sources/rate_limiter/token_bucket.dia new file mode 100644 index 00000000000..16761971337 Binary files /dev/null and b/_images/sources/rate_limiter/token_bucket.dia differ diff --git a/_images/sources/security/security_events.dia b/_images/sources/security/security_events.dia new file mode 100644 index 00000000000..0a8afa73179 Binary files /dev/null and b/_images/sources/security/security_events.dia differ diff --git a/best_practices.rst b/best_practices.rst index dcc0d9eb3f2..a955c35920a 100644 --- a/best_practices.rst +++ b/best_practices.rst @@ -194,17 +194,20 @@ you'll need to configure services (or parts of them) manually. YAML is the format recommended to configure services because it's friendly to newcomers and concise, but Symfony also supports XML and PHP configuration. -Use Annotations to Define the Doctrine Entity Mapping -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Use Attributes to Define the Doctrine Entity Mapping +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Doctrine entities are plain PHP objects that you store in some "database". Doctrine only knows about your entities through the mapping metadata configured for your model classes. -Doctrine supports several metadata formats, but it's recommended to use -annotations because they are by far the most convenient and agile way of setting +Doctrine supports several metadata formats, but it's recommended to use PHP +attributes because they are by far the most convenient and agile way of setting up and looking for mapping information. +If your PHP version doesn't support attributes yet, use annotations, which is +similar but requires installing some extra dependencies in your project. + Controllers ----------- @@ -223,12 +226,13 @@ important parts of your application. .. _best-practice-controller-annotations: -Use Annotations to Configure Routing, Caching and Security -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Use Attributes or Annotations to Configure Routing, Caching and Security +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Using annotations for routing, caching and security simplifies configuration. -You don't need to browse several files created with different formats (YAML, XML, -PHP): all the configuration is just where you need it and it only uses one format. +Using attributes or annotations for routing, caching and security simplifies +configuration. You don't need to browse several files created with different +formats (YAML, XML, PHP): all the configuration is just where you need it and +it only uses one format. Don't Use Annotations to Configure the Controller Template ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -243,7 +247,7 @@ Use Dependency Injection to Get Services If you extend the base ``AbstractController``, you can only access to the most common services (e.g ``twig``, ``router``, ``doctrine``, etc.), directly from the -container via ``$this->container->get()`` or ``$this->get()``. +container via ``$this->container->get()``. Instead, you must use dependency injection to fetch services by :ref:`type-hinting action method arguments ` or constructor arguments. @@ -367,14 +371,15 @@ Use the ``auto`` Password Hasher The :ref:`auto password hasher ` automatically selects the best possible encoder/hasher depending on your PHP installation. -Currently, it tries to use ``sodium`` by default and falls back to ``bcrypt``. +Currently, the default auto hasher is ``bcrypt``. Use Voters to Implement Fine-grained Security Restrictions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If your security logic is complex, you should create custom :doc:`security voters ` instead of defining long expressions -inside the ``@Security`` annotation. +inside the ``#[Security]`` attribute (or in the ``@Security`` annotation if your +PHP version doesn't support attributes yet). Web Assets ---------- diff --git a/bundles.rst b/bundles.rst index bf5a144d4ce..ed194614c34 100644 --- a/bundles.rst +++ b/bundles.rst @@ -28,7 +28,6 @@ file:: Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], - Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], // this bundle is enabled only in 'dev' and 'test', so you can't use it in 'prod' diff --git a/bundles/best_practices.rst b/bundles/best_practices.rst index c6e0521db82..f18cdba8352 100644 --- a/bundles/best_practices.rst +++ b/bundles/best_practices.rst @@ -22,8 +22,9 @@ interoperability standard for PHP namespaces and class names: it starts with a vendor segment, followed by zero or more category segments, and it ends with the namespace short name, which must end with ``Bundle``. -A namespace becomes a bundle as soon as you add a bundle class to it. The -bundle class name must follow these rules: +A namespace becomes a bundle as soon as you add "a bundle class" to it (which is +a class that extends :class:`Symfony\\Component\\HttpKernel\\Bundle\\Bundle`). +The bundle class name must follow these rules: * Use only alphanumeric characters and underscores; * Use a StudlyCaps name (i.e. camelCase with an uppercase first letter); @@ -82,11 +83,6 @@ The following is the recommended directory structure of an AcmeBlogBundle: ├── LICENSE └── README.md -.. versionadded:: 4.4 - - This directory convention was introduced in Symfony 4.4 and can be used only - when requiring ``symfony/http-kernel`` 4.4 or superior. - This directory structure requires to configure the bundle path to its root directory as follows:: @@ -127,8 +123,8 @@ Type Directory Commands ``src/Command/`` Controllers ``src/Controller/`` Service Container Extensions ``src/DependencyInjection/`` -Doctrine ORM entities (when not using annotations) ``src/Entity/`` -Doctrine ODM documents (when not using annotations) ``src/Document/`` +Doctrine ORM entities ``src/Entity/`` +Doctrine ODM documents ``src/Document/`` Event Listeners ``src/EventListener/`` Configuration (routes, services, etc.) ``config/`` Web Assets (CSS, JS, images) ``public/`` @@ -166,6 +162,15 @@ standard Symfony autoloading instead. A bundle should also not embed third-party libraries written in JavaScript, CSS or any other language. +Doctrine Entities/Documents +--------------------------- + +If the bundle includes Doctrine ORM entities and/or ODM documents, it's +recommended to define their mapping using XML files stored in +``Resources/config/doctrine/``. This allows to override that mapping using the +:doc:`standard Symfony mechanism to override bundle parts `. +This is not possible when using annotations/attributes to define the mapping. + Tests ----- @@ -198,15 +203,15 @@ A bundle should at least test: * All supported major Symfony versions (e.g. both ``4.x`` and ``5.x`` if support is claimed for both). -Thus, a bundle supporting PHP 7.3, 7.4 and 8.0, and Symfony 3.4 and 4.x should +Thus, a bundle supporting PHP 7.3, 7.4 and 8.0, and Symfony 4.4 and 5.x should have at least this test matrix: =========== =============== =================== PHP version Symfony version Composer flags =========== =============== =================== -7.3 ``3.*`` ``--prefer-lowest`` -7.4 ``4.*`` -8.0 ``4.*`` +7.3 ``4.*`` ``--prefer-lowest`` +7.4 ``5.*`` +8.0 ``5.*`` =========== =============== =================== .. tip:: diff --git a/bundles/configuration.rst b/bundles/configuration.rst index 25254b7efcb..1742457fb36 100644 --- a/bundles/configuration.rst +++ b/bundles/configuration.rst @@ -40,9 +40,11 @@ as integration of other related components: .. code-block:: php - $container->loadFromExtension('framework', [ - 'form' => true, - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework->form()->enabled(true); + }; Using the Bundle Extension -------------------------- @@ -81,12 +83,13 @@ can add some configuration that looks like this: .. code-block:: php // config/packages/acme_social.php - $container->loadFromExtension('acme_social', [ - 'twitter' => [ - 'client_id' => 123, - 'client_secret' => 'your_secret', - ], - ]); + use Symfony\Config\AcmeSocialConfig; + + return static function (AcmeSocialConfig $acmeSocial) { + $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, @@ -199,10 +202,6 @@ The ``Configuration`` class to handle the sample configuration looks like:: } } -.. deprecated:: 4.2 - - Not passing the root node name to ``TreeBuilder`` was deprecated in Symfony 4.2. - .. seealso:: The ``Configuration`` class can be much more complicated than shown here, diff --git a/cache.rst b/cache.rst index 9982c33a7cf..eb734db5859 100644 --- a/cache.rst +++ b/cache.rst @@ -27,13 +27,9 @@ The following example shows a typical usage of the cache:: // ... and to remove the cache key $pool->delete('my_cache_key'); -Symfony supports Cache Contracts, PSR-6/16 and Doctrine Cache interfaces. +Symfony supports Cache Contracts and PSR-6/16 interfaces. You can read more about these at the :doc:`component documentation `. -.. versionadded:: 4.2 - - The cache contracts were introduced in Symfony 4.2. - .. _cache-configuration-with-frameworkbundle: Configuring Cache with FrameworkBundle @@ -89,23 +85,26 @@ adapter (template) they use by using the ``app`` and ``system`` key like: .. code-block:: php // config/packages/cache.php - $container->loadFromExtension('framework', [ - 'cache' => [ - 'app' => 'cache.adapter.filesystem', - 'system' => 'cache.adapter.system', - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework->cache() + ->app('cache.adapter.filesystem') + ->system('cache.adapter.system') + ; + }; + The Cache component comes with a series of adapters pre-configured: * :doc:`cache.adapter.apcu ` * :doc:`cache.adapter.array ` -* :doc:`cache.adapter.doctrine ` * :doc:`cache.adapter.filesystem ` * :doc:`cache.adapter.memcached ` * :doc:`cache.adapter.pdo ` * :doc:`cache.adapter.psr6 ` * :doc:`cache.adapter.redis ` +* :ref:`cache.adapter.redis_tag_aware ` (Redis adapter optimized to work with tags) Some of these adapters could be configured via shortcuts. Using these shortcuts will create pools with service IDs that follow the pattern ``cache.[type]``. @@ -119,8 +118,6 @@ will create pools with service IDs that follow the pattern ``cache.[type]``. cache: directory: '%kernel.cache_dir%/pools' # Only used with cache.adapter.filesystem - # service: cache.doctrine - default_doctrine_provider: 'app.doctrine_cache' # service: cache.psr6 default_psr6_provider: 'app.my_psr6_service' # service: cache.redis @@ -144,7 +141,6 @@ will create pools with service IDs that follow the pattern ``cache.[type]``. loadFromExtension('framework', [ - 'cache' => [ - // Only used with cache.adapter.filesystem - 'directory' => '%kernel.cache_dir%/pools', + use Symfony\Config\FrameworkConfig; - // Service: cache.doctrine - 'default_doctrine_provider' => 'app.doctrine_cache', + return static function (FrameworkConfig $framework) { + $framework->cache() + // Only used with cache.adapter.filesystem + ->directory('%kernel.cache_dir%/pools') // Service: cache.psr6 - 'default_psr6_provider' => 'app.my_psr6_service', + ->defaultPsr6Provider('app.my_psr6_service') // Service: cache.redis - 'default_redis_provider' => 'redis://localhost', + ->defaultRedisProvider('redis://localhost') // Service: cache.memcached - 'default_memcached_provider' => 'memcached://localhost', + ->defaultMemcachedProvider('memcached://localhost') // Service: cache.pdo - 'default_pdo_provider' => 'doctrine.dbal.default_connection', - ], - ]); + ->defaultPdoProvider('doctrine.dbal.default_connection') + ; + }; + +.. _cache-create-pools: Creating Custom (Namespaced) Pools ---------------------------------- @@ -264,43 +260,36 @@ You can also create more customized pools: .. code-block:: php // config/packages/cache.php - $container->loadFromExtension('framework', [ - 'cache' => [ - 'default_memcached_provider' => 'memcached://localhost', - 'pools' => [ - // creates a "custom_thing.cache" service - // autowireable via "CacheInterface $customThingCache" - // uses the "app" cache configuration - 'custom_thing.cache' => [ - 'adapter' => 'cache.app', - ], - - // creates a "my_cache_pool" service - // autowireable via "CacheInterface $myCachePool" - 'my_cache_pool' => [ - 'adapter' => 'cache.adapter.filesystem', - ], - - // uses the default_memcached_provider from above - 'acme.cache' => [ - 'adapter' => 'cache.adapter.memcached', - ], - - // control adapter's configuration - 'foobar.cache' => [ - 'adapter' => 'cache.adapter.memcached', - 'provider' => 'memcached://user:password@example.com', - ], - - // uses the "foobar.cache" pool as its backend but controls - // the lifetime and (like all pools) has a separate cache namespace - 'short_cache' => [ - 'adapter' => 'foobar.cache', - 'default_lifetime' => 60, - ], - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $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 @@ -439,26 +428,25 @@ and use that when configuring the pool. // config/packages/cache.php use Symfony\Component\Cache\Adapter\RedisAdapter; - - $container->loadFromExtension('framework', [ - 'cache' => [ - 'pools' => [ - 'cache.my_redis' => [ - 'adapter' => 'cache.adapter.redis', - 'provider' => 'app.my_custom_redis_provider', - ], - ], - ], - ]); - - $container->register('app.my_custom_redis_provider', \Redis::class) - ->setFactory([RedisAdapter::class, 'createConnection']) - ->addArgument('redis://localhost') - ->addArgument([ - 'retry_interval' => 2, - 'timeout' => 10 - ]) - ; + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Config\FrameworkConfig; + + return static function (ContainerBuilder $container, FrameworkConfig $framework) { + $framework->cache() + ->pool('cache.my_redis') + ->adapters(['cache.adapter.redis']) + ->provider('app.my_custom_redis_provider'); + + + $container->register('app.my_custom_redis_provider', \Redis::class) + ->setFactory([RedisAdapter::class, 'createConnection']) + ->addArgument('redis://localhost') + ->addArgument([ + 'retry_interval' => 2, + 'timeout' => 10 + ]) + ; + }; Creating a Cache Chain ---------------------- @@ -479,10 +467,6 @@ If an error happens when storing an item in a pool, Symfony stores it in the other pools and no exception is thrown. Later, when the item is retrieved, Symfony stores the item automatically in all the missing pools. -.. versionadded:: 4.4 - - Support for configuring a chain using ``framework.cache.pools`` was introduced in Symfony 4.4. - .. configuration-block:: .. code-block:: yaml @@ -522,20 +506,19 @@ Symfony stores the item automatically in all the missing pools. .. code-block:: php // config/packages/cache.php - $container->loadFromExtension('framework', [ - 'cache' => [ - 'pools' => [ - 'my_cache_pool' => [ - 'default_lifetime' => 31536000, // One year - 'adapters' => [ - 'cache.adapter.array', - 'cache.adapter.apcu', - ['name' => 'cache.adapter.redis', 'provider' => 'redis://user:password@example.com'], - ], - ], - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework->cache() + ->pool('my_cache_pool') + ->defaultLifetime(31536000) // One year + ->adapters([ + 'cache.adapter.array', + 'cache.adapter.apcu', + ['name' => 'cache.adapter.redis', 'provider' => 'redis://user:password@example.com'], + ]) + ; + }; Using Cache Tags ---------------- @@ -614,16 +597,15 @@ to enable this feature. This could be added by using the following configuration .. code-block:: php // config/packages/cache.php - $container->loadFromExtension('framework', [ - 'cache' => [ - 'pools' => [ - 'my_cache_pool' => [ - 'adapter' => 'cache.adapter.redis', - 'tags' => true, - ], - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework->cache() + ->pool('my_cache_pool') + ->tags(true) + ->adapters(['cache.adapter.redis']) + ; + }; Tags are stored in the same pool by default. This is good in most scenarios. But sometimes it might be better to store the tags in a different pool. That could be @@ -664,19 +646,20 @@ achieved by specifying the adapter. .. code-block:: php // config/packages/cache.php - $container->loadFromExtension('framework', [ - 'cache' => [ - 'pools' => [ - 'my_cache_pool' => [ - 'adapter' => 'cache.adapter.redis', - 'tags' => 'tag_pool', - ], - 'tag_pool' => [ - 'adapter' => 'cache.adapter.apcu', - ], - ], - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework->cache() + ->pool('my_cache_pool') + ->tags('tag_pool') + ->adapters(['cache.adapter.redis']) + ; + + $framework->cache() + ->pool('tag_pool') + ->adapters(['cache.adapter.apcu']) + ; + }; .. note:: @@ -705,10 +688,6 @@ To see all available cache pools: $ php bin/console cache:pool:list -.. versionadded:: 4.3 - - The ``cache:pool:list`` command was introduced in Symfony 4.3. - Clear one pool: .. code-block:: terminal @@ -726,3 +705,84 @@ Clear all caches everywhere: .. code-block:: terminal $ php bin/console cache:pool:clear cache.global_clearer + +Encrypting the Cache +-------------------- + +To encrypt the cache using ``libsodium``, you can use the +:class:`Symfony\\Component\\Cache\\Marshaller\\SodiumMarshaller`. + +First, you need to generate a secure key and add it to your :doc:`secret +store ` as ``CACHE_DECRYPTION_KEY``: + +.. code-block:: terminal + + $ php -r 'echo base64_encode(sodium_crypto_box_keypair());' + +Then, register the ``SodiumMarshaller`` service using this key: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/cache.yaml + + # ... + services: + Symfony\Component\Cache\Marshaller\SodiumMarshaller: + decorates: cache.default_marshaller + arguments: + - ['%env(base64:CACHE_DECRYPTION_KEY)%'] + # use multiple keys in order to rotate them + #- ['%env(base64:CACHE_DECRYPTION_KEY)%', '%env(base64:OLD_CACHE_DECRYPTION_KEY)%'] + - '@Symfony\Component\Cache\Marshaller\SodiumMarshaller.inner' + + .. code-block:: xml + + + + + + + + + + + env(base64:CACHE_DECRYPTION_KEY) + + + + + + + + + .. code-block:: php + + // config/packages/cache.php + use Symfony\Component\Cache\Marshaller\SodiumMarshaller; + use Symfony\Component\DependencyInjection\ChildDefinition; + use Symfony\Component\DependencyInjection\Reference; + + // ... + $container->setDefinition(SodiumMarshaller::class, new ChildDefinition('cache.default_marshaller')) + ->addArgument(['env(base64:CACHE_DECRYPTION_KEY)']) + // use multiple keys in order to rotate them + //->addArgument(['env(base64:CACHE_DECRYPTION_KEY)', 'env(base64:OLD_CACHE_DECRYPTION_KEY)']) + ->addArgument(new Reference(SodiumMarshaller::class.'.inner')); + +.. caution:: + + This will encrypt the values of the cache items, but not the cache keys. Be + careful not the leak sensitive data in the keys. + +When configuring multiple keys, the first key will be used for reading and +writing, and the additional key(s) will only be used for reading. Once all +cache items encrypted with the old key have expired, you can completely remove +``OLD_CACHE_DECRYPTION_KEY``. diff --git a/components/asset.rst b/components/asset.rst index 5be1003ef15..076a759bd9c 100644 --- a/components/asset.rst +++ b/components/asset.rst @@ -51,6 +51,8 @@ Installation Usage ----- +.. _asset-packages: + Asset Packages ~~~~~~~~~~~~~~ @@ -165,6 +167,34 @@ In those cases, use the echo $package->getUrl('css/app.css'); // result: build/css/app.b916426ea1d10021f3f17ce8031f93c2.css +If you request an asset that is *not found* in the ``rev-manifest.json`` file, +the original - *unmodified* - asset path will be returned. The ``$strictMode`` +argument helps debug issues because it throws an exception when the asset is not +listed in the manifest:: + + use Symfony\Component\Asset\Package; + use Symfony\Component\Asset\VersionStrategy\JsonManifestVersionStrategy; + + // The value of $strictMode can be specific per environment "true" for debugging and "false" for stability. + $strictMode = true; + // assumes the JSON file above is called "rev-manifest.json" + $package = new Package(new JsonManifestVersionStrategy(__DIR__.'/rev-manifest.json', null, $strictMode)); + + echo $package->getUrl('not-found.css'); + // error: + +If your JSON file is not on your local filesystem but is accessible over HTTP, +use the :class:`Symfony\\Component\\Asset\\VersionStrategy\\RemoteJsonManifestVersionStrategy` +with the :doc:`HttpClient component `:: + + use Symfony\Component\Asset\Package; + use Symfony\Component\Asset\VersionStrategy\RemoteJsonManifestVersionStrategy; + use Symfony\Component\HttpClient\HttpClient; + + $httpClient = HttpClient::create(); + $manifestUrl = 'https://cdn.example.com/rev-manifest.json'; + $package = new Package(new RemoteJsonManifestVersionStrategy($manifestUrl, $httpClient)); + Custom Version Strategies ......................... @@ -184,12 +214,12 @@ every day:: $this->version = date('Ymd'); } - public function getVersion($path) + public function getVersion(string $path) { return $this->version; } - public function applyVersion($path) + public function applyVersion(string $path) { return sprintf('%s?v=%s', $path, $this->getVersion($path)); } diff --git a/components/browser_kit.rst b/components/browser_kit.rst index 76c0e33d5e1..4a81ba30b43 100644 --- a/components/browser_kit.rst +++ b/components/browser_kit.rst @@ -40,13 +40,7 @@ The component only provides an abstract client and does not provide any backend ready to use for the HTTP layer. To create your own client, you must extend the ``AbstractBrowser`` class and implement the :method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::doRequest` method. - -.. deprecated:: 4.3 - - In Symfony 4.3 and earlier versions, the ``AbstractBrowser`` class was called - ``Client`` (which is now deprecated). - -The ``doRequest()`` method accepts a request and should return a response:: +This method accepts a request and should return a response:: namespace Acme; @@ -66,7 +60,7 @@ The ``doRequest()`` method accepts a request and should return a response:: For a simple implementation of a browser based on the HTTP layer, have a look at the :class:`Symfony\\Component\\BrowserKit\\HttpBrowser` provided by :ref:`this component `. For an implementation based -on ``HttpKernelInterface``, have a look at the :class:`Symfony\\Component\\HttpKernel\\Client` +on ``HttpKernelInterface``, have a look at the :class:`Symfony\\Component\\HttpKernel\\HttpClientKernel` provided by the :doc:`HttpKernel component `. Making Requests @@ -86,6 +80,16 @@ The value returned by the ``request()`` method is an instance of the :doc:`DomCrawler component `, which allows accessing and traversing HTML elements programmatically. +The :method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::jsonRequest` method, +which defines the same arguments as the ``request()`` method, is a shortcut to +convert the request parameters into a JSON string and set the needed HTTP headers:: + + use Acme\Client; + + $client = new Client(); + // this encodes parameters as JSON and sets the required CONTENT_TYPE and HTTP_ACCEPT headers + $crawler = $client->jsonRequest('GET', '/', ['some_parameter' => 'some_value']); + The :method:`Symfony\\Component\\BrowserKit\\AbstractBrowser::xmlHttpRequest` method, which defines the same arguments as the ``request()`` method, is a shortcut to make AJAX requests:: @@ -167,6 +171,28 @@ provides access to the form properties (e.g. ``$form->getUri()``, // submit that form $crawler = $client->submit($form); +Custom Header Handling +~~~~~~~~~~~~~~~~~~~~~~ + +The optional HTTP headers passed to the ``request()`` method follows the FastCGI +request format (uppercase, underscores instead of dashes and prefixed with ``HTTP_``). +Before saving those headers to the request, they are lower-cased, with ``HTTP_`` +stripped, and underscores converted into dashes. + +If you're making a request to an application that has special rules about header +capitalization or punctuation, override the ``getHeaders()`` method, which must +return an associative array of headers:: + + protected function getHeaders(Request $request): array + { + $headers = parent::getHeaders($request); + if (isset($request->getServer()['api_key'])) { + $headers['api_key'] = $request->getServer()['api_key']; + } + + return $headers; + } + Cookies ------- @@ -317,10 +343,6 @@ dedicated web crawler or scraper such as `Goutte`_:: '.table-list-header-toggle a:nth-child(1)' )->text()); -.. versionadded:: 4.3 - - The feature to make external HTTP requests was introduced in Symfony 4.3. - Learn more ---------- diff --git a/components/cache.rst b/components/cache.rst index 9a1b6611f8d..270081d2e3c 100644 --- a/components/cache.rst +++ b/components/cache.rst @@ -16,9 +16,8 @@ The Cache Component .. tip:: - The component also contains adapters to convert between PSR-6, PSR-16 and - Doctrine caches. See :doc:`/components/cache/psr6_psr16_adapters` and - :doc:`/components/cache/adapters/doctrine_adapter`. + The component also contains adapters to convert between PSR-6 and PSR-16. + See :doc:`/components/cache/psr6_psr16_adapters`. Installation ------------ @@ -159,7 +158,7 @@ concepts: **Adapter** It implements the actual caching mechanism to store the information in the filesystem, in a database, etc. The component provides several ready to use - adapters for common caching backends (Redis, APCu, Doctrine, PDO, etc.) + adapters for common caching backends (Redis, APCu, PDO, etc.) Basic Usage (PSR-6) ------------------- @@ -195,6 +194,34 @@ Now you can create, retrieve, update and delete items using this cache pool:: For a list of all of the supported adapters, see :doc:`/components/cache/cache_pools`. +Marshalling (Serializing) Data +------------------------------ + +.. note:: + + `Marshalling`_ and `serializing`_ are similar concepts. Serializing is the + process of translating an object state into a format that can be stored + (e.g. in a file). Marshalling is the process of translating both the object + state and its codebase into a format that can be stored or transmitted. + + Unmarshalling an object produces a copy of the original object, possibly by + automatically loading the class definitions of the object. + +Symfony uses *marshallers* (classes which implement +:class:`Symfony\\Component\\Cache\\Marshaller\\MarshallerInterface`) to process +the cache items before storing them. + +The :class:`Symfony\\Component\\Cache\\Marshaller\\DefaultMarshaller` uses PHP's +``serialize()`` or ``igbinary_serialize()`` if the `Igbinary extension`_ is installed. +There are other *marshallers* that can encrypt or compress the data before storing it:: + + use Symfony\Component\Cache\Adapter\RedisAdapter; + use Symfony\Component\Cache\DefaultMarshaller; + use Symfony\Component\Cache\DeflateMarshaller; + + $marshaller = new DeflateMarshaller(new DefaultMarshaller()); + $cache = new RedisAdapter(new \Redis(), 'namespace', 0, $marshaller); + Advanced Usage -------------- @@ -208,3 +235,6 @@ Advanced Usage .. _`Cache Contracts`: https://github.com/symfony/contracts/blob/master/Cache/CacheInterface.php .. _`Stampede prevention`: https://en.wikipedia.org/wiki/Cache_stampede .. _Probabilistic early expiration: https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration +.. _`Marshalling`: https://en.wikipedia.org/wiki/Marshalling_(computer_science) +.. _`serializing`: https://en.wikipedia.org/wiki/Serialization +.. _`Igbinary extension`: https://github.com/igbinary/igbinary diff --git a/components/cache/adapters/array_cache_adapter.rst b/components/cache/adapters/array_cache_adapter.rst index ffdc1d60dd0..baa7f840590 100644 --- a/components/cache/adapters/array_cache_adapter.rst +++ b/components/cache/adapters/array_cache_adapter.rst @@ -8,10 +8,7 @@ Array Cache Adapter Generally, this adapter is useful for testing purposes, as its contents are stored in memory and not persisted outside the running PHP process in any way. It can also be useful while warming up caches, due to the :method:`Symfony\\Component\\Cache\\Adapter\\ArrayAdapter::getValues` -method. - -This adapter can be passed a default cache lifetime as its first parameter, and a boolean that -toggles serialization as its second parameter:: +method:: use Symfony\Component\Cache\Adapter\ArrayAdapter; @@ -23,5 +20,13 @@ toggles serialization as its second parameter:: $defaultLifetime = 0, // if ``true``, the values saved in the cache are serialized before storing them - $storeSerialized = true + $storeSerialized = true, + + // the maximum lifetime (in seconds) of the entire cache (after this time, the + // entire cache is deleted to avoid stale data from consuming memory) + $maxLifetime = 0, + + // the maximum number of items that can be stored in the cache. When the limit + // is reached, cache follows the LRU model (least recently used items are deleted) + $maxItems = 0 ); diff --git a/components/cache/adapters/couchbasebucket_adapter.rst b/components/cache/adapters/couchbasebucket_adapter.rst new file mode 100644 index 00000000000..e5043978690 --- /dev/null +++ b/components/cache/adapters/couchbasebucket_adapter.rst @@ -0,0 +1,146 @@ +.. index:: + single: Cache Pool + single: Couchbase Cache + +.. _couchbase-adapter: + +Couchbase Bucket Cache Adapter +============================== + +This adapter stores the values in-memory using one (or more) `Couchbase server`_ +instances. Unlike the :ref:`APCu adapter `, and similarly to the +:ref:`Memcached adapter `, it is not limited to the current server's +shared memory; you can store contents independent of your PHP environment. +The ability to utilize a cluster of servers to provide redundancy and/or fail-over +is also available. + +.. caution:: + + **Requirements:** The `Couchbase PHP extension`_ as well as a `Couchbase server`_ + must be installed, active, and running to use this adapter. Version ``2.6`` or + less than 3.0 of the `Couchbase PHP extension`_ is required for this adapter. + +This adapter expects a `Couchbase Bucket`_ instance to be passed as the first +parameter. A namespace and default cache lifetime can optionally be passed as +the second and third parameters:: + + use Symfony\Component\Cache\Adapter\CouchbaseBucketAdapter; + + $cache = new CouchbaseBucketAdapter( + // the client object that sets options and adds the server instance(s) + $client, + + // the name of bucket + $bucket, + + // a string prefixed to the keys of the items stored in this cache + $namespace, + + // the default lifetime (in seconds) for cache items that do not define their + // own lifetime, with a value 0 causing items to be stored indefinitely + $defaultLifetime + ); + + +Configure the Connection +------------------------ + +The :method:`Symfony\\Component\\Cache\\Adapter\\CouchbaseBucketAdapter::createConnection` +helper method allows creating and configuring a `Couchbase Bucket`_ class instance using a +`Data Source Name (DSN)`_ or an array of DSNs:: + + use Symfony\Component\Cache\Adapter\CouchbaseBucketAdapter; + + // pass a single DSN string to register a single server with the client + $client = CouchbaseBucketAdapter::createConnection( + 'couchbase://localhost' + // the DSN can include config options (pass them as a query string): + // 'couchbase://localhost:11210?operationTimeout=10' + // 'couchbase://localhost:11210?operationTimeout=10&configTimeout=20' + ); + + // pass an array of DSN strings to register multiple servers with the client + $client = CouchbaseBucketAdapter::createConnection([ + 'couchbase://10.0.0.100', + 'couchbase://10.0.0.101', + 'couchbase://10.0.0.102', + // etc... + ]); + + // a single DSN can define multiple servers using the following syntax: + // host[hostname-or-IP:port] (where port is optional). Sockets must include a trailing ':' + $client = CouchbaseBucketAdapter::createConnection( + 'couchbase:?host[localhost]&host[localhost:12345]' + ); + + +Configure the Options +--------------------- + +The :method:`Symfony\\Component\\Cache\\Adapter\\CouchbaseBucketAdapter::createConnection` +helper method also accepts an array of options as its second argument. The +expected format is an associative array of ``key => value`` pairs representing +option names and their respective values:: + + use Symfony\Component\Cache\Adapter\CouchbaseBucketAdapter; + + $client = CouchbaseBucketAdapter::createConnection( + // a DSN string or an array of DSN strings + [], + + // associative array of configuration options + [ + 'username' => 'xxxxxx', + 'password' => 'yyyyyy', + 'configTimeout' => '100', + ] + ); + +Available Options +~~~~~~~~~~~~~~~~~ + +``username`` (type: ``string``) + Username for connection ``CouchbaseCluster``. + +``password`` (type: ``string``) + Password of connection ``CouchbaseCluster``. + +``operationTimeout`` (type: ``int``, default: ``2500000``) + The operation timeout (in microseconds) is the maximum amount of time the library will + wait for an operation to receive a response before invoking its callback with a failure status. + +``configTimeout`` (type: ``int``, default: ``5000000``) + How long (in microseconds) the client will wait to obtain the initial configuration. + +``configNodeTimeout`` (type: ``int``, default: ``2000000``) + Per-node configuration timeout (in microseconds). + +``viewTimeout`` (type: ``int``, default: ``75000000``) + The I/O timeout (in microseconds) for HTTP requests to Couchbase Views API. + +``httpTimeout`` (type: ``int``, default: ``75000000``) + The I/O timeout (in microseconds) for HTTP queries (management API). + +``configDelay`` (type: ``int``, default: ``10000``) + Config refresh throttling + Modify the amount of time (in microseconds) before the configuration error threshold will forcefully be set to its maximum number forcing a configuration refresh. + +``htconfigIdleTimeout`` (type: ``int``, default: ``4294967295``) + Idling/Persistence for HTTP bootstrap (in microseconds). + +``durabilityInterval`` (type: ``int``, default: ``100000``) + The time (in microseconds) the client will wait between repeated probes to a given server. + +``durabilityTimeout`` (type: ``int``, default: ``5000000``) + The time (in microseconds) the client will spend sending repeated probes to a given key's vBucket masters and replicas before they are deemed not to have satisfied the durability requirements. + +.. tip:: + + Reference the `Couchbase Bucket`_ extension's `predefined constants`_ documentation + for additional information about the available options. + +.. _`Couchbase PHP extension`: https://docs.couchbase.com/sdk-api/couchbase-php-client-2.6.0/files/couchbase.html +.. _`predefined constants`: https://docs.couchbase.com/sdk-api/couchbase-php-client-2.6.0/classes/Couchbase.Bucket.html +.. _`Couchbase server`: https://couchbase.com/ +.. _`Couchbase Bucket`: https://docs.couchbase.com/sdk-api/couchbase-php-client-2.6.0/classes/Couchbase.Bucket.html +.. _`Data Source Name (DSN)`: https://en.wikipedia.org/wiki/Data_source_name diff --git a/components/cache/adapters/couchbasecollection_adapter.rst b/components/cache/adapters/couchbasecollection_adapter.rst new file mode 100644 index 00000000000..f00e54a6e2b --- /dev/null +++ b/components/cache/adapters/couchbasecollection_adapter.rst @@ -0,0 +1,143 @@ +.. index:: + single: Cache Pool + single: Couchabase Cache + +.. _couchbase-collection-adapter: + +Couchbase Collection Cache Adapter +================================== + +This adapter stores the values in-memory using one (or more) `Couchbase server`_ +instances. Unlike the :ref:`APCu adapter `, and similarly to the +:ref:`Memcached adapter `, it is not limited to the current server's +shared memory; you can store contents independent of your PHP environment. +The ability to utilize a cluster of servers to provide redundancy and/or fail-over +is also available. + +.. caution:: + + **Requirements:** The `Couchbase PHP extension`_ as well as a `Couchbase server`_ + must be installed, active, and running to use this adapter. Version ``3.0`` or + greater of the `Couchbase PHP extension`_ is required for this adapter. + +This adapter expects a `Couchbase Collection`_ instance to be passed as the first +parameter. A namespace and default cache lifetime can optionally be passed as +the second and third parameters:: + + use Symfony\Component\Cache\Adapter\CouchbaseCollectionAdapter; + + $cache = new CouchbaseCollectionAdapter( + // the client object that sets options and adds the server instance(s) + $client, + + // a string prefixed to the keys of the items stored in this cache + $namespace, + + // the default lifetime (in seconds) for cache items that do not define their + // own lifetime, with a value 0 causing items to be stored indefinitely + $defaultLifetime + ); + + +Configure the Connection +------------------------ + +The :method:`Symfony\\Component\\Cache\\Adapter\\CouchbaseCollectionAdapter::createConnection` +helper method allows creating and configuring a `Couchbase Collection`_ class instance using a +`Data Source Name (DSN)`_ or an array of DSNs:: + + use Symfony\Component\Cache\Adapter\CouchbaseCollectionAdapter; + + // pass a single DSN string to register a single server with the client + $client = CouchbaseCollectionAdapter::createConnection( + 'couchbase://localhost' + // the DSN can include config options (pass them as a query string): + // 'couchbase://localhost:11210?operationTimeout=10' + // 'couchbase://localhost:11210?operationTimeout=10&configTimout=20' + ); + + // pass an array of DSN strings to register multiple servers with the client + $client = CouchbaseCollectionAdapter::createConnection([ + 'couchbase://10.0.0.100', + 'couchbase://10.0.0.101', + 'couchbase://10.0.0.102', + // etc... + ]); + + // a single DSN can define multiple servers using the following syntax: + // host[hostname-or-IP:port] (where port is optional). Sockets must include a trailing ':' + $client = CouchbaseCollectionAdapter::createConnection( + 'couchbase:?host[localhost]&host[localhost:12345]' + ); + + +Configure the Options +--------------------- + +The :method:`Symfony\\Component\\Cache\\Adapter\\CouchbaseCollectionAdapter::createConnection` +helper method also accepts an array of options as its second argument. The +expected format is an associative array of ``key => value`` pairs representing +option names and their respective values:: + + use Symfony\Component\Cache\Adapter\CouchbaseCollectionAdapter; + + $client = CouchbaseCollectionAdapter::createConnection( + // a DSN string or an array of DSN strings + [], + + // associative array of configuration options + [ + 'username' => 'xxxxxx', + 'password' => 'yyyyyy', + 'configTimeout' => '100', + ] + ); + +Available Options +~~~~~~~~~~~~~~~~~ + +``username`` (type: ``string``) + Username for connection ``CouchbaseCluster``. + +``password`` (type: ``string``) + Password of connection ``CouchbaseCluster``. + +``operationTimeout`` (type: ``int``, default: ``2500000``) + The operation timeout (in microseconds) is the maximum amount of time the library will + wait for an operation to receive a response before invoking its callback with a failure status. + +``configTimeout`` (type: ``int``, default: ``5000000``) + How long (in microseconds) the client will wait to obtain the initial configuration. + +``configNodeTimeout`` (type: ``int``, default: ``2000000``) + Per-node configuration timeout (in microseconds). + +``viewTimeout`` (type: ``int``, default: ``75000000``) + The I/O timeout (in microseconds) for HTTP requests to Couchbase Views API. + +``httpTimeout`` (type: ``int``, default: ``75000000``) + The I/O timeout (in microseconds) for HTTP queries (management API). + +``configDelay`` (type: ``int``, default: ``10000``) + Config refresh throttling + Modify the amount of time (in microseconds) before the configuration error threshold will forcefully be set to its maximum number forcing a configuration refresh. + +``htconfigIdleTimeout`` (type: ``int``, default: ``4294967295``) + Idling/Persistence for HTTP bootstrap (in microseconds). + +``durabilityInterval`` (type: ``int``, default: ``100000``) + The time (in microseconds) the client will wait between repeated probes to a given server. + +``durabilityTimeout`` (type: ``int``, default: ``5000000``) + The time (in microseconds) the client will spend sending repeated probes to a given key's vBucket masters and replicas before they are deemed not to have satisfied the durability requirements. + +.. tip:: + + Reference the `Couchbase Collection`_ extension's `predefined constants`_ documentation + for additional information about the available options. + +.. _`Couchbase PHP extension`: https://docs.couchbase.com/sdk-api/couchbase-php-client/namespaces/couchbase.html +.. _`predefined constants`: https://docs.couchbase.com/sdk-api/couchbase-php-client/classes/Couchbase-Bucket.html +.. _`Couchbase server`: https://couchbase.com/ +.. _`Couchbase Collection`: https://docs.couchbase.com/sdk-api/couchbase-php-client/classes/Couchbase-Collection.html +.. _`Data Source Name (DSN)`: https://en.wikipedia.org/wiki/Data_source_name diff --git a/components/cache/adapters/doctrine_adapter.rst b/components/cache/adapters/doctrine_adapter.rst deleted file mode 100644 index 198ae19338c..00000000000 --- a/components/cache/adapters/doctrine_adapter.rst +++ /dev/null @@ -1,42 +0,0 @@ -.. index:: - single: Cache Pool - single: Doctrine Cache - -.. _doctrine-adapter: - -Doctrine Cache Adapter -====================== - -This adapter wraps any class extending the `Doctrine Cache`_ abstract provider, allowing -you to use these providers in your application as if they were Symfony Cache adapters. - -This adapter expects a ``\Doctrine\Common\Cache\CacheProvider`` instance as its first -parameter, and optionally a namespace and default cache lifetime as its second and -third parameters:: - - use Doctrine\Common\Cache\CacheProvider; - use Doctrine\Common\Cache\SQLite3Cache; - use Symfony\Component\Cache\Adapter\DoctrineAdapter; - - $provider = new SQLite3Cache(new \SQLite3(__DIR__.'/cache/data.sqlite'), 'youTableName'); - - $cache = new DoctrineAdapter( - - // a cache provider instance - CacheProvider $provider, - - // a string prefixed to the keys of the items stored in this cache - $namespace = '', - - // the default lifetime (in seconds) for cache items that do not define their - // own lifetime, with a value 0 causing items to be stored indefinitely (i.e. - // until the database table is truncated or its rows are otherwise deleted) - $defaultLifetime = 0 - ); - -.. tip:: - - A :class:`Symfony\\Component\\Cache\\DoctrineProvider` class is also provided by the - component to use any PSR6-compatible implementations with Doctrine-compatible classes. - -.. _`Doctrine Cache`: https://github.com/doctrine/cache diff --git a/components/cache/adapters/memcached_adapter.rst b/components/cache/adapters/memcached_adapter.rst index 1b8103433e1..009ead59cbd 100644 --- a/components/cache/adapters/memcached_adapter.rst +++ b/components/cache/adapters/memcached_adapter.rst @@ -70,10 +70,6 @@ helper method allows creating and configuring a `Memcached`_ class instance usin 'memcached:?host[localhost]&host[localhost:12345]&host[/some/memcached.sock:]=3' ); -.. versionadded:: 4.2 - - The option to define multiple servers in a single DSN was introduced in Symfony 4.2. - The `Data Source Name (DSN)`_ for this adapter must use the following format: .. code-block:: text diff --git a/components/cache/adapters/pdo_doctrine_dbal_adapter.rst b/components/cache/adapters/pdo_doctrine_dbal_adapter.rst index b840da76de7..9239f276f6a 100644 --- a/components/cache/adapters/pdo_doctrine_dbal_adapter.rst +++ b/components/cache/adapters/pdo_doctrine_dbal_adapter.rst @@ -7,16 +7,26 @@ PDO & Doctrine DBAL Cache Adapter ================================= -This adapter stores the cache items in an SQL database. It requires a :phpclass:`PDO`, -`Doctrine DBAL Connection`_, or `Data Source Name (DSN)`_ as its first parameter, and -optionally a namespace, default cache lifetime, and options array as its second, -third, and forth parameters:: +The PDO and Doctrine DBAL adapters store the cache items in a table of an SQL database. + +.. note:: + + These adapters implement :class:`Symfony\\Component\\Cache\\PruneableInterface`, + allowing for manual :ref:`pruning of expired cache entries ` + by calling the ``prune()`` method. + +Using PHP PDO +------------- + +The :class:`Symfony\\Component\\Cache\\Adapter\\PdoAdapter` requires a :phpclass:`PDO`, +or `Data Source Name (DSN)`_ as its first parameter. You can pass a namespace, +default cache lifetime, and options array as the other optional arguments:: use Symfony\Component\Cache\Adapter\PdoAdapter; $cache = new PdoAdapter( - // a PDO, a Doctrine DBAL connection or DSN for lazy connecting through PDO + // a PDO connection or DSN for lazy connecting through PDO $databaseConnectionOrDSN, // the string prefixed to the keys of the items stored in this cache @@ -40,13 +50,43 @@ your code. .. tip:: When passed a `Data Source Name (DSN)`_ string (instead of a database connection - class instance), the connection will be lazy-loaded when needed. + class instance), the connection will be lazy-loaded when needed. DBAL Connection + are lazy-loaded by default; some additional options may be necessary to detect + the database engine and version without opening the connection. + +Using Doctrine DBAL +------------------- + +The :class:`Symfony\\Component\\Cache\\Adapter\\DoctrineDbalAdapter` requires a +`Doctrine DBAL Connection`_, or `Doctrine DBAL URL`_ as its first parameter. +You can pass a namespace, default cache lifetime, and options array as the other +optional arguments:: + + use Symfony\Component\Cache\Adapter\DoctrineDbalAdapter; + + $cache = new DoctrineDbalAdapter( + + // a Doctrine DBAL connection or DBAL URL + $databaseConnectionOrURL, + + // the string prefixed to the keys of the items stored in this cache + $namespace = '', + + // the default lifetime (in seconds) for cache items that do not define their + // own lifetime, with a value 0 causing items to be stored indefinitely (i.e. + // until the database table is truncated or its rows are otherwise deleted) + $defaultLifetime = 0, + + // an array of options for configuring the database table and connection + $options = [] + ); .. note:: - This adapter implements :class:`Symfony\\Component\\Cache\\PruneableInterface`, - allowing for manual :ref:`pruning of expired cache entries ` by - calling its ``prune()`` method. + DBAL Connection are lazy-loaded by default; some additional options may be + necessary to detect the database engine and version without opening the + connection. .. _`Doctrine DBAL Connection`: https://github.com/doctrine/dbal/blob/master/src/Connection.php +.. _`Doctrine DBAL URL`: https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url .. _`Data Source Name (DSN)`: https://en.wikipedia.org/wiki/Data_source_name diff --git a/components/cache/adapters/redis_adapter.rst b/components/cache/adapters/redis_adapter.rst index 05462bda38c..4ffbbb6e53b 100644 --- a/components/cache/adapters/redis_adapter.rst +++ b/components/cache/adapters/redis_adapter.rst @@ -96,21 +96,13 @@ Below are common examples of valid DSNs showing a combination of available value ); `Redis Sentinel`_, which provides high availability for Redis, is also supported -when using the Predis library. Use the ``redis_sentinel`` parameter to set the -name of your service group:: +when using the PHP Redis Extension v5.2+ or the Predis library. Use the ``redis_sentinel`` +parameter to set the name of your service group:: RedisAdapter::createConnection( 'redis:?host[redis1:26379]&host[redis2:26379]&host[redis3:26379]&redis_sentinel=mymaster' ); -.. versionadded:: 4.2 - - The option to define multiple servers in a single DSN was introduced in Symfony 4.2. - -.. versionadded:: 4.4 - - Redis Sentinel support was introduced in Symfony 4.4. - .. note:: See the :class:`Symfony\\Component\\Cache\\Traits\\RedisTrait` for more options diff --git a/components/cache/psr6_psr16_adapters.rst b/components/cache/psr6_psr16_adapters.rst index 28a41fca0e7..6b98d26744b 100644 --- a/components/cache/psr6_psr16_adapters.rst +++ b/components/cache/psr6_psr16_adapters.rst @@ -46,10 +46,6 @@ this use-case:: // now use this wherever you want $githubApiClient = new GitHubApiClient($psr6Cache); -.. versionadded:: 4.3 - - The ``Psr16Adapter`` class was introduced in Symfony 4.3. - Using a PSR-6 Cache Object as a PSR-16 Cache -------------------------------------------- @@ -87,8 +83,4 @@ this use-case:: // now use this wherever you want $githubApiClient = new GitHubApiClient($psr16Cache); -.. versionadded:: 4.3 - - The ``Psr16Cache`` class was introduced in Symfony 4.3. - .. _`PSR-16`: https://www.php-fig.org/psr/psr-16/ diff --git a/components/config/definition.rst b/components/config/definition.rst index 2864bddb570..8ad8ae1b0c9 100644 --- a/components/config/definition.rst +++ b/components/config/definition.rst @@ -68,10 +68,6 @@ implements the :class:`Symfony\\Component\\Config\\Definition\\ConfigurationInte } } -.. deprecated:: 4.2 - - Not passing the root node name to ``TreeBuilder`` was deprecated in Symfony 4.2. - Adding Node Definitions to the Tree ----------------------------------- @@ -447,11 +443,15 @@ method:: ->children() ->integerNode('old_option') // this outputs the following generic deprecation message: - // The child node "old_option" at path "..." is deprecated. - ->setDeprecated() + // Since acme/package 1.2: The child node "old_option" at path "..." is deprecated. + ->setDeprecated('acme/package', '1.2') // you can also pass a custom deprecation message (%node% and %path% placeholders are available): - ->setDeprecated('The "%node%" option is deprecated. Use "new_config_option" instead.') + ->setDeprecated( + 'acme/package', + '1.2', + 'The "%node%" option is deprecated. Use "new_config_option" instead.' + ) ->end() ->end() ; diff --git a/components/console/events.rst b/components/console/events.rst index 7183c2e75f7..a4bdcdfb5b9 100644 --- a/components/console/events.rst +++ b/components/console/events.rst @@ -154,4 +154,70 @@ Listeners receive a It is then dispatched just after the ``ConsoleEvents::ERROR`` event. The exit code received in this case is the exception code. +The ``ConsoleEvents::SIGNAL`` Event +----------------------------------- + +**Typical Purposes**: To perform some actions after the command execution was interrupted. + +`Signals`_ are asynchronous notifications sent to a process in order to notify +it of an event that occurred. For example, when you press ``Ctrl + C`` in a +command, the operating system sends the ``SIGINT`` signal to it. + +When a command is interrupted, Symfony dispatches the ``ConsoleEvents::SIGNAL`` +event. Listen to this event so you can perform some actions (e.g. logging some +results, cleaning some temporary files, etc.) before finishing the command execution. + +Listeners receive a +:class:`Symfony\\Component\\Console\\Event\\ConsoleSignalEvent` event:: + + use Symfony\Component\Console\ConsoleEvents; + use Symfony\Component\Console\Event\ConsoleSignalEvent; + + $dispatcher->addListener(ConsoleEvents::SIGNAL, function (ConsoleSignalEvent $event) { + + // gets the signal number + $signal = $event->getHandlingSignal(); + + if (\SIGINT === $signal) { + echo "bye bye!"; + } + }); + +.. tip:: + + All the available signals (``SIGINT``, ``SIGQUIT``, etc.) are defined as + `constants of the PCNTL PHP extension`_. + +If you use the Console component inside a Symfony application, commands can +handle signals themselves. To do so, implement the +``SignalableCommandInterface`` and subscribe to one or more signals:: + + // src/Command/SomeCommand.php + namespace App\Command; + + use Symfony\Component\Console\Command\Command; + use Symfony\Component\Console\Command\SignalableCommandInterface; + + class SomeCommand extends Command implements SignalableCommandInterface + { + // ... + + public function getSubscribedSignals(): array + { + // return here any of the constants defined by PCNTL extension + return [\SIGINT, \SIGTERM]; + } + + public function handleSignal(int $signal) + { + if (\SIGINT === $signal) { + // ... + } + + // ... + } + } + .. _`reserved exit codes`: https://www.tldp.org/LDP/abs/html/exitcodes.html +.. _`Signals`: https://en.wikipedia.org/wiki/Signal_(IPC) +.. _`constants of the PCNTL PHP extension`: https://www.php.net/manual/en/pcntl.constants.php diff --git a/components/console/helpers/cursor.rst b/components/console/helpers/cursor.rst new file mode 100644 index 00000000000..c7a6556a9cb --- /dev/null +++ b/components/console/helpers/cursor.rst @@ -0,0 +1,99 @@ +.. index:: + single: Console Helpers; Cursor Helper + +Cursor Helper +============= + +The :class:`Symfony\\Component\\Console\\Cursor` allows you to change the +cursor position in a console command. This allows you to write on any position +of the output: + +.. image:: /_images/components/console/cursor.gif + :align: center + +.. code-block:: php + + // src/Command/MyCommand.php + namespace App\Command; + + use Symfony\Component\Console\Command\Command; + use Symfony\Component\Console\Cursor; + use Symfony\Component\Console\Input\InputInterface; + use Symfony\Component\Console\Output\OutputInterface; + + class MyCommand extends Command + { + // ... + + public function execute(InputInterface $input, OutputInterface $output): int + { + // ... + + $cursor = new Cursor($output); + + // moves the cursor to a specific column (1st argument) and + // row (2nd argument) position + $cursor->moveToPosition(7, 11); + + // and write text on this position using the output + $output->write('My text'); + + // ... + } + } + +Using the cursor +---------------- + +Moving the cursor +................. + +There are few methods to control moving the command cursor:: + + // moves the cursor 1 line up from its current position + $cursor->moveUp(); + + // moves the cursor 3 lines up from its current position + $cursor->moveUp(3); + + // same for down + $cursor->moveDown(); + + // moves the cursor 1 column right from its current position + $cursor->moveRight(); + + // moves the cursor 3 columns right from its current position + $cursor->moveRight(3); + + // same for left + $cursor->moveLeft(); + + // move the cursor to a specific (column, row) position from the + // top-left position of the terminal + $cursor->moveToPosition(7, 11); + +You can get the current command's cursor position by using:: + + $position = $cursor->getCurrentPosition(); + // $position[0] // columns (aka x coordinate) + // $position[1] // rows (aka y coordinate) + +Clearing output +............... + +The cursor can also clear some output on the screen:: + + // clears all the output from the current line + $cursor->clearLine(); + + // clears all the output from the current line after the current position + $cursor->clearLineAfter(); + + // clears all the output from the cursors' current position to the end of the screen + $cursor->clearOutput(); + + // clears the entire screen + $cursor->clearScreen(); + +You also can leverage the :method:`Symfony\\Component\\Console\\Cursor::show` +and :method:`Symfony\\Component\\Console\\Cursor::hide` methods on the cursor. diff --git a/components/console/helpers/index.rst b/components/console/helpers/index.rst index 5f328d47472..09546769655 100644 --- a/components/console/helpers/index.rst +++ b/components/console/helpers/index.rst @@ -13,6 +13,7 @@ The Console Helpers questionhelper table debug_formatter + cursor The Console component comes with some useful helpers. These helpers contain functions to ease some common tasks. diff --git a/components/console/helpers/map.rst.inc b/components/console/helpers/map.rst.inc index 68e1e722a87..8f9ce0ca0f3 100644 --- a/components/console/helpers/map.rst.inc +++ b/components/console/helpers/map.rst.inc @@ -4,3 +4,4 @@ * :doc:`/components/console/helpers/questionhelper` * :doc:`/components/console/helpers/table` * :doc:`/components/console/helpers/debug_formatter` +* :doc:`/components/console/helpers/cursor` diff --git a/components/console/helpers/progressbar.rst b/components/console/helpers/progressbar.rst index 2e2de44e399..2a2c9473cff 100644 --- a/components/console/helpers/progressbar.rst +++ b/components/console/helpers/progressbar.rst @@ -56,11 +56,6 @@ you can also set the current progress by calling the to redraw every N iterations. By default, redraw frequency is **100ms** or **10%** of your ``max``. - .. versionadded:: 4.4 - - The ``minSecondsBetweenRedraws()`` and ``maxSecondsBetweenRedraws()`` - methods were introduced in Symfony 4.4. - If you don't know the exact number of steps in advance, set it to a reasonable value and then call the ``setMaxSteps()`` method to update it as needed:: @@ -130,10 +125,6 @@ The previous code will output: 1/2 [==============>-------------] 50% 2/2 [============================] 100% -.. versionadded:: 4.3 - - The ``iterate()`` method was introduced in Symfony 4.3. - Customizing the Progress Bar ---------------------------- @@ -322,11 +313,6 @@ to display it can be customized:: $progressBar->advance(); } - .. versionadded:: 4.4 - - The ``minSecondsBetweenRedraws`` and ``maxSecondsBetweenRedraws()`` methods - were introduced in Symfony 4.4. - Custom Placeholders ~~~~~~~~~~~~~~~~~~~ @@ -363,7 +349,7 @@ placeholder before displaying the progress bar:: // 0/100 -- Start $progressBar->setMessage('Task is in progress...'); - $progressBar->advance(); + $progressBar->advance(); // 1/100 -- Task is in progress... Messages can be combined with custom placeholders too. In this example, the diff --git a/components/console/helpers/questionhelper.rst b/components/console/helpers/questionhelper.rst index d6cdb7c67ab..d5ddf83a2e3 100644 --- a/components/console/helpers/questionhelper.rst +++ b/components/console/helpers/questionhelper.rst @@ -40,7 +40,7 @@ the following to your command:: $question = new ConfirmationQuestion('Continue with this action?', false); if (!$helper->ask($input, $output, $question)) { - return 0; + return Command::SUCCESS; } } } @@ -105,6 +105,7 @@ from a predefined list:: $helper = $this->getHelper('question'); $question = new ChoiceQuestion( 'Please select your favorite color (defaults to red)', + // choices can also be PHP objects that implement __toString() method ['red', 'blue', 'yellow'], 0 ); @@ -214,10 +215,6 @@ provide a callback function to dynamically generate suggestions:: $filePath = $helper->ask($input, $output, $question); } -.. versionadded:: 4.3 - - The ``setAutocompleterCallback()`` method was introduced in Symfony 4.3. - Do not Trim the Answer ~~~~~~~~~~~~~~~~~~~~~~ @@ -238,9 +235,30 @@ You can also specify if you want to not trim the answer by setting it directly w $name = $helper->ask($input, $output, $question); } -.. versionadded:: 4.4 +Accept Multiline Answers +~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the question helper stops reading user input when it receives a newline +character (i.e., when the user hits ``ENTER`` once). However, you may specify that +the response to a question should allow multiline answers by passing ``true`` to +:method:`Symfony\\Component\\Console\\Question\\Question::setMultiline`:: + + use Symfony\Component\Console\Question\Question; + + // ... + public function execute(InputInterface $input, OutputInterface $output) + { + // ... + $helper = $this->getHelper('question'); - The ``setTrimmable()`` method was introduced in Symfony 4.4. + $question = new Question('How do you solve world peace?'); + $question->setMultiline(true); + + $answer = $helper->ask($input, $output, $question); + } + +Multiline questions stop reading user input after receiving an end-of-transmission +control character (``Ctrl-D`` on Unix systems or ``Ctrl-Z`` on Windows). Hiding the User's Response ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -326,6 +344,8 @@ method:: of the validator. If the answer is invalid, don't throw exceptions in the normalizer and let the validator handle those errors. +.. _console-validate-question-answer: + Validating the Answer --------------------- @@ -370,6 +390,22 @@ If you reach this max number it will use the default value. Using ``null`` means the number of attempts is infinite. The user will be asked as long as they provide an invalid answer and will only be able to proceed if their input is valid. +.. tip:: + + You can even use the :doc:`Validator ` component to + validate the input by using the :method:`Symfony\\Component\\Validator\\Validation::createCallable` + method:: + + use Symfony\Component\Validator\Constraints\Regex; + use Symfony\Component\Validator\Validation; + + $question = new Question('Please enter the name of the bundle', 'AcmeDemoBundle'); + $validation = Validation::createCallable(new Regex([ + 'pattern' => '/^[a-zA-Z]+Bundle$', + 'message' => 'The name of the bundle should be suffixed with \'Bundle\'', + ])); + $question->setValidator($validation); + Validating a Hidden Response ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -403,8 +439,6 @@ Testing a Command that Expects Input If you want to write a unit test for a command which expects some kind of input from the command line, you need to set the inputs that the command expects:: - use Symfony\Component\Console\Helper\HelperSet; - use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Tester\CommandTester; // ... diff --git a/components/console/helpers/table.rst b/components/console/helpers/table.rst index 9b479d0f9a6..533e5466f56 100644 --- a/components/console/helpers/table.rst +++ b/components/console/helpers/table.rst @@ -265,6 +265,35 @@ Here is a full list of things you can customize: This method can also be used to override a built-in style. +In addition to the built-in table styles, you can also apply different styles +to each table cell via :class:`Symfony\\Component\\Console\\Helper\\TableCellStyle`:: + + use Symfony\Component\Console\Helper\Table; + use Symfony\Component\Console\Helper\TableCellStyle; + + $table = new Table($output); + + $table->setRows([ + [ + '978-0804169127', + new TableCell( + 'Divine Comedy', + [ + 'style' => new TableCellStyle([ + 'align' => 'center', + 'fg' => 'red', + 'bg' => 'green', + + // or + 'cellFormat' => '%s', + ]) + ] + ) + ], + ]); + + $table->render(); + Spanning Multiple Columns and Rows ---------------------------------- diff --git a/components/console/single_command_tool.rst b/components/console/single_command_tool.rst index ff4c2be8f1c..08a51cd943d 100644 --- a/components/console/single_command_tool.rst +++ b/components/console/single_command_tool.rst @@ -12,27 +12,22 @@ it is possible to remove this need by declaring a single command application:: register('echo') - ->addArgument('foo', InputArgument::OPTIONAL, 'The directory') - ->addOption('bar', null, InputOption::VALUE_REQUIRED) - ->setCode(function(InputInterface $input, OutputInterface $output) { - // output arguments and options - }) - ->getApplication() - ->setDefaultCommand('echo', true) // Single command application + use Symfony\Component\Console\SingleCommandApplication; + + (new SingleCommandApplication()) + ->setName('My Super Command') // Optional + ->setVersion('1.0.0') // Optional + ->addArgument('foo', InputArgument::OPTIONAL, 'The directory') + ->addOption('bar', null, InputOption::VALUE_REQUIRED) + ->setCode(function (InputInterface $input, OutputInterface $output) { + // output arguments and options + }) ->run(); -The :method:`Symfony\\Component\\Console\\Application::setDefaultCommand` method -accepts a boolean as the second parameter. If true, the command ``echo`` will then -always be used, without having to pass its name. - You can still register a command as usual:: #!/usr/bin/env php @@ -49,3 +44,7 @@ You can still register a command as usual:: $application->setDefaultCommand($command->getName(), true); $application->run(); + +The :method:`Symfony\\Component\\Console\\Application::setDefaultCommand` method +accepts a boolean as second parameter. If true, the command ``echo`` will then +always be used, without having to pass its name. diff --git a/components/contracts.rst b/components/contracts.rst index 1f1cc3f6adc..a1ae32192f6 100644 --- a/components/contracts.rst +++ b/components/contracts.rst @@ -61,7 +61,7 @@ convention. For example: { "...": "...", "provide": { - "symfony/cache-implementation": "1.0" + "symfony/cache-implementation": "3.0" } } diff --git a/components/css_selector.rst b/components/css_selector.rst index 0d3dbf8dbf9..649a34293a4 100644 --- a/components/css_selector.rst +++ b/components/css_selector.rst @@ -98,10 +98,6 @@ Pseudo-classes are partially supported: ``li:first-of-type``) but not with the ``*`` selector). * Supported: ``*:only-of-type``. -.. versionadded:: 4.4 - - The support for ``*:only-of-type`` was introduced in Symfony 4.4. - Learn more ---------- diff --git a/components/dependency_injection.rst b/components/dependency_injection.rst index 486f89e599d..91aacc91b01 100644 --- a/components/dependency_injection.rst +++ b/components/dependency_injection.rst @@ -299,15 +299,14 @@ config files: $services = $configurator->services(); $services->set('mailer', 'Mailer') - ->args(['%mailer.transport%']) + ->args([param('mailer.transport')]) ; $services->set('newsletter_manager', 'NewsletterManager') - ->call('setMailer', [ref('mailer')]) + ->call('setMailer', [service('mailer')]) ; }; - Learn More ---------- diff --git a/components/dom_crawler.rst b/components/dom_crawler.rst index fd35b5e7fd8..02ddadff58c 100644 --- a/components/dom_crawler.rst +++ b/components/dom_crawler.rst @@ -77,10 +77,6 @@ tree. The DomCrawler component will use it automatically when the content has an HTML5 doctype. - .. versionadded:: 4.3 - - The automatic support of the html5-php library was introduced in Symfony 4.3. - Node Filtering ~~~~~~~~~~~~~~ @@ -170,10 +166,6 @@ Verify if the current node matches a selector:: $crawler->matches('p.lorem'); -.. versionadded:: 4.4 - - The ``matches()`` method was introduced in Symfony 4.4. - Node Traversing ~~~~~~~~~~~~~~~ @@ -195,10 +187,10 @@ Get the same level nodes after or before the current selection:: $crawler->filter('body > p')->nextAll(); $crawler->filter('body > p')->previousAll(); -Get all the child or parent nodes:: +Get all the child or ancestor nodes:: $crawler->filter('body')->children(); - $crawler->filter('body > p')->parents(); + $crawler->filter('body > p')->ancestors(); Get all the direct child nodes matching a CSS selector:: @@ -208,10 +200,6 @@ Get the first parent (heading toward the document root) of the element that matc $crawler->closest('p.lorem'); -.. versionadded:: 4.4 - - The ``closest()`` method was introduced in Symfony 4.4. - .. note:: All the traversal methods return a new :class:`Symfony\\Component\\DomCrawler\\Crawler` @@ -233,17 +221,16 @@ Access the value of the first node of the current selection:: // avoid the exception passing an argument that text() returns when node does not exist $message = $crawler->filterXPath('//body/p')->text('Default text content'); - // pass TRUE as the second argument of text() to remove all extra white spaces, including - // the internal ones (e.g. " foo\n bar baz \n " is returned as "foo bar baz") - $crawler->filterXPath('//body/p')->text('Default text content', true); + // by default, text() trims white spaces, including the internal ones + // (e.g. " foo\n bar baz \n " is returned as "foo bar baz") + // pass FALSE as the second argument to return the original text unchanged + $crawler->filterXPath('//body/p')->text('Default text content', false); -.. versionadded:: 4.3 - - The default argument of ``text()`` was introduced in Symfony 4.3. - -.. versionadded:: 4.4 - - The option to trim white spaces in ``text()`` was introduced in Symfony 4.4. + // innerText() is similar to text() but only returns the text that is + // the direct descendant of the current node, excluding any child nodes + $text = $crawler->filterXPath('//body/p')->innerText(); + // if content is

Foo Bar

+ // innerText() returns 'Foo' and text() returns 'Foo Bar' Access the attribute value of the first node of the current selection:: @@ -261,10 +248,6 @@ Extract attribute and/or node values from the list of nodes:: Special attribute ``_text`` represents a node value, while ``_name`` represents the element name (the HTML tag name). - .. versionadded:: 4.3 - - The special attribute ``_name`` was introduced in Symfony 4.3. - Call an anonymous function on each node of the list:: use Symfony\Component\DomCrawler\Crawler; @@ -360,19 +343,11 @@ and :phpclass:`DOMNode` objects:: // avoid the exception passing an argument that html() returns when node does not exist $html = $crawler->html('Default HTML content'); - .. versionadded:: 4.3 - - The default argument of ``html()`` was introduced in Symfony 4.3. - Or you can get the outer HTML of the first node using :method:`Symfony\\Component\\DomCrawler\\Crawler::outerHtml`:: $html = $crawler->outerHtml(); - .. versionadded:: 4.4 - - The ``outerHtml()`` method was introduced in Symfony 4.4. - Expression Evaluation ~~~~~~~~~~~~~~~~~~~~~ @@ -524,10 +499,6 @@ useful methods for working with forms:: $method = $form->getMethod(); $name = $form->getName(); -.. versionadded:: 4.4 - - The ``getName()`` method was introduced in Symfony 4.4. - The :method:`Symfony\\Component\\DomCrawler\\Form::getUri` method does more than just return the ``action`` attribute of the form. If the form method is GET, then it mimics the browser's behavior and returns the ``action`` @@ -661,6 +632,19 @@ the whole form or specific field(s):: $form->disableValidation(); $form['country']->select('Invalid value'); +Resolving a URI +~~~~~~~~~~~~~~~ + +The :class:`Symfony\\Component\\DomCrawler\\UriResolver` class takes an URI +(relative, absolute, fragment, etc.) and turns it into an absolute URI against +another given base URI:: + + use Symfony\Component\DomCrawler\UriResolver; + + UriResolver::resolve('/foo', 'http://localhost/bar/foo/'); // http://localhost/foo + UriResolver::resolve('?a=b', 'http://localhost/bar#foo'); // http://localhost/bar?a=b + UriResolver::resolve('../../', 'http://localhost/'); // http://localhost/ + Learn more ---------- diff --git a/components/event_dispatcher.rst b/components/event_dispatcher.rst index dd1ce9c310a..45955506e5c 100644 --- a/components/event_dispatcher.rst +++ b/components/event_dispatcher.rst @@ -254,21 +254,11 @@ determine which instance is passed. Note that ``AddEventAliasesPass`` has to be processed before ``RegisterListenersPass``. - By default, the listeners pass assumes that the event dispatcher's service + The listeners pass assumes that the event dispatcher's service id is ``event_dispatcher``, that event listeners are tagged with the ``kernel.event_listener`` tag, that event subscribers are tagged with the ``kernel.event_subscriber`` tag and that the alias mapping is - stored as parameter ``event_dispatcher.event_aliases``. You can change these - default values by passing custom values to the constructors of - ``RegisterListenersPass`` and ``AddEventAliasesPass``. - -.. versionadded:: 4.3 - - Aliasing event names is possible since Symfony 4.3. - -.. versionadded:: 4.4 - - The ``AddEventAliasesPass`` class was introduced in Symfony 4.4. + stored as parameter ``event_dispatcher.event_aliases``. .. _event_dispatcher-closures-as-listeners: diff --git a/components/event_dispatcher/traceable_dispatcher.rst b/components/event_dispatcher/traceable_dispatcher.rst index 87d58023445..33a98a2336b 100644 --- a/components/event_dispatcher/traceable_dispatcher.rst +++ b/components/event_dispatcher/traceable_dispatcher.rst @@ -41,10 +41,10 @@ to register event listeners and dispatch events:: $traceableEventDispatcher->dispatch($event, 'event.the_name'); After your application has been processed, you can use the -:method:`Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcherInterface::getCalledListeners` +:method:`Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher::getCalledListeners` method to retrieve an array of event listeners that have been called in your application. Similarly, the -:method:`Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcherInterface::getNotCalledListeners` +:method:`Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher::getNotCalledListeners` method returns an array of event listeners that have not been called:: // ... diff --git a/components/expression_language/syntax.rst b/components/expression_language/syntax.rst index b78ac907ca8..a4c17f81a27 100644 --- a/components/expression_language/syntax.rst +++ b/components/expression_language/syntax.rst @@ -21,10 +21,6 @@ The component supports: * **null** - ``null`` * **exponential** - also known as scientific (e.g. ``1.99E+3`` or ``1e-2``) - .. versionadded:: 4.4 - - The ``exponential`` literal was introduced in Symfony 4.4. - .. caution:: A backslash (``\``) must be escaped by 4 backslashes (``\\\\``) in a string diff --git a/components/filesystem.rst b/components/filesystem.rst index 46c88d73d7d..d99e6036a27 100644 --- a/components/filesystem.rst +++ b/components/filesystem.rst @@ -4,7 +4,8 @@ The Filesystem Component ======================== - The Filesystem component provides basic utilities for the filesystem. + The Filesystem component provides platform-independent utilities for + filesystem operations and for file/directory paths manipulation. Installation ------------ @@ -18,20 +19,26 @@ Installation Usage ----- -The :class:`Symfony\\Component\\Filesystem\\Filesystem` class is the unique -endpoint for filesystem operations:: +The component contains two main classes called :class:`Symfony\\Component\\Filesystem\\Filesystem` +and :class:`Symfony\\Component\\Filesystem\\Path`:: use Symfony\Component\Filesystem\Exception\IOExceptionInterface; use Symfony\Component\Filesystem\Filesystem; + use Symfony\Component\Filesystem\Path; $filesystem = new Filesystem(); try { - $filesystem->mkdir(sys_get_temp_dir().'/'.random_int(0, 1000)); + $filesystem->mkdir( + Path::normalize(sys_get_temp_dir().'/'.random_int(0, 1000)), + ); } catch (IOExceptionInterface $exception) { echo "An error occurred while creating your directory at ".$exception->getPath(); } +Filesystem Utilities +-------------------- + ``mkdir`` ~~~~~~~~~ @@ -224,6 +231,11 @@ Its behavior is the following:: * 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`` ~~~~~~~~~~~~~~~~~~~~ @@ -272,6 +284,8 @@ 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`` ~~~~~~~~~~~~ @@ -293,10 +307,190 @@ The ``file.txt`` file contains ``Hello World`` now. contents at the end of some file:: $filesystem->appendToFile('logs.txt', 'Email sent to user@example.com'); + // the third argument tells whether the file should be locked when writing to it + $filesystem->appendToFile('logs.txt', 'Email sent to user@example.com', true); If either the file or its containing directory doesn't exist, this method creates them before appending the contents. +Path Manipulation Utilities +--------------------------- + +Dealing with file paths usually involves some difficulties: + +- Platform differences: file paths look different on different platforms. UNIX + file paths start with a slash ("/"), while Windows file paths start with a + system drive ("C:"). UNIX uses forward slashes, while Windows uses backslashes + by default. +- Absolute/relative paths: web applications frequently need to deal with absolute + and relative paths. Converting one to the other properly is tricky and repetitive. + +:class:`Symfony\\Component\\Filesystem\\Path` provides utility methods to tackle +those issues. + +Canonicalization +~~~~~~~~~~~~~~~~ + +Returns the shortest path name equivalent to the given path. It applies the +following rules iteratively until no further processing can be done: + +- "." segments are removed; +- ".." segments are resolved; +- backslashes ("\\") are converted into forward slashes ("/"); +- root paths ("/" and "C:/") always terminate with a slash; +- non-root paths never terminate with a slash; +- schemes (such as "phar://") are kept; +- replace "~" with the user's home directory. + +You can canonicalize a path with :method:`Symfony\\Component\\Filesystem\\Path::canonicalize`:: + + echo Path::canonicalize('/var/www/vhost/webmozart/../config.ini'); + // => /var/www/vhost/config.ini + +You can pass absolute paths and relative paths to the +:method:`Symfony\\Component\\Filesystem\\Path::canonicalize` method. When a +relative path is passed, ".." segments at the beginning of the path are kept:: + + echo Path::canonicalize('../uploads/../config/config.yaml'); + // => ../config/config.yaml + +Malformed paths are returned unchanged:: + + echo Path::canonicalize('C:Programs/PHP/php.ini'); + // => C:Programs/PHP/php.ini + +Converting Absolute/Relative Paths +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Absolute/relative paths can be converted with the methods +:method:`Symfony\\Component\\Filesystem\\Path::makeAbsolute` +and :method:`Symfony\\Component\\Filesystem\\Path::makeRelative`. + +:method:`Symfony\\Component\\Filesystem\\Path::makeAbsolute` method expects a +relative path and a base path to base that relative path upon:: + + echo Path::makeAbsolute('config/config.yaml', '/var/www/project'); + // => /var/www/project/config/config.yaml + +If an absolute path is passed in the first argument, the absolute path is +returned unchanged:: + + echo Path::makeAbsolute('/usr/share/lib/config.ini', '/var/www/project'); + // => /usr/share/lib/config.ini + +The method resolves ".." segments, if there are any:: + + echo Path::makeAbsolute('../config/config.yaml', '/var/www/project/uploads'); + // => /var/www/project/config/config.yaml + +This method is very useful if you want to be able to accept relative paths (for +example, relative to the root directory of your project) and absolute paths at +the same time. + +:method:`Symfony\\Component\\Filesystem\\Path::makeRelative` is the inverse +operation to :method:`Symfony\\Component\\Filesystem\\Path::makeAbsolute`:: + + echo Path::makeRelative('/var/www/project/config/config.yaml', '/var/www/project'); + // => config/config.yaml + +If the path is not within the base path, the method will prepend ".." segments +as necessary:: + + echo Path::makeRelative('/var/www/project/config/config.yaml', '/var/www/project/uploads'); + // => ../config/config.yaml + +Use :method:`Symfony\\Component\\Filesystem\\Path::makeAbsolute` and +:method:`Symfony\\Component\\Filesystem\\Path::makeRelative` to check whether a +path is absolute or relative:: + + Path::isAbsolute('C:\Programs\PHP\php.ini') + // => true + +All four methods internally canonicalize the passed path. + +Finding Longest Common Base Paths +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When you store absolute file paths on the file system, this leads to a lot of +duplicated information:: + + return [ + '/var/www/vhosts/project/httpdocs/config/config.yaml', + '/var/www/vhosts/project/httpdocs/config/routing.yaml', + '/var/www/vhosts/project/httpdocs/config/services.yaml', + '/var/www/vhosts/project/httpdocs/images/banana.gif', + '/var/www/vhosts/project/httpdocs/uploads/images/nicer-banana.gif', + ]; + +Especially when storing many paths, the amount of duplicated information is +noticeable. You can use :method:`Symfony\\Component\\Filesystem\\Path::getLongestCommonBasePath` +to check a list of paths for a common base path:: + + Path::getLongestCommonBasePath( + '/var/www/vhosts/project/httpdocs/config/config.yaml', + '/var/www/vhosts/project/httpdocs/config/routing.yaml', + '/var/www/vhosts/project/httpdocs/config/services.yaml', + '/var/www/vhosts/project/httpdocs/images/banana.gif', + '/var/www/vhosts/project/httpdocs/uploads/images/nicer-banana.gif' + ); + // => /var/www/vhosts/project/httpdocs + +Use this path together with :method:`Symfony\\Component\\Filesystem\\Path::makeRelative` +to shorten the stored paths:: + + $bp = '/var/www/vhosts/project/httpdocs'; + + return [ + $bp.'/config/config.yaml', + $bp.'/config/routing.yaml', + $bp.'/config/services.yaml', + $bp.'/images/banana.gif', + $bp.'/uploads/images/nicer-banana.gif', + ]; + +:method:`Symfony\\Component\\Filesystem\\Path::getLongestCommonBasePath` always +returns canonical paths. + +Use :method:`Symfony\\Component\\Filesystem\\Path::isBasePath` to test whether a +path is a base path of another path:: + + Path::isBasePath("/var/www", "/var/www/project"); + // => true + + Path::isBasePath("/var/www", "/var/www/project/.."); + // => true + + Path::isBasePath("/var/www", "/var/www/project/../.."); + // => false + +Finding Directories/Root Directories +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +PHP offers the function :phpfunction:`dirname` to obtain the directory path of a +file path. This method has a few quirks:: + +- `dirname()` does not accept backslashes on UNIX +- `dirname("C:/Programs")` returns "C:", not "C:/" +- `dirname("C:/")` returns ".", not "C:/" +- `dirname("C:")` returns ".", not "C:/" +- `dirname("Programs")` returns ".", not "" +- `dirname()` does not canonicalize the result + +:method:`Symfony\\Component\\Filesystem\\Path::getDirectory` fixes these +shortcomings:: + + echo Path::getDirectory("C:\Programs"); + // => C:/ + +Additionally, you can use :method:`Symfony\\Component\\Filesystem\\Path::getRoot` +to obtain the root of a path:: + + echo Path::getRoot("/etc/apache2/sites-available"); + // => / + + echo Path::getRoot("C:\Programs\Apache\Config"); + // => C:/ + Error Handling -------------- diff --git a/components/finder.rst b/components/finder.rst index 0279b967b0a..ee6165e15bb 100644 --- a/components/finder.rst +++ b/components/finder.rst @@ -141,16 +141,21 @@ default when looking for files and directories, but you can change this with the $finder->ignoreVCS(false); -If the search directory contains a ``.gitignore`` file, you can reuse those -rules to exclude files and directories from the results with the +If the search directory and its subdirectories contain ``.gitignore`` files, you +can reuse those rules to exclude files and directories from the results with the :method:`Symfony\\Component\\Finder\\Finder::ignoreVCSIgnored` method:: // excludes files/directories matching the .gitignore patterns $finder->ignoreVCSIgnored(true); -.. versionadded:: 4.3 +The rules of a directory always override the rules of its parent directories. - The ``ignoreVCSIgnored()`` method was introduced in Symfony 4.3. +.. note:: + + Git looks for ``.gitignore`` files starting from the repository root directory. + Symfony's Finder behavior is different and it looks for ``.gitignore`` files + starting from the directory used to search files/directories. To be consistent + with Git behavior, you should explicitly search from the Git repository root. File Name ~~~~~~~~~ @@ -248,11 +253,6 @@ Multiple paths can be excluded by chaining calls or passing an array:: // same as above $finder->notPath(['first/dir', 'other/dir']); -.. versionadded:: 4.2 - - Support for passing arrays to ``notPath()`` was introduced in Symfony - 4.2 - File Size ~~~~~~~~~ diff --git a/components/form.rst b/components/form.rst index dfbcfdfdcb4..93befdf3d1d 100644 --- a/components/form.rst +++ b/components/form.rst @@ -121,16 +121,17 @@ The following snippet adds CSRF protection to the form factory:: use Symfony\Component\Form\Extension\Csrf\CsrfExtension; use Symfony\Component\Form\Forms; - use Symfony\Component\HttpFoundation\Session\Session; + use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Security\Csrf\CsrfTokenManager; use Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator; use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage; - // creates a Session object from the HttpFoundation component - $session = new Session(); + // creates a RequestStack object using the current request + $requestStack = new RequestStack(); + $requestStack->push($request); $csrfGenerator = new UriSafeTokenGenerator(); - $csrfStorage = new SessionTokenStorage($session); + $csrfStorage = new SessionTokenStorage($requestStack); $csrfManager = new CsrfTokenManager($csrfGenerator, $csrfStorage); $formFactory = Forms::createFormFactoryBuilder() @@ -646,6 +647,12 @@ method: } } +.. caution:: + + The form's ``createView()`` method should be called *after* ``handleRequest()`` is + called. Otherwise, when using :doc:`form events `, changes done + in the ``*_SUBMIT`` events won't be applied to the view (like validation errors). + This defines a common form "workflow", which contains 3 different possibilities: 1) On the initial GET request (i.e. when the user "surfs" to your page), @@ -777,4 +784,4 @@ Learn more /form/* .. _Twig: https://twig.symfony.com -.. _`Twig Configuration`: https://twig.symfony.com/doc/2.x/intro.html +.. _`Twig Configuration`: https://twig.symfony.com/doc/3.x/intro.html diff --git a/components/http_foundation.rst b/components/http_foundation.rst index 4ebce4526f0..8e31999220f 100644 --- a/components/http_foundation.rst +++ b/components/http_foundation.rst @@ -81,19 +81,21 @@ can be accessed via several public properties: (``$request->headers->get('User-Agent')``). Each property is a :class:`Symfony\\Component\\HttpFoundation\\ParameterBag` -instance (or a sub-class of), which is a data holder class: +instance (or a subclass of), which is a data holder class: -* ``request``: :class:`Symfony\\Component\\HttpFoundation\\ParameterBag`; +* ``request``: :class:`Symfony\\Component\\HttpFoundation\\ParameterBag` or + :class:`Symfony\\Component\\HttpFoundation\\InputBag` if the data is + coming from ``$_POST`` parameters; -* ``query``: :class:`Symfony\\Component\\HttpFoundation\\ParameterBag`; +* ``query``: :class:`Symfony\\Component\\HttpFoundation\\InputBag`; -* ``cookies``: :class:`Symfony\\Component\\HttpFoundation\\ParameterBag`; +* ``cookies``: :class:`Symfony\\Component\\HttpFoundation\\InputBag`; * ``attributes``: :class:`Symfony\\Component\\HttpFoundation\\ParameterBag`; -* ``files``: :class:`Symfony\\Component\\HttpFoundation\\FileBag`; +* ``files``: :class:`Symfony\\Component\\HttpFoundation\\FileBag`; -* ``server``: :class:`Symfony\\Component\\HttpFoundation\\ServerBag`; +* ``server``: :class:`Symfony\\Component\\HttpFoundation\\ServerBag`; * ``headers``: :class:`Symfony\\Component\\HttpFoundation\\HeaderBag`. @@ -161,18 +163,19 @@ 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'] $request->query->get('foo[bar]'); // returns null - $request->query->get('foo')['bar']; + $request->query->all()['foo']['bar']; // returns 'baz' .. _component-foundation-attributes: @@ -188,9 +191,14 @@ Finally, the raw data sent with the request body can be accessed using $content = $request->getContent(); -For instance, this may be useful to process a JSON string sent to the +For instance, this may be useful to process an XML string sent to the application by a remote service using the HTTP POST method. +If the request body is a JSON string, it can be accessed using +:method:`Symfony\\Component\\HttpFoundation\\Request::toArray`:: + + $data = $request->toArray(); + Identifying a Request ~~~~~~~~~~~~~~~~~~~~~ @@ -236,9 +244,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 +280,10 @@ this complexity and defines some methods for the most common tasks:: HeaderUtils::unquote('"foo \"bar\""'); // => 'foo "bar"' + // Parses a query string but maintains dots (PHP parse_str() replaces '.' by '_') + HeaderUtils::parseQuery('foo[bar.baz]=qux'); + // => ['foo' => ['bar.baz' => 'qux']] + Accessing ``Accept-*`` Headers Data ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -319,10 +331,6 @@ are also supported:: Anonymizing IP Addresses ~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 4.4 - - The ``anonymize()`` method was introduced in Symfony 4.4. - An increasingly common need for applications to comply with user protection regulations is to anonymize IP addresses before logging and storing them for analysis purposes. Use the ``anonymize()`` method from the @@ -447,9 +455,17 @@ method takes an instance of You can clear a cookie via the :method:`Symfony\\Component\\HttpFoundation\\ResponseHeaderBag::clearCookie` method. -Note you can create a -:class:`Symfony\\Component\\HttpFoundation\\Cookie` object from a raw header -value using :method:`Symfony\\Component\\HttpFoundation\\Cookie::fromString`. +In addition to the ``Cookie::create()`` method, you can create a ``Cookie`` +object from a raw header value using :method:`Symfony\\Component\\HttpFoundation\\Cookie::fromString` +method. You can also use the ``with*()`` methods to change some Cookie property (or +to build the entire Cookie using a fluent interface). Each ``with*()`` method returns +a new object with the modified property:: + + $cookie = Cookie::create('foo') + ->withValue('bar') + ->withExpires(strtotime('Fri, 20-May-2011 15:25:52 GMT')) + ->withDomain('.example.com') + ->withSecure(true); Managing the HTTP Cache ~~~~~~~~~~~~~~~~~~~~~~~ @@ -481,13 +497,18 @@ can be used to set the most commonly used cache information in one method call:: $response->setCache([ - 'etag' => 'abcdef', - 'last_modified' => new \DateTime(), - 'max_age' => 600, - 's_maxage' => 600, - 'private' => false, - 'public' => true, - 'immutable' => true, + 'must_revalidate' => false, + 'no_cache' => false, + 'no_store' => false, + 'no_transform' => false, + 'public' => true, + 'private' => false, + 'proxy_revalidate' => false, + 'max_age' => 600, + 's_maxage' => 600, + 'immutable' => true, + 'last_modified' => new \DateTime(), + 'etag' => 'abcdef', ]); To check if the Response validators (``ETag``, ``Last-Modified``) match a @@ -684,7 +705,7 @@ The ``JsonResponse`` class sets the ``Content-Type`` header to .. caution:: To avoid XSSI `JSON Hijacking`_, you should pass an associative array - as the outer-most array to ``JsonResponse`` and not an indexed array so + as the outermost array to ``JsonResponse`` and not an indexed array so that the final result is an object (e.g. ``{"object": "not inside an array"}``) instead of an array (e.g. ``[{"object": "inside an array"}]``). Read the `OWASP guidelines`_ for more information. @@ -712,6 +733,65 @@ Session The session information is in its own document: :doc:`/components/http_foundation/sessions`. +Safe Content Preference +----------------------- + +Some web sites have a "safe" mode to assist those who don't want to be exposed +to content to which they might object. The `RFC 8674`_ specification defines a +way for user agents to ask for safe content to a server. + +The specification does not define what content might be considered objectionable, +so the concept of "safe" is not precisely defined. Rather, the term is interpreted +by the server and within the scope of each web site that chooses to act upon this information. + +Symfony offers two methods to interact with this preference: + +* :method:`Symfony\\Component\\HttpFoundation\\Request::preferSafeContent`; +* :method:`Symfony\\Component\\HttpFoundation\\Response::setContentSafe`; + +The following example shows how to detect if the user agent prefers "safe" content:: + + if ($request->preferSafeContent()) { + $response = new Response($alternativeContent); + // this informs the user we respected their preferences + $response->setContentSafe(); + + return $response; + +Generating Relative and Absolute URLs +------------------------------------- + +Generating absolute and relative URLs for a given path is a common need +in some applications. In Twig templates you can use the +:ref:`absolute_url() ` and +:ref:`relative_path() ` functions to do that. + +The :class:`Symfony\\Component\\HttpFoundation\\UrlHelper` class provides the +same functionality for PHP code via the ``getAbsoluteUrl()`` and ``getRelativePath()`` +methods. You can inject this as a service anywhere in your application:: + + // src/Normalizer/UserApiNormalizer.php + namespace App\Normalizer; + + use Symfony\Component\HttpFoundation\UrlHelper; + + class UserApiNormalizer + { + private UrlHelper $urlHelper; + + public function __construct(UrlHelper $urlHelper) + { + $this->urlHelper = $urlHelper; + } + + public function normalize($user) + { + return [ + 'avatar' => $this->urlHelper->getAbsoluteUrl($user->avatar()->path()), + ]; + } + } + Learn More ---------- @@ -729,3 +809,4 @@ Learn More .. _Apache: https://tn123.org/mod_xsendfile/ .. _`JSON Hijacking`: https://haacked.com/archive/2009/06/25/json-hijacking.aspx/ .. _OWASP guidelines: https://cheatsheetseries.owasp.org/cheatsheets/AJAX_Security_Cheat_Sheet.html#always-return-json-with-an-object-on-the-outside +.. _RFC 8674: https://tools.ietf.org/html/rfc8674 diff --git a/components/http_foundation/sessions.rst b/components/http_foundation/sessions.rst index e8ca28d35d3..197693f32ac 100644 --- a/components/http_foundation/sessions.rst +++ b/components/http_foundation/sessions.rst @@ -166,9 +166,6 @@ and "Remember Me" login settings or other user based state information. :class:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBag` This is the standard default implementation. -:class:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\NamespacedAttributeBag` - This implementation allows for attributes to be stored in a structured namespace. - :class:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface` has the API @@ -237,15 +234,6 @@ So any processing of this might quickly get ugly, even adding a token to the arr $tokens['c'] = $value; $session->set('tokens', $tokens); -With structured namespacing, the key can be translated to the array -structure like this using a namespace character (which defaults to ``/``):: - - // ... - use Symfony\Component\HttpFoundation\Session\Attribute\NamespacedAttributeBag; - - $session = new Session(new NativeSessionStorage(), new NamespacedAttributeBag()); - $session->set('tokens/c', $value); - Flash Messages ~~~~~~~~~~~~~~ diff --git a/components/http_kernel.rst b/components/http_kernel.rst index 0bdb8c24b2f..370e960c95f 100644 --- a/components/http_kernel.rst +++ b/components/http_kernel.rst @@ -65,8 +65,8 @@ that system:: */ public function handle( Request $request, - $type = self::MASTER_REQUEST, - $catch = true + int $type = self::MAIN_REQUEST, + bool $catch = true ); } @@ -524,22 +524,10 @@ object, which you can use to access the original exception via the method. A typical listener on this event will check for a certain type of exception and create an appropriate error ``Response``. -.. versionadded:: 4.4 - - The :method:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent::getThrowable` and - :method:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent::setThrowable` methods - were introduced in Symfony 4.4. - -.. deprecated:: 4.4 - - The :method:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent::getException` and - :method:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent::setException` methods - are deprecated since Symfony 4.4. - For example, to generate a 404 page, you might throw a special type of exception and then add a listener on this event that looks for this exception and creates and returns a 404 ``Response``. In fact, the HttpKernel component -comes with an :class:`Symfony\\Component\\HttpKernel\\EventListener\\ExceptionListener`, +comes with an :class:`Symfony\\Component\\HttpKernel\\EventListener\\ErrorListener`, which if you choose to use, will do this and more by default (see the sidebar below for more details). @@ -553,10 +541,10 @@ below for more details). There are two main listeners to ``kernel.exception`` when using the Symfony Framework. - **ExceptionListener in the HttpKernel Component** + **ErrorListener in the HttpKernel Component** The first comes core to the HttpKernel component - and is called :class:`Symfony\\Component\\HttpKernel\\EventListener\\ExceptionListener`. + and is called :class:`Symfony\\Component\\HttpKernel\\EventListener\\ErrorListener`. The listener has several goals: 1) The thrown exception is converted into a @@ -621,19 +609,6 @@ kernel.terminate ``KernelEvents::TERMINATE`` :class:`Sym kernel.exception ``KernelEvents::EXCEPTION`` :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` =========================== ====================================== ======================================================================== -.. deprecated:: 4.3 - - Since Symfony 4.3, most of the event classes were renamed. - The following old classes were deprecated: - - * `GetResponseEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\RequestEvent` - * `FilterControllerEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\ControllerEvent` - * `FilterControllerArgumentsEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\ControllerArgumentsEvent` - * `GetResponseForControllerResultEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\ViewEvent` - * `FilterResponseEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\ResponseEvent` - * `PostResponseEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\TerminateEvent` - * `GetResponseForExceptionEvent` renamed to :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` - .. _http-kernel-working-example: A full Working Example @@ -720,12 +695,12 @@ argument as follows:: This creates another full request-response cycle where this new ``Request`` is transformed into a ``Response``. The only difference internally is that some -listeners (e.g. security) may only act upon the master request. Each listener +listeners (e.g. security) may only act upon the main request. Each listener is passed some subclass of :class:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent`, -whose :method:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent::isMasterRequest` -can be used to check if the current request is a "master" or "sub" request. +whose :method:`Symfony\\Component\\HttpKernel\\Event\\KernelEvent::isMainRequest` +method can be used to check if the current request is a "main" or "sub" request. -For example, a listener that only needs to act on the master request may +For example, a listener that only needs to act on the main request may look like this:: use Symfony\Component\HttpKernel\Event\RequestEvent; @@ -733,7 +708,7 @@ look like this:: public function onKernelRequest(RequestEvent $event) { - if (!$event->isMasterRequest()) { + if (!$event->isMainRequest()) { return; } diff --git a/components/inflector.rst b/components/inflector.rst deleted file mode 100644 index 960cb04d4ba..00000000000 --- a/components/inflector.rst +++ /dev/null @@ -1,64 +0,0 @@ -.. index:: - single: Inflector - single: Components; Inflector - -The Inflector Component -======================= - - The Inflector component converts English words between their singular and - plural forms. - -Installation ------------- - -.. code-block:: terminal - - $ composer require symfony/inflector - -.. include:: /components/require_autoload.rst.inc - -When you May Need an Inflector ------------------------------- - -In some scenarios such as code generation and code introspection, it's usually -required to convert words from/to singular/plural. For example, if you need to -know which property is associated with an *adder* method, you must convert from -plural to singular (``addStories()`` method -> ``$story`` property). - -Although most human languages define simple pluralization rules, they also -define lots of exceptions. For example, the general rule in English is to add an -``s`` at the end of the word (``book`` -> ``books``) but there are lots of -exceptions even for common words (``woman`` -> ``women``, ``life`` -> ``lives``, -``news`` -> ``news``, ``radius`` -> ``radii``, etc.) - -This component abstracts all those pluralization rules so you can convert -from/to singular/plural with confidence. However, due to the complexity of the -human languages, this component only provides support for the English language. - -Usage ------ - -The Inflector component provides two static methods to convert from/to -singular/plural:: - - use Symfony\Component\Inflector\Inflector; - - Inflector::singularize('alumni'); // 'alumnus' - Inflector::singularize('knives'); // 'knife' - Inflector::singularize('mice'); // 'mouse' - - Inflector::pluralize('grandchild'); // 'grandchildren' - Inflector::pluralize('news'); // 'news' - Inflector::pluralize('bacterium'); // 'bacteria' - -Sometimes it's not possible to determine a unique singular/plural form for the -given word. In those cases, the methods return an array with all the possible -forms:: - - use Symfony\Component\Inflector\Inflector; - - Inflector::singularize('indices'); // ['index', 'indix', 'indice'] - Inflector::singularize('leaves'); // ['leaf', 'leave', 'leaff'] - - Inflector::pluralize('matrix'); // ['matrices', 'matrixes'] - Inflector::pluralize('person'); // ['persons', 'people'] diff --git a/components/intl.rst b/components/intl.rst index 70e602cc1c2..8e5ce01be50 100644 --- a/components/intl.rst +++ b/components/intl.rst @@ -6,7 +6,6 @@ The Intl Component ================== This component provides access to the localization data of the `ICU library`_. - It also provides a PHP replacement layer for the C `intl extension`_. .. caution:: @@ -30,30 +29,6 @@ Installation .. include:: /components/require_autoload.rst.inc -If you install the component via Composer, the following classes and functions -of the intl extension will be automatically provided if the intl extension is -not loaded: - -* :phpclass:`Collator` -* :phpclass:`IntlDateFormatter` -* :phpclass:`Locale` -* :phpclass:`NumberFormatter` -* :phpfunction:`intl_error_name` -* :phpfunction:`intl_is_failure` -* :phpfunction:`intl_get_error_code` -* :phpfunction:`intl_get_error_message` - -When the intl extension is not available, the following classes are used to -replace the intl classes: - -* :class:`Symfony\\Component\\Intl\\Collator\\Collator` -* :class:`Symfony\\Component\\Intl\\DateFormatter\\IntlDateFormatter` -* :class:`Symfony\\Component\\Intl\\Locale\\Locale` -* :class:`Symfony\\Component\\Intl\\NumberFormatter\\NumberFormatter` -* :class:`Symfony\\Component\\Intl\\Globals\\IntlGlobals` - -Composer automatically exposes these classes in the global namespace. - Accessing ICU Data ------------------ @@ -120,14 +95,6 @@ You may convert codes between two-letter alpha2 and three-letter alpha3 codes:: $alpha2Code = Languages::getAlpha2Code($alpha3Code); -.. versionadded:: 4.3 - - The ``Languages`` class was introduced in Symfony 4.3. - -.. versionadded:: 4.4 - - The full support for alpha3 codes was introduced in Symfony 4.4. - The ``Scripts`` class provides access to the optional four-letter script code that can follow the language code according to the `Unicode ISO 15924 Registry`_ (e.g. ``HANS`` in ``zh_HANS`` for simplified Chinese and ``HANT`` in ``zh_HANT`` @@ -159,10 +126,6 @@ to catching the exception, you can also check if a given script code is valid:: $isValidScript = Scripts::exists($scriptCode); -.. versionadded:: 4.3 - - The ``Scripts`` class was introduced in Symfony 4.3. - Country Names ~~~~~~~~~~~~~ @@ -219,14 +182,6 @@ You may convert codes between two-letter alpha2 and three-letter alpha3 codes:: $alpha2Code = Countries::getAlpha2Code($alpha3Code); -.. versionadded:: 4.3 - - The ``Countries`` class was introduced in Symfony 4.3. - -.. versionadded:: 4.4 - - The support for alpha3 codes was introduced in Symfony 4.4. - Locales ~~~~~~~ @@ -262,10 +217,6 @@ to catching the exception, you can also check if a given locale code is valid:: $isValidLocale = Locales::exists($localeCode); -.. versionadded:: 4.3 - - The ``Locales`` class was introduced in Symfony 4.3. - Currencies ~~~~~~~~~~ @@ -286,15 +237,37 @@ as some of their information (symbol, fraction digits, etc.):: $symbol = Currencies::getSymbol('INR'); // => '₹' - $fractionDigits = Currencies::getFractionDigits('INR'); - // => 2 +The fraction digits methods return the number of decimal digits to display when +formatting numbers with this currency. Depending on the currency, this value +can change if the number is used in cash transactions or in other scenarios +(e.g. accounting):: + + // Indian rupee defines the same value for both + $fractionDigits = Currencies::getFractionDigits('INR'); // returns: 2 + $cashFractionDigits = Currencies::getCashFractionDigits('INR'); // returns: 2 + + // Swedish krona defines different values + $fractionDigits = Currencies::getFractionDigits('SEK'); // returns: 2 + $cashFractionDigits = Currencies::getCashFractionDigits('SEK'); // returns: 0 - $roundingIncrement = Currencies::getRoundingIncrement('INR'); - // => 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):: -All methods (except for ``getFractionDigits()`` and ``getRoundingIncrement()``) -accept the translation locale as the last, optional parameter, which defaults to -the current default locale:: + // Indian rupee defines the same value for both + $roundingIncrement = Currencies::getRoundingIncrement('INR'); // returns: 0 + $cashRoundingIncrement = Currencies::getCashRoundingIncrement('INR'); // returns: 0 + + // Canadian dollar defines different values because they have eliminated + // the smaller coins (1-cent and 2-cent) and prices in cash must be rounded to + // 5 cents (e.g. if price is 7.42 you pay 7.40; if price is 7.48 you pay 7.50) + $roundingIncrement = Currencies::getRoundingIncrement('CAD'); // returns: 0 + $cashRoundingIncrement = Currencies::getCashRoundingIncrement('CAD'); // returns: 5 + +All methods (except for ``getFractionDigits()``, ``getCashFractionDigits()``, +``getRoundingIncrement()`` and ``getCashRoundingIncrement()``) accept the +translation locale as the last, optional parameter, which defaults to the +current default locale:: $currencies = Currencies::getNames('de'); // => ['AFN' => 'Afghanischer Afghani', 'EGP' => 'Ägyptisches Pfund', ...] @@ -308,10 +281,6 @@ to catching the exception, you can also check if a given currency code is valid: $isValidCurrency = Currencies::exists($currencyCode); -.. versionadded:: 4.3 - - The ``Currencies`` class was introduced in Symfony 4.3. - .. _component-intl-timezones: Timezones @@ -390,10 +359,6 @@ to catching the exception, you can also check if a given timezone ID is valid:: $isValidTimezone = Timezones::exists($timezoneId); -.. versionadded:: 4.3 - - The ``Timezones`` class was introduced in Symfony 4.3. - Learn more ---------- @@ -407,7 +372,6 @@ Learn more /reference/forms/types/locale /reference/forms/types/timezone -.. _intl extension: https://www.php.net/manual/en/book.intl.php .. _install the intl extension: https://www.php.net/manual/en/intl.setup.php .. _ICU library: http://site.icu-project.org/ .. _`Unicode ISO 15924 Registry`: https://www.unicode.org/iso15924/iso15924-codes.html diff --git a/components/ldap.rst b/components/ldap.rst index 0c164d305dc..21ef7a8bcfb 100644 --- a/components/ldap.rst +++ b/components/ldap.rst @@ -146,6 +146,9 @@ delete existing ones:: $phoneNumber = $entry->getAttribute('phoneNumber'); $isContractor = $entry->hasAttribute('contractorCompany'); + // attribute names in getAttribute() and hasAttribute() methods are case-sensitive + // pass FALSE as the second method argument to make them case-insensitive + $isContractor = $entry->hasAttribute('contractorCompany', false); $entry->setAttribute('email', ['fabpot@symfony.com']); $entryManager->update($entry); diff --git a/components/lock.rst b/components/lock.rst index 60b53a3a906..76e4badb587 100644 --- a/components/lock.rst +++ b/components/lock.rst @@ -36,11 +36,6 @@ which in turn requires another class to manage the storage of locks:: $store = new SemaphoreStore(); $factory = new LockFactory($store); -.. versionadded:: 4.4 - - The ``Symfony\Component\Lock\LockFactory`` class was introduced in Symfony - 4.4. In previous versions it was called ``Symfony\Component\Lock\Factory``. - The lock is created by calling the :method:`Symfony\\Component\\Lock\\LockFactory::createLock` method. Its first argument is an arbitrary string that represents the locked resource. Then, a call to the :method:`Symfony\\Component\\Lock\\LockInterface::acquire` @@ -111,20 +106,24 @@ can be created, pass ``true`` as the argument of the ``acquire()`` method. This is called a **blocking lock** because the execution of your application stops until the lock is acquired. -Some of the built-in ``Store`` classes support this feature. When they don't, -they can be decorated with the ``RetryTillSaveStore`` class:: +Some of the built-in ``Store`` classes support this feature. use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\Store\RedisStore; - use Symfony\Component\Lock\Store\RetryTillSaveStore; $store = new RedisStore(new \Predis\Client('tcp://localhost:6379')); - $store = new RetryTillSaveStore($store); $factory = new LockFactory($store); $lock = $factory->createLock('notification-flush'); $lock->acquire(true); +When the provided store does not implement the +:class:`Symfony\\Component\\Lock\\BlockingStoreInterface` interface, the +``Lock`` class will retry to acquire the lock in a non-blocking way until the +lock is acquired. However, the ``Lock`` class also provides the default logic to +acquire locks in blocking mode when the store does not implement the +``BlockingStoreInterface`` interface. + Expiring Locks -------------- @@ -203,7 +202,7 @@ as seconds) and ``isExpired()`` (which returns a boolean). Automatically Releasing The Lock ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Locks are automatically released when their Lock objects are destructed. This is +Locks are automatically released when their Lock objects are destroyed. This is an implementation detail that will be important when sharing Locks between processes. In the example below, ``pcntl_fork()`` creates two processes and the Lock will be released automatically as soon as one process finishes:: @@ -232,6 +231,58 @@ To disable this behavior, set to ``false`` the third argument of ``LockFactory::createLock()``. That will make the lock acquired for 3600 seconds or until ``Lock::release()`` is called. +Shared Locks +------------ + +A shared or `readers–writer lock`_ is a synchronization primitive that allows +concurrent access for read-only operations, while write operations require +exclusive access. This means that multiple threads can read the data in parallel +but an exclusive lock is needed for writing or modifying data. They are used for +example for data structures that cannot be updated atomically and are invalid +until the update is complete. + +Use the :method:`Symfony\\Component\\Lock\\SharedLockInterface::acquireRead` method +to acquire a read-only lock, and the existing +:method:`Symfony\\Component\\Lock\\LockInterface::acquire` method to acquire a +write lock:: + + $lock = $factory->createLock('user'.$user->id); + if (!$lock->acquireRead()) { + return; + } + +Similar to the ``acquire()`` method, pass ``true`` as the argument of ``acquireRead()`` +to acquire the lock in a blocking mode:: + + $lock = $factory->createLock('user'.$user->id); + $lock->acquireRead(true); + +.. note:: + + The `priority policy`_ of Symfony's shared locks depends on the underlying + store (e.g. Redis store prioritizes readers vs writers). + +When a read-only lock is acquired with the method ``acquireRead()``, it's +possible to **promote** the lock, and change it to write lock, by calling the +``acquire()`` method:: + + $lock = $factory->createLock('user'.$userId); + $lock->acquireRead(true); + + if (!$this->shouldUpdate($userId)) { + return; + } + + $lock->acquire(true); // Promote the lock to write lock + $this->update($userId); + +In the same way, it's possible to **demote** a write lock, and change it to a +read-only lock by calling the ``acquireRead()`` method. + +When the provided store does not implement the +:class:`Symfony\\Component\\Lock\\SharedLockStoreInterface` interface, the +``Lock`` class will fallback to a write lock by calling the ``acquire()`` method. + The Owner of The Lock --------------------- @@ -286,22 +337,20 @@ Locks are created and managed in ``Stores``, which are classes that implement The component includes the following built-in store types: -============================================ ====== ======== ======== -Store Scope Blocking Expiring -============================================ ====== ======== ======== -:ref:`FlockStore ` local yes no -:ref:`MemcachedStore ` remote no yes -:ref:`PdoStore ` remote no yes -:ref:`RedisStore ` remote no yes -:ref:`SemaphoreStore ` local yes no -:ref:`ZookeeperStore ` remote no no -============================================ ====== ======== ======== - -.. versionadded:: 4.4 - - The ``PersistingStoreInterface`` and ``BlockingStoreInterface`` interfaces were - introduced in Symfony 4.4. In previous versions there was only one interface - called ``Symfony\Component\Lock\StoreInterface``. +========================================================== ====== ======== ======== ======= +Store Scope Blocking Expiring Sharing +========================================================== ====== ======== ======== ======= +:ref:`FlockStore ` local yes no yes +:ref:`MemcachedStore ` remote no yes no +:ref:`MongoDbStore ` remote no yes no +:ref:`PdoStore ` remote no yes no +:ref:`DoctrineDbalStore ` remote no yes no +:ref:`PostgreSqlStore ` remote yes no yes +:ref:`DoctrineDbalPostgreSqlStore ` remote yes no yes +:ref:`RedisStore ` remote no yes yes +:ref:`SemaphoreStore ` local yes no no +:ref:`ZookeeperStore ` remote no no no +========================================================== ====== ======== ======== ======= .. _lock-store-flock: @@ -345,44 +394,150 @@ support blocking, and expects a TTL to avoid stalled locks:: Memcached does not support TTL lower than 1 second. +.. _lock-store-mongodb: + +MongoDbStore +~~~~~~~~~~~~ + +The MongoDbStore saves locks on a MongoDB server ``>=2.2``, it requires a +``\MongoDB\Collection`` or ``\MongoDB\Client`` from `mongodb/mongodb`_ or a +`MongoDB Connection String`_. +This store does not support blocking and expects a TTL to +avoid stalled locks:: + + use Symfony\Component\Lock\Store\MongoDbStore; + + $mongo = 'mongodb://localhost/database?collection=lock'; + $options = [ + 'gcProbablity' => 0.001, + 'database' => 'myapp', + 'collection' => 'lock', + 'uriOptions' => [], + 'driverOptions' => [], + ]; + $store = new MongoDbStore($mongo, $options); + +The ``MongoDbStore`` takes the following ``$options`` (depending on the first parameter type): + +============= ================================================================================================ +Option Description +============= ================================================================================================ +gcProbablity Should a TTL Index be created expressed as a probability from 0.0 to 1.0 (Defaults to ``0.001``) +database The name of the database +collection The name of the collection +uriOptions Array of uri options for `MongoDBClient::__construct`_ +driverOptions Array of driver options for `MongoDBClient::__construct`_ +============= ================================================================================================ + +When the first parameter is a: + +``MongoDB\Collection``: + +- ``$options['database']`` is ignored +- ``$options['collection']`` is ignored + +``MongoDB\Client``: + +- ``$options['database']`` is mandatory +- ``$options['collection']`` is mandatory + +MongoDB Connection String: + +- ``$options['database']`` is used otherwise ``/path`` from the DSN, at least one is mandatory +- ``$options['collection']`` is used otherwise ``?collection=`` from the DSN, at least one is mandatory + +.. note:: + + The ``collection`` querystring parameter is not part of the `MongoDB Connection String`_ definition. + It is used to allow constructing a ``MongoDbStore`` using a `Data Source Name (DSN)`_ without ``$options``. + .. _lock-store-pdo: PdoStore ~~~~~~~~ -The PdoStore saves locks in an SQL database. It requires a `PDO`_ connection, a -`Doctrine DBAL Connection`_, or a `Data Source Name (DSN)`_. This store does not -support blocking, and expects a TTL to avoid stalled locks:: +The PdoStore saves locks in an SQL database. It is identical to DoctrineDbalStore +but requires a `PDO`_ connection or a `Data Source Name (DSN)`_. This store does +not support blocking, and expects a TTL to avoid stalled locks:: use Symfony\Component\Lock\Store\PdoStore; - // a PDO, a Doctrine DBAL connection or DSN for lazy connecting through PDO - $databaseConnectionOrDSN = 'mysql:host=127.0.0.1;dbname=lock'; + // a PDO or DSN for lazy connecting through PDO + $databaseConnectionOrDSN = 'mysql:host=127.0.0.1;dbname=app'; $store = new PdoStore($databaseConnectionOrDSN, ['db_username' => 'myuser', 'db_password' => 'mypassword']); .. note:: This store does not support TTL lower than 1 second. -Before storing locks in the database, you must create the table that stores -the information. The store provides a method called -:method:`Symfony\\Component\\Lock\\Store\\PdoStore::createTable` -to set up this table for you according to the database engine used:: +The table where values are stored is created automatically on the first call to +the :method:`Symfony\\Component\\Lock\\Store\\PdoStore::save` method. +You can also create this table explicitly by calling the +:method:`Symfony\\Component\\Lock\\Store\\PdoStore::createTable` method in +your code. - try { - $store->createTable(); - } catch (\PDOException $exception) { - // the table could not be created for some reason - } +.. _lock-store-dbal: -A great way to set up the table in production is to call the ``createTable()`` -method in your local computer and then generate a -:ref:`database migration `: +DoctrineDbalStore +~~~~~~~~~~~~~~~~~ -.. code-block:: terminal +The DoctrineDbalStore saves locks in an SQL database. It is identical to PdoStore +but requires a `Doctrine DBAL Connection`_, or a `Doctrine DBAL URL`_. This store +does not support blocking, and expects a TTL to avoid stalled locks:: - $ php bin/console doctrine:migrations:diff - $ php bin/console doctrine:migrations:migrate + use Symfony\Component\Lock\Store\DoctrineDbalStore; + + // a Doctrine DBAL connection or DSN + $connectionOrURL = 'mysql://myuser:mypassword@127.0.0.1/app'; + $store = new DoctrineDbalStore($connectionOrURL); + +.. note:: + + This store does not support TTL lower than 1 second. + +The table where values are stored is created automatically on the first call to +the :method:`Symfony\\Component\\Lock\\Store\\DoctrineDbalStore::save` method. +You can also add this table to your schema by calling +:method:`Symfony\\Component\\Lock\\Store\\DoctrineDbalStore::configureSchema` method +in your code or create this table explicitly by calling the +:method:`Symfony\\Component\\Lock\\Store\\DoctrineDbalStore::createTable` method. + +.. _lock-store-pgsql: + +PostgreSqlStore +~~~~~~~~~~~~~~~ + +The PostgreSqlStore and DoctrineDbalPostgreSqlStore uses `Advisory Locks`_ provided by PostgreSQL. +It is identical to DoctrineDbalPostgreSqlStore but requires `PDO`_ connection or +a `Data Source Name (DSN)`_. It supports native blocking, as well as sharing +locks:: + + use Symfony\Component\Lock\Store\PostgreSqlStore; + + // a PDO instance or DSN for lazy connecting through PDO + $databaseConnectionOrDSN = 'pgsql:host=localhost;port=5634;dbname=lock'; + $store = new PostgreSqlStore($databaseConnectionOrDSN, ['db_username' => 'myuser', 'db_password' => 'mypassword']); + +In opposite to the ``PdoStore``, the ``PostgreSqlStore`` does not need a table to +store locks and it does not expire. + +.. _lock-store-dbal-pgsql: + +DoctrineDbalPostgreSqlStore +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The DoctrineDbalPostgreSqlStore uses `Advisory Locks`_ provided by PostgreSQL. +It is identical to PostgreSqlStore but requires a `Doctrine DBAL Connection`_ or +a `Doctrine DBAL URL`_. It supports native blocking, as well as sharing locks:: + + use Symfony\Component\Lock\Store\DoctrineDbalPostgreSqlStore; + + // a Doctrine Connection or DSN + $databaseConnectionOrDSN = 'postgresql+advisory://myuser:mypassword@127.0.0.1:5634/lock'; + $store = new DoctrineDbalPostgreSqlStore($databaseConnectionOrDSN); + +In opposite to the ``DoctrineDbalStore``, the ``DoctrineDbalPostgreSqlStore`` does not need a table to +store locks and does not expire. .. _lock-store-redis: @@ -481,7 +636,9 @@ Remote Stores ~~~~~~~~~~~~~ Remote stores (:ref:`MemcachedStore `, +:ref:`MongoDbStore `, :ref:`PdoStore `, +:ref:`PostgreSqlStore `, :ref:`RedisStore ` and :ref:`ZookeeperStore `) use a unique token to recognize the true owner of the lock. This token is stored in the @@ -501,6 +658,7 @@ Expiring Stores ~~~~~~~~~~~~~~~ Expiring stores (:ref:`MemcachedStore `, +:ref:`MongoDbStore `, :ref:`PdoStore ` and :ref:`RedisStore `) guarantee that the lock is acquired only for the defined duration of time. If @@ -621,6 +779,47 @@ method uses the Memcached's ``flush()`` method which purges and removes everythi The method ``flush()`` must not be called, or locks should be stored in a dedicated Memcached service away from Cache. +MongoDbStore +~~~~~~~~~~~~ + +.. caution:: + + The locked resource name is indexed in the ``_id`` field of the lock + collection. Beware that in MongoDB an indexed field's value can be + `a maximum of 1024 bytes in length`_ inclusive of structural overhead. + +A TTL index must be used to automatically clean up expired locks. +Such an index can be created manually: + +.. code-block:: javascript + + db.lock.createIndex( + { "expires_at": 1 }, + { "expireAfterSeconds": 0 } + ) + +Alternatively, the method ``MongoDbStore::createTtlIndex(int $expireAfterSeconds = 0)`` +can be called once to create the TTL index during database setup. Read more +about `Expire Data from Collections by Setting TTL`_ in MongoDB. + +.. tip:: + + ``MongoDbStore`` will attempt to automatically create a TTL index. + It's recommended to set constructor option ``gcProbablity = 0.0`` to + disable this behavior if you have manually dealt with TTL index creation. + +.. caution:: + + This store relies on all PHP application and database nodes to have + synchronized clocks for lock expiry to occur at the correct time. To ensure + locks don't expire prematurely; the lock TTL should be set with enough extra + time in ``expireAfterSeconds`` to account for any clock drift between nodes. + +``writeConcern`` and ``readConcern`` are not specified by MongoDbStore meaning +the collection's settings will take effect. +``readPreference`` is ``primary`` for all queries. +Read more about `Replica Set Read and Write Semantics`_ in MongoDB. + PdoStore ~~~~~~~~~~ @@ -645,6 +844,20 @@ have synchronized clocks. To ensure locks don't expire prematurely; the TTLs should be set with enough extra time to account for any clock drift between nodes. +PostgreSqlStore +~~~~~~~~~~~~~~~ + +The PdoStore relies on the `Advisory Locks`_ properties of the PostgreSQL +database. That means that by using :ref:`PostgreSqlStore ` +the locks will be automatically released at the end of the session in case the +client cannot unlock for any reason. + +If the PostgreSQL service or the machine hosting it restarts, every lock would +be lost without notifying the running processes. + +If the TCP connection is lost, the PostgreSQL may release locks without +notifying the application. + RedisStore ~~~~~~~~~~ @@ -747,10 +960,20 @@ instance, during the deployment of a new version. Processes with new configuration must not be started while old processes with old configuration are still running. +.. _`a maximum of 1024 bytes in length`: https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit .. _`ACID`: https://en.wikipedia.org/wiki/ACID +.. _`Advisory Locks`: https://www.postgresql.org/docs/current/explicit-locking.html +.. _`Data Source Name (DSN)`: https://en.wikipedia.org/wiki/Data_source_name +.. _`Doctrine DBAL Connection`: https://github.com/doctrine/dbal/blob/master/src/Connection.php +.. _`Doctrine DBAL URL`: https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url +.. _`Expire Data from Collections by Setting TTL`: https://docs.mongodb.com/manual/tutorial/expire-data/ .. _`locks`: https://en.wikipedia.org/wiki/Lock_(computer_science) -.. _`PHP semaphore functions`: https://www.php.net/manual/en/book.sem.php +.. _`MongoDB Connection String`: https://docs.mongodb.com/manual/reference/connection-string/ +.. _`mongodb/mongodb`: https://packagist.org/packages/mongodb/mongodb +.. _`MongoDBClient::__construct`: https://docs.mongodb.com/php-library/current/reference/method/MongoDBClient__construct/ .. _`PDO`: https://www.php.net/pdo -.. _`Doctrine DBAL Connection`: https://github.com/doctrine/dbal/blob/master/lib/Doctrine/DBAL/Connection.php -.. _`Data Source Name (DSN)`: https://en.wikipedia.org/wiki/Data_source_name +.. _`PHP semaphore functions`: https://www.php.net/manual/en/book.sem.php +.. _`Replica Set Read and Write Semantics`: https://docs.mongodb.com/manual/applications/replication/ .. _`ZooKeeper`: https://zookeeper.apache.org/ +.. _`readers–writer lock`: https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock +.. _`priority policy`: https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock#Priority_policies diff --git a/components/messenger.rst b/components/messenger.rst index 0772293eab1..7ca24d02ec3 100644 --- a/components/messenger.rst +++ b/components/messenger.rst @@ -77,15 +77,9 @@ middleware stack. The component comes with a set of middleware that you can use. When using the message bus with Symfony's FrameworkBundle, the following middleware are configured for you: -#. :class:`Symfony\\Component\\Messenger\\Middleware\\LoggingMiddleware` (logs the processing of your messages) -#. :class:`Symfony\\Component\\Messenger\\Middleware\\SendMessageMiddleware` (enables asynchronous processing) +#. :class:`Symfony\\Component\\Messenger\\Middleware\\SendMessageMiddleware` (enables asynchronous processing, logs the processing of your messages if you provide a logger) #. :class:`Symfony\\Component\\Messenger\\Middleware\\HandleMessageMiddleware` (calls the registered handler(s)) -.. deprecated:: 4.3 - - The ``LoggingMiddleware`` is deprecated since Symfony 4.3 and will be - removed in 5.0. Pass a logger to ``SendMessageMiddleware`` instead. - Example:: use App\Message\MyMessage; @@ -168,6 +162,8 @@ Here are some important envelope stamps that are shipped with the Symfony Messen to configure the serialization groups used by the transport. #. :class:`Symfony\\Component\\Messenger\\Stamp\\ValidationStamp`, to configure the validation groups used when the validation middleware is enabled. +#. :class:`Symfony\\Component\\Messenger\\Stamp\\ErrorDetailsStamp`, + an internal stamp when a message fails due to an exception in the handler. Instead of dealing directly with the messages in the middleware you receive the envelope. Hence you can inspect the envelope content and its stamps, or add any:: @@ -332,12 +328,6 @@ do is to write your own CSV receiver:: } } -.. versionadded:: 4.3 - - In Symfony 4.3, the ``ReceiverInterface`` has changed its methods as shown - in the example above. You may need to update your code if you used this - interface in previous Symfony versions. - Receiver and Sender on the same Bus ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/components/mime.rst b/components/mime.rst index 965c7a377ae..a641283716e 100644 --- a/components/mime.rst +++ b/components/mime.rst @@ -9,10 +9,6 @@ The Mime Component The Mime component allows manipulating the MIME messages used to send emails and provides utilities related to MIME types. -.. versionadded:: 4.3 - - The Mime component was introduced in Symfony 4.3. - Installation ------------ diff --git a/components/options_resolver.rst b/components/options_resolver.rst index 4d556d86fda..7a499045aa8 100644 --- a/components/options_resolver.rst +++ b/components/options_resolver.rst @@ -335,6 +335,8 @@ is thrown:: In sub-classes, you can use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::addAllowedTypes` to add additional allowed types without erasing the ones already set. +.. _optionsresolver-validate-value: + Value Validation ~~~~~~~~~~~~~~~~ @@ -376,6 +378,21 @@ returns ``true`` for acceptable values and ``false`` for invalid values:: // return true or false }); +.. tip:: + + You can even use the :doc:`Validator ` component to validate the + input by using the :method:`Symfony\\Component\\Validator\\Validation::createIsValidCallable` + method:: + + use Symfony\Component\OptionsResolver\OptionsResolver; + use Symfony\Component\Validator\Constraints\Length; + use Symfony\Component\Validator\Validation; + + // ... + $resolver->setAllowedValues('transport', Validation::createIsValidCallable( + new Length(['min' => 10 ]) + )); + In sub-classes, you can use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::addAllowedValues` to add additional allowed values without erasing the ones already set. @@ -440,10 +457,6 @@ This way, the ``$value`` argument will receive the previously normalized value, otherwise you can prepend the new normalizer by passing ``true`` as third argument. -.. versionadded:: 4.3 - - The ``addNormalizer()`` method was introduced in Symfony 4.3. - Default Values that Depend on another Option ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -721,6 +734,51 @@ In same way, parent options can access to the nested options as normal arrays:: The fact that an option is defined as nested means that you must pass an array of values to resolve it at runtime. +Prototype Options +~~~~~~~~~~~~~~~~~ + +There are situations where you will have to resolve and validate a set of +options that may repeat many times within another option. Let's imagine a +``connections`` option that will accept an array of database connections +with ``host``, ``database``, ``user`` and ``password`` each. + +The best way to implement this is to define the ``connections`` option as prototype:: + + $resolver->setDefault('connections', function (OptionsResolver $connResolver) { + $connResolver + ->setPrototype(true) + ->setRequired(['host', 'database']) + ->setDefaults(['user' => 'root', 'password' => null]); + }); + +According to the prototype definition in the example above, it is possible +to have multiple connection arrays like the following:: + + $resolver->resolve([ + 'connections' => [ + 'default' => [ + 'host' => '127.0.0.1', + 'database' => 'symfony', + ], + 'test' => [ + 'host' => '127.0.0.1', + 'database' => 'symfony_test', + 'user' => 'test', + 'password' => 'test', + ], + // ... + ], + ]); + +The array keys (``default``, ``test``, etc.) of this prototype option are +validation-free and can be any arbitrary value that helps differentiate the +connections. + +.. note:: + + A prototype option can only be defined inside a nested option and + during its resolution it will expect an array of arrays. + Deprecating the Option ~~~~~~~~~~~~~~~~~~~~~~ @@ -730,12 +788,18 @@ method:: $resolver ->setDefined(['hostname', 'host']) - // this outputs the following generic deprecation message: - // The option "hostname" is deprecated. - ->setDeprecated('hostname') - // you can also pass a custom deprecation message - ->setDeprecated('hostname', 'The option "hostname" is deprecated, use "host" instead.') + // this outputs the following generic deprecation message: + // Since acme/package 1.2: The option "hostname" is deprecated. + ->setDeprecated('hostname', 'acme/package', '1.2') + + // you can also pass a custom deprecation message (%name% placeholder is available) + ->setDeprecated( + 'hostname', + 'acme/package', + '1.2', + 'The option "hostname" is deprecated, use "host" instead.' + ) ; .. note:: @@ -760,7 +824,7 @@ the option:: ->setDefault('encryption', null) ->setDefault('port', null) ->setAllowedTypes('port', ['null', 'int']) - ->setDeprecated('port', function (Options $options, $value) { + ->setDeprecated('port', 'acme/package', '1.2', function (Options $options, $value) { if (null === $value) { return 'Passing "null" to option "port" is deprecated, pass an integer instead.'; } @@ -782,6 +846,36 @@ the option:: This closure receives as argument the value of the option after validating it and before normalizing it when the option is being resolved. +Chaining Option Configurations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In many cases you may need to define multiple configurations for each option. +For example, suppose the ``InvoiceMailer`` class has an ``host`` option that is required +and a ``transport`` option which can be one of ``sendmail``, ``mail`` and ``smtp``. +You can improve the readability of the code avoiding to duplicate option name for +each configuration using the :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::define` +method:: + + // ... + class InvoiceMailer + { + // ... + public function configureOptions(OptionsResolver $resolver) + { + // ... + $resolver->define('host') + ->required() + ->default('smtp.example.org') + ->allowedTypes('string') + ->info('The IP address or hostname'); + + $resolver->define('transport') + ->required() + ->default('transport') + ->allowedValues('sendmail', 'mail', 'smtp'); + } + } + Performance Tweaks ~~~~~~~~~~~~~~~~~~ diff --git a/components/phpunit_bridge.rst b/components/phpunit_bridge.rst index dfff0e39cfe..0ee30f1a79f 100644 --- a/components/phpunit_bridge.rst +++ b/components/phpunit_bridge.rst @@ -161,18 +161,14 @@ each test suite's results in their own section. Trigger Deprecation Notices --------------------------- -Deprecation notices can be triggered by using:: +Deprecation notices can be triggered by using ``trigger_deprecation`` from +the ``symfony/deprecation-contracts`` package:: - @trigger_error('Your deprecation message', E_USER_DEPRECATED); + // indicates something is deprecated since version 1.3 of vendor-name/packagename + trigger_deprecation('vendor-name/package-name', '1.3', 'Your deprecation message'); -You can also require the ``symfony/deprecation-contracts`` package that provides -a global ``trigger_deprecation()`` function for this usage. - -Without the `@-silencing operator`_, users would need to opt-out from deprecation -notices. Silencing by default swaps this behavior and allows users to opt-in -when they are ready to cope with them (by adding a custom error handler like the -one provided by this bridge). When not silenced, deprecation notices will appear -in the **Unsilenced** section of the deprecation report. + // you can also use printf format (all arguments after the message will be used) + trigger_deprecation('...', '1.3', 'Value "%s" is deprecated, use ... instead.', $value); Mark Tests as Legacy -------------------- @@ -293,6 +289,30 @@ Here is a summary that should help you pick the right configuration: | | cannot afford to use one of the modes above. | +------------------------+-----------------------------------------------------+ +Baseline Deprecations +..................... + +If your application has some deprecations that you can't fix for some reasons, +you can tell Symfony to ignore them. The trick is to create a file with the +allowed deprecations and define it as the "deprecation baseline". Deprecations +inside that file are ignored but the rest of deprecations are still reported. + +First, generate the file with the allowed deprecations (run the same command +whenever you want to update the existing file): + +.. code-block:: terminal + + $ SYMFONY_DEPRECATIONS_HELPER='generateBaseline=true&baselineFile=./tests/allowed.json' ./vendor/bin/simple-phpunit + +This command stores all the deprecations reported while running tests in the +given file path and encoded in JSON. + +Then, you can run the following command to use that file and ignore those deprecations: + +.. code-block:: terminal + + $ SYMFONY_DEPRECATIONS_HELPER='baselineFile=./tests/allowed.json' ./vendor/bin/simple-phpunit + Disabling the Verbose Output ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -300,6 +320,10 @@ By default, the bridge will display a detailed output with the number of deprecations and where they arise. If this is too much for you, you can use ``SYMFONY_DEPRECATIONS_HELPER=verbose=0`` to turn the verbose output off. +It's also possible to change verbosity per deprecation type. For example, using +``quiet[]=indirect&quiet[]=other`` will hide details for deprecations of types +"indirect" and "other". + Disabling the Deprecation Helper ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -330,9 +354,21 @@ time. This can be disabled with the ``debug-class-loader`` option. -.. versionadded:: 4.2 +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 - The ``DebugClassLoader`` integration was introduced in Symfony 4.2. + $ php bin/console debug:container --deprecations + +Log Deprecations +~~~~~~~~~~~~~~~~ + +For turning the verbose output off and write it to a log file instead you can use +``SYMFONY_DEPRECATIONS_HELPER='logFile=/path/deprecations.log'``. .. _write-assertions-about-deprecations: @@ -341,21 +377,34 @@ Write Assertions about Deprecations When adding deprecations to your code, you might like writing tests that verify that they are triggered as required. To do so, the bridge provides the -``@expectedDeprecation`` annotation that you can use on your test methods. +``expectDeprecation()`` method that you can use on your test methods. It requires you to pass the expected message, given in the same format as for the `PHPUnit's assertStringMatchesFormat()`_ method. If you expect more than one -deprecation message for a given test method, you can use the annotation several +deprecation message for a given test method, you can use the method several times (order matters):: - /** - * @group legacy - * @expectedDeprecation This "%s" method is deprecated. - * @expectedDeprecation The second argument of the "%s" method is deprecated. - */ - public function testDeprecatedCode() + use PHPUnit\Framework\TestCase; + use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; + + class MyTest extends TestCase { - @trigger_error('This "Foo" method is deprecated.', E_USER_DEPRECATED); - @trigger_error('The second argument of the "Bar" method is deprecated.', E_USER_DEPRECATED); + use ExpectDeprecationTrait; + + /** + * @group legacy + */ + public function testDeprecatedCode() + { + // test some code that triggers the following deprecation: + // trigger_deprecation('vendor-name/package-name', '5.1', 'This "Foo" method is deprecated.'); + $this->expectDeprecation('Since vendor-name/package-name 5.1: This "%s" method is deprecated'); + + // ... + + // test some code that triggers the following deprecation: + // trigger_deprecation('vendor-name/package-name', '4.4', 'The second argument of the "Bar" method is deprecated.'); + $this->expectDeprecation('Since vendor-name/package-name 4.4: The second argument of the "%s" method is deprecated.'); + } } Display the Full Stack Trace @@ -394,10 +443,6 @@ the test suite cannot use the latest versions of PHPUnit because: Polyfills for the Unavailable Methods ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 4.4 - - This feature was introduced in Symfony 4.4. - When using the ``simple-phpunit`` script, PHPUnit Bridge injects polyfills for most methods of the ``TestCase`` and ``Assert`` classes (e.g. ``expectException()``, ``expectExceptionMessage()``, ``assertContainsEquals()``, etc.). This allows writing @@ -407,10 +452,6 @@ older PHPUnit versions. Removing the Void Return Type ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 4.4 - - This feature was introduced in Symfony 4.4. - When running the ``simple-phpunit`` script with the ``SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT`` environment variable set to ``1``, the PHPUnit bridge will alter the code of PHPUnit to remove the return type (introduced in PHPUnit 8) from ``setUp()``, @@ -446,10 +487,6 @@ call to the ``doSetUp()``, ``doTearDown()``, ``doSetUpBeforeClass()`` and Using Namespaced PHPUnit Classes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 4.4 - - This feature was introduced in Symfony 4.4. - The PHPUnit bridge adds namespaced class aliases for most of the PHPUnit classes declared without namespaces (e.g. ``PHPUnit_Framework_Assert``), allowing you to always use the namespaced class declaration even when the test is executed with @@ -619,20 +656,19 @@ functions: Use Case ~~~~~~~~ -Consider the following example that uses the ``checkMX`` option of the ``Email`` -constraint to test the validity of the email domain:: +Consider the following example that tests a custom class called ``DomainValidator`` +which defines a ``checkDnsRecord`` option to also validate that a domain is +associated to a valid host:: + use App\Validator\DomainValidator; use PHPUnit\Framework\TestCase; - use Symfony\Component\Validator\Constraints\Email; class MyTest extends TestCase { public function testEmail() { - $validator = ...; - $constraint = new Email(['checkMX' => true]); - - $result = $validator->validate('foo@example.com', $constraint); + $validator = new DomainValidator(['checkDnsRecord' => true]); + $isValid = $validator->validate('example.com'); // ... } @@ -642,22 +678,23 @@ In order to avoid making a real network connection, add the ``@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() { - DnsMock::withMockedHosts(['example.com' => [['type' => 'MX']]]); + DnsMock::withMockedHosts([ + 'example.com' => [['type' => 'A', 'ip' => '1.2.3.4']], + ]); - $validator = ...; - $constraint = new Email(['checkMX' => true]); - - $result = $validator->validate('foo@example.com', $constraint); + $validator = new DomainValidator(['checkDnsRecord' => true]); + $isValid = $validator->validate('example.com'); // ... } @@ -860,6 +897,10 @@ If you have installed the bridge through Composer, you can run it by calling e.g It's also possible to set ``SYMFONY_PHPUNIT_VERSION`` as a real env var (not defined in a :ref:`dotenv file `). + In the same way, ``SYMFONY_MAX_PHPUNIT_VERSION`` will set the maximum version + of PHPUnit to be considered. This is useful when testing a framework that does + not support the latest version(s) of PHPUnit. + .. tip:: If you still need to use ``prophecy`` (but not ``symfony/yaml``), @@ -867,6 +908,13 @@ If you have installed the bridge through Composer, you can run it by calling e.g It's also possible to set this env var in the ``phpunit.xml.dist`` file. +.. tip:: + + It is also possible to require additional packages that will be installed along + the rest of the needed PHPUnit packages using the ``SYMFONY_PHPUNIT_REQUIRE`` + env variable. This is specially useful for installing PHPUnit plugins without + having to add them to your main ``composer.json`` file. + Code Coverage Listener ---------------------- diff --git a/components/process.rst b/components/process.rst index d8d585fe987..19be88a706b 100644 --- a/components/process.rst +++ b/components/process.rst @@ -102,10 +102,16 @@ with a non-zero code):: :method:`Symfony\\Component\\Process\\Process::getLastOutputTime` method. This method returns ``null`` if the process wasn't started! - .. versionadded:: 4.4 +Configuring Process Options +--------------------------- - The :method:`Symfony\\Component\\Process\\Process::getLastOutputTime` - method was introduced in Symfony 4.4. +Symfony uses the PHP :phpfunction:`proc_open` function to run the processes. +You can configure the options passed to the ``other_options`` argument of +``proc_open()`` using the ``setOptions()`` method:: + + $process = new Process(['...', '...', '...']); + // this option allows a subprocess to continue running after the main script exited + $process->setOptions(['create_new_console' => true]); Using Features From the OS Shell -------------------------------- @@ -150,10 +156,6 @@ enclosing a variable name into ``"${:`` and ``}"`` exactly, the process object will replace it with its escaped value, or will fail if the variable is not found in the list of environment variables attached to the command. -.. versionadded:: 4.4 - - Portable command lines were introduced in Symfony 4.4. - Setting Environment Variables for Processes ------------------------------------------- @@ -442,6 +444,10 @@ check regularly:: usleep(200000); } +.. tip:: + + You can get the process start time using the ``getStartTime()`` method. + .. _reference-process-signal: Process Idle Timeout diff --git a/components/property_access.rst b/components/property_access.rst index f9375516cf8..d3486d02996 100644 --- a/components/property_access.rst +++ b/components/property_access.rst @@ -169,11 +169,6 @@ This will produce: ``This person is an author`` Accessing a non Existing Property Path ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 4.3 - - The ``disableExceptionOnInvalidPropertyPath()`` method was introduced in - Symfony 4.3. - By default a :class:`Symfony\\Component\\PropertyAccess\\Exception\\NoSuchPropertyException` is thrown if the property path passed to :method:`Symfony\\Component\\PropertyAccess\\PropertyAccessor::getValue` does not exist. You can change this behavior using the @@ -196,6 +191,8 @@ method:: $value = $propertyAccessor->getValue($person, 'birthday'); +.. _components-property-access-magic-get: + Magic ``__get()`` Method ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -218,6 +215,11 @@ The ``getValue()`` method can also use the magic ``__get()`` method:: var_dump($propertyAccessor->getValue($person, 'Wouter')); // [...] +.. note:: + + The ``__get()`` method support is enabled by default. + See `Enable other Features`_ if you want to disable it. + .. _components-property-access-magic-call: Magic ``__call()`` Method @@ -276,6 +278,8 @@ also write to an array. This can be achieved using the // or // var_dump($person['first_name']); // 'Wouter' +.. _components-property-access-writing-to-objects: + Writing to Objects ------------------ @@ -352,6 +356,11 @@ see `Enable other Features`_:: var_dump($person->getWouter()); // [...] +.. note:: + + The ``__set()`` method support is enabled by default. + See `Enable other Features`_ if you want to disable it. + Writing to Array Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -391,10 +400,49 @@ The PropertyAccess component checks for methods called ``add()``. Both methods must be defined. For instance, in the previous example, the component looks for the ``addChild()`` and ``removeChild()`` methods to access the ``children`` property. -`The Inflector component`_ is used to find the singular of a property name. +`The String component`_ inflector is used to find the singular of a property name. If available, *adder* and *remover* methods have priority over a *setter* method. +Using non-standard adder/remover methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes, adder and remover methods don't use the standard ``add`` or ``remove`` prefix, like in this example:: + + // ... + class PeopleList + { + // ... + + public function joinPeople(string $people): void + { + $this->peoples[] = $people; + } + + public function leavePeople(string $people): void + { + foreach ($this->peoples as $id => $item) { + if ($people === $item) { + unset($this->peoples[$id]); + break; + } + } + } + } + + use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; + use Symfony\Component\PropertyAccess\PropertyAccessor; + + $list = new PeopleList(); + $reflectionExtractor = new ReflectionExtractor(null, null, ['join', 'leave']); + $propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, PropertyAccessor::THROW_ON_INVALID_PROPERTY_PATH, null, $reflectionExtractor, $reflectionExtractor); + $propertyAccessor->setValue($person, 'peoples', ['kevin', 'wouter']); + + var_dump($person->getPeoples()); // ['kevin', 'wouter'] + +Instead of calling ``add()`` and ``remove()``, the PropertyAccess +component will call ``join()`` and ``leave()`` methods. + Checking Property Paths ----------------------- @@ -462,14 +510,20 @@ configured to enable extra features. To do that you could use the // ... $propertyAccessorBuilder = PropertyAccess::createPropertyAccessorBuilder(); - // enables magic __call - $propertyAccessorBuilder->enableMagicCall(); + $propertyAccessorBuilder->enableMagicCall(); // enables magic __call + $propertyAccessorBuilder->enableMagicGet(); // enables magic __get + $propertyAccessorBuilder->enableMagicSet(); // enables magic __set + $propertyAccessorBuilder->enableMagicMethods(); // enables magic __get, __set and __call - // disables magic __call - $propertyAccessorBuilder->disableMagicCall(); + $propertyAccessorBuilder->disableMagicCall(); // disables magic __call + $propertyAccessorBuilder->disableMagicGet(); // disables magic __get + $propertyAccessorBuilder->disableMagicSet(); // disables magic __set + $propertyAccessorBuilder->disableMagicMethods(); // disables magic __get, __set and __call - // checks if magic __call handling is enabled + // checks if magic __call, __get or __set handling are enabled $propertyAccessorBuilder->isMagicCallEnabled(); // true or false + $propertyAccessorBuilder->isMagicGetEnabled(); // true or false + $propertyAccessorBuilder->isMagicSetEnabled(); // true or false // At the end get the configured property accessor $propertyAccessor = $propertyAccessorBuilder->getPropertyAccessor(); @@ -481,7 +535,7 @@ configured to enable extra features. To do that you could use the Or you can pass parameters directly to the constructor (not the recommended way):: - // ... - $propertyAccessor = new PropertyAccessor(true); // this enables handling of magic __call + // enable handling of magic __call, __set but not __get: + $propertyAccessor = new PropertyAccessor(PropertyAccessor::MAGIC_CALL | PropertyAccessor::MAGIC_SET); -.. _The Inflector component: https://github.com/symfony/inflector +.. _`The String component`: https://github.com/symfony/string diff --git a/components/property_info.rst b/components/property_info.rst index 8d86663c140..dfd22c9d2b3 100644 --- a/components/property_info.rst +++ b/components/property_info.rst @@ -122,7 +122,7 @@ class exposes public methods to extract several types of information: * :ref:`List of properties `: :method:`Symfony\\Component\\PropertyInfo\\PropertyListExtractorInterface::getProperties` * :ref:`Property type `: :method:`Symfony\\Component\\PropertyInfo\\PropertyTypeExtractorInterface::getTypes` - (including typed properties since PHP 7.4) + (including typed properties) * :ref:`Property description `: :method:`Symfony\\Component\\PropertyInfo\\PropertyDescriptionExtractorInterface::getShortDescription` and :method:`Symfony\\Component\\PropertyInfo\\PropertyDescriptionExtractorInterface::getLongDescription` * :ref:`Property access details `: :method:`Symfony\\Component\\PropertyInfo\\PropertyAccessExtractorInterface::isReadable` and :method:`Symfony\\Component\\PropertyInfo\\PropertyAccessExtractorInterface::isWritable` * :ref:`Property initializable through the constructor `: :method:`Symfony\\Component\\PropertyInfo\\PropertyInitializableExtractorInterface::isInitializable` @@ -293,10 +293,6 @@ string values: ``array``, ``bool``, ``callable``, ``float``, ``int``, Constants inside the :class:`Symfony\\Component\\PropertyInfo\\Type` class, in the form ``Type::BUILTIN_TYPE_*``, are provided for convenience. -.. versionadded:: 4.4 - - Support for typed properties (added in PHP 7.4) was introduced in Symfony 4.4. - ``Type::isNullable()`` ~~~~~~~~~~~~~~~~~~~~~~ @@ -327,10 +323,6 @@ this returns ``true`` if: ``@var SomeClass``, ``@var SomeClass``, ``@var Doctrine\Common\Collections\Collection``, etc.) -.. versionadded:: 4.2 - - The support of phpDocumentor collection types was introduced in Symfony 4.2. - ``Type::getCollectionKeyType()`` & ``Type::getCollectionValueType()`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -365,7 +357,7 @@ Using PHP reflection, the :class:`Symfony\\Component\\PropertyInfo\\Extractor\\R provides list, type and access information from setter and accessor methods. It can also give the type of a property (even extracting it from the constructor arguments), and if it is initializable through the constructor. It supports -return and scalar types for PHP 7:: +return and scalar types:: use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; @@ -384,11 +376,6 @@ return and scalar types for PHP 7:: // Initializable information $reflectionExtractor->isInitializable($class, $property); -.. versionadded:: 4.1 - - The feature to extract the property types from constructor arguments was - introduced in Symfony 4.1. - .. note:: When using the Symfony framework, this service is automatically registered @@ -447,8 +434,12 @@ with the ``property_info`` service in the Symfony Framework:: ); $serializerExtractor = new SerializerExtractor($serializerClassMetadataFactory); - // List information. - $serializerExtractor->getProperties($class); + // the `serializer_groups` option must be configured (may be set to null) + $serializerExtractor->getProperties($class, ['serializer_groups' => ['mygroup']]); + +If ``serializer_groups`` is set to ``null``, serializer groups metadata won't be +checked but you will get only the properties considered by the Serializer +Component (notably the ``@Ignore`` annotation is taken into account). DoctrineExtractor ~~~~~~~~~~~~~~~~~ @@ -504,10 +495,6 @@ service by defining it as a service with one or more of the following * ``property_info.initializable_extractor`` if it provides initializable information (it checks if a property can be initialized through the constructor). -.. versionadded:: 4.2 - - The ``property_info.initializable_extractor`` was introduced in Symfony 4.2. - .. _`phpDocumentor Reflection`: https://github.com/phpDocumentor/ReflectionDocBlock .. _`phpdocumentor/reflection-docblock`: https://packagist.org/packages/phpdocumentor/reflection-docblock .. _`Doctrine ORM`: https://www.doctrine-project.org/projects/orm.html diff --git a/components/runtime.rst b/components/runtime.rst new file mode 100644 index 00000000000..32b3bb14174 --- /dev/null +++ b/components/runtime.rst @@ -0,0 +1,489 @@ +.. index:: + single: Runtime + single: Components; Runtime + +The Runtime Component +====================== + + The Runtime Component decouples the bootstrapping logic from any global state + to make sure the application can run with runtimes like PHP-PM, ReactPHP, + Swoole, etc. without any changes. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/runtime + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +The Runtime component abstracts most bootstrapping logic as so-called +*runtimes*, allowing you to write front-controllers in a generic way. +For instance, the Runtime component allows Symfony's ``public/index.php`` +to look like this:: + + handle(Request::createFromGlobals())->send()``). + +.. caution:: + + If you use the Composer ``--no-plugins`` option, the ``autoload_runtime.php`` + file won't be created. + + If you use the Composer ``--no-scripts`` option, make sure your Composer version + is ``>=2.1.3``; otherwise the ``autoload_runtime.php`` file won't be created. + +To make a console application, the bootstrap code would look like:: + + #!/usr/bin/env php + setCode(function (InputInterface $input, OutputInterface $output) { + $output->write('Hello World'); + }); + + return $command; + }; + +:class:`Symfony\\Component\\Console\\Application` + Useful with console applications with more than one command. This will use the + :class:`Symfony\\Component\\Runtime\\Runner\\Symfony\\ConsoleApplicationRunner`:: + + setCode(function (InputInterface $input, OutputInterface $output) { + $output->write('Hello World'); + }); + + $app = new Application(); + $app->add($command); + $app->setDefaultCommand('hello', true); + + return $app; + }; + +The ``GenericRuntime`` and ``SymfonyRuntime`` also support these generic +applications: + +:class:`Symfony\\Component\\Runtime\\RunnerInterface` + The ``RunnerInterface`` is a way to use a custom application with the + generic Runtime:: + + '/var/task', + ]; + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + // ... + +You can also configure ``extra.runtime.options`` in ``composer.json``: + +.. code-block:: json + + { + "require": { + "...": "..." + }, + "extra": { + "runtime": { + "project_dir": "/var/task" + } + } + } + +The following options are supported by the ``SymfonyRuntime``: + +``env`` (default: ``APP_ENV`` environment variable, or ``"dev"``) + To define the name of the environment the app runs in. +``disable_dotenv`` (default: ``false``) + To disable looking for ``.env`` files. +``dotenv_path`` (default: ``.env``) + To define the path of dot-env files. +``use_putenv`` + To tell Dotenv to set env vars using ``putenv()`` (NOT RECOMMENDED). +``prod_envs`` (default: ``["prod"]``) + To define the names of the production envs. +``test_envs`` (default: ``["test"]``) + To define the names of the test envs. + +Besides these, the ``GenericRuntime`` and ``SymfonyRuntime`` also support +these options: + +``debug`` (default: the value of the env var defined by ``debug_var_name`` option + (usually, ``APP_DEBUG``), or ``true`` if such env var is not defined) + Toggles the :ref:`debug mode ` of Symfony applications (e.g. to + display errors) +``runtimes`` + Maps "application types" to a ``GenericRuntime`` implementation that + knows how to deal with each of them. +``error_handler`` (default: :class:`Symfony\\Component\\Runtime\\Internal\\BasicErrorHandler` or :class:`Symfony\\Component\\Runtime\\Internal\\SymfonyErrorHandler` for ``SymfonyRuntime``) + Defines the class to use to handle PHP errors. +``env_var_name`` (default: ``"APP_ENV"``) + Defines the name of the env var that stores the name of the + :ref:`configuration environment ` + to use when running the application. +``debug_var_name`` (default: ``"APP_DEBUG"``) + Defines the name of the env var that stores the value of the + :ref:`debug mode ` flag to use when running the application. + +Create Your Own Runtime +----------------------- + +This is an advanced topic that describes the internals of the Runtime component. + +Using the Runtime component will benefit maintainers because the bootstrap +logic could be versioned as a part of a normal package. If the application +author decides to use this component, the package maintainer of the Runtime +class will have more control and can fix bugs and add features. + +The Runtime component is designed to be totally generic and able to run any +application outside of the global state in 6 steps: + +#. The main entry point returns a *callable* (the "app") that wraps the application; +#. The *app callable* is passed to ``RuntimeInterface::getResolver()``, which returns + a :class:`Symfony\\Component\\Runtime\\ResolverInterface`. This resolver returns + an array with the app callable (or something that decorates this callable) at + index 0 and all its resolved arguments at index 1. +#. The *app callable* is invoked with its arguments, it will return an object that + represents the application. +#. This *application object* is passed to ``RuntimeInterface::getRunner()``, which + returns a :class:`Symfony\\Component\\Runtime\\RunnerInterface`: an instance + that knows how to "run" the application object. +#. The ``RunnerInterface::run(object $application)`` is called and it returns the + exit status code as `int`. +#. The PHP engine is terminated with this status code. + +When creating a new runtime, there are two things to consider: First, what arguments +will the end user use? Second, what will the user's application look like? + +For instance, imagine you want to create a runtime for `ReactPHP`_: + +**What arguments will the end user use?** + +For a generic ReactPHP application, no special arguments are +typically required. This means that you can use the +:class:`Symfony\\Component\\Runtime\\GenericRuntime`. + +**What will the user's application look like?** + +There is also no typical React application, so you might want to rely on +the `PSR-15`_ interfaces for HTTP request handling. + +However, a ReactPHP application will need some special logic to *run*. That logic +is added in a new class implementing :class:`Symfony\\Component\\Runtime\\RunnerInterface`:: + + use Psr\Http\Message\ServerRequestInterface; + use Psr\Http\Server\RequestHandlerInterface; + use React\EventLoop\Factory as ReactFactory; + use React\Http\Server as ReactHttpServer; + use React\Socket\Server as ReactSocketServer; + use Symfony\Component\Runtime\RunnerInterface; + + class ReactPHPRunner implements RunnerInterface + { + private $application; + private $port; + + public function __construct(RequestHandlerInterface $application, int $port) + { + $this->application = $application; + $this->port = $port; + } + + public function run(): int + { + $application = $this->application; + $loop = ReactFactory::create(); + + // configure ReactPHP to correctly handle the PSR-15 application + $server = new ReactHttpServer( + $loop, + function (ServerRequestInterface $request) use ($application) { + return $application->handle($request); + } + ); + + // start the ReactPHP server + $socket = new ReactSocketServer($this->port, $loop); + $server->listen($socket); + + $loop->run(); + + return 0; + } + } + +By extending the ``GenericRuntime``, you make sure that the application is +always using this ``ReactPHPRunner``:: + + use Symfony\Component\Runtime\GenericRuntime; + use Symfony\Component\Runtime\RunnerInterface; + + class ReactPHPRuntime extends GenericRuntime + { + private $port; + + public function __construct(array $options) + { + $this->port = $options['port'] ?? 8080; + parent::__construct($options); + } + + public function getRunner(?object $application): RunnerInterface + { + if ($application instanceof RequestHandlerInterface) { + return new ReactPHPRunner($application, $this->port); + } + + // if it's not a PSR-15 application, use the GenericRuntime to + // run the application (see "Resolvable Applications" above) + return parent::getRunner($application); + } + } + +The end user will now be able to create front controller like:: + + `:: - - use Symfony\Component\HttpKernel\Event\RequestEvent; - use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; - use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; - use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; - - class SomeAuthenticationListener - { - /** - * @var TokenStorageInterface - */ - private $tokenStorage; - - /** - * @var AuthenticationManagerInterface - */ - private $authenticationManager; - - /** - * @var string Uniquely identifies the secured area - */ - private $providerKey; - - // ... - - public function __invoke(RequestEvent $event) - { - $request = $event->getRequest(); - - $username = ...; - $password = ...; - - $unauthenticatedToken = new UsernamePasswordToken( - $username, - $password, - $this->providerKey - ); - - $authenticatedToken = $this - ->authenticationManager - ->authenticate($unauthenticatedToken); - - $this->tokenStorage->setToken($authenticatedToken); - } - } - -.. note:: - - A token can be of any class, as long as it implements - :class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface`. - -The Authentication Manager --------------------------- - -The default authentication manager is an instance of -:class:`Symfony\\Component\\Security\\Core\\Authentication\\AuthenticationProviderManager`:: - - use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager; - use Symfony\Component\Security\Core\Exception\AuthenticationException; - - // instances of Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface - $providers = [...]; - - $authenticationManager = new AuthenticationProviderManager($providers); - - try { - $authenticatedToken = $authenticationManager - ->authenticate($unauthenticatedToken); - } catch (AuthenticationException $exception) { - // authentication failed - } - -The ``AuthenticationProviderManager``, when instantiated, receives several -authentication providers, each supporting a different type of token. - -.. note:: - - You may write your own authentication manager, the only requirement is that - it implements :class:`Symfony\\Component\\Security\\Core\\Authentication\\AuthenticationManagerInterface`. - -.. _authentication_providers: - -Authentication Providers ------------------------- - -Each provider (since it implements -:class:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\AuthenticationProviderInterface`) -has a :method:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\AuthenticationProviderInterface::supports` method -by which the ``AuthenticationProviderManager`` -can determine if it supports the given token. If this is the case, the -manager then calls the provider's :method:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\AuthenticationProviderInterface::authenticate` method. -This method should return an authenticated token or throw an -:class:`Symfony\\Component\\Security\\Core\\Exception\\AuthenticationException` -(or any other exception extending it). - -Authenticating Users by their Username and Password -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An authentication provider will attempt to authenticate a user based on -the credentials they provided. Usually these are a username and a password. -Most web applications store their user's username and a hash of the user's -password combined with a randomly generated salt. This means that the average -authentication would consist of fetching the salt and the hashed password -from the user data storage, hash the password the user has just provided -(e.g. using a login form) with the salt and compare both to determine if -the given password is valid. - -This functionality is offered by the :class:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\DaoAuthenticationProvider`. -It fetches the user's data from a :class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`, -uses a :class:`Symfony\\Component\\Security\\Core\\Encoder\\PasswordEncoderInterface` -to create a hash of the password and returns an authenticated token if the -password was valid:: - - use Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider; - use Symfony\Component\Security\Core\Encoder\EncoderFactory; - use Symfony\Component\Security\Core\User\InMemoryUserProvider; - use Symfony\Component\Security\Core\User\UserChecker; - - $userProvider = new InMemoryUserProvider( - [ - 'admin' => [ - // password is "foo" - 'password' => '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg==', - 'roles' => ['ROLE_ADMIN'], - ], - ] - ); - - // for some extra checks: is account enabled, locked, expired, etc. - $userChecker = new UserChecker(); - - // an array of password encoders (see below) - $encoderFactory = new EncoderFactory(...); - - $daoProvider = new DaoAuthenticationProvider( - $userProvider, - $userChecker, - 'secured_area', - $encoderFactory - ); - - $daoProvider->authenticate($unauthenticatedToken); - -.. note:: - - The example above demonstrates the use of the "in-memory" user provider, - but you may use any user provider, as long as it implements - :class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`. - It is also possible to let multiple user providers try to find the user's - data, using the :class:`Symfony\\Component\\Security\\Core\\User\\ChainUserProvider`. - -The Password Encoder Factory -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The :class:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\DaoAuthenticationProvider` -uses an encoder factory to create a password encoder for a given type of -user. This allows you to use different encoding strategies for different -types of users. The default :class:`Symfony\\Component\\Security\\Core\\Encoder\\EncoderFactory` -receives an array of encoders:: - - use Acme\Entity\LegacyUser; - use Symfony\Component\Security\Core\Encoder\EncoderFactory; - use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder; - use Symfony\Component\Security\Core\User\User; - - $defaultEncoder = new MessageDigestPasswordEncoder('sha512', true, 5000); - $weakEncoder = new MessageDigestPasswordEncoder('md5', true, 1); - - $encoders = [ - User::class => $defaultEncoder, - LegacyUser::class => $weakEncoder, - // ... - ]; - $encoderFactory = new EncoderFactory($encoders); - -Each encoder should implement :class:`Symfony\\Component\\Security\\Core\\Encoder\\PasswordEncoderInterface` -or be an array with a ``class`` and an ``arguments`` key, which allows the -encoder factory to construct the encoder only when it is needed. - -Creating a custom Password Encoder -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -There are many built-in password encoders. But if you need to create your -own, it needs to follow these rules: - -#. The class must implement :class:`Symfony\\Component\\Security\\Core\\Encoder\\PasswordEncoderInterface` - (you can also extend :class:`Symfony\\Component\\Security\\Core\\Encoder\\BasePasswordEncoder`); - -#. The implementations of - :method:`Symfony\\Component\\Security\\Core\\Encoder\\PasswordEncoderInterface::encodePassword` - and - :method:`Symfony\\Component\\Security\\Core\\Encoder\\PasswordEncoderInterface::isPasswordValid` - must first of all make sure the password is not too long, i.e. the password length is no longer - than 4096 characters. This is for security reasons (see `CVE-2013-5750`_), and you can use the - :method:`Symfony\\Component\\Security\\Core\\Encoder\\BasePasswordEncoder::isPasswordTooLong` - method for this check:: - - use Symfony\Component\Security\Core\Encoder\BasePasswordEncoder; - use Symfony\Component\Security\Core\Exception\BadCredentialsException; - - class FoobarEncoder extends BasePasswordEncoder - { - public function encodePassword($raw, $salt) - { - if ($this->isPasswordTooLong($raw)) { - throw new BadCredentialsException('Invalid password.'); - } - - // ... - } - - public function isPasswordValid($encoded, $raw, $salt) - { - if ($this->isPasswordTooLong($raw)) { - return false; - } - - // ... - } - } - -Using Password Encoders -~~~~~~~~~~~~~~~~~~~~~~~ - -When the :method:`Symfony\\Component\\Security\\Core\\Encoder\\EncoderFactory::getEncoder` -method of the password encoder factory is called with the user object as -its first argument, it will return an encoder of type :class:`Symfony\\Component\\Security\\Core\\Encoder\\PasswordEncoderInterface` -which should be used to encode this user's password:: - - // a Acme\Entity\LegacyUser instance - $user = ...; - - // the password that was submitted, e.g. when registering - $plainPassword = ...; - - $encoder = $encoderFactory->getEncoder($user); - - // returns $weakEncoder (see above) - $encodedPassword = $encoder->encodePassword($plainPassword, $user->getSalt()); - - $user->setPassword($encodedPassword); - - // ... save the user - -Now, when you want to check if the submitted password (e.g. when trying to log -in) is correct, you can use:: - - // fetch the Acme\Entity\LegacyUser - $user = ...; - - // the submitted password, e.g. from the login form - $plainPassword = ...; - - $validPassword = $encoder->isPasswordValid( - $user->getPassword(), // the encoded password - $plainPassword, // the submitted password - $user->getSalt() - ); - -Authentication Events ---------------------- - -The security component provides the following authentication events: - -=============================== ======================================================================== ============================================================================== -Name Event Constant Argument Passed to the Listener -=============================== ======================================================================== ============================================================================== -security.authentication.success ``AuthenticationEvents::AUTHENTICATION_SUCCESS`` :class:`Symfony\\Component\\Security\\Core\\Event\\AuthenticationSuccessEvent` -security.authentication.failure ``AuthenticationEvents::AUTHENTICATION_FAILURE`` :class:`Symfony\\Component\\Security\\Core\\Event\\AuthenticationFailureEvent` -security.interactive_login ``SecurityEvents::INTERACTIVE_LOGIN`` :class:`Symfony\\Component\\Security\\Http\\Event\\InteractiveLoginEvent` -security.switch_user ``SecurityEvents::SWITCH_USER`` :class:`Symfony\\Component\\Security\\Http\\Event\\SwitchUserEvent` -security.logout_on_change ``Symfony\Component\Security\Http\Event\DeauthenticatedEvent::class`` :class:`Symfony\\Component\\Security\\Http\\Event\\DeauthenticatedEvent` -=============================== ======================================================================== ============================================================================== - -Authentication Success and Failure Events -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When a provider authenticates the user, a ``security.authentication.success`` -event is dispatched. But beware - this event may fire, for example, on *every* -request if you have session-based authentication, if ``always_authenticate_before_granting`` -is enabled or if the token is not authenticated before AccessListener is invoked. -See ``security.interactive_login`` below if you need to do something when a user *actually* logs in. - -When a provider attempts authentication but fails (i.e. throws an ``AuthenticationException``), -a ``security.authentication.failure`` event is dispatched. You could listen on -the ``security.authentication.failure`` event, for example, in order to log -failed login attempts. - -Security Events -~~~~~~~~~~~~~~~ - -The ``security.interactive_login`` event is triggered after a user has actively -logged into your website. It is important to distinguish this action from -non-interactive authentication methods, such as: - -* authentication based on your session. -* authentication using a HTTP basic header. - -You could listen on the ``security.interactive_login`` event, for example, in -order to give your user a welcome flash message every time they log in. - -The ``security.switch_user`` event is triggered every time you activate -the ``switch_user`` firewall listener. - -The ``Symfony\Component\Security\Http\Event\DeauthenticatedEvent`` event is triggered when a token has been deauthenticated -because of a user change. It can help you perform clean-up tasks. - -.. versionadded:: 4.3 - - The ``Symfony\Component\Security\Http\Event\DeauthenticatedEvent`` event was introduced in Symfony 4.3. - -.. seealso:: - - For more information on switching users, see - :doc:`/security/impersonating_user`. - -.. _`CVE-2013-5750`: https://symfony.com/blog/cve-2013-5750-security-issue-in-fosuserbundle-login-form diff --git a/components/security/authorization.rst b/components/security/authorization.rst deleted file mode 100644 index d1df7d1f8bb..00000000000 --- a/components/security/authorization.rst +++ /dev/null @@ -1,263 +0,0 @@ -.. index:: - single: Security, Authorization - -Authorization -============= - -When any of the authentication providers (see :ref:`authentication_providers`) -has verified the still-unauthenticated token, an authenticated token will -be returned. The authentication listener should set this token directly -in the :class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\Storage\\TokenStorageInterface` -using its :method:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\Storage\\TokenStorageInterface::setToken` -method. - -From then on, the user is authenticated, i.e. identified. Now, other parts -of the application can use the token to decide whether or not the user may -request a certain URI, or modify a certain object. This decision will be made -by an instance of :class:`Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManagerInterface`. - -An authorization decision will always be based on a few things: - -* The current token - For instance, the token's :method:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface::getRoles` - method may be used to retrieve the roles of the current user (e.g. - ``ROLE_SUPER_ADMIN``), or a decision may be based on the class of the token. -* A set of attributes - Each attribute stands for a certain right the user should have, e.g. - ``ROLE_ADMIN`` to make sure the user is an administrator. -* An object (optional) - Any object for which access control needs to be checked, like - an article or a comment object. - -.. _components-security-access-decision-manager: - -Access Decision Manager ------------------------ - -Since deciding whether or not a user is authorized to perform a certain -action can be a complicated process, the standard :class:`Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManager` -itself depends on multiple voters, and makes a final verdict based on all -the votes (either positive, negative or neutral) it has received. It -recognizes several strategies: - -``affirmative`` (default) - grant access as soon as there is one voter granting access; - -``consensus`` - grant access if there are more voters granting access than there are denying; - -``unanimous`` - only grant access if none of the voters has denied access. If all voters - abstained from voting, the decision is based on the ``allow_if_all_abstain`` - config option (which defaults to ``false``). - -Usage of the available options in detail:: - - use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; - - // instances of Symfony\Component\Security\Core\Authorization\Voter\VoterInterface - $voters = [...]; - - // one of "affirmative", "consensus", "unanimous" - $strategy = ...; - - // whether or not to grant access when all voters abstain - $allowIfAllAbstainDecisions = ...; - - // whether or not to grant access when there is no majority (applies only to the "consensus" strategy) - $allowIfEqualGrantedDeniedDecisions = ...; - - $accessDecisionManager = new AccessDecisionManager( - $voters, - $strategy, - $allowIfAllAbstainDecisions, - $allowIfEqualGrantedDeniedDecisions - ); - -.. seealso:: - - You can change the default strategy in the - :ref:`configuration `. - -Voters ------- - -Voters are instances -of :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface`, -which means they have to implement a few methods which allows the decision -manager to use them: - -``vote(TokenInterface $token, $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?:: - - use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver; - - $trustResolver = new AuthenticationTrustResolver(); - - $authenticatedVoter = new AuthenticatedVoter($trustResolver); - - // instance of Symfony\Component\Security\Core\Authentication\Token\TokenInterface - $token = ...; - - // any object - $object = ...; - - $vote = $authenticatedVoter->vote($token, $object, ['IS_AUTHENTICATED_FULLY']); - -RoleVoter -~~~~~~~~~ - -The :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\RoleVoter` -supports attributes starting with ``ROLE_`` and grants access to the user -when at least one required ``ROLE_*`` attribute can be found in the array of -roles returned by the token's :method:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface::getRoles` -method:: - - use Symfony\Component\Security\Core\Authorization\Voter\RoleVoter; - - $roleVoter = new RoleVoter('ROLE_'); - - $roleVoter->vote($token, $object, ['ROLE_ADMIN']); - -RoleHierarchyVoter -~~~~~~~~~~~~~~~~~~ - -The :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\RoleHierarchyVoter` -extends :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\RoleVoter` -and provides some additional functionality: it knows how to handle a -hierarchy of roles. For instance, a ``ROLE_SUPER_ADMIN`` role may have sub-roles -``ROLE_ADMIN`` and ``ROLE_USER``, so that when a certain object requires the -user to have the ``ROLE_ADMIN`` role, it grants access to users who in fact -have the ``ROLE_ADMIN`` role, but also to users having the ``ROLE_SUPER_ADMIN`` -role:: - - use Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter; - use Symfony\Component\Security\Core\Role\RoleHierarchy; - - $hierarchy = [ - 'ROLE_SUPER_ADMIN' => ['ROLE_ADMIN', 'ROLE_USER'], - ]; - - $roleHierarchy = new RoleHierarchy($hierarchy); - - $roleHierarchyVoter = new RoleHierarchyVoter($roleHierarchy); - -ExpressionVoter -~~~~~~~~~~~~~~~ - -The :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\ExpressionVoter` -grants access based on the evaluation of expressions created with the -:doc:`ExpressionLanguage component `. These -expressions have access to a number of -:ref:`special security variables `:: - - use Symfony\Component\ExpressionLanguage\Expression; - use Symfony\Component\Security\Core\Authorization\Voter\ExpressionVoter; - - // Symfony\Component\Security\Core\Authorization\ExpressionLanguage; - $expressionLanguage = ...; - - // instance of Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface - $trustResolver = ...; - - // Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface - $authorizationChecker = ...; - - $expressionVoter = new ExpressionVoter($expressionLanguage, $trustResolver, $authorizationChecker); - - // instance of Symfony\Component\Security\Core\Authentication\Token\TokenInterface - $token = ...; - - // any object - $object = ...; - - $expression = new Expression( - '"ROLE_ADMIN" in roles or (not is_anonymous() and user.isSuperAdmin())' - ); - - $vote = $expressionVoter->vote($token, $object, [$expression]); - -.. note:: - - When you make your own voter, you can use its constructor to inject any - dependencies it needs to come to a decision. - -Roles ------ - -Roles are strings that give expression to a certain right the user has (e.g. -*"edit a blog post"*, *"create an invoice"*). You can freely choose those -strings. The only requirement is that they must start with the ``ROLE_`` prefix -(e.g. ``ROLE_POST_EDIT``, ``ROLE_INVOICE_CREATE``). - -Using the Decision Manager --------------------------- - -The Access Listener -~~~~~~~~~~~~~~~~~~~ - -The access decision manager can be used at any point in a request to decide whether -or not the current user is entitled to access a given resource. One optional, -but useful, method for restricting access based on a URL pattern is the -:class:`Symfony\\Component\\Security\\Http\\Firewall\\AccessListener`, -which is one of the firewall listeners (see :ref:`firewall_listeners`) that -is triggered for each request matching the firewall map (see :ref:`firewall`). - -It uses an access map (which should be an instance of :class:`Symfony\\Component\\Security\\Http\\AccessMapInterface`) -which contains request matchers and a corresponding set of attributes that -are required for the current user to get access to the application:: - - use Symfony\Component\HttpFoundation\RequestMatcher; - use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; - use Symfony\Component\Security\Http\AccessMap; - use Symfony\Component\Security\Http\Firewall\AccessListener; - - $accessMap = new AccessMap(); - $tokenStorage = new TokenStorage(); - $requestMatcher = new RequestMatcher('^/admin'); - $accessMap->add($requestMatcher, ['ROLE_ADMIN']); - - $accessListener = new AccessListener( - $tokenStorage, - $accessDecisionManager, - $accessMap, - $authenticationManager - ); - -Authorization Checker -~~~~~~~~~~~~~~~~~~~~~ - -The access decision manager is also available to other parts of the application -via the :method:`Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationChecker::isGranted` -method of the :class:`Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationChecker`. -A call to this method will directly delegate the question to the access -decision manager:: - - use Symfony\Component\Security\Core\Authorization\AuthorizationChecker; - use Symfony\Component\Security\Core\Exception\AccessDeniedException; - - $authorizationChecker = new AuthorizationChecker( - $tokenStorage, - $authenticationManager, - $accessDecisionManager - ); - - if (!$authorizationChecker->isGranted('ROLE_ADMIN')) { - throw new AccessDeniedException(); - } - diff --git a/components/security/firewall.rst b/components/security/firewall.rst deleted file mode 100644 index adb0fae6e4a..00000000000 --- a/components/security/firewall.rst +++ /dev/null @@ -1,164 +0,0 @@ -.. index:: - single: Security, Firewall - -The Firewall and Authorization -============================== - -Central to the Security component is authorization. This is handled by an instance -of :class:`Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationCheckerInterface`. -When all steps in the process of authenticating the user have been taken successfully, -you can ask the authorization checker if the authenticated user has access to a -certain action or resource of the application:: - - use Symfony\Component\Security\Core\Authorization\AuthorizationChecker; - use Symfony\Component\Security\Core\Exception\AccessDeniedException; - - // instance of Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface - $tokenStorage = ...; - - // instance of Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface - $authenticationManager = ...; - - // instance of Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface - $accessDecisionManager = ...; - - $authorizationChecker = new AuthorizationChecker( - $tokenStorage, - $authenticationManager, - $accessDecisionManager - ); - - // ... authenticate the user - - if (!$authorizationChecker->isGranted('ROLE_ADMIN')) { - throw new AccessDeniedException(); - } - -.. note:: - - Read the dedicated articles to learn more about :doc:`/components/security/authentication` - and :doc:`/components/security/authorization`. - -.. _firewall: - -A Firewall for HTTP Requests ----------------------------- - -Authenticating a user is done by the firewall. An application may have -multiple secured areas, so the firewall is configured using a map of these -secured areas. For each of these areas, the map contains a request matcher -and a collection of listeners. The request matcher gives the firewall the -ability to find out if the current request points to a secured area. -The listeners are then asked if the current request can be used to authenticate -the user:: - - use Symfony\Component\HttpFoundation\RequestMatcher; - use Symfony\Component\Security\Http\Firewall\ExceptionListener; - use Symfony\Component\Security\Http\FirewallMap; - - $firewallMap = new FirewallMap(); - - $requestMatcher = new RequestMatcher('^/secured-area/'); - - // array of callables - $listeners = [...]; - - $exceptionListener = new ExceptionListener(...); - - $firewallMap->add($requestMatcher, $listeners, $exceptionListener); - -The firewall map will be given to the firewall as its first argument, together -with the event dispatcher that is used by the :class:`Symfony\\Component\\HttpKernel\\HttpKernel`:: - - use Symfony\Component\HttpKernel\KernelEvents; - use Symfony\Component\Security\Http\Firewall; - - // the EventDispatcher used by the HttpKernel - $dispatcher = ...; - - $firewall = new Firewall($firewallMap, $dispatcher); - - $dispatcher->addListener( - KernelEvents::REQUEST, - [$firewall, 'onKernelRequest'] - ); - -The firewall is registered to listen to the ``kernel.request`` event that -will be dispatched by the HttpKernel at the beginning of each request -it processes. This way, the firewall may prevent the user from going any -further than allowed. - -Firewall Config -~~~~~~~~~~~~~~~ - -The information about a given firewall, such as its name, provider, context, -entry point and access denied URL, is provided by instances of the -:class:`Symfony\\Bundle\\SecurityBundle\\Security\\FirewallConfig` class. - -This object can be accessed through the ``getFirewallConfig(Request $request)`` -method of the :class:`Symfony\\Bundle\\SecurityBundle\\Security\\FirewallMap` class and -through the ``getConfig()`` method of the -:class:`Symfony\\Bundle\\SecurityBundle\\Security\\FirewallContext` class. - -.. _firewall_listeners: - -Firewall Listeners -~~~~~~~~~~~~~~~~~~ - -When the firewall gets notified of the ``kernel.request`` event, it asks -the firewall map if the request matches one of the secured areas. The first -secured area that matches the request will return a set of corresponding -firewall listeners (which each is a callable). -These listeners will all be asked to handle the current request. This basically -means: find out if the current request contains any information by which -the user might be authenticated (for instance the Basic HTTP authentication -listener checks if the request has a header called ``PHP_AUTH_USER``). - -Exception Listener -~~~~~~~~~~~~~~~~~~ - -If any of the listeners throws an :class:`Symfony\\Component\\Security\\Core\\Exception\\AuthenticationException`, -the exception listener that was provided when adding secured areas to the -firewall map will jump in. - -The exception listener determines what happens next, based on the arguments -it received when it was created. It may start the authentication procedure, -perhaps ask the user to supply their credentials again (when they have only been -authenticated based on a "remember-me" cookie), or transform the exception -into an :class:`Symfony\\Component\\HttpKernel\\Exception\\AccessDeniedHttpException`, -which will eventually result in an "HTTP/1.1 403: Access Denied" response. - -Entry Points -~~~~~~~~~~~~ - -When the user is not authenticated at all (i.e. when the token storage -has no token yet), the firewall's entry point will be called to "start" -the authentication process. An entry point should implement -:class:`Symfony\\Component\\Security\\Http\\EntryPoint\\AuthenticationEntryPointInterface`, -which has only one method: :method:`Symfony\\Component\\Security\\Http\\EntryPoint\\AuthenticationEntryPointInterface::start`. -This method receives the current :class:`Symfony\\Component\\HttpFoundation\\Request` -object and the exception by which the exception listener was triggered. -The method should return a :class:`Symfony\\Component\\HttpFoundation\\Response` -object. This could be, for instance, the page containing the login form or, -in the case of Basic HTTP authentication, a response with a ``WWW-Authenticate`` -header, which will prompt the user to supply their username and password. - -Flow: Firewall, Authentication, Authorization ---------------------------------------------- - -Hopefully you can now see a little bit about how the "flow" of the security -context works: - -#. The Firewall is registered as a listener on the ``kernel.request`` event; -#. At the beginning of the request, the Firewall checks the firewall map - to see if any firewall should be active for this URL; -#. If a firewall is found in the map for this URL, its listeners are notified; -#. Each listener checks to see if the current request contains any authentication - information - a listener may (a) authenticate a user, (b) throw an - ``AuthenticationException``, or (c) do nothing (because there is no - authentication information on the request); -#. Once a user is authenticated, you'll use :doc:`/components/security/authorization` - to deny access to certain resources. - -Read the next articles to find out more about :doc:`/components/security/authentication` -and :doc:`/components/security/authorization`. diff --git a/components/security/secure_tools.rst b/components/security/secure_tools.rst deleted file mode 100644 index a9d6e0fec3a..00000000000 --- a/components/security/secure_tools.rst +++ /dev/null @@ -1,56 +0,0 @@ -Securely Generating Random Values -================================= - -The Symfony Security component comes with a collection of nice utilities -related to security. These utilities are used by Symfony, but you should -also use them if you want to solve the problem they address. - -.. note:: - - The functions described in this article were introduced in PHP 5.6 or 7. - For older PHP versions, a polyfill is provided by the - `Symfony Polyfill Component`_. - -Comparing Strings -~~~~~~~~~~~~~~~~~ - -The time it takes to compare two strings depends on their differences. This -can be used by an attacker when the two strings represent a password for -instance; it is known as a `Timing attack`_. - -When comparing two passwords, you should use the :phpfunction:`hash_equals` -function:: - - if (hash_equals($knownString, $userInput)) { - // ... - } - -Generating a Secure Random String -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Whenever you need to generate a secure random string, you are highly -encouraged to use the :phpfunction:`random_bytes` function:: - - $random = random_bytes(10); - -The function returns a random string, suitable for cryptographic use, of -the number bytes passed as an argument (10 in the above example). - -.. tip:: - - The ``random_bytes()`` function returns a binary string which may contain - the ``\0`` character. This can cause trouble in several common scenarios, - such as storing this value in a database or including it as part of the - URL. The solution is to hash the value returned by ``random_bytes()`` with - a hashing function such as :phpfunction:`md5` or :phpfunction:`sha1`. - -Generating a Secure Random Number -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you need to generate a cryptographically secure random integer, you should -use the :phpfunction:`random_int` function:: - - $random = random_int(1, 10); - -.. _`Timing attack`: https://en.wikipedia.org/wiki/Timing_attack -.. _`Symfony Polyfill Component`: https://github.com/symfony/polyfill diff --git a/components/semaphore.rst b/components/semaphore.rst new file mode 100644 index 00000000000..81bf2b6fe39 --- /dev/null +++ b/components/semaphore.rst @@ -0,0 +1,77 @@ +.. index:: + single: Semaphore + single: Components; Semaphore + +The Semaphore Component +======================= + + The Semaphore Component manages `semaphores`_, a mechanism to provide + exclusive access to a shared resource. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/semaphore + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +In computer science, a semaphore is a variable or abstract data type used to +control access to a common resource by multiple processes in a concurrent +system such as a multitasking operating system. The main difference +with :doc:`locks ` is that semaphores allow more than one process to +access a resource, whereas locks only allow one process. + +Create semaphores with the :class:`Symfony\\Component\\Semaphore\\SemaphoreFactory` +class, which in turn requires another class to manage the storage:: + + use Symfony\Component\Semaphore\SemaphoreFactory; + use Symfony\Component\Semaphore\Store\RedisStore; + + $redis = new Redis(); + $redis->connect('172.17.0.2'); + + $store = new RedisStore($redis); + $factory = new SemaphoreFactory($store); + +The semaphore is created by calling the +:method:`Symfony\\Component\\Semaphore\\SemaphoreFactory::createSemaphore` +method. Its first argument is an arbitrary string that represents the locked +resource. Its second argument is the maximum number of processes allowed. Then, a +call to the :method:`Symfony\\Component\\Semaphore\\SemaphoreInterface::acquire` +method will try to acquire the semaphore:: + + // ... + $semaphore = $factory->createSemaphore('pdf-invoice-generation', 2); + + if ($semaphore->acquire()) { + // The resource "pdf-invoice-generation" is locked. + // Here you can safely compute and generate the invoice. + + $semaphore->release(); + } + +If the semaphore can not be acquired, the method returns ``false``. The +``acquire()`` method can be safely called repeatedly, even if the semaphore is +already acquired. + +.. note:: + + Unlike other implementations, the Semaphore component distinguishes + semaphores instances even when they are created for the same resource. If a + semaphore has to be used by several services, they should share the same + ``Semaphore`` instance returned by the ``SemaphoreFactory::createSemaphore`` + method. + +.. tip:: + + If you don't release the semaphore explicitly, it will be released + automatically on instance destruction. In some cases, it can be useful to + lock a resource across several requests. To disable the automatic release + behavior, set the fifth argument of the ``createSemaphore()`` method to ``false``. + +.. _`semaphores`: https://en.wikipedia.org/wiki/Semaphore_(programming) diff --git a/components/serializer.rst b/components/serializer.rst index c26cd480134..27d503e49ed 100644 --- a/components/serializer.rst +++ b/components/serializer.rst @@ -229,11 +229,6 @@ normalized data, instead of the denormalizer re-creating them. Note that arrays of objects. Those will still be replaced when present in the normalized data. -.. versionadded:: 4.3 - - The ``AbstractObjectNormalizer::DEEP_OBJECT_TO_POPULATE`` option was - introduced in Symfony 4.3. - .. _component-serializer-attributes-groups: Attributes Groups @@ -322,6 +317,26 @@ Then, create your groups definition: // ... } + .. code-block:: php-attributes + + 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: @@ -420,9 +435,88 @@ As for groups, attributes can be selected during both the serialization and dese Ignoring Attributes ------------------- -As an option, there's a way to ignore attributes from the origin object. -To remove those attributes provide an array via the ``AbstractNormalizer::IGNORED_ATTRIBUTES`` -key in the ``context`` parameter of the desired serializer method:: +All attributes are included by default when serializing objects. There are two +options to ignore some of those attributes. + +Option 1: Using ``@Ignore`` Annotation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. configuration-block:: + + .. code-block:: php-annotations + + namespace App\Model; + + use Symfony\Component\Serializer\Annotation\Ignore; + + class MyClass + { + public $foo; + + /** + * @Ignore() + */ + public $bar; + } + + .. code-block:: php-attributes + + namespace App\Model; + + use Symfony\Component\Serializer\Annotation\Ignore; + + class MyClass + { + public $foo; + + #[Ignore] + public $bar; + } + + .. code-block:: yaml + + App\Model\MyClass: + attributes: + bar: + ignore: true + + .. code-block:: xml + + + + + + true + + + + +You can now ignore specific attributes during serialization:: + + use App\Model\MyClass; + use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; + use Symfony\Component\Serializer\Serializer; + + $obj = new MyClass(); + $obj->foo = 'foo'; + $obj->bar = 'bar'; + + $normalizer = new ObjectNormalizer($classMetadataFactory); + $serializer = new Serializer([$normalizer]); + + $data = $serializer->normalize($obj); + // $data = ['foo' => 'foo']; + +Option 2: Using the Context +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Pass an array with the names of the attributes to ignore using the +``AbstractNormalizer::IGNORED_ATTRIBUTES`` key in the ``context`` of the +serializer method:: use Acme\Person; use Symfony\Component\Serializer\Encoder\JsonEncoder; @@ -440,12 +534,6 @@ key in the ``context`` parameter of the desired serializer method:: $serializer = new Serializer([$normalizer], [$encoder]); $serializer->serialize($person, 'json', [AbstractNormalizer::IGNORED_ATTRIBUTES => ['age']]); // Output: {"name":"foo"} -.. deprecated:: 4.2 - - The :method:`Symfony\\Component\\Serializer\\Normalizer\\AbstractNormalizer::setIgnoredAttributes` - method that was used as an alternative to the ``AbstractNormalizer::IGNORED_ATTRIBUTES`` option - was deprecated in Symfony 4.2. - .. _component-serializer-converting-property-names-when-serializing-and-deserializing: Converting Property Names when Serializing and Deserializing @@ -476,12 +564,12 @@ A custom name converter can handle such cases:: class OrgPrefixNameConverter implements NameConverterInterface { - public function normalize($propertyName) + public function normalize(string $propertyName) { return 'org_'.$propertyName; } - public function denormalize($propertyName) + public function denormalize(string $propertyName) { // removes 'org_' prefix return 'org_' === substr($propertyName, 0, 4) ? substr($propertyName, 4) : $propertyName; @@ -606,6 +694,25 @@ defines a ``Person`` entity with a ``firstName`` property: // ... } + .. code-block:: php-attributes + + 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: @@ -645,12 +752,6 @@ and ``remove``. Using Callbacks to Serialize Properties with Object Instances ------------------------------------------------------------- -.. deprecated:: 4.2 - - The :method:`Symfony\\Component\\Serializer\\Normalizer\\AbstractNormalizer::setCallbacks` - method is deprecated since Symfony 4.2. Use the ``callbacks`` - key of the context instead. - When serializing, you can set a callback to format a specific object property:: use App\Model\Person; @@ -683,11 +784,6 @@ When serializing, you can set a callback to format a specific object property:: $serializer->serialize($person, 'json'); // Output: {"name":"cordoval", "age": 34, "createdAt": "2014-03-22T09:43:12-0500"} -.. deprecated:: 4.2 - - The :method:`Symfony\\Component\\Serializer\\Normalizer\\AbstractNormalizer::setCallbacks` is deprecated since - Symfony 4.2, use the "callbacks" key of the context instead. - .. _component-serializer-normalizers: Normalizers @@ -765,10 +861,6 @@ The Serializer component provides several built-in normalizers: This normalizer converts :phpclass:`DateTimeZone` objects into strings that represent the name of the timezone according to the `list of PHP timezones`_. - .. versionadded:: 4.3 - - The ``DateTimeZoneNormalizer`` was introduced in Symfony 4.3. - :class:`Symfony\\Component\\Serializer\\Normalizer\\DataUriNormalizer` This normalizer converts :phpclass:`SplFileInfo` objects into a `data URI`_ string (``data:...``) such that files can be embedded into serialized data. @@ -777,25 +869,37 @@ The Serializer component provides several built-in normalizers: This normalizer converts :phpclass:`DateInterval` objects into strings. By default, it uses the ``P%yY%mM%dDT%hH%iM%sS`` format. +:class:`Symfony\\Component\\Serializer\\Normalizer\\FormErrorNormalizer` + This normalizer works with classes that implement + :class:`Symfony\\Component\\Form\\FormInterface`. + + It will get errors from the form and normalize them into a normalized array. + :class:`Symfony\\Component\\Serializer\\Normalizer\\ConstraintViolationListNormalizer` This normalizer converts objects that implement :class:`Symfony\\Component\\Validator\\ConstraintViolationListInterface` into a list of errors according to the `RFC 7807`_ standard. - .. versionadded:: 4.1 - - The ``ConstraintViolationListNormalizer`` was introduced in Symfony 4.1. - :class:`Symfony\\Component\\Serializer\\Normalizer\\ProblemNormalizer` Normalizes errors according to the API Problem spec `RFC 7807`_. - .. versionadded:: 4.4 - - The ``ProblemNormalizer`` was introduced in Symfony 4.4. - :class:`Symfony\\Component\\Serializer\\Normalizer\\CustomNormalizer` Normalizes a PHP object using an object that implements :class:`Symfony\\Component\\Serializer\\Normalizer\\NormalizableInterface`. +:class:`Symfony\\Component\\Serializer\\Normalizer\\UidNormalizer` + This normalizer converts objects that implement + :class:`Symfony\\Component\\Uid\\AbstractUid` into strings. + The default normalization format for objects that implement :class:`Symfony\\Component\\Uid\\Uuid` + is the `RFC 4122`_ format (example: ``d9e7a184-5d5b-11ea-a62a-3499710062d0``). + The default normalization format for objects that implement :class:`Symfony\\Component\\Uid\\Ulid` + is the Base 32 format (example: ``01E439TP9XJZ9RPFH3T1PYBCR8``). + You can change the string format by setting the serializer context option + ``UidNormalizer::NORMALIZATION_FORMAT_KEY`` to ``UidNormalizer::NORMALIZATION_FORMAT_BASE_58``, + ``UidNormalizer::NORMALIZATION_FORMAT_BASE_32`` or ``UidNormalizer::NORMALIZATION_FORMAT_RFC_4122``. + + Also it can denormalize ``uuid`` or ``ulid`` strings to :class:`Symfony\\Component\\Uid\\Uuid` + or :class:`Symfony\\Component\\Uid\\Ulid`. The format does not matter. + .. note:: You can also create your own Normalizer to use another structure. Read more at @@ -930,6 +1034,8 @@ Option Description D ``csv_delimiter`` Sets the field delimiter separating values (one ``,`` character only) ``csv_enclosure`` Sets the field enclosure (one character only) ``"`` +``csv_end_of_line`` Sets the character(s) used to mark the end of each ``\n`` + line in the CSV file ``csv_escape_char`` Sets the escape character (at most one character) empty string ``csv_key_separator`` Sets the separator for array's keys during its ``.`` flattening @@ -938,7 +1044,7 @@ Option Description D and ``$options = ['csv_headers' => ['a', 'b', 'c']]`` then ``serialize($data, 'csv', $options)`` returns ``a,b,c\n1,2,3`` ``[]``, inferred from input data's keys -``csv_escape_formulas`` Escapes fields containing formulas by prepending them ``false`` +``csv_escape_formulas`` Escapes fields containing formulas by prepending them ``false`` with a ``\t`` character ``as_collection`` Always returns results as a collection, even if only ``true`` one line is decoded. @@ -946,10 +1052,6 @@ Option Description D ``output_utf8_bom`` Outputs special `UTF-8 BOM`_ along with encoded data ``false`` ======================= ===================================================== ========================== -.. versionadded:: 4.4 - - The ``output_utf8_bom`` option was introduced in Symfony 4.4. - The ``XmlEncoder`` ~~~~~~~~~~~~~~~~~~ @@ -1003,8 +1105,7 @@ always as a collection. .. tip:: XML comments are ignored by default when decoding contents, but this - behavior can be changed with the optional ``$decoderIgnoredNodeTypes`` argument of - the ``XmlEncoder`` class constructor. + behavior can be changed with the optional context key ``XmlEncoder::DECODER_IGNORED_NODE_TYPES``. Data with ``#comment`` keys are encoded to XML comments by default. This can be changed with the optional ``$encoderIgnoredNodeTypes`` argument of the @@ -1042,11 +1143,6 @@ Option Description generated XML ============================== ================================================= ========================== -.. versionadded:: 4.2 - - The ``decoder_ignored_node_types`` and ``encoder_ignored_node_types`` - options were introduced in Symfony 4.2. - Example with custom ``context``:: use Symfony\Component\Serializer\Encoder\XmlEncoder; @@ -1124,6 +1220,35 @@ to ``true``:: .. _component-serializer-handling-circular-references: +Collecting Type Errors While Denormalizing +------------------------------------------ + +When denormalizing a payload to an object with typed properties, you'll get an +exception if the payload contains properties that don't have the same type as +the object. + +In those situations, use the ``COLLECT_DENORMALIZATION_ERRORS`` option to +collect all exceptions at once, and to get the object partially denormalized:: + + try { + $dto = $serializer->deserialize($request->getContent(), MyDto::class, 'json', [ + DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true, + ]); + } catch (PartialDenormalizationException $e) { + $violations = new ConstraintViolationList(); + /** @var NotNormalizableValueException $exception */ + foreach ($e->getErrors() as $exception) { + $message = sprintf('The type must be one of "%s" ("%s" given).', implode(', ', $exception->getExpectedTypes()), $exception->getCurrentType()); + $parameters = []; + if ($exception->canUseMessageForUser()) { + $parameters['hint'] = $exception->getMessage(); + } + $violations->add(new ConstraintViolation($message, '', $parameters, null, $exception->getPath(), null)); + } + + return $this->json($violations, 400); + } + Handling Circular References ---------------------------- @@ -1217,12 +1342,6 @@ having unique identifiers:: var_dump($serializer->serialize($org, 'json')); // {"name":"Les-Tilleuls.coop","members":[{"name":"K\u00e9vin", organization: "Les-Tilleuls.coop"}]} -.. deprecated:: 4.2 - - The :method:`Symfony\\Component\\Serializer\\Normalizer\\AbstractNormalizer::setCircularReferenceHandler` - method is deprecated since Symfony 4.2. Use the ``AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER`` - key of the context instead. - Handling Serialization Depth ---------------------------- @@ -1274,6 +1393,20 @@ Here, we set it to 2 for the ``$child`` property: // ... } + .. code-block:: php-attributes + + namespace Acme; + + use Symfony\Component\Serializer\Annotation\MaxDepth; + + class MyObj + { + #[MaxDepth(2)] + public $child; + + // ... + } + .. code-block:: yaml Acme\MyObj: @@ -1375,12 +1508,6 @@ having unique identifiers:: ]; */ -.. deprecated:: 4.2 - - The :method:`Symfony\\Component\\Serializer\\Normalizer\\AbstractNormalizer::setMaxDepthHandler` - method is deprecated since Symfony 4.2. Use the ``max_depth_handler`` - key of the context instead. - Handling Arrays --------------- @@ -1590,6 +1717,23 @@ and ``BitBucketCodeRepository`` classes: // ... } + .. code-block:: php-attributes + + namespace App; + + use App\BitBucketCodeRepository; + use App\GitHubCodeRepository; + use Symfony\Component\Serializer\Annotation\DiscriminatorMap; + + #[DiscriminatorMap(typeProperty: 'type', mapping: [ + 'github' => GitHubCodeRepository::class, + 'bitbucket' => BitBucketCodeRepository::class, + ])] + abstract class CodeRepository + { + // ... + } + .. code-block:: yaml App\CodeRepository: @@ -1657,5 +1801,6 @@ Learn more .. _`Value Objects`: https://en.wikipedia.org/wiki/Value_object .. _`API Platform`: https://api-platform.com .. _`list of PHP timezones`: https://www.php.net/manual/en/timezones.php +.. _`RFC 4122`: https://tools.ietf.org/html/rfc4122 .. _`PHP reflection`: https://php.net/manual/en/book.reflection.php .. _`data URI`: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs diff --git a/components/string.rst b/components/string.rst new file mode 100644 index 00000000000..c71d68a704e --- /dev/null +++ b/components/string.rst @@ -0,0 +1,560 @@ +.. index:: + single: String + single: Components; String + +The String Component +==================== + + The String component provides a single object-oriented API to work with + three "unit systems" of strings: bytes, code points and grapheme clusters. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/string + +.. include:: /components/require_autoload.rst.inc + +What is a String? +----------------- + +You can skip this section if you already know what a *"code point"* or a +*"grapheme cluster"* are in the context of handling strings. Otherwise, read +this section to learn about the terminology used by this component. + +Languages like English require a very limited set of characters and symbols to +display any content. Each string is a series of characters (letters or symbols) +and they can be encoded even with the most limited standards (e.g. `ASCII`_). + +However, other languages require thousands of symbols to display their contents. +They need complex encoding standards such as `Unicode`_ and concepts like +"character" no longer make sense. Instead, you have to deal with these terms: + +* `Code points`_: they are the atomic units of information. A string is a series + of code points. Each code point is a number whose meaning is given by the + `Unicode`_ standard. For example, the English letter ``A`` is the ``U+0041`` + code point and the Japanese *kana* ``の`` is the ``U+306E`` code point. +* `Grapheme clusters`_: they are a sequence of one or more code points which are + displayed as a single graphical unit. For example, the Spanish letter ``ñ`` is + a grapheme cluster that contains two code points: ``U+006E`` = ``n`` (*"latin + small letter N"*) + ``U+0303`` = ``◌̃`` (*"combining tilde"*). +* Bytes: they are the actual information stored for the string contents. Each + code point can require one or more bytes of storage depending on the standard + being used (UTF-8, UTF-16, etc.). + +The following image displays the bytes, code points and grapheme clusters for +the same word written in English (``hello``) and Hindi (``नमस्ते``): + +.. image:: /_images/components/string/bytes-points-graphemes.png + :align: center + +Usage +----- + +Create a new object of type :class:`Symfony\\Component\\String\\ByteString`, +:class:`Symfony\\Component\\String\\CodePointString` or +:class:`Symfony\\Component\\String\\UnicodeString`, pass the string contents as +their arguments and then use the object-oriented API to work with those strings:: + + use Symfony\Component\String\UnicodeString; + + $text = (new UnicodeString('This is a déjà-vu situation.')) + ->trimEnd('.') + ->replace('déjà-vu', 'jamais-vu') + ->append('!'); + // $text = 'This is a jamais-vu situation!' + + $content = new UnicodeString('नमस्ते दुनिया'); + if ($content->ignoreCase()->startsWith('नमस्ते')) { + // ... + } + +Method Reference +---------------- + +Methods to Create String Objects +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +First, you can create objects prepared to store strings as bytes, code points +and grapheme clusters with the following classes:: + + use Symfony\Component\String\ByteString; + use Symfony\Component\String\CodePointString; + use Symfony\Component\String\UnicodeString; + + $foo = new ByteString('hello'); + $bar = new CodePointString('hello'); + // UnicodeString is the most commonly used class + $baz = new UnicodeString('hello'); + +Use the ``wrap()`` static method to instantiate more than one string object:: + + $contents = ByteString::wrap(['hello', 'world']); // $contents = ByteString[] + $contents = UnicodeString::wrap(['I', '❤️', 'Symfony']); // $contents = UnicodeString[] + + // use the unwrap method to make the inverse conversion + $contents = UnicodeString::unwrap([ + new UnicodeString('hello'), new UnicodeString('world'), + ]); // $contents = ['hello', 'world'] + +If you work with lots of String objects, consider using the shortcut functions +to make your code more concise:: + + // the b() function creates byte strings + use function Symfony\Component\String\b; + + // both lines are equivalent + $foo = new ByteString('hello'); + $foo = b('hello'); + + // the u() function creates Unicode strings + use function Symfony\Component\String\u; + + // both lines are equivalent + $foo = new UnicodeString('hello'); + $foo = u('hello'); + + // the s() function creates a byte string or Unicode string + // depending on the given contents + use function Symfony\Component\String\s; + + // creates a ByteString object + $foo = s("\xfe\xff"); + // creates a UnicodeString object + $foo = s('अनुच्छेद'); + +There are also some specialized constructors:: + + // ByteString can create a random string of the given length + $foo = ByteString::fromRandom(12); + // by default, random strings use A-Za-z0-9 characters; you can restrict + // the characters to use with the second optional argument + $foo = ByteString::fromRandom(6, 'AEIOU0123456789'); + $foo = ByteString::fromRandom(10, 'qwertyuiop'); + + // CodePointString and UnicodeString can create a string from code points + $foo = UnicodeString::fromCodePoints(0x928, 0x92E, 0x938, 0x94D, 0x924, 0x947); + // equivalent to: $foo = new UnicodeString('नमस्ते'); + +Methods to Transform String Objects +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each string object can be transformed into the other two types of objects:: + + $foo = ByteString::fromRandom(12)->toCodePointString(); + $foo = (new CodePointString('hello'))->toUnicodeString(); + $foo = UnicodeString::fromCodePoints(0x68, 0x65, 0x6C, 0x6C, 0x6F)->toByteString(); + + // the optional $toEncoding argument defines the encoding of the target string + $foo = (new CodePointString('hello'))->toByteString('Windows-1252'); + // the optional $fromEncoding argument defines the encoding of the original string + $foo = (new ByteString('さよなら'))->toCodePointString('ISO-2022-JP'); + +If the conversion is not possible for any reason, you'll get an +:class:`Symfony\\Component\\String\\Exception\\InvalidArgumentException`. + +There is also a method to get the bytes stored at some position:: + + // ('नमस्ते' bytes = [224, 164, 168, 224, 164, 174, 224, 164, 184, + // 224, 165, 141, 224, 164, 164, 224, 165, 135]) + b('नमस्ते')->bytesAt(0); // [224] + u('नमस्ते')->bytesAt(0); // [224, 164, 168] + + b('नमस्ते')->bytesAt(1); // [164] + u('नमस्ते')->bytesAt(1); // [224, 164, 174] + +Methods Related to Length and White Spaces +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + // returns the number of graphemes, code points or bytes of the given string + $word = 'नमस्ते'; + (new ByteString($word))->length(); // 18 (bytes) + (new CodePointString($word))->length(); // 6 (code points) + (new UnicodeString($word))->length(); // 4 (graphemes) + + // some symbols require double the width of others to represent them when using + // a monospaced font (e.g. in a console). This method returns the total width + // needed to represent the entire word + $word = 'नमस्ते'; + (new ByteString($word))->width(); // 18 + (new CodePointString($word))->width(); // 4 + (new UnicodeString($word))->width(); // 4 + // if the text contains multiple lines, it returns the max width of all lines + $text = "<<width(); // 14 + + // only returns TRUE if the string is exactly an empty string (not even white spaces) + u('hello world')->isEmpty(); // false + u(' ')->isEmpty(); // false + u('')->isEmpty(); // true + + // removes all white spaces from the start and end of the string and replaces two + // or more consecutive white spaces inside contents by a single white space + u(" \n\n hello world \n \n")->collapseWhitespace(); // 'hello world' + +Methods to Change Case +~~~~~~~~~~~~~~~~~~~~~~ + +:: + + // changes all graphemes/code points to lower case + u('FOO Bar')->lower(); // 'foo bar' + + // when dealing with different languages, uppercase/lowercase is not enough + // there are three cases (lower, upper, title), some characters have no case, + // case is context-sensitive and locale-sensitive, etc. + // this method returns a string that you can use in case-insensitive comparisons + u('FOO Bar')->folded(); // 'foo bar' + u('Die O\'Brian Straße')->folded(); // "die o'brian strasse" + + // changes all graphemes/code points to upper case + u('foo BAR')->upper(); // 'FOO BAR' + + // changes all graphemes/code points to "title case" + u('foo bar')->title(); // 'Foo bar' + u('foo bar')->title(true); // 'Foo Bar' + + // changes all graphemes/code points to camelCase + u('Foo: Bar-baz.')->camel(); // 'fooBarBaz' + // changes all graphemes/code points to snake_case + u('Foo: Bar-baz.')->snake(); // 'foo_bar_baz' + // other cases can be achieved by chaining methods. E.g. PascalCase: + u('Foo: Bar-baz.')->camel()->title(); // 'FooBarBaz' + +The methods of all string classes are case-sensitive by default. You can perform +case-insensitive operations with the ``ignoreCase()`` method:: + + u('abc')->indexOf('B'); // null + u('abc')->ignoreCase()->indexOf('B'); // 1 + +Methods to Append and Prepend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + // adds the given content (one or more strings) at the beginning/end of the string + u('world')->prepend('hello'); // 'helloworld' + u('world')->prepend('hello', ' '); // 'hello world' + + u('hello')->append('world'); // 'helloworld' + u('hello')->append(' ', 'world'); // 'hello world' + + // adds the given content at the beginning of the string (or removes it) to + // make sure that the content starts exactly with that content + u('Name')->ensureStart('get'); // 'getName' + u('getName')->ensureStart('get'); // 'getName' + u('getgetName')->ensureStart('get'); // 'getName' + // this method is similar, but works on the end of the content instead of on the beginning + u('User')->ensureEnd('Controller'); // 'UserController' + u('UserController')->ensureEnd('Controller'); // 'UserController' + u('UserControllerController')->ensureEnd('Controller'); // 'UserController' + + // returns the contents found before/after the first occurrence of the given string + u('hello world')->before('world'); // 'hello ' + u('hello world')->before('o'); // 'hell' + u('hello world')->before('o', true); // 'hello' + + u('hello world')->after('hello'); // ' world' + u('hello world')->after('o'); // ' world' + u('hello world')->after('o', true); // 'o world' + + // returns the contents found before/after the last occurrence of the given string + u('hello world')->beforeLast('o'); // 'hello w' + u('hello world')->beforeLast('o', true); // 'hello wo' + + u('hello world')->afterLast('o'); // 'rld' + u('hello world')->afterLast('o', true); // 'orld' + +Methods to Pad and Trim +~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + // makes a string as long as the first argument by adding the given + // string at the beginning, end or both sides of the string + u(' Lorem Ipsum ')->padBoth(20, '-'); // '--- Lorem Ipsum ----' + u(' Lorem Ipsum')->padStart(20, '-'); // '-------- Lorem Ipsum' + u('Lorem Ipsum ')->padEnd(20, '-'); // 'Lorem Ipsum --------' + + // repeats the given string the number of times passed as argument + u('_.')->repeat(10); // '_._._._._._._._._._.' + + // removes the given characters (by default, white spaces) from the string + u(' Lorem Ipsum ')->trim(); // 'Lorem Ipsum' + u('Lorem Ipsum ')->trim('m'); // 'Lorem Ipsum ' + u('Lorem Ipsum')->trim('m'); // 'Lorem Ipsu' + + u(' Lorem Ipsum ')->trimStart(); // 'Lorem Ipsum ' + u(' Lorem Ipsum ')->trimEnd(); // ' Lorem Ipsum' + + // removes the given content from the start/end of the string + u('file-image-0001.png')->trimPrefix('file-'); // 'image-0001.png' + u('file-image-0001.png')->trimPrefix('image-'); // 'file-image-0001.png' + u('file-image-0001.png')->trimPrefix('file-image-'); // '0001.png' + u('template.html.twig')->trimSuffix('.html'); // 'template.html.twig' + u('template.html.twig')->trimSuffix('.twig'); // 'template.html' + u('template.html.twig')->trimSuffix('.html.twig'); // 'template' + // when passing an array of prefix/sufix, only the first one found is trimmed + u('file-image-0001.png')->trimPrefix(['file-', 'image-']); // 'image-0001.png' + u('template.html.twig')->trimSuffix(['.twig', '.html']); // 'template.html' + +Methods to Search and Replace +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + // checks if the string starts/ends with the given string + u('https://symfony.com')->startsWith('https'); // true + u('report-1234.pdf')->endsWith('.pdf'); // true + + // checks if the string contents are exactly the same as the given contents + u('foo')->equalsTo('foo'); // true + + // checks if the string content match the given regular expression. + u('avatar-73647.png')->match('/avatar-(\d+)\.png/'); + // result = ['avatar-73647.png', '73647', null] + + // You can pass flags for preg_match() as second argument. If PREG_PATTERN_ORDER + // or PREG_SET_ORDER are passed, preg_match_all() will be used. + u('206-555-0100 and 800-555-1212')->match('/\d{3}-\d{3}-\d{4}/', \PREG_PATTERN_ORDER); + // result = [['206-555-0100', '800-555-1212']] + + // checks if the string contains any of the other given strings + u('aeiou')->containsAny('a'); // true + u('aeiou')->containsAny(['ab', 'efg']); // false + u('aeiou')->containsAny(['eio', 'foo', 'z']); // true + + // finds the position of the first occurrence of the given string + // (the second argument is the position where the search starts and negative + // values have the same meaning as in PHP functions) + u('abcdeabcde')->indexOf('c'); // 2 + u('abcdeabcde')->indexOf('c', 2); // 2 + u('abcdeabcde')->indexOf('c', -4); // 7 + u('abcdeabcde')->indexOf('eab'); // 4 + u('abcdeabcde')->indexOf('k'); // null + + // finds the position of the last occurrence of the given string + // (the second argument is the position where the search starts and negative + // values have the same meaning as in PHP functions) + u('abcdeabcde')->indexOfLast('c'); // 7 + u('abcdeabcde')->indexOfLast('c', 2); // 7 + u('abcdeabcde')->indexOfLast('c', -4); // 2 + u('abcdeabcde')->indexOfLast('eab'); // 4 + u('abcdeabcde')->indexOfLast('k'); // null + + // replaces all occurrences of the given string + u('http://symfony.com')->replace('http://', 'https://'); // 'https://symfony.com' + // replaces all occurrences of the given regular expression + u('(+1) 206-555-0100')->replaceMatches('/[^A-Za-z0-9]++/', ''); // '12065550100' + // you can pass a callable as the second argument to perform advanced replacements + u('123')->replaceMatches('/\d/', function ($match) { + return '['.$match[0].']'; + }); // result = '[1][2][3]' + +Methods to Join, Split, Truncate and Reverse +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + // uses the string as the "glue" to merge all the given strings + u(', ')->join(['foo', 'bar']); // 'foo, bar' + + // breaks the string into pieces using the given delimiter + u('template_name.html.twig')->split('.'); // ['template_name', 'html', 'twig'] + // you can set the maximum number of pieces as the second argument + u('template_name.html.twig')->split('.', 2); // ['template_name', 'html.twig'] + + // returns a substring which starts at the first argument and has the length of the + // second optional argument (negative values have the same meaning as in PHP functions) + u('Symfony is great')->slice(0, 7); // 'Symfony' + u('Symfony is great')->slice(0, -6); // 'Symfony is' + u('Symfony is great')->slice(11); // 'great' + u('Symfony is great')->slice(-5); // 'great' + + // reduces the string to the length given as argument (if it's longer) + u('Lorem Ipsum')->truncate(3); // 'Lor' + u('Lorem Ipsum')->truncate(80); // 'Lorem Ipsum' + // the second argument is the character(s) added when a string is cut + // (the total length includes the length of this character(s)) + u('Lorem Ipsum')->truncate(8, '…'); // 'Lorem I…' + // if the third argument is false, the last word before the cut is kept + // even if that generates a string longer than the desired length + u('Lorem Ipsum')->truncate(8, '…', false); // 'Lorem Ipsum' + +:: + + // breaks the string into lines of the given length + u('Lorem Ipsum')->wordwrap(4); // 'Lorem\nIpsum' + // by default it breaks by white space; pass TRUE to break unconditionally + u('Lorem Ipsum')->wordwrap(4, "\n", true); // 'Lore\nm\nIpsu\nm' + + // replaces a portion of the string with the given contents: + // the second argument is the position where the replacement starts; + // the third argument is the number of graphemes/code points removed from the string + u('0123456789')->splice('xxx'); // 'xxx' + u('0123456789')->splice('xxx', 0, 2); // 'xxx23456789' + u('0123456789')->splice('xxx', 0, 6); // 'xxx6789' + u('0123456789')->splice('xxx', 6); // '012345xxx' + + // breaks the string into pieces of the length given as argument + u('0123456789')->chunk(3); // ['012', '345', '678', '9'] + + // reverses the order of the string contents + u('foo bar')->reverse(); // 'rab oof' + u('さよなら')->reverse(); // 'らなよさ' + +Methods Added by ByteString +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These methods are only available for ``ByteString`` objects:: + + // returns TRUE if the string contents are valid UTF-8 contents + b('Lorem Ipsum')->isUtf8(); // true + b("\xc3\x28")->isUtf8(); // false + +Methods Added by CodePointString and UnicodeString +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These methods are only available for ``CodePointString`` and ``UnicodeString`` +objects:: + + // transliterates any string into the latin alphabet defined by the ASCII encoding + // (don't use this method to build a slugger because this component already provides + // a slugger, as explained later in this article) + u('नमस्ते')->ascii(); // 'namaste' + u('さよなら')->ascii(); // 'sayonara' + u('спасибо')->ascii(); // 'spasibo' + + // returns an array with the code point or points stored at the given position + // (code points of 'नमस्ते' graphemes = [2344, 2350, 2360, 2340] + u('नमस्ते')->codePointsAt(0); // [2344] + u('नमस्ते')->codePointsAt(2); // [2360] + +`Unicode equivalence`_ is the specification by the Unicode standard that +different sequences of code points represent the same character. For example, +the Swedish letter ``å`` can be a single code point (``U+00E5`` = *"latin small +letter A with ring above"*) or a sequence of two code points (``U+0061`` = +*"latin small letter A"* + ``U+030A`` = *"combining ring above"*). The +``normalize()`` method allows to pick the normalization mode:: + + // these encode the letter as a single code point: U+00E5 + u('å')->normalize(UnicodeString::NFC); + u('å')->normalize(UnicodeString::NFKC); + // these encode the letter as two code points: U+0061 + U+030A + u('å')->normalize(UnicodeString::NFD); + u('å')->normalize(UnicodeString::NFKD); + +Slugger +------- + +In some contexts, such as URLs and file/directory names, it's not safe to use +any Unicode character. A *slugger* transforms a given string into another string +that only includes safe ASCII characters:: + + use Symfony\Component\String\Slugger\AsciiSlugger; + + $slugger = new AsciiSlugger(); + $slug = $slugger->slug('Wôrķšƥáçè ~~sèťtïñğš~~'); + // $slug = 'Workspace-settings' + + // you can also pass an array with additional character substitutions + $slugger = new AsciiSlugger('en', ['en' => ['%' => 'percent', '€' => 'euro']]); + $slug = $slugger->slug('10% or 5€'); + // $slug = '10-percent-or-5-euro' + + // if there is no symbols map for your locale (e.g. 'en_GB') then the parent locale's symbols map + // will be used instead (i.e. 'en') + $slugger = new AsciiSlugger('en_GB', ['en' => ['%' => 'percent', '€' => 'euro']]); + $slug = $slugger->slug('10% or 5€'); + // $slug = '10-percent-or-5-euro' + + // for more dynamic substitutions, pass a PHP closure instead of an array + $slugger = new AsciiSlugger('en', function ($string, $locale) { + return str_replace('❤️', 'love', $string); + }); + +The separator between words is a dash (``-``) by default, but you can define +another separator as the second argument:: + + $slug = $slugger->slug('Wôrķšƥáçè ~~sèťtïñğš~~', '/'); + // $slug = 'Workspace/settings' + +The slugger transliterates the original string into the Latin script before +applying the other transformations. The locale of the original string is +detected automatically, but you can define it explicitly:: + + // this tells the slugger to transliterate from Korean language + $slugger = new AsciiSlugger('ko'); + + // you can override the locale as the third optional parameter of slug() + $slug = $slugger->slug('...', '-', 'fa'); + +In a Symfony application, you don't need to create the slugger yourself. Thanks +to :doc:`service autowiring `, you can inject a +slugger by type-hinting a service constructor argument with the +:class:`Symfony\\Component\\String\\Slugger\\SluggerInterface`. The locale of +the injected slugger is the same as the request locale:: + + use Symfony\Component\String\Slugger\SluggerInterface; + + class MyService + { + private $slugger; + + public function __construct(SluggerInterface $slugger) + { + $this->slugger = $slugger; + } + + public function someMethod() + { + $slug = $this->slugger->slug('...'); + } + } + +.. _string-inflector: + +Inflector +--------- + +In some scenarios such as code generation and code introspection, you need to +convert words from/to singular/plural. For example, to know the property +associated with an *adder* method, you must convert from plural +(``addStories()`` method) to singular (``$story`` property). + +Most human languages have simple pluralization rules, but at the same time they +define lots of exceptions. For example, the general rule in English is to add an +``s`` at the end of the word (``book`` -> ``books``) but there are lots of +exceptions even for common words (``woman`` -> ``women``, ``life`` -> ``lives``, +``news`` -> ``news``, ``radius`` -> ``radii``, etc.) + +This component provides an :class:`Symfony\\Component\\String\\Inflector\\EnglishInflector` +class to convert English words from/to singular/plural with confidence:: + + use Symfony\Component\String\Inflector\EnglishInflector; + + $inflector = new EnglishInflector(); + + $result = $inflector->singularize('teeth'); // ['tooth'] + $result = $inflector->singularize('radii'); // ['radius'] + $result = $inflector->singularize('leaves'); // ['leaf', 'leave', 'leaff'] + + $result = $inflector->pluralize('bacterium'); // ['bacteria'] + $result = $inflector->pluralize('news'); // ['news'] + $result = $inflector->pluralize('person'); // ['persons', 'people'] + +The value returned by both methods is always an array because sometimes it's not +possible to determine a unique singular/plural form for the given word. + +.. _`ASCII`: https://en.wikipedia.org/wiki/ASCII +.. _`Unicode`: https://en.wikipedia.org/wiki/Unicode +.. _`Code points`: https://en.wikipedia.org/wiki/Code_point +.. _`Grapheme clusters`: https://en.wikipedia.org/wiki/Grapheme +.. _`Unicode equivalence`: https://en.wikipedia.org/wiki/Unicode_equivalence diff --git a/components/uid.rst b/components/uid.rst new file mode 100644 index 00000000000..d81e593f812 --- /dev/null +++ b/components/uid.rst @@ -0,0 +1,471 @@ +.. index:: + single: UID + single: Components; UID + +The UID Component +================= + + The UID component provides utilities to work with `unique identifiers`_ (UIDs) + such as UUIDs and ULIDs. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/uid + +.. include:: /components/require_autoload.rst.inc + +.. _uuid: + +UUIDs +----- + +`UUIDs`_ (*universally unique identifiers*) are one of the most popular UIDs in +the software industry. UUIDs are 128-bit numbers usually represented as five +groups of hexadecimal characters: ``xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx`` +(the ``M`` digit is the UUID version and the ``N`` digit is the UUID variant). + +Generating UUIDs +~~~~~~~~~~~~~~~~ + +Use the named constructors of the ``Uuid`` class or any of the specific classes +to create each type of UUID:: + + use Symfony\Component\Uid\Uuid; + + // UUID type 1 generates the UUID using the MAC address of your device and a timestamp. + // Both are obtained automatically, so you don't have to pass any constructor argument. + $uuid = Uuid::v1(); // $uuid is an instance of Symfony\Component\Uid\UuidV1 + + // UUID type 4 generates a random UUID, so you don't have to pass any constructor argument. + $uuid = Uuid::v4(); // $uuid is an instance of Symfony\Component\Uid\UuidV4 + + // UUID type 3 and 5 generate a UUID hashing the given namespace and name. Type 3 uses + // MD5 hashes and Type 5 uses SHA-1. The namespace is another UUID (e.g. a Type 4 UUID) + // and the name is an arbitrary string (e.g. a product name; if it's unique). + $namespace = Uuid::v4(); + $name = $product->getUniqueName(); + + $uuid = Uuid::v3($namespace, $name); // $uuid is an instance of Symfony\Component\Uid\UuidV3 + $uuid = Uuid::v5($namespace, $name); // $uuid is an instance of Symfony\Component\Uid\UuidV5 + + // the namespaces defined by RFC 4122 (see https://tools.ietf.org/html/rfc4122#appendix-C) + // are available as PHP constants and as string values + $uuid = Uuid::v3(Uuid::NAMESPACE_DNS, $name); // same as: Uuid::v3('dns', $name); + $uuid = Uuid::v3(Uuid::NAMESPACE_URL, $name); // same as: Uuid::v3('url', $name); + $uuid = Uuid::v3(Uuid::NAMESPACE_OID, $name); // same as: Uuid::v3('oid', $name); + $uuid = Uuid::v3(Uuid::NAMESPACE_X500, $name); // same as: Uuid::v3('x500', $name); + + // UUID type 6 is not part of the UUID standard. It's lexicographically sortable + // (like ULIDs) and contains a 60-bit timestamp and 63 extra unique bits. + // It's defined in http://gh.peabody.io/uuidv6/ + $uuid = Uuid::v6(); // $uuid is an instance of Symfony\Component\Uid\UuidV6 + +If your UUID value is already generated in another format, use any of the +following methods to create a ``Uuid`` object from it:: + + // all the following examples would generate the same Uuid object + $uuid = Uuid::fromString('d9e7a184-5d5b-11ea-a62a-3499710062d0'); + $uuid = Uuid::fromBinary("\xd9\xe7\xa1\x84\x5d\x5b\x11\xea\xa6\x2a\x34\x99\x71\x00\x62\xd0"); + $uuid = Uuid::fromBase32('6SWYGR8QAV27NACAHMK5RG0RPG'); + $uuid = Uuid::fromBase58('TuetYWNHhmuSQ3xPoVLv9M'); + $uuid = Uuid::fromRfc4122('d9e7a184-5d5b-11ea-a62a-3499710062d0'); + +Converting UUIDs +~~~~~~~~~~~~~~~~ + +Use these methods to transform the UUID object into different bases:: + + $uuid = Uuid::fromString('d9e7a184-5d5b-11ea-a62a-3499710062d0'); + + $uuid->toBinary(); // string(16) "\xd9\xe7\xa1\x84\x5d\x5b\x11\xea\xa6\x2a\x34\x99\x71\x00\x62\xd0" + $uuid->toBase32(); // string(26) "6SWYGR8QAV27NACAHMK5RG0RPG" + $uuid->toBase58(); // string(22) "TuetYWNHhmuSQ3xPoVLv9M" + $uuid->toRfc4122(); // string(36) "d9e7a184-5d5b-11ea-a62a-3499710062d0" + +Working with UUIDs +~~~~~~~~~~~~~~~~~~ + +UUID objects created with the ``Uuid`` class can use the following methods +(which are equivalent to the ``uuid_*()`` method of the PHP extension):: + + use Symfony\Component\Uid\NilUuid; + use Symfony\Component\Uid\Uuid; + + // checking if the UUID is null (note that the class is called + // NilUuid instead of NullUuid to follow the UUID standard notation) + $uuid = Uuid::v4(); + $uuid instanceof NilUuid; // false + + // checking the type of UUID + use Symfony\Component\Uid\UuidV4; + $uuid = Uuid::v4(); + $uuid instanceof UuidV4; // true + + // getting the UUID datetime (it's only available in certain UUID types) + $uuid = Uuid::v1(); + $uuid->getDateTime(); // returns a \DateTimeImmutable instance + + // checking if a given value is valid as UUID + $isValid = Uuid::isValid($uuid); // true or false + + // comparing UUIDs and checking for equality + $uuid1 = Uuid::v1(); + $uuid4 = Uuid::v4(); + $uuid1->equals($uuid4); // false + + // this method returns: + // * int(0) if $uuid1 and $uuid4 are equal + // * int > 0 if $uuid1 is greater than $uuid4 + // * int < 0 if $uuid1 is less than $uuid4 + $uuid1->compare($uuid4); // e.g. int(4) + +Storing UUIDs in Databases +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you :doc:`use Doctrine `, consider using the ``uuid`` Doctrine +type, which converts to/from UUID objects automatically:: + + // src/Entity/Product.php + namespace App\Entity; + + use Doctrine\ORM\Mapping as ORM; + + /** + * @ORM\Entity(repositoryClass="App\Repository\ProductRepository") + */ + class Product + { + /** + * @ORM\Column(type="uuid") + */ + private $someProperty; + + // ... + } + +There's also a Doctrine generator to help auto-generate UUID values for the +entity primary keys:: + + namespace App\Entity; + + use Doctrine\ORM\Mapping as ORM; + use Symfony\Component\Uid\Uuid; + + class User implements UserInterface + { + /** + * @ORM\Id + * @ORM\Column(type="uuid", unique=true) + * @ORM\GeneratedValue(strategy="CUSTOM") + * @ORM\CustomIdGenerator(class="doctrine.uuid_generator") + */ + private $id; + + public function getId(): ?Uuid + { + return $this->id; + } + + // ... + } + +When using built-in Doctrine repository methods (e.g. ``findOneBy()``), Doctrine +knows how to convert these UUID types to build the SQL query +(e.g. ``->findOneBy(['user' => $user->getUuid()])``). However, when using DQL +queries or building the query yourself, you'll need to set ``uuid`` as the type +of the UUID parameters:: + + // src/Repository/ProductRepository.php + + // ... + class ProductRepository extends ServiceEntityRepository + { + // ... + + public function findUserProducts(User $user): array + { + $qb = $this->createQueryBuilder('p') + // ... + // add 'uuid' as the third argument to tell Doctrine that this is a UUID + ->setParameter('user', $user->getUuid(), 'uuid') + + // alternatively, you can convert it to a value compatible with + // the type inferred by Doctrine + ->setParameter('user', $user->getUuid()->toBinary()) + ; + + // ... + } + } + +.. _ulid: + +ULIDs +----- + +`ULIDs`_ (*Universally Unique Lexicographically Sortable Identifier*) are 128-bit +numbers usually represented as a 26-character string: ``TTTTTTTTTTRRRRRRRRRRRRRRRR`` +(where ``T`` represents a timestamp and ``R`` represents the random bits). + +ULIDs are an alternative to UUIDs when using those is impractical. They provide +128-bit compatibility with UUID, they are lexicographically sortable and they +are encoded as 26-character strings (vs 36-character UUIDs). + +.. note:: + + If you generate more than one ULID during the same millisecond in the + same process then the random portion is incremented by one bit in order + to provide monotonicity for sorting. The random portion is not random + compared to the previous ULID in this case. + +Generating ULIDs +~~~~~~~~~~~~~~~~ + +Instantiate the ``Ulid`` class to generate a random ULID value:: + + use Symfony\Component\Uid\Ulid; + + $ulid = new Ulid(); // e.g. 01AN4Z07BY79KA1307SR9X4MV3 + +If your ULID value is already generated in another format, use any of the +following methods to create a ``Ulid`` object from it:: + + // all the following examples would generate the same Ulid object + $ulid = Ulid::fromString('01E439TP9XJZ9RPFH3T1PYBCR8'); + $ulid = Ulid::fromBinary("\x01\x71\x06\x9d\x59\x3d\x97\xd3\x8b\x3e\x23\xd0\x6d\xe5\xb3\x08"); + $ulid = Ulid::fromBase32('01E439TP9XJZ9RPFH3T1PYBCR8'); + $ulid = Ulid::fromBase58('1BKocMc5BnrVcuq2ti4Eqm'); + $ulid = Ulid::fromRfc4122('0171069d-593d-97d3-8b3e-23d06de5b308'); + +There's also a special ``NilUlid`` class to represent ULID ``null`` values:: + + use Symfony\Component\Uid\NilUlid; + + $ulid = new NilUlid(); + // equivalent to $ulid = new Ulid('00000000000000000000000000'); + +Converting ULIDs +~~~~~~~~~~~~~~~~ + +Use these methods to transform the ULID object into different bases:: + + $ulid = Ulid::fromString('01E439TP9XJZ9RPFH3T1PYBCR8'); + + $ulid->toBinary(); // string(16) "\x01\x71\x06\x9d\x59\x3d\x97\xd3\x8b\x3e\x23\xd0\x6d\xe5\xb3\x08" + $ulid->toBase32(); // string(26) "01E439TP9XJZ9RPFH3T1PYBCR8" + $ulid->toBase58(); // string(22) "1BKocMc5BnrVcuq2ti4Eqm" + $ulid->toRfc4122(); // string(36) "0171069d-593d-97d3-8b3e-23d06de5b308" + +Working with ULIDs +~~~~~~~~~~~~~~~~~~ + +ULID objects created with the ``Ulid`` class can use the following methods:: + + use Symfony\Component\Uid\Ulid; + + $ulid1 = new Ulid(); + $ulid2 = new Ulid(); + + // checking if a given value is valid as ULID + $isValid = Ulid::isValid($ulidValue); // true or false + + // getting the ULID datetime + $ulid1->getDateTime(); // returns a \DateTimeImmutable instance + + // comparing ULIDs and checking for equality + $ulid1->equals($ulid2); // false + // this method returns $ulid1 <=> $ulid2 + $ulid1->compare($ulid2); // e.g. int(-1) + +Storing ULIDs in Databases +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you :doc:`use Doctrine `, consider using the ``ulid`` Doctrine +type, which converts to/from ULID objects automatically:: + + // src/Entity/Product.php + namespace App\Entity; + + use Doctrine\ORM\Mapping as ORM; + + /** + * @ORM\Entity(repositoryClass="App\Repository\ProductRepository") + */ + class Product + { + /** + * @ORM\Column(type="ulid") + */ + private $someProperty; + + // ... + } + +There's also a Doctrine generator to help auto-generate ULID values for the +entity primary keys:: + + namespace App\Entity; + + use Doctrine\ORM\Mapping as ORM; + use Symfony\Component\Uid\Ulid; + + class Product + { + /** + * @ORM\Id + * @ORM\Column(type="ulid", unique=true) + * @ORM\GeneratedValue(strategy="CUSTOM") + * @ORM\CustomIdGenerator(class="doctrine.ulid_generator") + */ + private $id; + + public function getId(): ?Ulid + { + return $this->id; + } + + // ... + + } + +When using built-in Doctrine repository methods (e.g. ``findOneBy()``), Doctrine +knows how to convert these ULID types to build the SQL query +(e.g. ``->findOneBy(['user' => $user->getUlid()])``). However, when using DQL +queries or building the query yourself, you'll need to set ``ulid`` as the type +of the ULID parameters:: + + // src/Repository/ProductRepository.php + + // ... + class ProductRepository extends ServiceEntityRepository + { + // ... + + public function findUserProducts(User $user): array + { + $qb = $this->createQueryBuilder('p') + // ... + // add 'ulid' as the third argument to tell Doctrine that this is a ULID + ->setParameter('user', $user->getUlid(), 'ulid') + + // alternatively, you can convert it to a value compatible with + // the type inferred by Doctrine + ->setParameter('user', $user->getUlid()->toBinary()) + ; + + // ... + } + } + +Generating and Inspecting UUIDs/ULIDs in the Console +---------------------------------------------------- + +This component provides several commands to generate and inspect UUIDs/ULIDs in +the console. They are not enabled by default, so you must add the following +configuration in your application before using these commands: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + services: + Symfony\Component\Uid\Command\GenerateUlidCommand: ~ + Symfony\Component\Uid\Command\GenerateUuidCommand: ~ + Symfony\Component\Uid\Command\InspectUlidCommand: ~ + Symfony\Component\Uid\Command\InspectUuidCommand: ~ + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\Uid\Command\GenerateUlidCommand; + use Symfony\Component\Uid\Command\GenerateUuidCommand; + use Symfony\Component\Uid\Command\InspectUlidCommand; + use Symfony\Component\Uid\Command\InspectUuidCommand; + + return static function (ContainerConfigurator $configurator): void { + // ... + + $services + ->set(GenerateUlidCommand::class) + ->set(GenerateUuidCommand::class) + ->set(InspectUlidCommand::class) + ->set(InspectUuidCommand::class); + }; + +Now you can generate UUIDs/ULIDs as follows (add the ``--help`` option to the +commands to learn about all their options): + +.. code-block:: terminal + + # generate 1 random-based UUID + $ php bin/console uuid:generate --random-based + + # generate 1 time-based UUID with a specific node + $ php bin/console uuid:generate --time-based=now --node=fb3502dc-137e-4849-8886-ac90d07f64a7 + + # generate 2 UUIDs and output them in base58 format + $ php bin/console uuid:generate --count=2 --format=base58 + + # generate 1 ULID with the current time as the timestamp + $ php bin/console ulid:generate + + # generate 1 ULID with a specific timestamp + $ php bin/console ulid:generate --time="2021-02-02 14:00:00" + + # generate 2 ULIDs and ouput them in RFC4122 format + $ php bin/console ulid:generate --count=2 --format=rfc4122 + +In addition to generating new UIDs, you can also inspect them with the following +commands to show all the information for a given UID: + +.. code-block:: terminal + + $ php bin/console uuid:inspect d0a3a023-f515-4fe0-915c-575e63693998 + ---------------------- -------------------------------------- + Label Value + ---------------------- -------------------------------------- + Version 4 + Canonical (RFC 4122) d0a3a023-f515-4fe0-915c-575e63693998 + Base 58 SmHvuofV4GCF7QW543rDD9 + Base 32 6GMEG27X8N9ZG92Q2QBSHPJECR + ---------------------- -------------------------------------- + + $ php bin/console ulid:inspect 01F2TTCSYK1PDRH73Z41BN1C4X + --------------------- -------------------------------------- + Label Value + --------------------- -------------------------------------- + Canonical (Base 32) 01F2TTCSYK1PDRH73Z41BN1C4X + Base 58 1BYGm16jS4kX3VYCysKKq6 + RFC 4122 0178b5a6-67d3-0d9b-889c-7f205750b09d + --------------------- -------------------------------------- + Timestamp 2021-04-09 08:01:24.947 + --------------------- -------------------------------------- + +.. _`unique identifiers`: https://en.wikipedia.org/wiki/UID +.. _`UUIDs`: https://en.wikipedia.org/wiki/Universally_unique_identifier +.. _`ULIDs`: https://github.com/ulid/spec diff --git a/components/validator/resources.rst b/components/validator/resources.rst index 4de84a7cb49..cd02404f765 100644 --- a/components/validator/resources.rst +++ b/components/validator/resources.rst @@ -106,14 +106,15 @@ prefixed classes included in doc block comments (``/** ... */``). For example:: } To enable the annotation loader, call the -:method:`Symfony\\Component\\Validator\\ValidatorBuilder::enableAnnotationMapping` -method. It takes an optional annotation reader instance, which defaults to -``Doctrine\Common\Annotations\AnnotationReader``:: +:method:`Symfony\\Component\\Validator\\ValidatorBuilder::enableAnnotationMapping` method +and then call ``addDefaultDoctrineAnnotationReader()`` to use Doctrine's +annotation reader:: use Symfony\Component\Validator\Validation; $validator = Validation::createValidatorBuilder() - ->enableAnnotationMapping() + ->enableAnnotationMapping(true) + ->addDefaultDoctrineAnnotationReader() ->getValidator(); To disable the annotation loader after it was enabled, call @@ -134,7 +135,8 @@ multiple mappings:: use Symfony\Component\Validator\Validation; $validator = Validation::createValidatorBuilder() - ->enableAnnotationMapping() + ->enableAnnotationMapping(true) + ->addDefaultDoctrineAnnotationReader() ->addMethodMapping('loadValidatorMetadata') ->addXmlMapping('validator/validation.xml') ->getValidator(); @@ -158,10 +160,6 @@ implement the PSR-6 interface :class:`Psr\\Cache\\CacheItemPoolInterface`):: ->setMappingCache(new SomePsr6Cache()) ->getValidator(); -.. versionadded:: 4.4 - - Support for PSR-6 compatible mapping caches was introduced in Symfony 4.4. - .. note:: The loaders already use a singleton load mechanism. That means that the diff --git a/components/var_dumper.rst b/components/var_dumper.rst index a607ddeb59b..ccb06c4a8dc 100644 --- a/components/var_dumper.rst +++ b/components/var_dumper.rst @@ -66,7 +66,7 @@ current PHP SAPI: You can also select the output format explicitly defining the ``VAR_DUMPER_FORMAT`` environment variable and setting its value to either - ``html`` or ``cli``. + ``html``, ``cli`` or :ref:`server `. .. note:: @@ -186,6 +186,35 @@ Then you can use the following command to start a server out-of-the-box: $ ./vendor/bin/var-dump-server [OK] Server listening on tcp://127.0.0.1:9912 +.. _var-dumper-dump-server-format: + +Configuring the Dump Server with Environment Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you prefer to not modify the application configuration (e.g. to quickly debug +a project given to you) use the ``VAR_DUMPER_FORMAT`` env var. + +First, start the server as usual: + +.. code-block:: terminal + + $ ./vendor/bin/var-dump-server + +Then, run your code with the ``VAR_DUMPER_FORMAT=server`` env var by configuring +this value in the :ref:`.env file of your application `. For +console commands, you can also define this env var as follows: + +.. code-block:: terminal + + $ VAR_DUMPER_FORMAT=server [your-cli-command] + +.. note:: + + The host used by the ``server`` format is the one configured in the + ``VAR_DUMPER_SERVER`` env var or ``127.0.0.1:9912`` if none is defined. + If you prefer, you can also configure the host in the ``VAR_DUMPER_FORMAT`` + env var like this: ``VAR_DUMPER_FORMAT=tcp://127.0.0.1:1234``. + DebugBundle and Twig Integration -------------------------------- @@ -225,11 +254,7 @@ option. Read more about this and other options in finished, press ``Esc.`` to hide the box again. If you want to use your browser search input, press ``Ctrl. + F`` or - ``Cmd. + F`` again while having focus on VarDumper's search input. - - .. versionadded:: 4.4 - - The feature to use the browser search input was introduced in Symfony 4.4. + ``Cmd. + F`` again while focusing on VarDumper's search input. Using the VarDumper Component in your PHPUnit Test Suite -------------------------------------------------------- @@ -258,11 +283,6 @@ The ``VarDumperTestTrait`` also includes these other methods: is called automatically after each case to reset the custom configuration made in ``setUpVarDumper()``. -.. versionadded:: 4.4 - - The ``setUpVarDumper()`` and ``tearDownVarDumper()`` methods were introduced - in Symfony 4.4. - Example:: use PHPUnit\Framework\TestCase; diff --git a/components/yaml.rst b/components/yaml.rst index ba6c0849db2..d5675a417e9 100644 --- a/components/yaml.rst +++ b/components/yaml.rst @@ -389,10 +389,6 @@ you can dump them as ``~`` with the ``DUMP_NULL_AS_TILDE`` flag:: $dumped = Yaml::dump(['foo' => null], 2, 4, Yaml::DUMP_NULL_AS_TILDE); // foo: ~ -.. versionadded:: 4.4 - - The flag to dump ``null`` as ``~`` was introduced in Symfony 4.4. - Syntax Validation ~~~~~~~~~~~~~~~~~ @@ -436,6 +432,9 @@ Then, execute the script for validating contents: # or contents passed to STDIN $ cat path/to/file.yaml | php lint.php + # you can also exclude one or more files from linting + $ php lint.php path/to/directory --exclude=path/to/directory/foo.yaml --exclude=path/to/directory/bar.yaml + The result is written to STDOUT and uses a plain text format by default. Add the ``--format`` option to get the output in JSON format: diff --git a/components/yaml/yaml_format.rst b/components/yaml/yaml_format.rst index d2b7e62e5d2..aa4ef007beb 100644 --- a/components/yaml/yaml_format.rst +++ b/components/yaml/yaml_format.rst @@ -122,7 +122,7 @@ Numbers .. code-block:: yaml # an octal - 014 + 0o14 .. code-block:: yaml diff --git a/configuration.rst b/configuration.rst index 905e1e65d8a..85e3e23adf4 100644 --- a/configuration.rst +++ b/configuration.rst @@ -71,8 +71,40 @@ readable. These are the main advantages and disadvantages of each format: and validation for it. :doc:`Learn the YAML syntax `; * **XML**: autocompleted/validated by most IDEs and is parsed natively by PHP, but sometimes it generates configuration considered too verbose. `Learn the XML syntax`_; -* **PHP**: very powerful and it allows you to create dynamic configuration, but the - resulting configuration is less readable than the other formats. +* **PHP**: very powerful and it allows you to create dynamic configuration with + arrays or a :ref:`ConfigBuilder `. + +.. note:: + + By default Symfony only loads the configuration files defined in YAML + format. If you define configuration in XML and/or PHP formats, update the + ``src/Kernel.php`` file to add support for the ``.xml`` and ``.php`` file + extensions by overriding the + :method:`Symfony\\Component\\HttpKernel\\Kernel::configureContainer` method:: + + // src/Kernel.php + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Component\HttpKernel\Kernel as BaseKernel; + + class Kernel extends BaseKernel + { + // ... + + private function configureContainer(ContainerConfigurator $container): void + { + $configDir = $this->getConfigDir(); + + $container->import($configDir.'/{packages}/*.{yaml,php}'); + $container->import($configDir.'/{packages}/'.$this->environment.'/*.{yaml,php}'); + + if (is_file($configDir.'/services.yaml')) { + $container->import($configDir.'/services.yaml'); + $container->import($configDir.'/{services}_'.$this->environment.'.yaml'); + } else { + $container->import($configDir.'/{services}.php'); + } + } + } Importing Configuration Files ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -144,10 +176,6 @@ configuration files, even if they use a different format: // ... -.. versionadded:: 4.4 - - The ``not_found`` option value for ``ignore_errors`` was introduced in Symfony 4.4. - .. _config-parameter-intro: .. _config-parameters-yml: .. _configuration-parameters: @@ -407,6 +435,90 @@ In reality, each environment differs only somewhat from others. This means that all environments share a large base of common configuration, which is put in files directly in the ``config/packages/`` directory. +.. tip:: + + You can also define options for different environments in a single + configuration file using the special ``when`` keyword: + + .. configuration-block:: + + .. code-block:: yaml + + # config/packages/webpack_encore.yaml + webpack_encore: + # ... + output_path: '%kernel.project_dir%/public/build' + strict_mode: true + cache: false + + # cache is enabled only in the "prod" environment + when@prod: + webpack_encore: + cache: true + + # disable strict mode only in the "test" environment + when@test: + webpack_encore: + strict_mode: false + + # YAML syntax allows to reuse contents using "anchors" (&some_name) and "aliases" (*some_name). + # In this example, 'test' configuration uses the exact same configuration as in 'prod' + when@prod: &webpack_prod + webpack_encore: + # ... + when@test: *webpack_prod + + .. code-block:: xml + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Config\WebpackEncoreConfig; + + return static function (WebpackEncoreConfig $webpackEncore, ContainerConfigurator $container) { + $webpackEncore + ->outputPath('%kernel.project_dir%/public/build') + ->strictMode(true) + ->cache(false) + ; + + // cache is enabled only in the "prod" environment + if ('prod' === $container->env()) { + $webpackEncore->cache(true); + } + + // disable strict mode only in the "test" environment + if ('test' === $container->env()) { + $webpackEncore->strictMode(false); + } + }; + .. seealso:: See the ``configureContainer()`` method of @@ -527,6 +639,8 @@ This example shows how you could configure the database connection using an env 'dbal' => [ // by convention the env var names are always uppercase 'url' => '%env(resolve:DATABASE_URL)%', + // or + 'url' => env('DATABASE_URL')->resolve(), ], ]); }; @@ -627,10 +741,6 @@ Define a default value in case the environment variable is not set: DB_USER= DB_PASS=${DB_USER:-root}pass # results in DB_PASS=rootpass -.. versionadded:: 4.4 - - The support for default values has been introduced in Symfony 4.4. - Embed commands via ``$()`` (not supported on Windows): .. code-block:: bash @@ -730,8 +840,37 @@ you can encrypt the value using the :doc:`secrets management system ` for all the +bundles installed in your application. By convention they all live in the +namespace ``Symfony\Config``:: + + // config/packages/security.php + use Symfony\Config\SecurityConfig; + + return static function (SecurityConfig $security) { + $security->firewall('main') + ->pattern('^/*') + ->lazy(true) + ->anonymous(); + + $security + ->roleHierarchy('ROLE_ADMIN', ['ROLE_USER']) + ->roleHierarchy('ROLE_SUPER_ADMIN', ['ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH']) + ->accessControl() + ->path('^/user') + ->role('ROLE_USER'); + + $security->accessControl(['path' => '^/admin', 'roles' => 'ROLE_ADMIN']); + }; + +.. note:: + + Only root classes in the namespace ``Symfony\Config`` are ConfigBuilders. + Nested configs (e.g. ``\Symfony\Config\Framework\CacheConfig``) are regular + PHP objects which aren't autowired when using them as an argument type. + Keep Going! ----------- diff --git a/configuration/dot-env-changes.rst b/configuration/dot-env-changes.rst index 316bfa01aba..6679600e908 100644 --- a/configuration/dot-env-changes.rst +++ b/configuration/dot-env-changes.rst @@ -45,16 +45,12 @@ If you created your application after November 15th 2018, you don't need to make any changes! Otherwise, here is the list of changes you'll need to make - these changes can be made to any Symfony 3.4 or higher app: -#. Create a new `config/bootstrap.php`_ file in your project. This file loads Composer's - autoloader and loads all the ``.env`` files as needed (note: in an earlier recipe, - this file was called ``src/.bootstrap.php``; if you are upgrading from Symfony 3.3 - or 4.1, use the `3.3/config/bootstrap.php`_ file instead). +#. Update your ``public/index.php`` file to add the code of the `public/index.php`_ + file provided by Symfony. If you've customized this file, make sure to keep + those changes (but add the rest of the changes made by Symfony). -#. Update your `public/index.php`_ (`index.php diff`_) file to load the new ``config/bootstrap.php`` - file. If you've customized this file, make sure to keep those changes (but use - the rest of the changes). - -#. Update your `bin/console`_ file to load the new ``config/bootstrap.php`` file. +#. Update your ``bin/console`` file to add the code of the `bin/console`_ file + provided by Symfony. #. Update ``.gitignore``: @@ -86,14 +82,11 @@ changes can be made to any Symfony 3.4 or higher app: You can also update the `comment on the top of .env`_ to reflect the new changes. #. If you're using PHPUnit, you will also need to `create a new .env.test`_ file - and update your `phpunit.xml.dist file`_ so it loads the ``config/bootstrap.php`` + and update your `phpunit.xml.dist file`_ so it loads the ``tests/bootstrap.php`` file. -.. _`config/bootstrap.php`: https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/4.2/config/bootstrap.php -.. _`3.3/config/bootstrap.php`: https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/3.3/config/bootstrap.php -.. _`public/index.php`: https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/4.2/public/index.php -.. _`index.php diff`: https://github.com/symfony/recipes/compare/8a4e5555e30d5dff64275e2788a901f31a214e79...86e2b6795c455f026e5ab0cba2aff2c7a18511f7#diff-7d73eabd1e5eb7d969ddf9a7ce94f954 -.. _`bin/console`: https://github.com/symfony/recipes/blob/master/symfony/console/3.3/bin/console +.. _`public/index.php`: https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/5.2/public/index.php +.. _`bin/console`: https://github.com/symfony/recipes/blob/master/symfony/console/5.1/bin/console .. _`comment on the top of .env`: https://github.com/symfony/recipes/blob/master/symfony/flex/1.0/.env .. _`create a new .env.test`: https://github.com/symfony/recipes/blob/master/symfony/phpunit-bridge/3.3/.env.test .. _`phpunit.xml.dist file`: https://github.com/symfony/recipes/blob/master/symfony/phpunit-bridge/3.3/phpunit.xml.dist diff --git a/configuration/env_var_processors.rst b/configuration/env_var_processors.rst index e8421290481..84bccba97d5 100644 --- a/configuration/env_var_processors.rst +++ b/configuration/env_var_processors.rst @@ -44,11 +44,17 @@ processor to turn the value of the ``HTTP_PORT`` env var into an integer: .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'router' => [ - 'http_port' => '%env(int:HTTP_PORT)%', - ], - ]); + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework->router() + ->httpPort('%env(int:HTTP_PORT)%') + // or + ->httpPort(env('HTTP_PORT')->int()) + ; + }; Built-In Environment Variable Processors ---------------------------------------- @@ -90,10 +96,15 @@ Symfony provides the following env var processors: .. code-block:: php // config/packages/framework.php - $container->setParameter('env(SECRET)', 'some_secret'); - $container->loadFromExtension('framework', [ - 'secret' => '%env(string:SECRET)%', - ]); + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Config\FrameworkConfig; + + return static function (ContainerBuilder $container, FrameworkConfig $framework) { + $container->setParameter('env(SECRET)', 'some_secret'); + $framework->secret(env('SECRET')->string()); + }; ``env(bool:FOO)`` Casts ``FOO`` to a bool (``true`` values are ``'true'``, ``'on'``, ``'yes'`` @@ -131,10 +142,50 @@ Symfony provides the following env var processors: .. code-block:: php // config/packages/framework.php - $container->setParameter('env(HTTP_METHOD_OVERRIDE)', 'true'); - $container->loadFromExtension('framework', [ - 'http_method_override' => '%env(bool:HTTP_METHOD_OVERRIDE)%', - ]); + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Config\FrameworkConfig; + + return static function (ContainerBuilder $container, FrameworkConfig $framework) { + $container->setParameter('env(HTTP_METHOD_OVERRIDE)', 'true'); + $framework->httpMethodOverride(env('HTTP_METHOD_OVERRIDE')->bool()); + }; + +``env(not:FOO)`` + Casts ``FOO`` to a bool (just as ``env(bool:...)`` does) except it returns the inverted value + (falsy values are returned as ``true``, truthy values are returned as ``false``): + + .. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + parameters: + safe_for_production: '%env(not:APP_DEBUG)%' + + .. code-block:: xml + + + + + + + %env(not:APP_DEBUG)% + + + + + .. code-block:: php + + // config/services.php + $container->setParameter('safe_for_production', '%env(not:APP_DEBUG)%'); ``env(int:FOO)`` Casts ``FOO`` to an int. @@ -164,7 +215,9 @@ Symfony provides the following env var processors: xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:security="http://symfony.com/schema/dic/security" xsi:schemaLocation="http://symfony.com/schema/dic/services - https://symfony.com/schema/dic/services/services-1.0.xsd"> + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/security + https://symfony.com/schema/dic/security/security-1.0.xsd"> Symfony\Component\HttpFoundation\Request::METHOD_HEAD @@ -178,15 +231,15 @@ Symfony provides the following env var processors: .. code-block:: php // config/packages/security.php - $container->setParameter('env(HEALTH_CHECK_METHOD)', 'Symfony\Component\HttpFoundation\Request::METHOD_HEAD'); - $container->loadFromExtension('security', [ - 'access_control' => [ - [ - 'path' => '^/health-check$', - 'methods' => '%env(const:HEALTH_CHECK_METHOD)%', - ], - ], - ]); + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Config\SecurityConfig; + + return static function (ContainerBuilder $container, SecurityConfig $security) { + $container->setParameter('env(HEALTH_CHECK_METHOD)', 'Symfony\Component\HttpFoundation\Request::METHOD_HEAD'); + $security->accessControl() + ->path('^/health-check$') + ->methods([env('HEALTH_CHECK_METHOD')->const()]); + }; ``env(base64:FOO)`` Decodes the content of ``FOO``, which is a base64 encoded string. @@ -227,10 +280,15 @@ Symfony provides the following env var processors: .. code-block:: php // config/packages/framework.php - $container->setParameter('env(TRUSTED_HOSTS)', '["10.0.0.1", "10.0.0.2"]'); - $container->loadFromExtension('framework', [ - 'trusted_hosts' => '%env(json:TRUSTED_HOSTS)%', - ]); + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Config\FrameworkConfig; + + return static function (ContainerBuilder $container, FrameworkConfig $framework) { + $container->setParameter('env(TRUSTED_HOSTS)', '["10.0.0.1", "10.0.0.2"]'); + $framework->trustedHosts(env('TRUSTED_HOSTS')->json()); + }; ``env(resolve:FOO)`` If the content of ``FOO`` includes container parameters (with the syntax @@ -311,10 +369,15 @@ Symfony provides the following env var processors: .. code-block:: php // config/packages/framework.php - $container->setParameter('env(TRUSTED_HOSTS)', '10.0.0.1,10.0.0.2'); - $container->loadFromExtension('framework', [ - 'trusted_hosts' => '%env(csv:TRUSTED_HOSTS)%', - ]); + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Config\FrameworkConfig; + + return static function (ContainerBuilder $container, FrameworkConfig $framework) { + $container->setParameter('env(TRUSTED_HOSTS)', '10.0.0.1,10.0.0.2'); + $framework->trustedHosts(env('TRUSTED_HOSTS')->csv()); + }; ``env(file:FOO)`` Returns the contents of a file whose path is the value of the ``FOO`` env var: @@ -397,10 +460,6 @@ Symfony provides the following env var processors: 'auth' => '%env(require:PHP_FILE)%', ]); - .. versionadded:: 4.3 - - The ``require`` processor was introduced in Symfony 4.3. - ``env(trim:FOO)`` Trims the content of ``FOO`` env var, removing whitespaces from the beginning and end of the string. This is especially useful in combination with the @@ -443,10 +502,6 @@ Symfony provides the following env var processors: 'auth' => '%env(trim:file:AUTH_FILE)%', ]); - .. versionadded:: 4.3 - - The ``trim`` processor was introduced in Symfony 4.3. - ``env(key:FOO:BAR)`` Retrieves the value associated with the key ``FOO`` from the array whose contents are stored in the ``BAR`` env var: @@ -528,10 +583,6 @@ Symfony provides the following env var processors: When the fallback parameter is omitted (e.g. ``env(default::API_KEY)``), then the returned value is ``null``. - .. versionadded:: 4.3 - - The ``default`` processor was introduced in Symfony 4.3. - ``env(url:FOO)`` Parses an absolute URL and returns its components as an associative array. @@ -601,10 +652,6 @@ Symfony provides the following env var processors: In order to ease extraction of the resource from the URL, the leading ``/`` is trimmed from the ``path`` component. - .. versionadded:: 4.3 - - The ``url`` processor was introduced in Symfony 4.3. - ``env(query_string:FOO)`` Parses the query string part of the given URL and returns its components as an associative array. @@ -651,10 +698,6 @@ Symfony provides the following env var processors: ], ]); - .. versionadded:: 4.3 - - The ``query_string`` processor was introduced in Symfony 4.3. - It is also possible to combine any number of processors: .. configuration-block:: @@ -717,7 +760,7 @@ create a class that implements class LowercasingEnvVarProcessor implements EnvVarProcessorInterface { - public function getEnv($prefix, $name, \Closure $getEnv) + public function getEnv(string $prefix, string $name, \Closure $getEnv) { $env = $getEnv($name); diff --git a/configuration/front_controllers_and_kernel.rst b/configuration/front_controllers_and_kernel.rst index b7b70456cb7..b0048e43e1d 100644 --- a/configuration/front_controllers_and_kernel.rst +++ b/configuration/front_controllers_and_kernel.rst @@ -190,10 +190,13 @@ parameter used, for example, to turn Twig's debug mode on: .. code-block:: php - $container->loadFromExtension('twig', [ - 'debug' => '%kernel.debug%', + // config/packages/twig.php + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig) { // ... - ]); + $twig->debug('%kernel.debug%'); + }; The Environments ---------------- @@ -248,13 +251,12 @@ includes the following: The cached "service container" that represents the cached application configuration. -``UrlGenerator.php`` - The PHP class generated from the routing configuration and used when - generating URLs. +``url_generating_routes.php`` + The cached routing configuration used when generating URLs. -``UrlMatcher.php`` - The PHP class used for route matching - look here to see the compiled regular - expression logic used to match incoming URLs to different routes. +``url_matching_routes.php`` + The cached configuration used for route matching - look here to see the compiled + regular expression logic used to match incoming URLs to different routes. ``twig/`` This directory contains all the cached Twig templates. diff --git a/configuration/micro_kernel_trait.rst b/configuration/micro_kernel_trait.rst index 16402bd5a54..66e9aae2bbe 100644 --- a/configuration/micro_kernel_trait.rst +++ b/configuration/micro_kernel_trait.rst @@ -24,12 +24,11 @@ Next, create an ``index.php`` file that defines the kernel class and runs it:: // index.php use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; - use Symfony\Component\Config\Loader\LoaderInterface; - use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Kernel as BaseKernel; - use Symfony\Component\Routing\RouteCollectionBuilder; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; require __DIR__.'/vendor/autoload.php'; @@ -37,29 +36,27 @@ Next, create an ``index.php`` file that defines the kernel class and runs it:: { use MicroKernelTrait; - public function registerBundles() + public function registerBundles(): array { return [ - new Symfony\Bundle\FrameworkBundle\FrameworkBundle() + new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), ]; } - protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader) + protected function configureContainer(ContainerConfigurator $c): void { // PHP equivalent of config/packages/framework.yaml - $c->loadFromExtension('framework', [ + $c->extension('framework', [ 'secret' => 'S0ME_SECRET' ]); } - protected function configureRoutes(RouteCollectionBuilder $routes) + protected function configureRoutes(RoutingConfigurator $routes): void { - // kernel is a service that points to this class - // optional 3rd argument is the route name - $routes->add('/random/{limit}', 'kernel::randomNumber'); + $routes->add('random_number', '/random/{limit}')->controller([$this, 'randomNumber']); } - public function randomNumber($limit) + public function randomNumber(int $limit): JsonResponse { return new JsonResponse([ 'number' => random_int(0, $limit), @@ -91,15 +88,15 @@ that define your bundles, your services and your routes: **registerBundles()** This is the same ``registerBundles()`` that you see in a normal kernel. -**configureContainer(ContainerBuilder $c, LoaderInterface $loader)** +**configureContainer(ContainerConfigurator $c)** This method builds and configures the container. In practice, you will use - ``loadFromExtension`` to configure different bundles (this is the equivalent + ``extension()`` to configure different bundles (this is the equivalent of what you see in a normal ``config/packages/*`` file). You can also register services directly in PHP or load external configuration files (shown below). -**configureRoutes(RouteCollectionBuilder $routes)** +**configureRoutes(RoutingConfigurator $routes)** Your job in this method is to add routes to the application. The - ``RouteCollectionBuilder`` has methods that make adding routes in PHP more + ``RoutingConfigurator`` has methods that make adding routes in PHP more fun. You can also load external routing files (shown below). Advanced Example: Twig, Annotations and the Web Debug Toolbar @@ -134,16 +131,15 @@ hold the kernel. Now it looks like this:: namespace App; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; - use Symfony\Component\Config\Loader\LoaderInterface; - use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\HttpKernel\Kernel as BaseKernel; - use Symfony\Component\Routing\RouteCollectionBuilder; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; class Kernel extends BaseKernel { use MicroKernelTrait; - public function registerBundles() + public function registerBundles(): array { $bundles = [ new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(), @@ -151,45 +147,52 @@ hold the kernel. Now it looks like this:: ]; if ($this->getEnvironment() == 'dev') { - $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); + $bundles[] = new \Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); } return $bundles; } - protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader) + protected function configureContainer(ContainerConfigurator $c): void { - $loader->load(__DIR__.'/../config/framework.yaml'); + $c->import(__DIR__.'/../config/framework.yaml'); + + // register all classes in /src/ as service + $c->services() + ->load('App\\', __DIR__.'/*') + ->autowire() + ->autoconfigure() + ; // configure WebProfilerBundle only if the bundle is enabled if (isset($this->bundles['WebProfilerBundle'])) { - $c->loadFromExtension('web_profiler', [ + $c->extension('web_profiler', [ 'toolbar' => true, 'intercept_redirects' => false, ]); } } - protected function configureRoutes(RouteCollectionBuilder $routes) + protected function configureRoutes(RoutingConfigurator $routes): void { // import the WebProfilerRoutes, only if the bundle is enabled if (isset($this->bundles['WebProfilerBundle'])) { - $routes->import('@WebProfilerBundle/Resources/config/routing/wdt.xml', '/_wdt'); - $routes->import('@WebProfilerBundle/Resources/config/routing/profiler.xml', '/_profiler'); + $routes->import('@WebProfilerBundle/Resources/config/routing/wdt.xml')->prefix('/_wdt'); + $routes->import('@WebProfilerBundle/Resources/config/routing/profiler.xml')->prefix('/_profiler'); } // load the annotation routes - $routes->import(__DIR__.'/../src/Controller/', '/', 'annotation'); + $routes->import(__DIR__.'/Controller/', 'annotation'); } // optional, to use the standard Symfony cache directory - public function getCacheDir() + public function getCacheDir(): string { return __DIR__.'/../var/cache/'.$this->getEnvironment(); } // optional, to use the standard Symfony logs directory - public function getLogDir() + public function getLogDir(): string { return __DIR__.'/../var/log'; } @@ -231,12 +234,15 @@ because the configuration started to get bigger: .. code-block:: php // config/framework.php - $container->loadFromExtension('framework', [ - 'secret' => 'S0ME_SECRET', - 'profiler' => [ - 'only_exceptions' => false, - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework + ->secret('SOME_SECRET') + ->profiler() + ->onlyExceptions(false) + ; + }; This also loads annotation routes from an ``src/Controller/`` directory, which has one file in it:: @@ -245,6 +251,7 @@ has one file in it:: namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; class MicroController extends AbstractController @@ -252,7 +259,7 @@ has one file in it:: /** * @Route("/random/{limit}") */ - public function randomNumber($limit) + public function randomNumber(int $limit): Response { $number = random_int(0, $limit); @@ -327,7 +334,6 @@ As before you can use the :doc:`Symfony Local Web Server .. code-block:: terminal - cd public/ $ symfony server:start Then visit the page in your browser: http://localhost:8000/random/10 diff --git a/configuration/multiple_kernels.rst b/configuration/multiple_kernels.rst index 029b4c1e5fb..9c24633106a 100644 --- a/configuration/multiple_kernels.rst +++ b/configuration/multiple_kernels.rst @@ -82,25 +82,15 @@ Kernel. Be sure to also change the location of the cache, logs and configuration files so they don't collide with the files from ``src/Kernel.php``:: // src/ApiKernel.php - use Symfony\Component\Config\Loader\LoaderInterface; - use Symfony\Component\DependencyInjection\ContainerBuilder; - use Symfony\Component\HttpKernel\Kernel; + use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use Symfony\Component\HttpKernel\Kernel as BaseKernel; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; class ApiKernel extends Kernel { use MicroKernelTrait; - public function registerBundles() - { - // load only the bundles strictly needed for the API - $contents = require $this->getProjectDir().'/config/api_bundles.php'; - foreach ($contents as $class => $envs) { - if ($envs[$this->environment] ?? $envs['all'] ?? false) { - yield new $class(); - } - } - } - public function getProjectDir(): string { return \dirname(__DIR__); @@ -108,7 +98,7 @@ files so they don't collide with the files from ``src/Kernel.php``:: public function getCacheDir(): string { - return $this->getProjectDir().'/var/cache/api/'.$this->getEnvironment(); + return $this->getProjectDir().'/var/cache/api/'.$this->environment; } public function getLogDir(): string @@ -116,23 +106,33 @@ files so they don't collide with the files from ``src/Kernel.php``:: return $this->getProjectDir().'/var/log/api'; } - public function configureContainer(ContainerBuilder $container, LoaderInterface $loader) + protected function configureContainer(ContainerConfigurator $container): void { - $container->addResource(new FileResource($this->getProjectDir().'/config/api_bundles.php')); - $container->setParameter('container.dumper.inline_factories', true); - $confDir = $this->getProjectDir().'/config/api'; - - $loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{packages}/'.$this->environment.'/*'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob'); - $loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob'); + $container->import('../config/api/{packages}/*.yaml'); + $container->import('../config/api/{packages}/'.$this->environment.'/*.yaml'); + + if (is_file(\dirname(__DIR__).'/config/api/services.yaml')) { + $container->import('../config/api/services.yaml'); + $container->import('../config/api/{services}_'.$this->environment.'.yaml'); + } else { + $container->import('../config/api/{services}.php'); + } } - protected function configureRoutes(RouteCollectionBuilder $routes): void + protected function configureRoutes(RoutingConfigurator $routes): void { - $confDir = $this->getProjectDir().'/config/api'; + $routes->import('../config/api/{routes}/'.$this->environment.'/*.yaml'); + $routes->import('../config/api/{routes}/*.yaml'); // ... load only the config routes strictly needed for the API } + + // If you need to run some logic to decide which bundles to load, + // you might prefer to use the registerBundles() method instead + private function getBundlesPath(): string + { + // load only the bundles strictly needed for the API + return $this->getProjectDir().'/config/api_bundles.php'; + } } Step 3) Define the Kernel Configuration diff --git a/configuration/override_dir_structure.rst b/configuration/override_dir_structure.rst index a1af58ba5db..73f65c9171f 100644 --- a/configuration/override_dir_structure.rst +++ b/configuration/override_dir_structure.rst @@ -25,7 +25,58 @@ override it to create your own structure: │ ├─ cache/ │ ├─ log/ │ └─ ... - └─ vendor/ + ├─ vendor/ + └─ .env + +.. _override-env-dir: + +Override the Environment (DotEnv) Files Directory +------------------------------------------------- + +By default, the :ref:`.env configuration file ` is located at +the root directory of the project. If you store it in a different location, +define the ``runtime.dotenv_path`` option in the ``composer.json`` file: + +.. code-block:: json + + { + "...": "...", + "extra": { + "...": "...", + "runtime": { + "dotenv_path": "my/custom/path/to/.env" + } + } + } + +Then, update your Composer files (running ``composer update``, for instance), +so that the ``vendor/autoload_runtime.php`` files gets regenerated with the new +``.env`` path. + +You can also set up different ``.env`` paths for your console and web server +calls. Edit the ``public/index.php`` and/or ``bin/console`` files to define the +new file path. + +Console script:: + + // bin/console + + // ... + $_SERVER['APP_RUNTIME_OPTIONS']['dotenv_path'] = 'some/custom/path/to/.env'; + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + // ... + +Web front-controller:: + + // public/index.php + + // ... + $_SERVER['APP_RUNTIME_OPTIONS']['dotenv_path'] = 'another/custom/path/to/.env'; + + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + // ... + .. _override-config-dir: @@ -41,8 +92,8 @@ at your project root directory. Override the Cache Directory ---------------------------- -You can change the default cache directory by overriding the ``getCacheDir()`` -method in the ``Kernel`` class of your application:: +Changing the cache directory can be achieved by overriding the +``getCacheDir()`` method in the ``Kernel`` class of your application:: // src/Kernel.php @@ -61,6 +112,9 @@ In this code, ``$this->environment`` is the current environment (i.e. ``dev``). In this case you have changed the location of the cache directory to ``var/{environment}/cache/``. +You can also change the cache directory defining an environment variable named +``APP_CACHE_DIR`` whose value is the full path of the cache folder. + .. caution:: You should keep the cache directory different for each environment, @@ -73,9 +127,11 @@ In this case you have changed the location of the cache directory to Override the Log Directory -------------------------- -Overriding the ``var/log/`` directory is the same as overriding the ``var/cache/`` -directory. The only difference is that you need to override the ``getLogDir()`` -method:: +Overriding the ``var/log/`` directory is almost the same as overriding the +``var/cache/`` directory. + +You can do it overriding the ``getLogDir()`` method in the ``Kernel`` class of +your application:: // src/Kernel.php @@ -92,6 +148,9 @@ method:: Here you have changed the location of the directory to ``var/{environment}/log/``. +You can also change the log directory defining an environment variable named +``APP_LOG_DIR`` whose value is the full path of the log folder. + .. _override-templates-dir: Override the Templates Directory @@ -132,9 +191,11 @@ for multiple directories): .. code-block:: php // config/packages/twig.php - $container->loadFromExtension('twig', [ - 'default_path' => '%kernel.project_dir%/resources/views', - ]); + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig) { + $twig->defaultPath('%kernel.project_dir%/resources/views'); + }; Override the Translations Directory ----------------------------------- @@ -176,11 +237,13 @@ configuration option to define your own translations directory (use :ref:`framew .. code-block:: php // config/packages/translation.php - $container->loadFromExtension('framework', [ - 'translator' => [ - 'default_path' => '%kernel.project_dir%/i18n', - ], - ]); + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework->translator() + ->defaultPath('%kernel.project_dir%/i18n') + ; + }; .. _override-web-dir: .. _override-the-web-directory: diff --git a/configuration/secrets.rst b/configuration/secrets.rst index ba0c05bb278..4e6b32ed763 100644 --- a/configuration/secrets.rst +++ b/configuration/secrets.rst @@ -4,10 +4,6 @@ How to Keep Sensitive Information Secret ======================================== -.. versionadded:: 4.4 - - The Secrets management was introduced in Symfony 4.4. - :ref:`Environment variables ` are the best way to store configuration that depends on where the application is run - for example, some API key that might be set to one value while developing locally and another value on production. @@ -18,10 +14,7 @@ store them by using Symfony's secrets management system - sometimes called a .. note:: - The Secrets system requires the sodium PHP extension that is bundled - with PHP 7.2. If you're using an earlier PHP version, you can - install the `libsodium`_ PHP extension or use the - `paragonie/sodium_compat`_ package. + The Secrets system requires the Sodium PHP extension. .. _secrets-generate-keys: @@ -52,7 +45,7 @@ running: .. code-block:: terminal - $ php bin/console secrets:generate-keys --env=prod + $ APP_RUNTIME_ENV=prod php bin/console secrets:generate-keys This will generate ``config/secrets/prod/prod.encrypt.public.php`` and ``config/secrets/prod/prod.decrypt.private.php``. @@ -82,7 +75,7 @@ Suppose you want to store your database password as a secret. By using the $ php bin/console secrets:set DATABASE_PASSWORD # set your production value - $ php bin/console secrets:set DATABASE_PASSWORD --env=prod + $ APP_RUNTIME_ENV=prod php bin/console secrets:set DATABASE_PASSWORD This will create a new file for the secret in ``config/secrets/dev`` and another in ``config/secrets/prod``. You can also set the secret in a few other ways: @@ -147,11 +140,14 @@ If you stored a ``DATABASE_PASSWORD`` secret, you can reference it by: .. code-block:: php // config/packages/doctrine.php - $container->loadFromExtension('doctrine', [ - 'dbal' => [ - 'password' => '%env(DATABASE_PASSWORD)%', - ], - ]); + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine) { + $doctrine->dbal() + ->connection('default') + ->password(env('DATABASE_PASSWORD')) + ; + }; The actual value will be resolved at runtime: container compilation and cache warmup don't need the **decryption key**. @@ -262,7 +258,7 @@ your secrets during deployment to the "local" vault: .. code-block:: terminal - $ php bin/console secrets:decrypt-to-local --force --env=prod + $ APP_RUNTIME_ENV=prod php bin/console secrets:decrypt-to-local --force This will write all the decrypted secrets into the ``.env.prod.local`` file. After doing this, the decryption key does *not* need to remain on the server(s). @@ -314,14 +310,12 @@ The secrets system is enabled by default and some of its behavior can be configu .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'secrets' => [ - // 'vault_directory' => '%kernel.project_dir%/config/secrets/%kernel.environment%', - // 'local_dotenv_file' => '%kernel.project_dir%/.env.%kernel.environment%.local', - // 'decryption_env_var' => 'base64:default::SYMFONY_DECRYPTION_SECRET', - ], - ]); - - -.. _`libsodium`: https://pecl.php.net/package/libsodium -.. _`paragonie/sodium_compat`: https://github.com/paragonie/sodium_compat + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework->secrets() + // ->vaultDirectory('%kernel.project_dir%/config/secrets/%kernel.environment%') + // ->localDotenvFile('%kernel.project_dir%/.env.%kernel.environment%.local') + // ->decryptionEnvVar('base64:default::SYMFONY_DECRYPTION_SECRET') + ; + }; diff --git a/console.rst b/console.rst index 0830ace0829..1d1e149afea 100644 --- a/console.rst +++ b/console.rst @@ -9,15 +9,86 @@ The Symfony framework provides lots of commands through the ``bin/console`` scri created with the :doc:`Console component `. You can also use it to create your own commands. -The Console: APP_ENV & APP_DEBUG ---------------------------------- +Running Commands +---------------- + +Each Symfony application comes with a large set of commands. You can use +the ``list`` command to view all available commands in the application: + +.. code-block:: terminal + + $ php bin/console list + ... + + Available commands: + about Display information about the current project + completion Dump the shell completion script + help Display help for a command + list List commands + assets + assets:install Install bundle's web assets under a public directory + cache + cache:clear Clear the cache + ... + +If you find the command you need, you can run it with the ``--help`` option +to view the command's documentation: + +.. code-block:: terminal + + $ php bin/console assets:install --help + +APP_ENV & APP_DEBUG +~~~~~~~~~~~~~~~~~~~ Console commands run in the :ref:`environment ` defined in the ``APP_ENV`` variable of the ``.env`` file, which is ``dev`` by default. It also reads the ``APP_DEBUG`` value to turn "debug" mode on or off (it defaults to ``1``, which is on). To run the command in another environment or debug mode, edit the value of ``APP_ENV`` -and ``APP_DEBUG``. +and ``APP_DEBUG``. You can also define this env vars when running the +command, for instance: + +.. code-block:: terminal + + # clears the cache for the prod environment + $ APP_ENV=prod php bin/console cache:clear + +.. _console-completion-setup: + +Console Completion +~~~~~~~~~~~~~~~~~~ + +If you are using the Bash shell, you can install Symfony's completion +script to get auto completion when typing commands in the terminal. All +commands support name and option completion, and some can even complete +values. + +.. image:: /_images/components/console/completion.gif + +First, make sure you installed and setup the "bash completion" package for +your OS (typically named ``bash-completion``). Then, install the Symfony +completion bash script *once* by running the ``completion`` command in a +Symfony app installed on your computer: + +.. code-block:: terminal + + $ php bin/console completion bash | sudo tee /etc/bash_completion.d/console-events-terminate + # after the installation, restart the shell + +Now you are all set to use the auto completion for all Symfony Console +applications on your computer. By default, you can get a list of complete +options by pressing the Tab key. + +.. tip:: + + Many PHP tools are built using the Symfony Console component (e.g. + Composer, PHPstan and Behat). If they are using version 5.4 or higher, + you can also install their completion script to enable console completion: + + .. code-block:: terminal + + $ php vendor/bin/phpstan completion bash | sudo tee /etc/bash_completion.d/phpstan Creating a Command ------------------ @@ -38,45 +109,64 @@ want a command to create a user:: // the name of the command (the part after "bin/console") protected static $defaultName = 'app:create-user'; - protected function configure(): void - { - // ... - } - protected function execute(InputInterface $input, OutputInterface $output): int { // ... put here the code to create the user // this method must return an integer number with the "exit status code" - // of the command. + // of the command. You can also use these constants to make code more readable // return this if there was no problem running the command - return 0; + // (it's equivalent to returning int(0)) + return Command::SUCCESS; // or return this if some error happened during the execution - // return 1; + // (it's equivalent to returning int(1)) + // return Command::FAILURE; + + // or return this to indicate incorrect command usage; e.g. invalid options + // or missing arguments (it's equivalent to returning int(2)) + // return Command::INVALID } } Configuring the Command ------------------------ +~~~~~~~~~~~~~~~~~~~~~~~ You can optionally define a description, help message and the -:doc:`input options and arguments `:: +:doc:`input options and arguments ` by overriding the +``configure()`` method:: + + // src/Command/CreateUserCommand.php // ... - protected function configure(): void + class CreateUserCommand extends Command { - $this - // the short description shown while running "php bin/console list" - ->setDescription('Creates a new user.') + // the command description shown when running "php bin/console list" + protected static $defaultDescription = 'Creates a new user.'; - // the full command description shown when running the command with - // the "--help" option - ->setHelp('This command allows you to create a user...') - ; + // ... + protected function configure(): void + { + $this + // the command help shown when running the command with the "--help" option + ->setHelp('This command allows you to create a user...') + ; + } } +.. tip:: + + Defining the ``$defaultDescription`` static property instead of using the + ``setDescription()`` method allows to get the command description without + instantiating its class. This makes the ``php bin/console list`` command run + much faster. + + If you want to always run the ``list`` command fast, add the ``--short`` option + to it (``php bin/console list --short``). This will avoid instantiating command + classes, but it won't show any description for commands that use the + ``setDescription()`` method instead of the static property. + The ``configure()`` method is called automatically at the end of the command constructor. If your command defines its own constructor, set the properties first and then call to the parent constructor, to make those properties @@ -110,15 +200,37 @@ available in the ``configure()`` method:: } Registering the Command ------------------------ +~~~~~~~~~~~~~~~~~~~~~~~ -Symfony commands must be registered as services and :doc:`tagged ` -with the ``console.command`` tag. If you're using the +In PHP 8 and newer versions, you can register the command by adding the +``AsCommand`` attribute to it:: + + // src/Command/CreateUserCommand.php + namespace App\Command; + + use Symfony\Component\Console\Attribute\AsCommand; + use Symfony\Component\Console\Command\Command; + + // the "name" and "description" arguments of AsCommand replace the + // static $defaultName and $defaultDescription properties + #[AsCommand( + name: 'app:create-user', + description: 'Creates a new user.', + hidden: false, + aliases: ['app:add-user'] + )] + class CreateUserCommand extends Command + { + // ... + } + +If you can't use PHP attributes, register the command as a service and +:doc:`tag it ` with the ``console.command`` tag. If you're using the :ref:`default services.yaml configuration `, this is already done for you, thanks to :ref:`autoconfiguration `. -Executing the Command ---------------------- +Running the Command +~~~~~~~~~~~~~~~~~~~ After configuring and registering the command, you can run it in the terminal: @@ -156,7 +268,7 @@ the console:: $output->write('You are about to '); $output->write('create a user.'); - return 0; + return Command::SUCCESS; } Now, try executing the command: @@ -215,7 +327,7 @@ method, which returns an instance of $section1->clear(2); // Output is now completely empty! - return 0; + return Command::SUCCESS; } } @@ -257,7 +369,7 @@ Use input options or arguments to pass information to the command:: // retrieve the argument value using getArgument() $output->writeln('Username: '.$input->getArgument('username')); - return 0; + return Command::SUCCESS; } Now, you can pass the username to the command: @@ -308,7 +420,7 @@ as a service, you can use normal dependency injection. Imagine you have a $output->writeln('User successfully generated!'); - return 0; + return Command::SUCCESS; } } @@ -332,14 +444,9 @@ command: :method:`Symfony\\Component\\Console\\Command\\Command::execute` *(required)* This method is executed after ``interact()`` and ``initialize()``. - It contains the logic you want the command to execute and it should + It contains the logic you want the command to execute and it must return an integer which will be used as the command `exit status`_. - .. deprecated:: 4.4 - - Not returning an integer with the exit status as the result of - ``execute()`` is deprecated since Symfony 4.4. - .. _console-testing-commands: Testing Commands @@ -374,6 +481,8 @@ console:: // e.g: '--some-option' => 'option_value', ]); + $commandTester->assertCommandIsSuccessful(); + // the output of the command in the console $output = $commandTester->getDisplay(); $this->assertStringContainsString('Username: Wouter', $output); @@ -382,6 +491,9 @@ console:: } } +If you are using a :doc:`single-command application `, +call ``setAutoExit(false)`` on it to get the command result in ``CommandTester``. + .. tip:: You can also test a whole console application by using @@ -400,7 +512,7 @@ console:: $application = new Application(); $application->setAutoExit(false); - + $tester = new ApplicationTester($application); .. note:: @@ -436,5 +548,6 @@ tools capable of helping you with different tasks: * :doc:`/components/console/helpers/table`: displays tabular data as a table * :doc:`/components/console/helpers/debug_formatter`: provides functions to output debug information when running an external program +* :doc:`/components/console/helpers/cursor`: allows to manipulate the cursor in the terminal .. _`exit status`: https://en.wikipedia.org/wiki/Exit_status diff --git a/console/coloring.rst b/console/coloring.rst index 774a2ab96fa..9a811599dc1 100644 --- a/console/coloring.rst +++ b/console/coloring.rst @@ -40,13 +40,20 @@ It is possible to define your own styles using the use Symfony\Component\Console\Formatter\OutputFormatterStyle; // ... - $outputStyle = new OutputFormatterStyle('red', 'yellow', ['bold', 'blink']); + $outputStyle = new OutputFormatterStyle('red', '#ff0', ['bold', 'blink']); $output->getFormatter()->setStyle('fire', $outputStyle); $output->writeln('foo'); -Available foreground and background colors are: ``black``, ``red``, ``green``, -``yellow``, ``blue``, ``magenta``, ``cyan`` and ``white``. +Any hex color is supported for foreground and background colors. Besides that, these named colors are supported: +``black``, ``red``, ``green``, ``yellow``, ``blue``, ``magenta``, ``cyan``, ``white``, +``gray``, ``bright-red``, ``bright-green``, ``bright-yellow``, ``bright-blue``, +``bright-magenta``, ``bright-cyan`` and ``bright-white``. + +.. note:: + + If the terminal doesn't support true colors, the nearest named color is used. + E.g. ``#c0392b`` is degraded to ``red`` or ``#f1c40f`` is degraded to ``yellow``. And available options are: ``bold``, ``underscore``, ``blink``, ``reverse`` (enables the "reverse video" mode where the background and foreground colors @@ -56,9 +63,12 @@ commonly used when asking the user to type sensitive information). You can also set these colors and options directly inside the tag name:: - // green text + // using named colors $output->writeln('foo'); + // using hexadecimal colors + $output->writeln('foo'); + // black text on a cyan background $output->writeln('foo'); @@ -77,10 +87,6 @@ You can also set these colors and options directly inside the tag name:: Displaying Clickable Links ~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 4.3 - - The feature to display clickable links was introduced in Symfony 4.3. - Commands can use the special ```` tag to display links similar to the ```` elements of web pages:: diff --git a/console/commands_as_services.rst b/console/commands_as_services.rst index 794ec8f46cb..6323f21ac50 100644 --- a/console/commands_as_services.rst +++ b/console/commands_as_services.rst @@ -46,7 +46,7 @@ For example, suppose you want to log something from within your command:: $this->logger->info('Waking up the sun'); // ... - return 0; + return Command::SUCCESS; } } @@ -63,13 +63,6 @@ command and start logging. work (e.g. making database queries), as that code will be run, even if you're using the console to execute a different command. -.. note:: - - In previous Symfony versions, you could make the command class extend from - :class:`Symfony\\Bundle\\FrameworkBundle\\Command\\ContainerAwareCommand` to - get services via ``$this->getContainer()->get('SERVICE_ID')``. This is - deprecated in Symfony 4.2 and it won't work in future Symfony versions. - .. _console-command-service-lazy-loading: Lazy Loading diff --git a/console/input.rst b/console/input.rst index b7534d21abd..cf8365455aa 100644 --- a/console/input.rst +++ b/console/input.rst @@ -53,7 +53,7 @@ You now have access to a ``last_name`` argument in your command:: $output->writeln($text.'!'); - return 0; + return Command::SUCCESS; } } @@ -199,7 +199,7 @@ separation at all (e.g. ``-i 5`` or ``-i5``). this situation, always place options after the command name, or avoid using a space to separate the option name from its value. -There are four option variants you can use: +There are five option variants you can use: ``InputOption::VALUE_IS_ARRAY`` This option accepts multiple values (e.g. ``--dir=/foo --dir=/bar``); @@ -217,7 +217,11 @@ There are four option variants you can use: This option may or may not have a value (e.g. ``--yell`` or ``--yell=loud``). -You can combine ``VALUE_IS_ARRAY`` with ``VALUE_REQUIRED`` or +``InputOption::VALUE_NEGATABLE`` + Accept either the flag (e.g. ``--yell``) or its negation (e.g. + ``--no-yell``). + +``VALUE_IS_ARRAY`` works only combined with ``VALUE_REQUIRED`` or ``VALUE_OPTIONAL`` like this:: $this @@ -300,4 +304,87 @@ The above code can be simplified as follows because ``false !== null``:: $yell = ($optionValue !== false); $yellLouder = ($optionValue === 'louder'); +Adding Argument/Option Value Completion +--------------------------------------- + +If :ref:`Console completion is installed `, +command and option names will be auto completed by the shell. However, you +can also implement value completion for the input in your commands. For +instance, you may want to complete all usernames from the database in the +``name`` argument of your greet command. + +To achieve this, override the ``complete()`` method in the command:: + + // ... + use Symfony\Component\Console\Completion\CompletionInput; + use Symfony\Component\Console\Completion\CompletionSuggestions; + + class GreetCommand extends Command + { + // ... + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('names')) { + // the user asks for completion input for the "names" option + + // the value the user already typed, e.g. when typing "app:greet Fa" before + // pressing Tab, this will contain "Fa" + $currentValue = $input->getCompletionValue(); + + // get the list of username names from somewhere (e.g. the database) + // you may use $currentValue to filter down the names + $availableUsernames = ...; + + // then add the retrieved names as suggested values + $suggestions->suggestValues($availableUsernames); + } + } + } + +That's all you need! Assuming users "Fabien" and "Fabrice" exist, pressing +tab after typing ``app:greet Fa`` will give you these names as a suggestion. + +.. tip:: + + The bash shell is able to handle huge amounts of suggestions and will + automatically filter the suggested values based on the existing input + from the user. You do not have to implement any filter logic in the + command. + + You may use ``CompletionInput::getCompletionValue()`` to get the + current input if that helps improving performance (e.g. by reducing the + number of rows fetched from the database). + +Testing the Completion script +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Console component comes with a special +:class:`Symfony\\Component\\Console\\Test\\CommandCompletionTester`` class +to help you unit test the completion logic:: + + // ... + use Symfony\Component\Console\Application; + + class GreetCommandTest extends TestCase + { + public function testComplete() + { + $application = new Application(); + $application->add(new GreetCommand()); + + // create a new tester with the greet command + $tester = new CommandCompletionTester($application->get('app:greet')); + + // complete the input without any existing input (the empty string represents + // the position of the cursor) + $suggestions = $tester->complete(['']); + $this->assertSame(['Fabien', 'Fabrice', 'Wouter'], $suggestions); + + // complete the input with "Fa" as input + $suggestions = $tester->complete(['Fa']); + $this->assertSame(['Fabien', 'Fabrice'], $suggestions); + } + } + .. _`docopt standard`: http://docopt.org/ diff --git a/console/lockable_trait.rst b/console/lockable_trait.rst index 98c94d82c57..02f635f5788 100644 --- a/console/lockable_trait.rst +++ b/console/lockable_trait.rst @@ -27,7 +27,7 @@ that adds two convenient methods to lock and release commands:: if (!$this->lock()) { $output->writeln('The command is already running in another process.'); - return 0; + return Command::SUCCESS; } // If you prefer to wait until the lock is released, use this: @@ -39,7 +39,7 @@ that adds two convenient methods to lock and release commands:: // automatically when the execution of the command ends $this->release(); - return 0; + return Command::SUCCESS; } } diff --git a/console/style.rst b/console/style.rst index 79a4971b2c8..cfe6502b3fd 100644 --- a/console/style.rst +++ b/console/style.rst @@ -152,10 +152,6 @@ Content Methods ] ); - .. versionadded:: 4.4 - - The ``horizontalTable()`` method was introduced in Symfony 4.4. - :method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::definitionList` It displays the given ``key => value`` pairs as a compact list of elements:: @@ -169,9 +165,10 @@ Content Methods ['foo4' => 'bar4'] ); - .. versionadded:: 4.4 - - The ``definitionList()`` method was introduced in Symfony 4.4. +:method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::createTable` + Creates an instance of :class:`Symfony\\Component\\Console\\Helper\\Table` + styled according to the Symfony Style Guide, which allows you to use + features such as dynamically appending rows. :method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::newLine` It displays a blank line in the command output. Although it may seem useful, @@ -251,6 +248,20 @@ Progress Bar Methods $io->progressFinish(); +:method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::progressIterate` + If your progress bar loops over an iterable collection, use the + ``progressIterate()`` helper:: + + $iterable = [1, 2]; + + foreach ($io->progressIterate($iterable) as $value) { + // ... do some work + } + +:method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::createProgressBar` + Creates an instance of :class:`Symfony\\Component\\Console\\Helper\\ProgressBar` + styled according to the Symfony Style Guide. + User Input Methods ~~~~~~~~~~~~~~~~~~ @@ -333,6 +344,23 @@ Result Methods 'Consectetur adipiscing elit', ]); +:method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::info` + It's similar to the ``success()`` method (the given string or array of strings + are displayed with a green background) but the ``[OK]`` label is not prefixed. + It's meant to be used once to display the final result of executing the given + command, without showing the result as a successful or failed one:: + + // use simple strings for short info messages + $io->info('Lorem ipsum dolor sit amet'); + + // ... + + // consider using arrays when displaying long info messages + $io->info([ + 'Lorem ipsum dolor sit amet', + 'Consectetur adipiscing elit', + ]); + :method:`Symfony\\Component\\Console\\Style\\SymfonyStyle::warning` It displays the given string or array of strings highlighted as a warning message (with a red background and the ``[WARNING]`` label). It's meant to be diff --git a/contributing/code/conventions.rst b/contributing/code/conventions.rst index 7a41d20cc7c..cd1d87b4282 100644 --- a/contributing/code/conventions.rst +++ b/contributing/code/conventions.rst @@ -146,40 +146,35 @@ A feature is marked as deprecated by adding a ``@deprecated`` PHPDoc to relevant classes, methods, properties, ...:: /** - * @deprecated since Symfony 2.8. + * @deprecated since Symfony 5.1. */ The deprecation message must indicate the version in which the feature was deprecated, and whenever possible, how it was replaced:: /** - * @deprecated since Symfony 2.8, use Replacement instead. + * @deprecated since Symfony 5.1, use Replacement instead. */ When the replacement is in another namespace than the deprecated class, its FQCN must be used:: /** - * @deprecated since Symfony 2.8, use A\B\Replacement instead. + * @deprecated since Symfony 5.1, use A\B\Replacement instead. */ -A PHP ``E_USER_DEPRECATED`` error must also be triggered to help people with the migration:: +A deprecation must also be triggered to help people with the migration +(requires the ``symfony/deprecation-contracts`` package):: - @trigger_error(sprintf('The "%s" class is deprecated since Symfony 2.8, use "%s" instead.', Deprecated::class, Replacement::class), E_USER_DEPRECATED); + trigger_deprecation('symfony/package-name', '5.1', 'The "%s" class is deprecated, use "%s" instead.', Deprecated::class, Replacement::class); -Without the `@-silencing operator`_, users would need to opt-out from deprecation -notices. Silencing swaps this behavior and allows users to opt-in when they are -ready to cope with them (by adding a custom error handler like the one used by -the Web Debug Toolbar or by the PHPUnit bridge). - -When deprecating a whole class the ``trigger_error()`` call should be placed -after the use declarations, like in this example from -`ServiceRouterLoader`_:: +When deprecating a whole class the ``trigger_deprecation()`` call should be placed +after the use declarations, like in this example from `ServiceRouterLoader`_:: namespace Symfony\Component\Routing\Loader\DependencyInjection; use Symfony\Component\Routing\Loader\ContainerLoader; - @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ServiceRouterLoader::class, ContainerLoader::class), E_USER_DEPRECATED); + trigger_deprecation('symfony/routing', '4.4', 'The "%s" class is deprecated, use "%s" instead.', ServiceRouterLoader::class, ContainerLoader::class); /** * @deprecated since Symfony 4.4, use Symfony\Component\Routing\Loader\ContainerLoader instead. @@ -237,8 +232,6 @@ to the ``CHANGELOG.md`` file of the impacted component: This task is mandatory and must be done in the same pull request. -.. _`@-silencing operator`: https://www.php.net/manual/en/language.operators.errorcontrol.php - Naming Commands and Options --------------------------- diff --git a/contributing/code/pull_requests.rst b/contributing/code/pull_requests.rst index 3d45dbd6c62..10365ca336e 100644 --- a/contributing/code/pull_requests.rst +++ b/contributing/code/pull_requests.rst @@ -31,7 +31,7 @@ Before working on Symfony, setup a friendly environment with the following software: * Git; -* PHP version 7.1.3 or above. +* PHP version 7.2.5 or above. Configure Git ~~~~~~~~~~~~~ @@ -129,7 +129,7 @@ work: ` (you may have to choose a higher branch if the feature you are fixing was introduced in a later version); -* ``5.x``, if you are adding a new feature. +* ``6.1``, if you are adding a new feature. The only exception is when a new :doc:`major Symfony version ` (5.0, 6.0, etc.) comes out every two years. Because of the @@ -152,7 +152,7 @@ topic branch: .. code-block:: terminal - $ git checkout -b BRANCH_NAME 5.x + $ git checkout -b BRANCH_NAME 6.1 Or, if you want to provide a bug fix for the ``4.4`` branch, first track the remote ``4.4`` branch locally: @@ -281,15 +281,15 @@ while to finish your changes): .. code-block:: terminal - $ git checkout 5.x + $ git checkout 6.1 $ git fetch upstream - $ git merge upstream/5.x + $ git merge upstream/6.1 $ git checkout BRANCH_NAME - $ git rebase 5.x + $ git rebase 6.1 .. tip:: - Replace ``5.x`` with the branch you selected previously (e.g. ``4.4``) + Replace ``6.1`` with the branch you selected previously (e.g. ``4.4``) if you are working on a bug fix. When doing the ``rebase`` command, you might have to fix merge conflicts. @@ -502,7 +502,7 @@ PR. Before re-submitting the PR, rebase with ``upstream/5.x`` or .. code-block:: terminal - $ git rebase -f upstream/5.x + $ git rebase -f upstream/6.1 $ git push --force origin BRANCH_NAME .. note:: diff --git a/contributing/code/reproducer.rst b/contributing/code/reproducer.rst index 771bd69eeac..6efae2a8ee8 100644 --- a/contributing/code/reproducer.rst +++ b/contributing/code/reproducer.rst @@ -65,8 +65,9 @@ to a route definition. Then, after creating your project: of controllers, actions, etc. as in your original application. #. Create a small controller and add your routing definition that shows the bug. #. Don't create or modify any other file. -#. Execute ``composer require symfony/web-server-bundle`` and use the ``server:run`` - command to browse to the new route and see if the bug appears or not. +#. Install the :doc:`local web server ` provided by Symfony + and use the ``symfony server:start`` command to browse to the new route and + see if the bug appears or not. #. If you can see the bug, you're done and you can already share the code with us. #. If you can't see the bug, you must keep making small changes. For example, if your original route was defined using XML, forget about the previous route diff --git a/contributing/code/standards.rst b/contributing/code/standards.rst index 134da5c1196..dff71281348 100644 --- a/contributing/code/standards.rst +++ b/contributing/code/standards.rst @@ -72,7 +72,7 @@ short example containing most features described below:: */ public function someDeprecatedMethod() { - @trigger_error(sprintf('The %s() method is deprecated since vendor-name/package-name 2.8 and will be removed in 3.0. Use Acme\Baz::someMethod() instead.', __METHOD__), E_USER_DEPRECATED); + trigger_deprecation('symfony/package-name', '5.1', 'The %s() method is deprecated, use Acme\Baz::someMethod() instead.', __METHOD__); return Baz::someMethod(); } @@ -187,10 +187,6 @@ Structure * Exception and error message strings must be concatenated using :phpfunction:`sprintf`; -* Calls to :phpfunction:`trigger_error` with type ``E_USER_DEPRECATED`` must be - switched to opt-in via ``@`` operator. - Read more at :ref:`contributing-code-conventions-deprecations`; - * Do not use ``else``, ``elseif``, ``break`` after ``if`` and ``case`` conditions which return or throw something; diff --git a/contributing/community/releases.rst b/contributing/community/releases.rst index 9a2e652ac48..6e993ea15d3 100644 --- a/contributing/community/releases.rst +++ b/contributing/community/releases.rst @@ -9,10 +9,10 @@ published through a *time-based model*: * A new **Symfony patch version** (e.g. 4.4.12, 5.1.9) comes out roughly every month. It only contains bug fixes, so you can safely upgrade your applications; -* A new **Symfony minor version** (e.g. 4.4, 5.0, 5.1) comes out every *six months*: +* A new **Symfony minor version** (e.g. 4.4, 5.1) comes out every *six months*: one in *May* and one in *November*. It contains bug fixes and new features, but it doesn't include any breaking change, so you can safely upgrade your applications; -* A new **Symfony major version** (e.g. 4.0, 5.0) comes out every *two years*. +* A new **Symfony major version** (e.g. 4.0, 5.0, 6.0) comes out every *two years*. It can contain breaking changes, so you may need to do some changes in your applications before upgrading. diff --git a/contributing/community/reviews.rst b/contributing/community/reviews.rst index bca36099eeb..ba08e4ffd36 100644 --- a/contributing/community/reviews.rst +++ b/contributing/community/reviews.rst @@ -150,7 +150,7 @@ Pick a pull request from the `PRs in need of review`_ and follow these steps: * Does the code break backward compatibility? If yes, does the PR header say so? * Does the PR contain deprecations? If yes, does the PR header say so? Does - the code contain ``trigger_error()`` statements for all deprecated + the code contain ``trigger_deprecation()`` statements for all deprecated features? * Are all deprecations and backward compatibility breaks documented in the latest UPGRADE-X.X.md file? Do those explanations contain "Before"/"After" diff --git a/contributing/documentation/format.rst b/contributing/documentation/format.rst index 1f6f1787918..c2b6d16fba3 100644 --- a/contributing/documentation/format.rst +++ b/contributing/documentation/format.rst @@ -104,6 +104,7 @@ Markup Format Use It to Display ``html+php`` PHP code blended with HTML ``ini`` INI ``php-annotations`` PHP Annotations +``php-attributes`` PHP Attributes =================== ====================================== Adding Links @@ -173,39 +174,39 @@ If you are documenting a brand new feature, a change or a deprecation that's been made in Symfony, you should precede your description of the change with the corresponding directive and a short description: -For a new feature or a behavior change use the ``.. versionadded:: 4.x`` +For a new feature or a behavior change use the ``.. versionadded:: 6.x`` directive: .. code-block:: rst - .. versionadded:: 4.2 + .. versionadded:: 6.2 - Named autowiring aliases have been introduced in Symfony 4.2. + ... ... ... was introduced in Symfony 6.2. If you are documenting a behavior change, it may be helpful to *briefly* describe how the behavior has changed: .. code-block:: rst - .. versionadded:: 4.2 + .. versionadded:: 6.2 - Support for ICU MessageFormat was introduced in Symfony 4.2. Prior to this, - pluralization was managed by the ``transChoice`` method. + ... ... ... was introduced in Symfony 6.2. Prior to this, + ... ... ... ... ... ... ... ... . -For a deprecation use the ``.. deprecated:: 4.x`` directive: +For a deprecation use the ``.. deprecated:: 6.x`` directive: .. code-block:: rst - .. deprecated:: 4.2 + .. deprecated:: 6.2 - Not passing the root node name to ``TreeBuilder`` was deprecated in Symfony 4.2. + ... ... ... was deprecated in Symfony 6.2. -Whenever a new major version of Symfony is released (e.g. 5.0, 6.0, etc), +Whenever a new major version of Symfony is released (e.g. 6.0, 7.0, etc), a new branch of the documentation is created from the ``master`` branch. At this point, all the ``versionadded`` and ``deprecated`` tags for Symfony versions that have a lower major version will be removed. For example, if -Symfony 5.0 were released today, 4.0 to 4.4 ``versionadded`` and ``deprecated`` -tags would be removed from the new ``5.0`` branch. +Symfony 6.0 were released today, 5.0 to 5.4 ``versionadded`` and ``deprecated`` +tags would be removed from the new ``6.0`` branch. .. _reStructuredText: https://docutils.sourceforge.io/rst.html .. _Sphinx: https://www.sphinx-doc.org/ diff --git a/contributing/documentation/standards.rst b/contributing/documentation/standards.rst index dc43258052e..8e266f68cab 100644 --- a/contributing/documentation/standards.rst +++ b/contributing/documentation/standards.rst @@ -191,7 +191,7 @@ In addition, documentation follows these rules: * trivial .. _`the Sphinx documentation`: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#literal-blocks -.. _`Twig Coding Standards`: https://twig.symfony.com/doc/2.x/coding_standards.html +.. _`Twig Coding Standards`: https://twig.symfony.com/doc/3.x/coding_standards.html .. _`reserved by the IANA`: https://tools.ietf.org/html/rfc2606#section-3 .. _`American English`: https://en.wikipedia.org/wiki/American_English .. _`American English Oxford Dictionary`: https://www.lexico.com/definition/american_english diff --git a/controller.rst b/controller.rst index e04bfd7a43d..a9a2a3adf99 100644 --- a/controller.rst +++ b/controller.rst @@ -400,7 +400,7 @@ Request object. Managing the Session -------------------- -Symfony provides a session service that you can use to store information +Symfony provides a session object that you can use to store information about the user between requests. Session is enabled by default, but will only be started if you read or write from it. diff --git a/controller/argument_value_resolver.rst b/controller/argument_value_resolver.rst index aaf1fa6d390..fe9a1a1b00d 100644 --- a/controller/argument_value_resolver.rst +++ b/controller/argument_value_resolver.rst @@ -49,20 +49,10 @@ In addition, some components and official bundles provide other value resolvers: :class:`Symfony\\Component\\Security\\Http\\Controller\\UserValueResolver` Injects the object that represents the current logged in user if type-hinted - with ``UserInterface``. Default value can be set to ``null`` in case - the controller can be accessed by anonymous users. It requires installing - the :doc:`Security component `. - -:class:`Symfony\\Bundle\\SecurityBundle\\SecurityUserValueResolver` - Injects the object that represents the current logged in user if type-hinted - with ``UserInterface``. Default value can be set to ``null`` in case - the controller can be accessed by anonymous users. It requires installing - the `SecurityBundle`_. - -.. deprecated:: 4.1 - - The ``SecurityUserValueResolver`` was deprecated in Symfony 4.1 in favor of - :class:`Symfony\\Component\\Security\\Http\\Controller\\UserValueResolver`. + with ``UserInterface``. You can also type-hint your own ``User`` class but you + must then add the ``#[CurrentUser]`` attribute to the argument. Default value + can be set to ``null`` in case the controller can be accessed by anonymous + users. It requires installing the :doc:`SecurityBundle `. Adding a Custom Value Resolver ------------------------------ @@ -81,7 +71,7 @@ with the ``User`` class:: { public function index(User $user) { - return new Response('Hello '.$user->getUsername().'!'); + return new Response('Hello '.$user->getUserIdentifier().'!'); } } @@ -233,11 +223,17 @@ and adding a priority. .. code-block:: php // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + use App\ArgumentResolver\UserValueResolver; - $container->autowire(UserValueResolver::class) - ->addTag('controller.argument_value_resolver', ['priority' => 50]) - ; + return static function (ContainerConfigurator $container) { + $services = $configurator->services(); + + $services->set(UserValueResolver::class) + ->tag('controller.argument_value_resolver', ['priority' => 50]) + ; + }; While adding a priority is optional, it's recommended to add one to make sure the expected value is injected. The built-in ``RequestAttributeValueResolver``, @@ -247,6 +243,13 @@ Otherwise, set a priority lower than ``100`` to make sure the argument resolver is not triggered when the ``Request`` attribute is present (for example, when passing the user along sub-requests). +To ensure your resolvers are added in the right position you can run the following +command to see which argument resolvers are present and in which order they run. + +.. code-block:: terminal + + $ php bin/console debug:container debug.argument_resolver.inner --show-arguments + .. tip:: As you can see in the ``UserValueResolver::supports()`` method, the user @@ -260,4 +263,3 @@ passing the user along sub-requests). .. _`@ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html .. _`yield`: https://www.php.net/manual/en/language.generators.syntax.php -.. _`SecurityBundle`: https://github.com/symfony/security-bundle diff --git a/controller/error_pages.rst b/controller/error_pages.rst index 3fe650054b8..a94294573a0 100644 --- a/controller/error_pages.rst +++ b/controller/error_pages.rst @@ -272,10 +272,12 @@ configuration option to point to it: .. code-block:: php // config/packages/framework.php - $container->loadFromExtension('framework', [ - 'error_controller' => 'App\Controller\ErrorController::show', + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { // ... - ]); + $framework->errorController('App\Controller\ErrorController::show'); + }; The :class:`Symfony\\Component\\HttpKernel\\EventListener\\ErrorListener` class used by the FrameworkBundle as a listener of the ``kernel.exception`` event creates @@ -316,8 +318,8 @@ error pages. .. note:: - If your listener calls ``setResponse()`` on the - :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent`, + If your listener calls ``setThrowable()`` on the + :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` event, propagation will be stopped and the response will be sent to the client. diff --git a/controller/service.rst b/controller/service.rst index 5d9fe9ade26..017b99c61c1 100644 --- a/controller/service.rst +++ b/controller/service.rst @@ -4,13 +4,29 @@ How to Define Controllers as Services ===================================== -In Symfony, a controller does *not* need to be registered as a service. But if you're -using the :ref:`default services.yaml configuration `, -your controllers *are* already registered as services. This means you can use dependency -injection like any other normal service. +In Symfony, a controller does *not* need to be registered as a service. But if +you're using the :ref:`default services.yaml configuration `, +and your controllers extend the `AbstractController`_ class, they *are* automatically +registered as services. This means you can use dependency injection like any +other normal service. + +If your controllers don't extend the `AbstractController`_ class, you must +explicitly mark your controller services as ``public``. Alternatively, you can +apply the ``controller.service_arguments`` tag to your controller services. This +will make the tagged services ``public`` and will allow you to inject services +in method parameters: -Referencing your Service from Routing -------------------------------------- +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + + # controllers are imported separately to make sure services can be injected + # as action arguments even if you don't extend any base controller class + App\Controller\: + resource: '../src/Controller/' + tags: ['controller.service_arguments'] Registering your controller as a service is the first step, but you also need to update your routing config to reference the service properly, so that Symfony @@ -41,6 +57,22 @@ a service like: ``App\Controller\HelloController::index``: } } + .. code-block:: php-attributes + + // src/Controller/HelloController.php + namespace App\Controller; + + use Symfony\Component\Routing\Annotation\Route; + + class HelloController + { + #[Route('/hello', name: 'hello', methods: ['GET'])] + public function index() + { + // ... + } + } + .. code-block:: yaml # config/routes.yaml @@ -105,6 +137,23 @@ which is a common practice when following the `ADR pattern`_ } } + .. code-block:: php-attributes + + // src/Controller/Hello.php + namespace App\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Annotation\Route; + + #[Route('/hello/{name}', name: 'hello')] + class Hello + { + public function __invoke($name = 'World') + { + return new Response(sprintf('Hello %s!', $name)); + } + } + .. code-block:: yaml # config/routes.yaml @@ -182,12 +231,11 @@ Base Controller Methods and Their Service Replacements ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The best way to see how to replace base ``Controller`` convenience methods is to -look at the `ControllerTrait`_ that holds its logic. +look at the `AbstractController`_ class that holds its logic. If you want to know what type-hints to use for each service, see the ``getSubscribedServices()`` method in `AbstractController`_. -.. _`Controller class source code`: https://github.com/symfony/symfony/blob/4.4/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php -.. _`ControllerTrait`: https://github.com/symfony/symfony/blob/4.4/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php +.. _`Controller class source code`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php .. _`AbstractController`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php .. _`ADR pattern`: https://en.wikipedia.org/wiki/Action%E2%80%93domain%E2%80%93responder diff --git a/controller/upload_file.rst b/controller/upload_file.rst index dad80ee957d..8f64fb10f80 100644 --- a/controller/upload_file.rst +++ b/controller/upload_file.rst @@ -129,13 +129,14 @@ Finally, you need to update the code of the controller that handles the form:: use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\String\Slugger\SluggerInterface; class ProductController extends AbstractController { /** * @Route("/product/new", name="app_product_new") */ - public function new(Request $request) + public function new(Request $request, SluggerInterface $slugger) { $product = new Product(); $form = $this->createForm(ProductType::class, $product); @@ -150,7 +151,7 @@ Finally, you need to update the code of the controller that handles the form:: if ($brochureFile) { $originalFilename = pathinfo($brochureFile->getClientOriginalName(), PATHINFO_FILENAME); // this is needed to safely include the file name as part of the URL - $safeFilename = transliterator_transliterate('Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename); + $safeFilename = $slugger->slug($originalFilename); $newFilename = $safeFilename.'-'.uniqid().'.'.$brochureFile->guessExtension(); // Move the file to the directory where brochures are stored @@ -173,8 +174,8 @@ Finally, you need to update the code of the controller that handles the form:: return $this->redirectToRoute('app_product_list'); } - return $this->render('product/new.html.twig', [ - 'form' => $form->createView(), + return $this->renderForm('product/new.html.twig', [ + 'form' => $form, ]); } } @@ -198,20 +199,14 @@ There are some important things to consider in the code of the above controller: #. A well-known security best practice is to never trust the input provided by users. This also applies to the files uploaded by your visitors. The ``UploadedFile`` class provides methods to get the original file extension - (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getExtension`), - the original file size (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientSize`) + (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientOriginalExtension`), + the original file size (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getSize`) and the original file name (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientOriginalName`). However, they are considered *not safe* because a malicious user could tamper that information. That's why it's always better to generate a unique name and use the :method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::guessExtension` method to let Symfony guess the right extension according to the file MIME type; -.. deprecated:: 4.1 - - The :method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientSize` - method was deprecated in Symfony 4.1 and will be removed in Symfony 5.0. - Use ``getSize()`` instead. - You can use the following code to link to the PDF brochure of a product: .. code-block:: html+twig @@ -244,20 +239,23 @@ logic to a separate service:: use Symfony\Component\HttpFoundation\File\Exception\FileException; use Symfony\Component\HttpFoundation\File\UploadedFile; + use Symfony\Component\String\Slugger\SluggerInterface; class FileUploader { private $targetDirectory; + private $slugger; - public function __construct($targetDirectory) + public function __construct($targetDirectory, SluggerInterface $slugger) { $this->targetDirectory = $targetDirectory; + $this->slugger = $slugger; } public function upload(UploadedFile $file) { $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME); - $safeFilename = transliterator_transliterate('Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename); + $safeFilename = $this->slugger->slug($originalFilename); $fileName = $safeFilename.'-'.uniqid().'.'.$file->guessExtension(); try { @@ -319,10 +317,17 @@ Then, define a service for this class: .. code-block:: php // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + use App\Service\FileUploader; - $container->autowire(FileUploader::class) - ->setArgument('$targetDirectory', '%brochures_directory%'); + return static function (ContainerConfigurator $container) { + $services = $configurator->services(); + + $services->set(FileUploader::class) + ->arg('$targetDirectory', '%brochures_directory%') + ; + }; Now you're ready to use this service in the controller:: diff --git a/create_framework/front_controller.rst b/create_framework/front_controller.rst index 733e764c94e..fded71a7b1c 100644 --- a/create_framework/front_controller.rst +++ b/create_framework/front_controller.rst @@ -38,7 +38,7 @@ Let's see it in action:: // framework/index.php require_once __DIR__.'/init.php'; - $name = $request->get('name', 'World'); + $name = $request->query->get('name', 'World'); $response->setContent(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8'))); $response->send(); @@ -98,7 +98,7 @@ Such a script might look like the following:: And here is for instance the new ``hello.php`` script:: // framework/hello.php - $name = $request->get('name', 'World'); + $name = $request->query->get('name', 'World'); $response->setContent(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8'))); In the ``front.php`` script, ``$map`` associates URL paths with their @@ -190,7 +190,7 @@ And the ``hello.php`` script can now be converted to a template: .. code-block:: html+php - get('name', 'World') ?> + query->get('name', 'World') ?> Hello diff --git a/create_framework/http_foundation.rst b/create_framework/http_foundation.rst index 14f4b00023b..dd838e9a5e2 100644 --- a/create_framework/http_foundation.rst +++ b/create_framework/http_foundation.rst @@ -141,7 +141,7 @@ Now, let's rewrite our application by using the ``Request`` and the $request = Request::createFromGlobals(); - $name = $request->get('name', 'World'); + $name = $request->query->get('name', 'World'); $response = new Response(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8'))); diff --git a/create_framework/http_kernel_httpkernelinterface.rst b/create_framework/http_kernel_httpkernelinterface.rst index 9bda9e5c731..29ddcc9c124 100644 --- a/create_framework/http_kernel_httpkernelinterface.rst +++ b/create_framework/http_kernel_httpkernelinterface.rst @@ -16,7 +16,7 @@ goal by making our framework implement ``HttpKernelInterface``:: */ public function handle( Request $request, - $type = self::MASTER_REQUEST, + $type = self::MAIN_REQUEST, $catch = true ); } @@ -39,7 +39,7 @@ Update your framework so that it implements this interface:: public function handle( Request $request, - $type = HttpKernelInterface::MASTER_REQUEST, + $type = HttpKernelInterface::MAIN_REQUEST, $catch = true ) { // ... @@ -118,22 +118,28 @@ The Response class contains methods that let you configure the HTTP cache. One of the most powerful is ``setCache()`` as it abstracts the most frequently used caching strategies into a single array:: - $date = date_create_from_format('Y-m-d H:i:s', '2005-10-15 10:00:00'); - $response->setCache([ - 'public' => true, - 'etag' => 'abcde', - 'last_modified' => $date, - 'max_age' => 10, - 's_maxage' => 10, + 'must_revalidate' => false, + 'no_cache' => false, + 'no_store' => false, + 'no_transform' => false, + 'public' => true, + 'private' => false, + 'proxy_revalidate' => false, + 'max_age' => 600, + 's_maxage' => 600, + 'immutable' => true, + 'last_modified' => new \DateTime(), + 'etag' => 'abcdef' ]); // it is equivalent to the following code $response->setPublic(); + $response->setMaxAge(600); + $response->setSharedMaxAge(600); + $response->setImmutable(); + $response->setLastModified(new \DateTime()); $response->setEtag('abcde'); - $response->setLastModified($date); - $response->setMaxAge(10); - $response->setSharedMaxAge(10); When using the validation model, the ``isNotModified()`` method allows you to cut on the response time by short-circuiting the response generation as early as diff --git a/create_framework/unit_testing.rst b/create_framework/unit_testing.rst index c3801481a3f..720e5f43e40 100644 --- a/create_framework/unit_testing.rst +++ b/create_framework/unit_testing.rst @@ -188,7 +188,7 @@ Response:: $response = $framework->handle(new Request()); $this->assertEquals(200, $response->getStatusCode()); - $this->assertContains('Yep, this is a leap year!', $response->getContent()); + $this->assertStringContainsString('Yep, this is a leap year!', $response->getContent()); } In this test, we simulate a route that matches and returns a simple diff --git a/deployment/proxies.rst b/deployment/proxies.rst index 285dd221b11..da0380be420 100644 --- a/deployment/proxies.rst +++ b/deployment/proxies.rst @@ -22,27 +22,71 @@ Solution: ``setTrustedProxies()`` --------------------------------- To fix this, you need to tell Symfony which reverse proxy IP addresses to trust -and what headers your reverse proxy uses to send information:: +and what headers your reverse proxy uses to send information: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + # the IP address (or range) of your proxy + trusted_proxies: '192.0.0.1,10.0.0.0/8' + # trust *all* "X-Forwarded-*" headers + trusted_headers: ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix'] + # or, if your proxy instead uses the "Forwarded" header + trusted_headers: ['forwarded'] + + .. code-block:: xml + + + + + + + + 192.0.0.1,10.0.0.0/8 + + + x-forwarded-for + x-forwarded-host + x-forwarded-proto + x-forwarded-port + x-forwarded-prefix + + + forwarded + + + + .. code-block:: php + + // config/packages/framework.php + use Symfony\Config\FrameworkConfig; + + return static function (FrameworkConfig $framework) { + $framework + // the IP address (or range) of your proxy + ->trustedProxies('192.0.0.1,10.0.0.0/8') + // trust *all* "X-Forwarded-*" headers (the ! prefix means to not trust those headers) + ->trustedHeaders(['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix']) + // or, if your proxy instead uses the "Forwarded" header + ->trustedHeaders(['forwarded']) + ; + }; - // public/index.php - - // ... - $request = Request::createFromGlobals(); - - // tell Symfony about your reverse proxy - Request::setTrustedProxies( - // the IP address (or range) of your proxy - ['192.0.0.1', '10.0.0.0/8'], - - // trust *all* "X-Forwarded-*" headers - Request::HEADER_X_FORWARDED_ALL - - // or, if your proxy instead uses the "Forwarded" header - // Request::HEADER_FORWARDED +.. caution:: - // or, if you're using AWS ELB - // Request::HEADER_X_FORWARDED_AWS_ELB - ); + Enabling the ``Request::HEADER_X_FORWARDED_HOST`` option exposes the + application to `HTTP Host header attacks`_. Make sure the proxy really + sends an ``x-forwarded-host`` header. The Request object has several ``Request::HEADER_*`` constants that control exactly *which* headers from your reverse proxy are trusted. The argument is a bit field, @@ -64,23 +108,16 @@ In this case, you'll need to - *very carefully* - trust *all* proxies. other than your load balancers. For AWS, this can be done with `security groups`_. #. Once you've guaranteed that traffic will only come from your trusted reverse - proxies, configure Symfony to *always* trust incoming request:: - - // public/index.php - - // ... - Request::setTrustedProxies( - // trust *all* requests (the 'REMOTE_ADDR' string is replaced at - // run time by $_SERVER['REMOTE_ADDR']) - ['127.0.0.1', 'REMOTE_ADDR'], + proxies, configure Symfony to *always* trust incoming request: - // if you're using ELB, otherwise use a constant from above - Request::HEADER_X_FORWARDED_AWS_ELB - ); + .. code-block:: yaml -.. versionadded:: 4.4 - - The support for the ``REMOTE_ADDR`` option was introduced in Symfony 4.4. + # config/packages/framework.yaml + framework: + # ... + # trust *all* requests (the 'REMOTE_ADDR' string is replaced at + # run time by $_SERVER['REMOTE_ADDR']) + trusted_proxies: '127.0.0.1,REMOTE_ADDR' That's it! It's critical that you prevent traffic from all non-trusted sources. If you allow outside traffic, they could "spoof" their true IP address and @@ -96,6 +133,12 @@ other information. # .env TRUSTED_PROXIES=127.0.0.1,REMOTE_ADDR + .. code-block:: yaml + + # config/packages/framework.yaml + framework: + # ... + trusted_proxies: '%env(TRUSTED_PROXIES)%' If you are also using a reverse proxy on top of your load balancer (e.g. `CloudFront`_), calling ``$request->server->get('REMOTE_ADDR')`` won't be @@ -107,11 +150,13 @@ trusted proxies. Custom Headers When Using a Reverse Proxy ----------------------------------------- -Some reverse proxies (like `CloudFront`_ with ``CloudFront-Forwarded-Proto``) may force you to use a custom header. -For instance you have ``Custom-Forwarded-Proto`` instead of ``X-Forwarded-Proto``. +Some reverse proxies (like `CloudFront`_ with ``CloudFront-Forwarded-Proto``) +may force you to use a custom header. For instance you have +``Custom-Forwarded-Proto`` instead of ``X-Forwarded-Proto``. -In this case, you'll need to set the header ``X-Forwarded-Proto`` with the value of -``Custom-Forwarded-Proto`` early enough in your application, i.e. before handling the request:: +In this case, you'll need to set the header ``X-Forwarded-Proto`` with the value +of ``Custom-Forwarded-Proto`` early enough in your application, i.e. before +handling the request:: // public/index.php @@ -123,4 +168,5 @@ In this case, you'll need to set the header ``X-Forwarded-Proto`` with the value .. _`security groups`: https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-groups.html .. _`CloudFront`: https://en.wikipedia.org/wiki/Amazon_CloudFront .. _`CloudFront IP ranges`: https://ip-ranges.amazonaws.com/ip-ranges.json +.. _`HTTP Host header attacks`: https://www.skeletonscribe.net/2013/05/practical-http-host-header-attacks.html .. _`nginx realip module`: http://nginx.org/en/docs/http/ngx_http_realip_module.html diff --git a/doctrine.rst b/doctrine.rst index 46770aa35b5..2d787cf1a80 100644 --- a/doctrine.rst +++ b/doctrine.rst @@ -233,9 +233,11 @@ already installed: If everything worked, you should see something like this: +.. code-block:: text + SUCCESS! - Next: Review the new migration "src/Migrations/Version20180207231217.php" + Next: Review the new migration "migrations/Version20211116204726.php" Then: Run the migration with php bin/console doctrine:migrations:migrate If you open this file, it contains the SQL needed to update your database! To run @@ -359,7 +361,7 @@ and save it:: // ... use App\Entity\Product; - use Doctrine\ORM\EntityManagerInterface; + use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\HttpFoundation\Response; class ProductController extends AbstractController @@ -367,11 +369,9 @@ and save it:: /** * @Route("/product", name="create_product") */ - public function createProduct(): Response + public function createProduct(ManagerRegistry $doctrine): Response { - // you can fetch the EntityManager via $this->getDoctrine() - // or you can add an argument to the action: createProduct(EntityManagerInterface $entityManager) - $entityManager = $this->getDoctrine()->getManager(); + $entityManager = $doctrine->getManager(); $product = new Product(); $product->setName('Keyboard'); @@ -397,26 +397,30 @@ you can query the database directly: .. code-block:: terminal - $ php bin/console doctrine:query:sql 'SELECT * FROM product' + $ php bin/console dbal:run-sql 'SELECT * FROM product' # on Windows systems not using Powershell, run this command instead: - # php bin/console doctrine:query:sql "SELECT * FROM product" + # php bin/console dbal:run-sql "SELECT * FROM product" Take a look at the previous example in more detail: .. _doctrine-entity-manager: -* **line 18** The ``$this->getDoctrine()->getManager()`` method gets Doctrine's +* **line 14** The ``ManagerRegistry $doctrine`` argument tells Symfony to + :ref:`inject the Doctrine service ` into the + controller method. + +* **line 16** The ``$doctrine->getManager()`` method gets Doctrine's *entity manager* object, which is the most important object in Doctrine. It's responsible for saving objects to, and fetching objects from, the database. -* **lines 20-23** In this section, you instantiate and work with the ``$product`` +* **lines 18-21** In this section, you instantiate and work with the ``$product`` object like any other normal PHP object. -* **line 26** The ``persist($product)`` call tells Doctrine to "manage" the +* **line 24** The ``persist($product)`` call tells Doctrine to "manage" the ``$product`` object. This does **not** cause a query to be made to the database. -* **line 29** When the ``flush()`` method is called, Doctrine looks through +* **line 27** When the ``flush()`` method is called, Doctrine looks through all of the objects that it's managing to see if they need to be persisted to the database. In this example, the ``$product`` object's data doesn't exist in the database, so the entity manager executes an ``INSERT`` query, @@ -498,10 +502,6 @@ doesn't replace the validation configuration entirely. You still need to add some :doc:`validation constraints ` to ensure that data provided by the user is correct. -.. versionadded:: 4.3 - - The automatic validation has been added in Symfony 4.3. - Fetching Objects from the Database ---------------------------------- @@ -520,11 +520,9 @@ be able to go to ``/product/1`` to see your new product:: /** * @Route("/product/{id}", name="product_show") */ - public function show(int $id): Response + public function show(ManagerRegistry $doctrine, int $id): Response { - $product = $this->getDoctrine() - ->getRepository(Product::class) - ->find($id); + $product = $doctrine->getRepository(Product::class)->find($id); if (!$product) { throw $this->createNotFoundException( @@ -575,7 +573,7 @@ job is to help you fetch entities of a certain class. Once you have a repository object, you have many helper methods:: - $repository = $this->getDoctrine()->getRepository(Product::class); + $repository = $doctrine->getRepository(Product::class); // look for a single Product by its primary key (usually "id") $product = $repository->find($id); @@ -671,9 +669,9 @@ with any PHP model:: /** * @Route("/product/edit/{id}") */ - public function update(int $id): Response + public function update(ManagerRegistry $doctrine, int $id): Response { - $entityManager = $this->getDoctrine()->getManager(); + $entityManager = $doctrine->getManager(); $product = $entityManager->getRepository(Product::class)->find($id); if (!$product) { @@ -722,8 +720,7 @@ You've already seen how the repository object allows you to run basic queries without any work:: // from inside a controller - $repository = $this->getDoctrine()->getRepository(Product::class); - + $repository = $doctrine->getRepository(Product::class); $product = $repository->find($id); But what if you need a more complex query? When you generated your entity with @@ -790,9 +787,7 @@ Now, you can call this method on the repository:: // from inside a controller $minPrice = 1000; - $products = $this->getDoctrine() - ->getRepository(Product::class) - ->findAllGreaterThanPrice($minPrice); + $products = $doctrine->getRepository(Product::class)->findAllGreaterThanPrice($minPrice); // ... diff --git a/doctrine/associations.rst b/doctrine/associations.rst index a3c138c008f..470e48059f2 100644 --- a/doctrine/associations.rst +++ b/doctrine/associations.rst @@ -171,6 +171,32 @@ the ``Product`` entity (and getter & setter methods): } } + .. code-block:: php-attributes + + // src/Entity/Product.php + namespace App\Entity; + + // ... + class Product + { + // ... + + #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: "products")] + private $category; + + public function getCategory(): ?Category + { + return $this->category; + } + + public function setCategory(?Category $category): self + { + $this->category = $category; + + return $this; + } + } + .. code-block:: yaml # src/Resources/config/doctrine/Product.orm.yml @@ -248,6 +274,38 @@ class that will hold these objects: // addProduct() and removeProduct() were also added } + .. code-block:: php-attributes + + // src/Entity/Category.php + namespace App\Entity; + + // ... + use Doctrine\Common\Collections\ArrayCollection; + use Doctrine\Common\Collections\Collection; + + class Category + { + // ... + + #[ORM\OneToMany(targetEntity: Product::class, mappedBy: "category")] + private $products; + + public function __construct() + { + $this->products = new ArrayCollection(); + } + + /** + * @return Collection|Product[] + */ + public function getProducts(): Collection + { + return $this->products; + } + + // addProduct() and removeProduct() were also added + } + .. code-block:: yaml # src/Resources/config/doctrine/Category.orm.yml @@ -320,6 +378,7 @@ Now you can see this new code in action! Imagine you're inside a controller:: // ... use App\Entity\Category; use App\Entity\Product; + use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\HttpFoundation\Response; class ProductController extends AbstractController @@ -327,7 +386,7 @@ Now you can see this new code in action! Imagine you're inside a controller:: /** * @Route("/product", name="product") */ - public function index(): Response + public function index(ManagerRegistry $doctrine): Response { $category = new Category(); $category->setName('Computer Peripherals'); @@ -340,7 +399,7 @@ Now you can see this new code in action! Imagine you're inside a controller:: // relates this product to the category $product->setCategory($category); - $entityManager = $this->getDoctrine()->getManager(); + $entityManager = $doctrine->getManager(); $entityManager->persist($category); $entityManager->persist($product); $entityManager->flush(); @@ -386,12 +445,9 @@ before. First, fetch a ``$product`` object and then access its related class ProductController extends AbstractController { - public function show(int $id): Response + public function show(ManagerRegistry $doctrine, int $id): Response { - $product = $this->getDoctrine() - ->getRepository(Product::class) - ->find($id); - + $product = $doctrine->getRepository(Product::class)->find($id); // ... $categoryName = $product->getCategory()->getName(); @@ -422,11 +478,9 @@ direction:: // ... class ProductController extends AbstractController { - public function showProducts(int $id): Response + public function showProducts(ManagerRegistry $doctrine, int $id): Response { - $category = $this->getDoctrine() - ->getRepository(Category::class) - ->find($id); + $category = $doctrine->getRepository(Category::class)->find($id); $products = $category->getProducts(); @@ -445,9 +499,7 @@ by adding JOINs. a "proxy" object in place of the true object. Look again at the above example:: - $product = $this->getDoctrine() - ->getRepository(Product::class) - ->find($id); + $product = $doctrine->getRepository(Product::class)->find($id); $category = $product->getCategory(); @@ -517,11 +569,9 @@ object and its related ``Category`` in one query:: // ... class ProductController extends AbstractController { - public function show(int $id): Response + public function show(ManagerRegistry $doctrine, int $id): Response { - $product = $this->getDoctrine() - ->getRepository(Product::class) - ->findOneByIdJoinedToCategory($id); + $product = $doctrine->getRepository(Product::class)->findOneByIdJoinedToCategory($id); $category = $product->getCategory(); @@ -597,16 +647,30 @@ on that ``Product`` will be set to ``null`` in the database. But, instead of setting the ``category_id`` to null, what if you want the ``Product`` to be *deleted* if it becomes "orphaned" (i.e. without a ``Category``)? To choose -that behavior, use the `orphanRemoval`_ option inside ``Category``:: +that behavior, use the `orphanRemoval`_ option inside ``Category``: - // src/Entity/Category.php +.. configuration-block:: - // ... + .. code-block:: php-annotations + + // src/Entity/Category.php + + // ... + + /** + * @ORM\OneToMany(targetEntity="App\Entity\Product", mappedBy="category", orphanRemoval=true) + */ + private $products; + + .. code-block:: php-attributes + + // src/Entity/Category.php + + // ... + + #[ORM\OneToMany(targetEntity: Product::class, mappedBy: "category", orphanRemoval: true)] + private $products; - /** - * @ORM\OneToMany(targetEntity="App\Entity\Product", mappedBy="category", orphanRemoval=true) - */ - private $products; Thanks to this, if the ``Product`` is removed from the ``Category``, it will be removed from the database entirely. diff --git a/doctrine/custom_dql_functions.rst b/doctrine/custom_dql_functions.rst index a6a47d4a18e..1d6a03fa597 100644 --- a/doctrine/custom_dql_functions.rst +++ b/doctrine/custom_dql_functions.rst @@ -57,24 +57,19 @@ In Symfony, you can register your custom DQL functions as follows: use App\DQL\NumericFunction; use App\DQL\SecondStringFunction; use App\DQL\StringFunction; + use Symfony\Config\DoctrineConfig; - $container->loadFromExtension('doctrine', [ - 'orm' => [ - // ... - 'dql' => [ - 'string_functions' => [ - 'test_string' => StringFunction::class, - 'second_string' => SecondStringFunction::class, - ], - 'numeric_functions' => [ - 'test_numeric' => NumericFunction::class, - ], - 'datetime_functions' => [ - 'test_datetime' => DatetimeFunction::class, - ], - ], - ], - ]); + return static function (DoctrineConfig $doctrine) { + $defaultDql = $doctrine->orm() + ->entityManager('default') + // ... + ->dql(); + + $defaultDql->stringFunction('test_string', StringFunction::class); + $defaultDql->stringFunction('second_string', SecondStringFunction::class); + $defaultDql->numericFunction('test_numeric', NumericFunction::class); + $defaultDql->datetimeFunction('test_datetime', DatetimeFunction::class); + }; .. note:: @@ -129,21 +124,15 @@ In Symfony, you can register your custom DQL functions as follows: // config/packages/doctrine.php use App\DQL\DatetimeFunction; + use Symfony\Config\DoctrineConfig; - $container->loadFromExtension('doctrine', [ - 'orm' => [ + return static function (DoctrineConfig $doctrine) { + $doctrine->orm() // ... - 'entity_managers' => [ - 'example_manager' => [ - // place your functions here - 'dql' => [ - 'datetime_functions' => [ - 'test_datetime' => DatetimeFunction::class, - ], - ], - ], - ], - ], - ]); + ->entityManager('example_manager') + // place your functions here + ->dql() + ->datetimeFunction('test_datetime', DatetimeFunction::class); + }; .. _`DQL User Defined Functions`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/cookbook/dql-user-defined-functions.html diff --git a/doctrine/dbal.rst b/doctrine/dbal.rst index ab1947bd1bb..a9d04674163 100644 --- a/doctrine/dbal.rst +++ b/doctrine/dbal.rst @@ -47,7 +47,7 @@ object:: // src/Controller/UserController.php namespace App\Controller; - use Doctrine\DBAL\Driver\Connection; + use Doctrine\DBAL\Connection; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; @@ -105,15 +105,13 @@ mapping types, read Doctrine's `Custom Mapping Types`_ section of their document // config/packages/doctrine.php use App\Type\CustomFirst; use App\Type\CustomSecond; + use Symfony\Config\DoctrineConfig; - $container->loadFromExtension('doctrine', [ - 'dbal' => [ - 'types' => [ - 'custom_first' => CustomFirst::class, - 'custom_second' => CustomSecond::class, - ], - ], - ]); + return static function (DoctrineConfig $doctrine) { + $dbal = $doctrine->dbal(); + $dbal->type('custom_first')->class(CustomFirst::class); + $dbal->type('custom_second')->class(CustomSecond::class); + }; Registering custom Mapping Types in the SchemaTool -------------------------------------------------- @@ -156,13 +154,13 @@ mapping type: .. code-block:: php // config/packages/doctrine.php - $container->loadFromExtension('doctrine', [ - 'dbal' => [ - 'mapping_types' => [ - 'enum' => 'string', - ], - ], - ]); + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine) { + $dbalDefault = $doctrine->dbal() + ->connection('default'); + $dbalDefault->mappingType('enum', 'string'); + }; .. _`PDO`: https://www.php.net/pdo .. _`Doctrine`: https://www.doctrine-project.org/ diff --git a/doctrine/events.rst b/doctrine/events.rst index b2a76b6ac56..dc3df4b6e20 100644 --- a/doctrine/events.rst +++ b/doctrine/events.rst @@ -166,7 +166,7 @@ with the ``doctrine.event_listener`` tag: # this is the only required option for the lifecycle listener tag event: 'postPersist' - # listeners can define their priority in case multiple listeners are associated + # listeners can define their priority in case multiple subscribers or listeners are associated # to the same event (default priority = 0; higher numbers = listener is run earlier) priority: 500 @@ -184,7 +184,7 @@ with the ``doctrine.event_listener`` tag: @@ -200,22 +200,28 @@ with the ``doctrine.event_listener`` tag: .. code-block:: php // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + use App\EventListener\SearchIndexer; - // listeners are applied by default to all Doctrine connections - $container->autowire(SearchIndexer::class) - ->addTag('doctrine.event_listener', [ - // this is the only required option for the lifecycle listener tag - 'event' => 'postPersist', + return static function (ContainerConfigurator $container) { + $services = $configurator->services(); + + // listeners are applied by default to all Doctrine connections + $services->set(SearchIndexer::class) + ->tag('doctrine.event_listener', [ + // this is the only required option for the lifecycle listener tag + 'event' => 'postPersist', - // listeners can define their priority in case multiple listeners are associated - // to the same event (default priority = 0; higher numbers = listener is run earlier) - 'priority' => 500, + // listeners can define their priority in case multiple subscribers or listeners are associated + // to the same event (default priority = 0; higher numbers = listener is run earlier) + 'priority' => 500, - # you can also restrict listeners to a specific Doctrine connection - 'connection' => 'default', - ]) - ; + # you can also restrict listeners to a specific Doctrine connection + 'connection' => 'default', + ]) + ; + }; .. tip:: @@ -223,6 +229,11 @@ with the ``doctrine.event_listener`` tag: Doctrine event is actually fired; whereas Doctrine subscribers are always loaded (and instantiated) by Symfony, making them less performant. +.. tip:: + + The value of the ``connection`` option can also be a + :ref:`configuration parameter `. + Doctrine Entity Listeners ------------------------- @@ -316,33 +327,35 @@ with the ``doctrine.orm.entity_listener`` tag: .. code-block:: php // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + use App\Entity\User; use App\EventListener\UserChangedNotifier; - $container->autowire(UserChangedNotifier::class) - ->addTag('doctrine.orm.entity_listener', [ - // These are the options required to define the entity listener: - 'event' => 'postUpdate', - 'entity' => User::class, + return static function (ContainerConfigurator $container) { + $services = $configurator->services(); - // These are other options that you may define if needed: + $services->set(UserChangedNotifier::class) + ->tag('doctrine.orm.entity_listener', [ + // These are the options required to define the entity listener: + 'event' => 'postUpdate', + 'entity' => User::class, - // set the 'lazy' option to TRUE to only instantiate listeners when they are used - // 'lazy' => true, + // These are other options that you may define if needed: - // set the 'entity_manager' option if the listener is not associated to the default manager - // 'entity_manager' => 'custom', + // set the 'lazy' option to TRUE to only instantiate listeners when they are used + // 'lazy' => true, - // by default, Symfony looks for a method called after the event (e.g. postUpdate()) - // if it doesn't exist, it tries to execute the '__invoke()' method, but you can - // configure a custom method name with the 'method' option - // 'method' => 'checkUserChanges', - ]) - ; + // set the 'entity_manager' option if the listener is not associated to the default manager + // 'entity_manager' => 'custom', -.. versionadded:: 4.4 - - Support for invokable listeners (using the ``__invoke()`` method) was introduced in Symfony 4.4. + // by default, Symfony looks for a method called after the event (e.g. postUpdate()) + // if it doesn't exist, it tries to execute the '__invoke()' method, but you can + // configure a custom method name with the 'method' option + // 'method' => 'checkUserChanges', + ]) + ; + }; Doctrine Lifecycle Subscribers ------------------------------ @@ -411,8 +424,8 @@ and DoctrineBundle 2.1 (released May 25, 2020) or newer, this example will alrea work! Otherwise, :ref:`create a service ` for this subscriber and :doc:`tag it ` with ``doctrine.event_subscriber``. -If you need to associate the subscriber with a specific Doctrine connection, you -must do that in the manual service configuration: +If you need to configure some option of the subscriber (e.g. its priority or +Doctrine connection to use) you must do that in the manual service configuration: .. configuration-block:: @@ -424,7 +437,14 @@ must do that in the manual service configuration: App\EventListener\DatabaseActivitySubscriber: tags: - - { name: 'doctrine.event_subscriber', connection: 'default' } + - name: 'doctrine.event_subscriber' + + # subscribers can define their priority in case multiple subscribers or listeners are associated + # to the same event (default priority = 0; higher numbers = listener is run earlier) + priority: 500 + + # you can also restrict listeners to a specific Doctrine connection + connection: 'default' .. code-block:: xml @@ -435,8 +455,13 @@ must do that in the manual service configuration: + - + @@ -444,11 +469,24 @@ must do that in the manual service configuration: .. code-block:: php // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + use App\EventListener\DatabaseActivitySubscriber; - $container->autowire(DatabaseActivitySubscriber::class) - ->addTag('doctrine.event_subscriber', ['connection' => 'default']) - ; + return static function (ContainerConfigurator $container) { + $services = $configurator->services(); + + $services->set(DatabaseActivitySubscriber::class) + ->tag('doctrine.event_subscriber'[ + // subscribers can define their priority in case multiple subscribers or listeners are associated + // to the same event (default priority = 0; higher numbers = listener is run earlier) + 'priority' => 500, + + // you can also restrict listeners to a specific Doctrine connection + 'connection' => 'default', + ]) + ; + }; .. tip:: diff --git a/doctrine/multiple_entity_managers.rst b/doctrine/multiple_entity_managers.rst index c609ad1291b..856e796a2e9 100644 --- a/doctrine/multiple_entity_managers.rst +++ b/doctrine/multiple_entity_managers.rst @@ -128,57 +128,47 @@ The following configuration code shows how you can configure two entity managers .. code-block:: php // config/packages/doctrine.php - $container->loadFromExtension('doctrine', [ - 'dbal' => [ - 'default_connection' => 'default', - 'connections' => [ - // configure these for your database server - 'default' => [ - 'url' => '%env(resolve:DATABASE_URL)%', - 'driver' => 'pdo_mysql', - 'server_version' => '5.7', - 'charset' => 'utf8mb4', - ], - // configure these for your database server - 'customer' => [ - 'url' => '%env(resolve:DATABASE_CUSTOMER_URL)%', - 'driver' => 'pdo_mysql', - 'server_version' => '5.7', - 'charset' => 'utf8mb4', - ], - ], - ], - - 'orm' => [ - 'default_entity_manager' => 'default', - 'entity_managers' => [ - 'default' => [ - 'connection' => 'default', - 'mappings' => [ - 'Main' => [ - 'is_bundle' => false, - 'type' => 'annotation', - 'dir' => '%kernel.project_dir%/src/Entity/Main', - 'prefix' => 'App\Entity\Main', - 'alias' => 'Main', - ], - ], - ], - 'customer' => [ - 'connection' => 'customer', - 'mappings' => [ - 'Customer' => [ - 'is_bundle' => false, - 'type' => 'annotation', - 'dir' => '%kernel.project_dir%/src/Entity/Customer', - 'prefix' => 'App\Entity\Customer', - 'alias' => 'Customer', - ], - ], - ], - ], - ], - ]); + use Symfony\Config\DoctrineConfig; + + return static function (DoctrineConfig $doctrine) { + $doctrine->dbal()->defaultConnection('default'); + + // configure these for your database server + $doctrine->dbal() + ->connection('default') + ->url(env('DATABASE_URL')->resolve()) + ->driver('pdo_mysql') + ->serverVersion('5.7') + ->charset('utf8mb4'); + + // configure these for your database server + $doctrine->dbal() + ->connection('customer') + ->url(env('DATABASE_CUSTOMER_URL')->resolve()) + ->driver('pdo_mysql') + ->serverVersion('5.7') + ->charset('utf8mb4'); + + $doctrine->orm()->defaultEntityManager('default'); + $emDefault = $doctrine->orm()->entityManager('default'); + $emDefault->connection('default'); + $emDefault->mapping('Main') + ->isBundle(false) + ->type('annotation') + ->dir('%kernel.project_dir%/src/Entity/Main') + ->prefix('App\Entity\Main') + ->alias('Main'); + + $emCustomer = $doctrine->orm()->entityManager('customer'); + $emCustomer->connection('customer'); + $emCustomer->mapping('Customer') + ->isBundle(false) + ->type('annotation') + ->dir('%kernel.project_dir%/src/Entity/Customer') + ->prefix('App\Entity\Customer') + ->alias('Customer') + ; + }; In this case, you've defined two entity managers and called them ``default`` and ``customer``. The ``default`` entity manager manages entities in the @@ -193,8 +183,8 @@ for each entity manager, but you are free to define the same connection for both the connection or entity manager, the default (i.e. ``default``) is used. If you use a different name than ``default`` for the default entity manager, - you will need to redefine the default entity manager in ``prod`` environment - configuration too: + you will need to redefine the default entity manager in the ``prod`` environment + configuration and in the Doctrine migrations configuration (if you use that): .. code-block:: yaml @@ -205,6 +195,13 @@ for each entity manager, but you are free to define the same connection for both # ... + .. code-block:: yaml + + # config/packages/doctrine_migrations.yaml + doctrine_migrations: + # ... + em: 'your default entity manager name' + When working with multiple connections to create your databases: .. code-block:: terminal @@ -235,20 +232,18 @@ the default entity manager (i.e. ``default``) is returned:: // ... use Doctrine\ORM\EntityManagerInterface; + use Doctrine\Persistence\ManagerRegistry; class UserController extends AbstractController { - public function index(EntityManagerInterface $entityManager): Response + public function index(ManagerRegistry $doctrine): Response { - // These methods also return the default entity manager, but it's preferred - // to get it by injecting EntityManagerInterface in the action method - $entityManager = $this->getDoctrine()->getManager(); - $entityManager = $this->getDoctrine()->getManager('default'); - $entityManager = $this->get('doctrine.orm.default_entity_manager'); + // Both methods return the default entity manager + $entityManager = $doctrine->getManager(); + $entityManager = $doctrine->getManager('default'); - // Both of these return the "customer" entity manager - $customerEntityManager = $this->getDoctrine()->getManager('customer'); - $customerEntityManager = $this->get('doctrine.orm.customer_entity_manager'); + // This method returns instead the "customer" entity manager + $customerEntityManager = $doctrine->getManager('customer'); // ... } @@ -270,29 +265,21 @@ The same applies to repository calls:: use AcmeStoreBundle\Entity\Customer; use AcmeStoreBundle\Entity\Product; + use Doctrine\Persistence\ManagerRegistry; // ... class UserController extends AbstractController { - public function index(): Response + public function index(ManagerRegistry $doctrine): Response { - // Retrieves a repository managed by the "default" em - $products = $this->getDoctrine() - ->getRepository(Product::class) - ->findAll() - ; + // Retrieves a repository managed by the "default" entity manager + $products = $doctrine->getRepository(Product::class)->findAll(); - // Explicit way to deal with the "default" em - $products = $this->getDoctrine() - ->getRepository(Product::class, 'default') - ->findAll() - ; + // Explicit way to deal with the "default" entity manager + $products = $doctrine->getRepository(Product::class, 'default')->findAll(); - // Retrieves a repository managed by the "customer" em - $customers = $this->getDoctrine() - ->getRepository(Customer::class, 'customer') - ->findAll() - ; + // Retrieves a repository managed by the "customer" entity manager + $customers = $doctrine->getRepository(Customer::class, 'customer')->findAll(); // ... } diff --git a/doctrine/registration_form.rst b/doctrine/registration_form.rst index 841e1960512..cf530a041e0 100644 --- a/doctrine/registration_form.rst +++ b/doctrine/registration_form.rst @@ -14,7 +14,7 @@ form you must: #. :doc:`Create a form ` to ask for the registration information (you can generate this with the ``make:registration-form`` command provided by the `MakerBundle`_); #. Create :doc:`a controller ` to :ref:`process the form `; -#. :ref:`Protect some parts of your application ` so that - only registered users can access them. +#. :ref:`Protect some parts of your application ` so that + only registered users can access to them. .. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html diff --git a/doctrine/resolve_target_entity.rst b/doctrine/resolve_target_entity.rst index 9be8730ba4a..6c1569d411e 100644 --- a/doctrine/resolve_target_entity.rst +++ b/doctrine/resolve_target_entity.rst @@ -139,15 +139,13 @@ about the replacement: // config/packages/doctrine.php use App\Entity\Customer; use App\Model\InvoiceSubjectInterface; + use Symfony\Config\DoctrineConfig; - $container->loadFromExtension('doctrine', [ - 'orm' => [ - // ... - 'resolve_target_entities' => [ - InvoiceSubjectInterface::class => Customer::class, - ], - ], - ]); + return static function (DoctrineConfig $doctrine) { + $orm = $doctrine->orm(); + // ... + $orm->resolveTargetEntity(InvoiceSubjectInterface::class, Customer::class); + }; Final Thoughts -------------- diff --git a/email.rst b/email.rst deleted file mode 100644 index 6f54812729a..00000000000 --- a/email.rst +++ /dev/null @@ -1,669 +0,0 @@ -.. index:: - single: Emails - -Swift Mailer -============ - -.. caution:: - - In Symfony 4.3, the :doc:`Mailer ` component was introduced and should - be used instead of Swift Mailer as it won't be maintained anymore as of November - 2021. - -Symfony provides a mailer feature based on the popular `Swift Mailer`_ library -via the `SwiftMailerBundle`_. This mailer supports sending messages with your -own mail servers as well as using popular email providers like `Mandrill`_, -`SendGrid`_, and `Amazon SES`_. - -Installation ------------- - -In applications using :ref:`Symfony Flex `, run this command to -install the Swift Mailer based mailer before using it: - -.. code-block:: terminal - - $ composer require symfony/swiftmailer-bundle - -If your application doesn't use Symfony Flex, follow the installation -instructions on `SwiftMailerBundle`_. - -.. _swift-mailer-configuration: - -Configuration -------------- - -The ``config/packages/swiftmailer.yaml`` file that's created when installing the -mailer provides all the initial config needed to send emails, except your mail -server connection details. Those parameters are defined in the ``MAILER_URL`` -environment variable in the ``.env`` file: - -.. code-block:: bash - - # .env (or override MAILER_URL in .env.local to avoid committing your changes) - - # use this to disable email delivery - MAILER_URL=null://localhost - - # use this to configure a traditional SMTP server - MAILER_URL=smtp://localhost:465?encryption=ssl&auth_mode=login&username=&password= - -.. caution:: - - If the username, password or host contain any character considered special in a - URI (such as ``+``, ``@``, ``$``, ``#``, ``/``, ``:``, ``*``, ``!``), you must - encode them. See `RFC 3986`_ for the full list of reserved characters or use the - :phpfunction:`urlencode` function to encode them. - -Refer to the :doc:`SwiftMailer configuration reference ` -for the detailed explanation of all the available config options. - -Sending Emails --------------- - -The Swift Mailer library works by creating, configuring and then sending -``Swift_Message`` objects. The "mailer" is responsible for the actual delivery -of the message and is accessible via the ``Swift_Mailer`` service. Overall, -sending an email is pretty straightforward:: - - public function index($name, \Swift_Mailer $mailer) - { - $message = (new \Swift_Message('Hello Email')) - ->setFrom('send@example.com') - ->setTo('recipient@example.com') - ->setBody( - $this->renderView( - // templates/emails/registration.html.twig - 'emails/registration.html.twig', - ['name' => $name] - ), - 'text/html' - ) - - // you can remove the following code if you don't define a text version for your emails - ->addPart( - $this->renderView( - // templates/emails/registration.txt.twig - 'emails/registration.txt.twig', - ['name' => $name] - ), - 'text/plain' - ) - ; - - $mailer->send($message); - - return $this->render(...); - } - -To keep things decoupled, the email body has been stored in a template and -rendered with the ``renderView()`` method. The ``registration.html.twig`` -template might look something like this: - -.. code-block:: html+twig - - {# templates/emails/registration.html.twig #} -

You did it! You registered!

- - Hi {{ name }}! You're successfully registered. - - {# example, assuming you have a route named "login" #} - To login, go to:
.... - - Thanks! - - {# Makes an absolute URL to the /images/logo.png file #} - - -The ``$message`` object supports many more options, such as including attachments, -adding HTML content, and much more. Refer to the `Creating Messages`_ section -of the Swift Mailer documentation for more details. - -.. _email-using-gmail: - -Using Gmail to Send Emails --------------------------- - -During development, you might prefer to send emails using Gmail instead of -setting up a regular SMTP server. To do that, update the ``MAILER_URL`` of your -``.env`` file to this: - -.. code-block:: bash - - # username is your full Gmail or Google Apps email address - MAILER_URL=gmail://username:password@localhost - -The ``gmail`` transport is a shortcut that uses the ``smtp`` transport, ``ssl`` -encryption, ``login`` auth mode and ``smtp.gmail.com`` host. If your app uses -other encryption or auth mode, you must override those values -(:doc:`see mailer config reference `): - -.. code-block:: bash - - # username is your full Gmail or Google Apps email address - MAILER_URL=gmail://username:password@localhost?encryption=tls&auth_mode=oauth - -If your Gmail account uses 2-Step-Verification, you must `generate an App password`_ -and use it as the value of the mailer password. You must also ensure that you -`allow less secure applications to access your Gmail account`_. - -Using Cloud Services to Send Emails ------------------------------------ - -Cloud mailing services are a popular option for companies that don't want to set -up and maintain their own reliable mail servers. To use these services in a -Symfony app, update the value of ``MAILER_URL`` in the ``.env`` -file. For example, for `Amazon SES`_ (Simple Email Service): - -.. code-block:: bash - - # The host will be different depending on your AWS zone - # The username/password credentials are obtained from the Amazon SES console - MAILER_URL=smtp://email-smtp.us-east-1.amazonaws.com:587?encryption=tls&username=YOUR_SES_USERNAME&password=YOUR_SES_PASSWORD - -Use the same technique for other mail services, as most of the time there is -nothing more to it than configuring an SMTP endpoint. - -How to Work with Emails during Development ------------------------------------------- - -When developing an application which sends email, you will often -not want to actually send the email to the specified recipient during -development. If you are using the SwiftmailerBundle with Symfony, you -can achieve this through configuration settings without having to make -any changes to your application's code at all. There are two main choices -when it comes to handling email during development: (a) disabling the -sending of email altogether or (b) sending all email to a specific -address (with optional exceptions). - -Disabling Sending -~~~~~~~~~~~~~~~~~ - -You can disable sending email by setting the ``disable_delivery`` option to -``true``, which is the default value used by Symfony in the ``test`` environment -(email messages will continue to be sent in the other environments): - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/test/swiftmailer.yaml - swiftmailer: - disable_delivery: true - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // config/packages/test/swiftmailer.php - $container->loadFromExtension('swiftmailer', [ - 'disable_delivery' => "true", - ]); - -.. _sending-to-a-specified-address: - -Sending to a Specified Address(es) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can also choose to have all email sent to a specific address or a list of addresses, instead -of the address actually specified when sending the message. This can be done -via the ``delivery_addresses`` option: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/dev/swiftmailer.yaml - swiftmailer: - delivery_addresses: ['dev@example.com'] - - .. code-block:: xml - - - - - - - dev@example.com - - - - .. code-block:: php - - // config/packages/dev/swiftmailer.php - $container->loadFromExtension('swiftmailer', [ - 'delivery_addresses' => ['dev@example.com'], - ]); - -Now, suppose you're sending an email to ``recipient@example.com`` in a controller:: - - public function index($name, \Swift_Mailer $mailer) - { - $message = (new \Swift_Message('Hello Email')) - ->setFrom('send@example.com') - ->setTo('recipient@example.com') - ->setBody( - $this->renderView( - // templates/hello/email.txt.twig - 'hello/email.txt.twig', - ['name' => $name] - ) - ) - ; - $mailer->send($message); - - return $this->render(...); - } - -In the ``dev`` environment, the email will instead be sent to ``dev@example.com``. -Swift Mailer will add an extra header to the email, ``X-Swift-To``, containing -the replaced address, so you can still see who it would have been sent to. - -.. note:: - - In addition to the ``to`` addresses, this will also stop the email being - sent to any ``CC`` and ``BCC`` addresses set for it. Swift Mailer will add - additional headers to the email with the overridden addresses in them. - These are ``X-Swift-Cc`` and ``X-Swift-Bcc`` for the ``CC`` and ``BCC`` - addresses respectively. - -.. _sending-to-a-specified-address-but-with-exceptions: - -Sending to a Specified Address but with Exceptions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Suppose you want to have all email redirected to a specific address, -(like in the above scenario to ``dev@example.com``). But then you may want -email sent to some specific email addresses to go through after all, and -not be redirected (even if it is in the dev environment). This can be done -by adding the ``delivery_whitelist`` option: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/dev/swiftmailer.yaml - swiftmailer: - delivery_addresses: ['dev@example.com'] - delivery_whitelist: - # all email addresses matching these regexes will be delivered - # like normal, as well as being sent to dev@example.com - - '/@specialdomain\.com$/' - - '/^admin@mydomain\.com$/' - - .. code-block:: xml - - - - - - - - /@specialdomain\.com$/ - /^admin@mydomain\.com$/ - dev@example.com - - - - .. code-block:: php - - // config/packages/dev/swiftmailer.php - $container->loadFromExtension('swiftmailer', [ - 'delivery_addresses' => ["dev@example.com"], - 'delivery_whitelist' => [ - // all email addresses matching these regexes will be delivered - // like normal, as well as being sent to dev@example.com - '/@specialdomain\.com$/', - '/^admin@mydomain\.com$/', - ], - ]); - -In the above example all email messages will be redirected to ``dev@example.com`` -and messages sent to the ``admin@mydomain.com`` address or to any email address -belonging to the domain ``specialdomain.com`` will also be delivered as normal. - -.. caution:: - - The ``delivery_whitelist`` option is ignored unless the ``delivery_addresses`` option is defined. - -Viewing from the Web Debug Toolbar -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can view any email sent during a single response when you are in the -``dev`` environment using the web debug toolbar. The email icon in the toolbar -will show how many emails were sent. If you click it, a report will open -showing the details of the sent emails. - -If you're sending an email and then immediately redirecting to another page, -the web debug toolbar will not display an email icon or a report on the next -page. - -Instead, you can set the ``intercept_redirects`` option to ``true`` in the -``dev`` environment, which will cause the redirect to stop and allow you to open -the report with details of the sent emails. - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/dev/web_profiler.yaml - web_profiler: - intercept_redirects: true - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // config/packages/dev/web_profiler.php - $container->loadFromExtension('web_profiler', [ - 'intercept_redirects' => 'true', - ]); - -.. tip:: - - Alternatively, you can open the profiler after the redirect and search - by the submit URL used on the previous request (e.g. ``/contact/handle``). - The profiler's search feature allows you to load the profiler information - for any past requests. - -.. tip:: - - In addition to the features provided by Symfony, there are applications that - can help you test emails during application development, like `MailCatcher`_, - `Mailtrap`_ and `MailHog`_. - -How to Spool Emails -------------------- - -The default behavior of the Symfony mailer is to send the email messages -immediately. You may, however, want to avoid the performance hit of the -communication to the email server, which could cause the user to wait for the -next page to load while the email is being sent. This can be avoided by choosing to -"spool" the emails instead of sending them directly. - -This makes the mailer to not attempt to send the email message but instead save -it somewhere such as a file. Another process can then read from the spool and -take care of sending the emails in the spool. Currently only spooling to file or -memory is supported. - -.. _email-spool-memory: - -Spool Using Memory -~~~~~~~~~~~~~~~~~~ - -When you use spooling to store the emails to memory, they will get sent right -before the kernel terminates. This means the email only gets sent if the whole -request got executed without any unhandled exception or any errors. To configure -this spool, use the following configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/swiftmailer.yaml - swiftmailer: - # ... - spool: { type: memory } - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // config/packages/swiftmailer.php - $container->loadFromExtension('swiftmailer', [ - // ... - 'spool' => ['type' => 'memory'], - ]); - -.. _spool-using-a-file: - -Spool Using Files -~~~~~~~~~~~~~~~~~ - -When you use the filesystem for spooling, Symfony creates a folder in the given -path for each mail service (e.g. "default" for the default service). This folder -will contain files for each email in the spool. So make sure this directory is -writable by Symfony (or your webserver/php)! - -In order to use the spool with files, use the following configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/swiftmailer.yaml - swiftmailer: - # ... - spool: - type: file - path: /path/to/spooldir - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // config/packages/swiftmailer.php - $container->loadFromExtension('swiftmailer', [ - // ... - - 'spool' => [ - 'type' => 'file', - 'path' => '/path/to/spooldir', - ], - ]); - -.. tip:: - - If you want to store the spool somewhere with your project directory, - remember that you can use the ``%kernel.project_dir%`` parameter to reference - the project's root: - - .. code-block:: yaml - - path: '%kernel.project_dir%/var/spool' - -Now, when your app sends an email, it will not actually be sent but instead -added to the spool. Sending the messages from the spool is done separately. -There is a console command to send the messages in the spool: - -.. code-block:: terminal - - $ APP_ENV=prod php bin/console swiftmailer:spool:send - -It has an option to limit the number of messages to be sent: - -.. code-block:: terminal - - $ APP_ENV=prod php bin/console swiftmailer:spool:send --message-limit=10 - -You can also set the time limit in seconds: - -.. code-block:: terminal - - $ APP_ENV=prod php bin/console swiftmailer:spool:send --time-limit=10 - -In practice you will not want to run this manually. Instead, the console command -should be triggered by a cron job or scheduled task and run at a regular -interval. - -.. caution:: - - When you create a message with SwiftMailer, it generates a ``Swift_Message`` - class. If the ``swiftmailer`` service is lazy loaded, it generates instead a - proxy class named ``Swift_Message_``. - - If you use the memory spool, this change is transparent and has no impact. - But when using the filesystem spool, the message class is serialized in - a file with the randomized class name. The problem is that this random - class name changes on every cache clear. - - So if you send a mail and then you clear the cache, on the next execution of - ``swiftmailer:spool:send`` an error will raise because the class - ``Swift_Message_`` doesn't exist (anymore). - - The solutions are either to use the memory spool or to load the - ``swiftmailer`` service without the ``lazy`` option (see :doc:`/service_container/lazy_services`). - -How to Test that an Email is Sent in a Functional Test ------------------------------------------------------- - -Sending emails with Symfony is pretty straightforward thanks to the -SwiftmailerBundle, which leverages the power of the `Swift Mailer`_ library. - -To functionally test that an email was sent, and even assert the email subject, -content or any other headers, you can use :doc:`the Symfony Profiler `. - -Start with a controller action that sends an email:: - - public function sendEmail($name, \Swift_Mailer $mailer) - { - $message = (new \Swift_Message('Hello Email')) - ->setFrom('send@example.com') - ->setTo('recipient@example.com') - ->setBody('You should see me from the profiler!') - ; - - $mailer->send($message); - - // ... - } - -In your functional test, use the ``swiftmailer`` collector on the profiler -to get information about the messages sent on the previous request:: - - // tests/Controller/MailControllerTest.php - namespace App\Tests\Controller; - - use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; - - class MailControllerTest extends WebTestCase - { - public function testMailIsSentAndContentIsOk() - { - $client = static::createClient(); - - // enables the profiler for the next request (it does nothing if the profiler is not available) - $client->enableProfiler(); - - $crawler = $client->request('POST', '/path/to/above/action'); - - $mailCollector = $client->getProfile()->getCollector('swiftmailer'); - - // checks that an email was sent - $this->assertSame(1, $mailCollector->getMessageCount()); - - $collectedMessages = $mailCollector->getMessages(); - $message = $collectedMessages[0]; - - // Asserting email data - $this->assertInstanceOf('Swift_Message', $message); - $this->assertSame('Hello Email', $message->getSubject()); - $this->assertSame('send@example.com', key($message->getFrom())); - $this->assertSame('recipient@example.com', key($message->getTo())); - $this->assertSame( - 'You should see me from the profiler!', - $message->getBody() - ); - } - } - -Troubleshooting -~~~~~~~~~~~~~~~ - -Problem: The Collector Object Is ``null`` -......................................... - -The email collector is only available when the profiler is enabled and collects -information, as explained in :doc:`/testing/profiling`. - -Problem: The Collector Doesn't Contain the Email -................................................ - -If a redirection is performed after sending the email (for example when you send -an email after a form is processed and before redirecting to another page), make -sure that the test client doesn't follow the redirects, as explained in -:doc:`/testing`. Otherwise, the collector will contain the information of the -redirected page and the email won't be accessible. - -.. _`MailCatcher`: https://github.com/sj26/mailcatcher -.. _`MailHog`: https://github.com/mailhog/MailHog -.. _`Mailtrap`: https://mailtrap.io/ -.. _`Swift Mailer`: https://swiftmailer.symfony.com/ -.. _`SwiftMailerBundle`: https://github.com/symfony/swiftmailer-bundle -.. _`Creating Messages`: https://swiftmailer.symfony.com/docs/messages.html -.. _`Mandrill`: https://mandrill.com/ -.. _`SendGrid`: https://sendgrid.com/ -.. _`Amazon SES`: https://aws.amazon.com/ses/ -.. _`generate an App password`: https://support.google.com/accounts/answer/185833 -.. _`allow less secure applications to access your Gmail account`: https://support.google.com/accounts/answer/6010255 -.. _`RFC 3986`: https://www.ietf.org/rfc/rfc3986.txt diff --git a/event_dispatcher.rst b/event_dispatcher.rst index 5ac86e55cd5..5184cad6b9a 100644 --- a/event_dispatcher.rst +++ b/event_dispatcher.rst @@ -67,12 +67,6 @@ The most common way to listen to an event is to register an **event listener**:: Check out the :doc:`Symfony events reference ` to see what type of object each event provides. -.. versionadded:: 4.3 - - The :class:`Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent` class was - introduced in Symfony 4.3. In previous versions it was called - ``Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent``. - Now that the class is created, you need to register it as a service and notify Symfony that it is a "listener" on the ``kernel.exception`` event by using a special "tag": @@ -106,11 +100,17 @@ using a special "tag": .. code-block:: php // config/services.php + namespace Symfony\Component\DependencyInjection\Loader\Configurator; + use App\EventListener\ExceptionListener; - $container->register(ExceptionListener::class) - ->tag('kernel.event_listener', ['event' => 'kernel.exception']) - ; + return function(ContainerConfigurator $configurator) { + $services = $configurator->services(); + + $services->set(ExceptionListener::class) + ->tag('kernel.event_listener', ['event' => 'kernel.exception']) + ; + }; Symfony follows this logic to decide which method to call inside the event listener class: @@ -118,7 +118,7 @@ listener class: #. If the ``kernel.event_listener`` tag defines the ``method`` attribute, that's the name of the method to be called; #. If no ``method`` attribute is defined, try to call the method whose name - is ``on`` + "camel-cased event name" (e.g. ``onKernelException()`` method for + is ``on`` + "PascalCased event name" (e.g. ``onKernelException()`` method for the ``kernel.exception`` event); #. If that method is not defined either, try to call the ``__invoke()`` magic method (which makes event listeners invokable); @@ -206,10 +206,10 @@ the ``EventSubscriber`` directory. Symfony takes care of the rest. Request Events, Checking Types ------------------------------ -A single page can make several requests (one master request, and then multiple +A single page can make several requests (one main request, and then multiple sub-requests - typically when :ref:`embedding controllers in templates `). For the core Symfony events, you might need to check to see if the event is for -a "master" request or a "sub request":: +a "main" request or a "sub request":: // src/EventListener/RequestListener.php namespace App\EventListener; @@ -220,8 +220,8 @@ a "master" request or a "sub request":: { public function onKernelRequest(RequestEvent $event) { - if (!$event->isMasterRequest()) { - // don't do anything if it's not the master request + if (!$event->isMainRequest()) { + // don't do anything if it's not the main request return; } @@ -275,11 +275,6 @@ name (FQCN) of the corresponding event class:: } } -.. versionadded:: 4.3 - - Referring Symfony's core events via the FQCN of the event class is possible - since Symfony 4.3. - Internally, the event FQCN are treated as aliases for the original event names. Since the mapping already happens when compiling the service container, event listeners and subscribers using FQCN instead of event names will appear under @@ -310,10 +305,6 @@ The compiler pass will always extend the existing list of aliases. Because of that, it is safe to register multiple instances of the pass with different configurations. -.. versionadded:: 4.4 - - The ``AddEventAliasesPass`` class was introduced in Symfony 4.4. - Debugging Event Listeners ------------------------- @@ -331,6 +322,21 @@ its name: $ php bin/console debug:event-dispatcher kernel.exception +or can get everything which partial matches the event name: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher kernel // matches "kernel.exception", "kernel.response" etc. + $ php bin/console debug:event-dispatcher Security // matches "Symfony\Component\Security\Http\Event\CheckPassportEvent" + +The :doc:`security ` system uses an event dispatcher per +firewall. Use the ``--dispatcher`` option to get the registered listeners +for a particular event dispatcher: + +.. code-block:: terminal + + $ php bin/console debug:event-dispatcher --dispatcher=security.event_dispatcher.main + Learn more ---------- diff --git a/form/bootstrap4.rst b/form/bootstrap4.rst index 31f7e50cf1a..bbcd0819369 100644 --- a/form/bootstrap4.rst +++ b/form/bootstrap4.rst @@ -55,13 +55,13 @@ configuration: .. code-block:: php // config/packages/twig.php - $container->loadFromExtension('twig', [ - 'form_themes' => [ - 'bootstrap_4_layout.html.twig', - ], + use Symfony\Config\TwigConfig; + + return static function (TwigConfig $twig) { + $twig->formThemes(['bootstrap_4_layout.html.twig']); // ... - ]); + }; If you prefer to apply the Bootstrap styles on a form to form basis, include the ``form_theme`` tag in the templates where those forms are used: @@ -93,7 +93,7 @@ you'll get the error messages displayed *twice*. Since form errors are rendered *inside* the ``